Skip to content

refactor(contracts): extract ironclaw_product_contracts and land the adapter half (WS1.4) - #6980

Merged
BenKurrek merged 32 commits into
mainfrom
ws1/product-contracts
Aug 1, 2026
Merged

BenKurrek merged 32 commits into
mainfrom
ws1/product-contracts

Conversation

@BenKurrek

@BenKurrek BenKurrek commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #6977 (→ #6975) — merges after them; diff shown is against that branch.

WS1.4 of the target architecture: carve the product tier's neutral contracts out of ironclaw_host_api into crates/ironclaw_product_contracts, and land the WS1.3 adapter half it exists to unblock — ChannelAdapter, ToolAdapter, and RestrictedEgress now live in ironclaw_extension_contracts where §6.1.2 assigns them. Flat crate directory today; the contracts/ family directory arrives in Wave 5.

git mv moved 15 modules at 83–100% similarity, so the diff reads as a move, not a rewrite:

→ ironclaw_product_contracts Owns
surface (from host_api::product_surface) ProductSurface, BoundProductSurface, ProductSurfaceCaller, the invoke/query/stream DTOs, ChannelInboundProductSurface, the ProductSurfaceError family
inbound inbound envelope/payload/ack/rejection DTOs + channel-inbound classification
outbound the product projection wire, approval prompt views, capability activity views, ProjectionCursor
projection ProjectionStream + the read/subscribe request DTOs
interaction_commands the channel-neutral interaction-reply grammar
operator_llm the operator LLM menu vocabulary
package_lifecycle (from extension_contracts) Lifecycle*, ChannelConnectStrategy, ChannelConfigField
→ ironclaw_extension_contracts Owns
channel_adapter ChannelAdapter + its whole DTO family, incl. ProductTriggerReason
tool_adapter ToolAdapter + RestrictedEgress
egress ProtocolHttpEgress, the Egress* types, DeliveryStatus/OutboundDeliverySink
external ExternalActorRef/ExternalConversationRef/ExternalEventId/ProductAttachmentDescriptor/Kind
auth_prompt the channel-rendered auth challenge family + render_channel_auth_prompt
test_support the §11.2.10 channel-adapter conformance suite + the egress/delivery fakes

The adapter-move mechanics

PR #6977's headline finding was that a type cannot leave host_api while a type staying in host_api names it, because host_api's allowlist is "no ironclaw_*". The corollary nobody had measured is that host_api::product_adapter is one connected component: channel_adapter names inbound::ProductTriggerReason; inbound names channel_adapter::ChannelAttachmentRef and outbound::ProjectionCursor; projection names inbound and outbound; interaction_commands names inbound; product_surface names all of them. So the adapters could only move once inbound, outbound, projection, interaction_commands, and product_surface moved too. That co-movement, not the ProductSurface trait alone, is the real content of WS1.4 — and it is why this slice is one PR rather than two.

Three placements were then forced by the one-way street (product_contracts → extension_contracts, never the reverse):

  • ProductTriggerReason went to the extension tier. It is stamped by the adapter on every NormalizedInboundMessage; leaving it in inbound would have inverted the dependency. It sits in channel_adapter beside the field that carries it.
  • external and egress went to the extension tier, as §6.1.2's "fed by" list already said. §6.1.3's "external product-halves" is empty at this base: all five external types — including ProductAttachmentDescriptor/Kind — are named by ChannelAdapter's own cone, so there is no product half to keep back.
  • identity and product_adapter_error could not leave host_api at all. host_api::user_identity names AdapterInstallationId; host_api::product_adapter::auth (which stays until WS1's sealed-evidence row moves it) names ProductAdapterError. Both contracts tiers reach them downward, which is the only placement serving both. §6.1.3's "fed by" naming product_adapter_error is not achievable while auth stays — recorded, not worked around.

Six more dispositions the lead sheet did not predict

1. §6.1.3's "auth/approval prompt-view DTOs" splits, and the auth half is extension-tier. ChannelAdapter's own OutboundPart::AuthPrompt carries Box<AuthPromptView>, and both shipped channel packages call render_channel_auth_prompt from deliver (slack_extension/src/channel.rs:157, telegram_extension/src/channel.rs:278). That is a membrane crossing by adapter signature, so the auth family is now extension_contracts::auth_prompt. The approval half (ApprovalPrompt*View), reached only by product and WebUI, stayed in product_contracts::outbound. One deliberate, recorded cost: two ~15-line display-text validators now exist in both crates. Hoisting them instead would have made a generic text validator part of the extension membrane's public API to serve a product-tier caller — the worse trade. WS1's "evict behavior from host_api" row already owns render_channel_auth_prompt and is where they converge.

2. package_lifecycle came back to product_contracts — the §6.1.3 ruling. WS1.3 placed it in extension_contracts as a forced co-mover and explicitly recorded it as interim rather than a decision. The § text is unambiguous (§6.1.3's Owns names "package_lifecycle UI projections"; §6.1.2's Owns does not), and the code agrees: LifecycleProductAction/LifecycleProductResponse are the vocabulary of LifecycleProductService, which §6.1.3 also assigns here, and its consumers are product/CLI, not the channel packages or lanes. Zero cost, because this crate may depend on extension_contracts, so the four §6.1.2 types it is typed on stay reachable from below.

The #6930 merge-down happened during review, and the ruling held. Slot 3's parent branch landed origin/ws1/extension-contracts@492f0f4d8 ("reconcile WS1.3 with hosted MCP registration"), which relocated host_api::hosted_mcp into ironclaw_extension_contracts — the disposition this PR had independently recommended from hosted_mcp's own module doc and its consumer set. Merging it down made the predicted constraint live: extension_contracts::hosted_mcp names LifecyclePackageId, and package_lifecycle had just moved to product_contracts, which is the forbidden direction.

Resolved by splitting the id from the projections, which is the only resolution the one-way street allows and the one recorded in advance: LifecyclePackageId (with LifecycleBlockerRef, which shares its bounded-string template — splitting them would have traded one home for two) now lives in a new ironclaw_extension_contracts::lifecycle_id module, and product_contracts::package_lifecycle imports it from below. The projections stayed product-tier per §6.1.3. The rejected alternative — dragging hosted_mcp up into product_contracts with the projections — would have handed ironclaw_mcp, ironclaw_extensions, ironclaw_auth, and ironclaw_host_runtime a product-tier edge, which is exactly what §6.1.2 exists to prevent. Four conflicted files were resolved by hand (extension_contracts/{CLAUDE.md,src/lib.rs}, extension_host/{extension_lifecycle_command.rs,product_lifecycle.rs}); parent-introduced references in webui/handlers.rs, extension_host/product_lifecycle.rs, and the new tests/integration/hosted_mcp_registration.rs were repointed to follow the move, never by resurrecting the old path. Full gauntlet re-run on the merged tree: fmt, workspace --all-targets --all-features check, cargo metadata --locked, the architecture suite, and clippy + tests on all six merge-affected crates.

3. AppEvent did not come, because it is dead. §6.1.3 and the WS1.4 row both assign "the 43-variant AppEvent wire enum (today common::event)" here, on the premise that a transport streams it to a client. Re-measured on this base: crates/ironclaw_common/src/event.rs is 1,234 lines with zero consumers outside ironclaw_common itself — the only other workspace reference is reborn_dependency_boundaries.rs:2332, an architecture test asserting the Reborn OpenAI-compatible routes must not stream it. Importing it would seed a brand-new contracts crate with dead weight and a §11.2.3 size-ceiling problem. It stays in common, flagged on the "Narrow ironclaw_common" row, which owns event.rs anyway. Inventory correction, not a scope cut.

4. Four re-export chains gave one trait two import paths, and all four are deleted. ChannelAdapter through ironclaw_product and through ironclaw_reborn_composition (a re-export of a re-export); OutboundDeliverySink + ProtocolHttpEgress through ironclaw_product; ProjectionStream and ChannelInboundProductSurface through ironclaw_product. Consumers repoint to the owning crate. Slot 3's scan caught the first three the moment the types gained an owner; the new scan pins the rest.

5. Two name collisions the new scan can see and the extension tier's could not, both same name, different layer rather than duplicate definitions, both recorded in COLLISION_EXEMPT with the colliding definition named: ProjectionCursor (ironclaw_event_projections/src/lib.rs:116 is the structured internal position { runtime: EventCursor, scope: ProjectionScope } with rebase semantics; ours is the opaque serde(transparent) wire token it stands for) and ProjectionSubscriptionRequest (ironclaw_outbound/src/types.rs:157 is the durable subscription-store record request, with a different cursor type and two extra fields). The shared name is the real defect and renaming is a §5.1 WS10 decision.

6. Slot 3's scan needed four collision exemptions too, surfaced when channel_adapter/external/tool_adapter arrived from host_api where it could not see them. ToolCall/ToolResult vs ironclaw_llm::provider are not the same concept (the LLM pair is Serialize wire vocabulary a model emitted; the extension pair is the in-process invocation envelope whose module doc says "nothing here serializes"). ExternalActorRef/ExternalConversationRef vs ironclaw_conversations::ids are the same concept declared twice — a real duplicate-surface finding, since conversations::ids carries a parallel copy of the adapter-identity vocabulary including AdapterInstallationId and ExternalEventId that the walk cannot even see (bounded_string_id! expansions, the same blind spot slot 3 recorded for LifecyclePackageRef). Exempted so the finding is visible and attributable rather than blocking; unifying them changes the conversations record grammar, which is a domain call.

Deferred by design, with the analysis those rows need

§6.1.3's Owns list also names the product-side ports and the command/view/capability descriptor types. Those are sourced from ironclaw_product, not from ironclaw_host_api, and the CHECKLIST assigns them elsewhere by name: WS2's first row ("flip extension_host's implemented ports to product_contracts/extension_contracts definitions — delivery resolver, reply-context source, admission, pairing sources, account-status") and WS6's operator row ("implements product_contracts ports; product dep dropped"). Moving a port definition without repointing its implementor buys no dependency-edge removal, and both moves are the §12.1c ordering-constrained class. This PR builds the crate they land in. The movability map, so those rows do not have to re-derive it:

Movable with import repointing only (11): ChannelDeliveryResolver + ResolvedChannelDelivery; DeliveryReplyContextSource + NoReplyContext; LlmConfigService + ActiveModelReader + the 15-type LLM DTO group (needs external secrecy on the manifest); OperatorStatusService, OperatorLogsService, OperatorServiceLifecycleService + their DTOs (carry or inline operator_surface_unavailable()); ChannelConfigProductService; AccountConnectionStatusSource; ProductSurfaceCommandDescriptor, ProductCapabilityDescriptor, ProductView.

Blocked, with the blocker (3): ProductOutboundTargetResolver — needs ValidatedReplyTargetBinding from ironclaw_outbound and ProductSurfaceFailure (product/src/error.rs, whose cone pulls ironclaw_turns::{TurnError, TurnErrorCategory}); both are forbidden deps, so it cannot move without type surgery. ProductCommandAdmissionService — needs ProductActionId/ActionFingerprintKey (product::action, itself needing ironclaw_turns) and ProductCommand (product::commands, 565 lines); note it has zero out-of-crate implementors, so moving it removes no edge — lowest value, highest cost. LifecycleProductService — single blocker, LifecycleProductContext::Command(Box<ProductCommandContext>), which inherits the previous cone; and extension_host's impl additionally imports ProductSurfaceFailure + lifecycle_product_surface_error, so even a fixed context would not fully cut that edge.

Exception delta: zero, by design and re-verified

Each of the 13 LAYER_MATRIX_EXCEPTIONS was re-read against this branch's base. Not one is a host_api, product, or product_contracts edge, so no contracts carve-out at this tier can dissolve any of them — the same structural reason WS1.3 recorded, re-checked rather than inherited. The five mcp/scripts lane exceptions still wait on the registry DTOs in ironclaw_extensions (CHECKLIST WS3's mcp row); conversations → turns is turn admission authority, not vocabulary, and is authority-gated to WS5 — not chased here, per the wave floor. Count stays at 13.

What this PR does delete is four re-export edges the layer matrix cannot see (one trait, two import paths), each now pinned.

New-crate rule inventory

  • §11.2.3 contracts purity — internal-dependency allowlist (ironclaw_host_api + ironclaw_extension_contracts, the one-way street §6.1.3 grants explicitly "for channel-facing DTO reuse"); an allowlist, not a blocklist, so a future product/operator edge cannot slip past a list of today's offenders. Plus the crate added to the shared framework/driver deny roster.
  • §11.2.4 port-location scan — new reborn_product_contract_location_scan.rs, 7 tests (2 gates + 5 self-tests). One home: every type the crate defines, discovered rather than enumerated. One import path: the crate's traits (discovered, so a new port inherits the rule) plus the three membrane value types the row names. Ships with positive and negative fixtures per WS10 — including the three real chains this PR deleted, asserted by exact (line, name) — and both discovery helpers assert they measured something before asserting anything else. Its module doc states plainly what is out of scope (ironclaw_product's ~120 DTO re-exports) and why.
  • boundary_rules() entry naming the edges whose appearance would be most damaging (ironclaw_product itself, ironclaw_operator and ironclaw_extension_host — the two crates whose ports it declares — and ironclaw_webui/ironclaw_host_ingress, since a transport edge would mean HTTP reached contracts).
  • Scan reach preserved, and one path-keyed gate repointed: the crate joined untrusted_ingress_paths_cannot_submit_host_trusted_inbound's roots; reborn_service_method_freeze_ratchet's HOST_PRODUCT_SURFACE_SOURCE constant now points at product_contracts/src/surface.rs — it failed loudly on the missing file, which is the property that gate was built with; and the extension-specificity allowlist's four outbound.rs entries were repointed to auth_prompt.rs, not added, so the shrink-only baseline is untouched (the gate reported both halves — 4 new hits and 4 stale entries — and the repoint zeroed both).
  • CI lane selectors: root members, [package.metadata.ironclaw] layer = "contracts", classify-test-scope.sh's shared arm, and reborn-crate-test-buckets.sh's product-workflow bucket (beside ironclaw_product — the bucket groups by what a change can break). Verified, not assumed: discover-reborn-package-crates.sh resolves it through the shipped-binary closure; both CI self-tests pass; the bash/python crate inventories agree at 66.
  • Guidance: crate CLAUDE.md (admission test, module table, the three rulings above, the deferred-by-design section), a crates/AGENTS.md row, ironclaw_extension_contracts/CLAUDE.md rewritten for its four arrivals and one departure, and the live docs/reborn/ + .claude/skills/ references repointed (how-to-port-channel-to-reborn.md, contracts/auth-product.md, webui/CLAUDE.md, webui/AGENTS.md, reborn-extension-surfaces/SKILL.md).

Un-masking

ironclaw_host_api 335 → 243 (−92, gained 0). ironclaw_extension_contracts 44 → 65 (+24 arrived, −3 departed). ironclaw_product_contracts has 71. Set-differencing the rosters leaves zero unaccounted names in either direction: the 92 host_api departures are exactly 24 (→ extension_contracts) + 68 (→ product_contracts), and the only three arrivals with no host_api origin are exactly the three package_lifecycle::tests::* that came from extension_contracts. No surviving test was edited for content — the moved files are 83–100% similar and every other test diff is an import line. Two host_api_contract.rs integration tests moved verbatim with the types they pin, into product_contracts/tests/product_contract.rs.

Verification

Lean gauntlet, all local (see the CI note below):

  • cargo fmt --all -- --check clean.
  • Per-touched-crate cargo clippy --all-targets --all-features -- -D warnings over all 15 touched crates: zero warnings. This is where a real masking bug surfaced — product_contracts' tests use ProtocolAuthEvidence::test_verified, which is behind host_api's test-support seam; it only compiled while host_api was in the same multi--p invocation (feature unification). Fixed with an explicit dev-dependency, not by widening a feature.
  • Per-touched-crate unfiltered cargo test -p <crate> --all-features: green. One observed flake, not a defect: ironclaw's smoke::serve_mounts_cli_login_route_without_sso failed once under parallel load and passed in isolation plus two consecutive full re-runs (145/145 each).
  • Full ironclaw_architecture suite green, including both location scans. Four enumerating gates failed first and were fixed rather than relaxed: the CLI's exact-dependency allowlist, the composition pub-use snapshot (docs/plans/composition-pubuse.snapshot line 43 deleted with the re-export), the extension-specificity allowlist (repointed), and the product-surface method-freeze path constant.
  • cargo check --workspace --all-targets --all-features clean, run last — workspace-root tests/ targets are invisible to -p lanes, and 3 of them needed repointing.
  • cargo metadata --locked resolves; git status re-checked after every cargo invocation.
  • All 10 exact-test selectors in scripts/reborn-e2e-rust.sh executed, each matching exactly one test (grep -Fcx = 1). (Method note: the first sweep reported 10 false failures because this shell is zsh, which does not word-split unquoted variables, and because cargo was consuming the loop's stdin — re-run under bash with </dev/null.)
  • No cross-crate include_str!/include_bytes! reaches into any file this PR moved code out of; scripts/ci/check-include-str-paths.sh passes (119 references).
  • scripts/ci/critical_mutation_gate.py --selection-only against this diff prints false — no named invariant source or watch path changed, so that gate has nothing to enforce here (substitute evidence per the dispatch limitation below).

CI note

While this PR targets ws1/extension-contracts, the four pull_request: branches: [main]-gated workflows do not attach; the evidence above is local. Full Reborn workflow dispatched against this branch: https://github.com/nearai/ironclaw/actions/runs/30670580505 at head 12fcc339a.

Judge that run by its per-job tally, not the roll-up. Per #6978, workflow_dispatch runs of reborn-tests.yml always fail the Tests (Reborn) roll-up structurally: the critical-mutation gate is pull_request-gated, so it skips under dispatch, and the roll-up disallows skipped for that job. The --selection-only result above is the substitute evidence for that one gate.

Coverage-floor recapture and any changed-coverage exemption are deliberately not pre-written: the exemption step is pull_request/merge_group-gated and produces no artifact under dispatch, so entries would be guesses. Both obligations are executed in steward mode once real gate output exists. Note for that step: tests/integration/coverage-floor.toml has no ironclaw_host_api entry, but ironclaw_product's numerator is untouched by this PR (no product source moved out of it) — only its import lines changed.

BenKurrek and others added 26 commits July 31, 2026 11:59
…ire the turns shims (WS1.1)

`ironclaw_host_api::turn` becomes the complete canonical turn vocabulary:
it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason`
gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three
`ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted —
`src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/`
module, whose `fakes.rs` moves beside the traits it implements in
`host_api::product_adapter::test_support`.

`ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a
host_api type that collided with the unrelated
`ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus
turns' bounded `gate:`-prefixed routing string). The alias is retired
rather than relocated, so the workspace now has exactly one `GateRef`.

The six vocabulary-only consumers — auth, event_streams, outbound,
telegram_extension, triggers, event_projections — import from
`ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency
entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not
waived but obsolete: the edges no longer exist. The §11.2.2 ratchet
baseline moves 20 → 15.

No behavior change. `RunOriginAdapter`'s validation error becomes
`Result<_, String>` (matching every other bounded ref in
`host_api::turn`) with a byte-identical message pinned by a test, so
both production `e.to_string()` call sites are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS1.1's box is ticked with what the change actually landed, including the
three lead-sheet corrections it turned up: the row named `TurnStatus` but
not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely
needed), `GateKind`/`BlockedReason` could not be left behind without
duplicating the single `GateKind -> TurnStatus` match table, and deleting
`ids.rs` forced retiring its `GateRef` alias rather than relocating it.

Two decisions confirmed outside the doc and never recorded:

- Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the
  owner, recorded retroactively; it was made in practice at program start.
- The `tools/` row's `default-members` trim — resolved as no trim.

Also surfaces #6963 on the WS0 blocking-prerequisite row's first line
(it was already cited mid-paragraph) and records the §11.2.2 exception
ratchet moving 20 -> 15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…TurnGateRef contract

CodeRabbit review round on #6967.

Import repoints (accepted): every `use` line this PR already rewrote now
names `ironclaw_host_api::turn` directly instead of routing through
`ironclaw_turns`' prelude — 57 files across extension_host, product,
composition, runner, loop_host, conversations, the integration harness,
and the stress tool, plus three inside `ironclaw_turns` itself so the
crate stops consuming its own facade. Import lines this PR did not touch
are left for their consumer's own repoint slot.

TurnGateRef contract pinned (refutation): two review comments claimed
`TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes
and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail
construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty,
<= 256 bytes, no control characters); `LoopGateRef` is the prefix-
validated family via `loop_ref!(.., "gate:")`. The misreading traces to
this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing
string"), which stated a minting convention as if it were validation.
That wording is corrected and the distinction is now pinned by a test.

Also: drop a stale cross-file line reference in a product test comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `tools/` row resolved the `default-members` trim as "no trim" but
kept a trailing "The `tools/`/`default-members` half is still open."
from before that decision, so the row asserted both states. Drop the
stale sentence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Finishes the remediation deferred last round, now that the settled run
(job 91246493989) provides authoritative line numbers. Manifest-only —
no .rs file changes, so the changed-line set the gate computes is
unchanged and these numbers stay valid for the next run.

Derived, not transcribed: the gate was replayed locally against its own
merged lcov from that run, reproducing CI's failure byte-identically
first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after
each entry. Final local result: 100.00% (138/138), branch 100% (4/4),
exit 0. All 45 gate self-tests pass.

Two classes, both verified rather than asserted:

- 13 files x 20 lines - declaration lines (fn params, return types,
  struct fields) whose only edit is the type NAME: GateRef ->
  TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X.
  Declarations are not executable, so these files contribute a zero
  denominator and trip the fail-closed empty_denominator branch.

- 7 lines x 3 files - executable, instrumented, and genuinely not
  exercised by the integration tier. Each checked against the base
  merged lcov (main @ 67088a4, the PR's own base sha): identical 0
  hits before and after, so no coverage was lost. approval_prompt_
  context_view is uncovered across its whole signature at base
  (lines 505-511); the background spawn-mode arm and the invalid-gate-
  ref error path likewise.

This includes the two entries I refused to guess last round -
turn_events.rs (three identical candidate lines by text; the settled
run disambiguates it as 510) and await_edge/store.rs (no verbatim twin
after the repoint; authoritatively 268-272).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oop (WS1.2)

Carve the loop tier's neutral contracts out of the turn kernel into a new
contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…new crate

Enforcement, CI registration, and guidance for the WS1.2 extraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he two crates

The claim types moved to ironclaw_loop_contracts with WS1.2; the validator
policy and the trusted applier stayed in the turn kernel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate
links to them no longer resolve; the TurnRunId link target became redundant
when the import repoint fully qualified it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removing the ironclaw_runner re-export (required by the new port-location
scan) left two workspace-root test-support files importing the turn kernel's
evidence types through it. They now import from ironclaw_turns::loop_exit
directly, which is the single sanctioned path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#6967 landed as a squash, so this branch carries the parent's original
commits while main carries their collapsed equivalent. Merging reconciles
the two shapes; the result must be main plus exactly the WS1.2 delta.

# Conflicts:
#	crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs
#	crates/ironclaw_host_api/src/turn.rs
#	crates/ironclaw_host_runtime/tests/memory_prompt_context.rs
#	crates/ironclaw_loop_contracts/src/host/checkpoint.rs
#	crates/ironclaw_loop_contracts/src/memory_context.rs
#	crates/ironclaw_loop_contracts/tests/memory_prompt_context_service.rs
#	crates/ironclaw_loop_host/src/subagent_spawn_port.rs
#	crates/ironclaw_product/src/communication_context.rs
#	crates/ironclaw_product/src/projection/tests.rs
#	crates/ironclaw_product/src/projection/turn_events.rs
#	crates/ironclaw_product/src/reborn_services/types.rs
#	crates/ironclaw_reborn_composition/src/llm_admin/openai_compat_serve.rs
#	crates/ironclaw_reborn_composition/src/runtime.rs
#	crates/ironclaw_reborn_composition/src/runtime/capability_host/tests.rs
#	crates/ironclaw_reborn_composition/src/runtime/tests/core.rs
#	crates/ironclaw_runner/src/loop_exit_applier/tests/mod.rs
#	crates/ironclaw_runner/src/loop_exit_applier/tests/support.rs
#	crates/ironclaw_runner/src/subagent/await_edge/resolver.rs
#	crates/ironclaw_turns/src/agent_turn_runtime.rs
#	crates/ironclaw_turns/src/coordinator.rs
#	crates/ironclaw_turns/src/lib.rs
#	crates/ironclaw_turns/src/loop_exit.rs
#	crates/ironclaw_turns/src/loop_exit/tests/mod.rs
#	crates/ironclaw_turns/src/origin.rs
#	crates/ironclaw_turns/src/process_projection/runtime.rs
#	crates/ironclaw_turns/src/process_projection/tests.rs
#	crates/ironclaw_turns/src/request.rs
#	crates/ironclaw_turns/src/status.rs
#	crates/ironclaw_turns/tests/agent_loop_host_contract.rs
#	docs/reborn/target-architecture/CHECKLIST.md
#	tests/integration/support/comm_context.rs
#	tests/integration/support/harness/mod.rs
#	tests/integration/support/harness/recorder.rs
#	tests/integration/support/triggered_submit.rs
#	tests/support/reborn_parity_qa/binary_e2e.rs
#	tools/ironclaw_stress/src/user_turn.rs
…scan

Two artifacts of collapsing onto main:

- Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts
  entry while main's dependency bump moved the workspace to 2.0.19. The
  auto-merge kept the stale pin because the bump predates the crate, so
  --locked builds failed.
- ironclaw_product's failure-summary test reaches into another crate's
  source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2
  moved that impl to ironclaw_loop_contracts, so the include still resolved
  and matched nothing. Repointed to follow the code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he dual import paths (WS1.3)

Carve the extension tier's neutral contracts out of the host API into a new
contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the
boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scripts/reborn-e2e-rust.sh pins exact test names for the deterministic
gate. The capability-failure rehydration test moved from
ironclaw_turns::run_profile::host::capability to
ironclaw_loop_contracts::host::capability, so its selector matched zero
tests and the gate failed closed.

Swept all 10 pinned selectors in that script (4 lib + 6 integration
target); this was the only stale one. Each now resolves to exactly one
test, verified by running the selector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The extension_contracts lock entry was generated before the parent branch
collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions
that no longer have [[package]] blocks. --locked lanes would have failed to
resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…merate deps

The composition pub-use snapshot still carried product's PreferenceTargetCodec
re-export, and the CLI's exact-dependency allowlist did not know the extension
tier's contracts crate. Both are enumerating gates, so both failed loudly rather
than passing vacuously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The visibility kit's sealed-strategy template exists to close a strategy set;
every trait here exists to be implemented outside the crate. State that, so the
absence reads as a decision rather than an omission.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The changed-coverage manifest carried a WS1.1 exemption for
crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2
moved that file into ironclaw_loop_contracts, so the gate's fail-closed
path validator rejected the manifest before reaching its line-level
verdict.

Deleted rather than repointed: WS1.1 merged, so those lines are baseline
on main, and this PR's diff pairs the file as a 99%-similarity rename
whose only changed lines are imports. Repointing would re-exempt lines the
gate no longer flags. All 19 remaining entries verified to resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tions

Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py,
added on main after the last merge-down; the WebUI-smoke and E2E roll-up
reds were purely the missing file.

Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py
against this PR's own merged lcov artifact until it exits 0 (100% line
244/244, 100% branch 8/8) - never estimated. Three classes:

- type-path repoints on declaration/expression fragments;
- verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution:
  those files are instrumented in this lcov and partially hit (loop_exit
  195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is
  measured. The same bodies were equally unexercised by the integration tier
  before the move, when they sat in ironclaw_turns and simply were not
  changed lines;
- one crate-root inner attribute the uninstrumentable-line classifier does
  not recognise on a declaration-only facade.

Also adds the #6524 declaration-only facade entry for the new crate's
lib.rs to the informational per-crate coverage summary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve the eight content conflicts and relocate host_api::hosted_mcp into
ironclaw_extension_contracts: it and package_lifecycle reference each other, so
once package_lifecycle moved, leaving hosted_mcp behind would have required
host_api to depend on a workspace crate. That cycle produces no git conflict --
the two sides are different files -- so it is recorded in the crate guide, the
checklist row, and the location scan's frozen names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…WS1.3 adapter half (WS1.4)

Carve the product tier's neutral contracts out of `ironclaw_host_api` into
`crates/ironclaw_product_contracts`, and — in the same change, because it is
what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress`
into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them.

`host_api::product_adapter` is one connected component: `channel_adapter`
names `inbound::ProductTriggerReason`, `inbound` names both
`channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`,
`projection` names inbound and outbound, `interaction_commands` names inbound,
and `product_surface` names all of them. Since `ironclaw_host_api` may hold no
internal dependency, the adapters could only leave once nothing that stays
behind names them — so the product DTO modules moved with them.

`git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move.

Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2
real gates + 5 self-tests with positive and negative fixtures) pins one home
and one import path for the product tier's ports; it fails on the four
re-export chains this change deletes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 975194c8-9946-4b05-ae39-b48b9cb49fe0

📥 Commits

Reviewing files that changed from the base of the PR and between 1d8e32c and b74e093.

📒 Files selected for processing (1)
  • tests/integration/changed-coverage-exemptions.toml

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added shared contracts for channel and tool adapters, lifecycle identifiers, operator model settings, and product-surface operations.
    • Added authentication prompt views with pairing details, validation, serialization, and channel-specific rendering.
    • Added reusable contract and delivery testing utilities.
  • Documentation

    • Updated architecture, integration, and channel-porting documentation for the new contract locations and completed milestones.
  • Refactor

    • Reorganized shared interfaces while preserving existing runtime behavior and workflows.

Walkthrough

The PR adds ironclaw_product_contracts, expands ironclaw_extension_contracts, removes migrated host API modules, updates workspace consumers, and adds architecture and CI enforcement for the new contract boundaries.

Changes

Contract extraction and migration

Layer / File(s) Summary
Contract crate foundation
crates/ironclaw_product_contracts/*, crates/ironclaw_extension_contracts/*
Adds product-surface, lifecycle, operator LLM, authentication, adapter, egress, and test-support contracts.
Architecture enforcement
crates/ironclaw_architecture/tests/*
Adds product-contract location and dependency scans, collision checks, and a service-method ratchet update.
Runtime and consumer migration
crates/ironclaw_product/*, crates/ironclaw_extension_host/*, crates/ironclaw_reborn_composition/*, crates/ironclaw_webui/*, crates/ironclaw_*extension/*
Updates traits, request/response types, implementations, and tests to use the dedicated contract crates.
Host API reduction and CI integration
crates/ironclaw_host_api/*, scripts/ci/*, docs/reborn/*, tests/integration/*
Removes migrated host API modules and re-exports, updates documentation, and registers the new crate in CI scopes and buckets.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: serrrfirat

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides strong technical detail and validation evidence, but it omits most required template sections, including change type, linked issue, security, database, rollback, and review follow-through. Rewrite the description using every template heading and explicitly state change type, linked issue, security and database impact, blast radius, rollback plan, review follow-through, and review track.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits format and accurately describes the contracts extraction and adapter migration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@railway-app

railway-app Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-6980 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Aug 1, 2026 at 1:43 am

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6980 July 31, 2026 22:39 Destroyed
…nstrument

With the auth-prompt and lifecycle-id holes tested, the gate reached 100%
line and 100% branch but still failed on four files 'contributing no
instrumented lines'. Each is a single type-position line — an enum variant
field, two parameter types, a struct field — whose only change is the
repointed path for a moved contract, wrapped onto its own line by rustfmt.
LLVM emits no coverage region for a type annotation, so no test can reach them.

Replayed against this PR's own merged lcov: exit 0, 285/285 lines, 38/38
branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6980 August 1, 2026 01:35 Destroyed
@BenKurrek

Copy link
Copy Markdown
Collaborator Author

Coordinator sign-off (Wave 1 slot 4 — WS1.4 ironclaw_product_contracts + the adapter co-movement).

  • The slice's real content was a measured discovery: host_api::product_adapter is one connected component, so the adapter traits could only leave as a 15-module co-movement — established by import-graph evidence before any code moved. Three placements were forced by naming-couplings and recorded as such; the package_lifecycle ruling followed §6.1.3's explicit Owns entry, with the feat(extensions): register hosted MCP servers #6930 collision resolved by splitting the lifecycle id downward rather than leaking product-tier edges into four kernel crates.
  • Collapse verified to the file: post-merge diff is the identical 209-file set to the pre-collapse delta with exactly one deliberate difference (adopting main's walk-exclusion into the new scan for consistency). The conflict-marker sweep caught a staged rename-conflict block that had escaped the UU list plus two swallowed base-marker lines — the class of artifact that ships silently.
  • The coverage close-out found a genuine regression and fixed it with tests, not exemptions: 134 lines + 22 branch arms in the two new modules had lost their unit coverage in the move (the owning crates' suites didn't follow) — now driven 134 → 0 by 15 new tests covering the auth-prompt render path both shipped channel packages call, the full validator rejection surface, and the wire round-trips. Seven lines exempted, each with per-site evidence of unreachability. Final replay: 285/285 lines, 38/38 branches, exit 0, byte-identical harness.
  • Un-masking exact (−92 from host_api, every name accounted); four enumerating gates updated-not-relaxed; AppEvent correctly left dead for the wave-closer's deletion; exceptions floor 13 re-verified.

Ready to merge. Stack note: #6981 (evidence-mint) syncs this branch down next; its collapse follows this merge.

@BenKurrek
BenKurrek merged commit 8120971 into main Aug 1, 2026
64 checks passed
@BenKurrek
BenKurrek deleted the ws1/product-contracts branch August 1, 2026 03:19
BenKurrek added a commit that referenced this pull request Aug 1, 2026
…ss grants (WS1.5) (#6981)

* refactor(contracts): complete the turn vocabulary in host_api and retire the turns shims (WS1.1)

`ironclaw_host_api::turn` becomes the complete canonical turn vocabulary:
it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason`
gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three
`ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted —
`src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/`
module, whose `fakes.rs` moves beside the traits it implements in
`host_api::product_adapter::test_support`.

`ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a
host_api type that collided with the unrelated
`ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus
turns' bounded `gate:`-prefixed routing string). The alias is retired
rather than relocated, so the workspace now has exactly one `GateRef`.

The six vocabulary-only consumers — auth, event_streams, outbound,
telegram_extension, triggers, event_projections — import from
`ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency
entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not
waived but obsolete: the edges no longer exist. The §11.2.2 ratchet
baseline moves 20 → 15.

No behavior change. `RunOriginAdapter`'s validation error becomes
`Result<_, String>` (matching every other bounded ref in
`host_api::turn`) with a byte-identical message pinned by a test, so
both production `e.to_string()` call sites are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(target-architecture): tick WS1.1 and close PLAN decision round #1

WS1.1's box is ticked with what the change actually landed, including the
three lead-sheet corrections it turned up: the row named `TurnStatus` but
not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely
needed), `GateKind`/`BlockedReason` could not be left behind without
duplicating the single `GateKind -> TurnStatus` match table, and deleting
`ids.rs` forced retiring its `GateRef` alias rather than relocating it.

Two decisions confirmed outside the doc and never recorded:

- Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the
  owner, recorded retroactively; it was made in practice at program start.
- The `tools/` row's `default-members` trim — resolved as no trim.

Also surfaces #6963 on the WS0 blocking-prerequisite row's first line
(it was already cited mid-paragraph) and records the §11.2.2 exception
ratchet moving 20 -> 15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): repoint touched imports to host_api and pin the TurnGateRef contract

CodeRabbit review round on #6967.

Import repoints (accepted): every `use` line this PR already rewrote now
names `ironclaw_host_api::turn` directly instead of routing through
`ironclaw_turns`' prelude — 57 files across extension_host, product,
composition, runner, loop_host, conversations, the integration harness,
and the stress tool, plus three inside `ironclaw_turns` itself so the
crate stops consuming its own facade. Import lines this PR did not touch
are left for their consumer's own repoint slot.

TurnGateRef contract pinned (refutation): two review comments claimed
`TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes
and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail
construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty,
<= 256 bytes, no control characters); `LoopGateRef` is the prefix-
validated family via `loop_ref!(.., "gate:")`. The misreading traces to
this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing
string"), which stated a minting convention as if it were validation.
That wording is corrected and the distinction is now pinned by a test.

Also: drop a stale cross-file line reference in a product test comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(target-architecture): remove the row-97 self-contradiction

The `tools/` row resolved the `default-members` trim as "no trim" but
kept a trailing "The `tools/`/`default-members` half is still open."
from before that decision, so the row asserted both states. Drop the
stale sentence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(coverage): complete the WS1.1 changed-coverage exemptions

Finishes the remediation deferred last round, now that the settled run
(job 91246493989) provides authoritative line numbers. Manifest-only —
no .rs file changes, so the changed-line set the gate computes is
unchanged and these numbers stay valid for the next run.

Derived, not transcribed: the gate was replayed locally against its own
merged lcov from that run, reproducing CI's failure byte-identically
first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after
each entry. Final local result: 100.00% (138/138), branch 100% (4/4),
exit 0. All 45 gate self-tests pass.

Two classes, both verified rather than asserted:

- 13 files x 20 lines - declaration lines (fn params, return types,
  struct fields) whose only edit is the type NAME: GateRef ->
  TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X.
  Declarations are not executable, so these files contribute a zero
  denominator and trip the fail-closed empty_denominator branch.

- 7 lines x 3 files - executable, instrumented, and genuinely not
  exercised by the integration tier. Each checked against the base
  merged lcov (main @ 67088a4, the PR's own base sha): identical 0
  hits before and after, so no coverage was lost. approval_prompt_
  context_view is uncovered across its whole signature at base
  (lines 505-511); the background spawn-mode arm and the invalid-gate-
  ref error path likewise.

This includes the two entries I refused to guess last round -
turn_events.rs (three identical candidate lines by text; the settled
run disambiguates it as 510) and await_edge/store.rs (no verbatim twin
after the repoint; authoritatively 268-272).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): extract ironclaw_loop_contracts and flip agent_loop (WS1.2)

Carve the loop tier's neutral contracts out of the turn kernel into a new
contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): pin the loop-contract boundary and register the new crate

Enforcement, CI registration, and guidance for the WS1.2 extraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(reborn): split the loop-exit contract's ownership claim across the two crates

The claim types moved to ironclaw_loop_contracts with WS1.2; the validator
policy and the trusted applier stayed in the turn kernel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(contracts): repoint the three intra-doc links the crate split broke

The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate
links to them no longer resolve; the TurnRunId link target became redundant
when the import repoint fully qualified it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tests): repoint the loop-exit evidence imports the port rule moved

Removing the ironclaw_runner re-export (required by the new port-location
scan) left two workspace-root test-support files importing the turn kernel's
evidence types through it. They now import from ironclaw_turns::loop_exit
directly, which is the single sanctioned path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(collapse): reconcile the lock pin and the moved failure-category scan

Two artifacts of collapsing onto main:

- Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts
  entry while main's dependency bump moved the workspace to 2.0.19. The
  auto-merge kept the stale pin because the bump predates the crate, so
  --locked builds failed.
- ironclaw_product's failure-summary test reaches into another crate's
  source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2
  moved that impl to ironclaw_loop_contracts, so the include still resolved
  and matched nothing. Repointed to follow the code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): extract ironclaw_extension_contracts and close the dual import paths (WS1.3)

Carve the extension tier's neutral contracts out of the host API into a new
contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the
boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): repoint the exact-test selector WS1.2 moved

scripts/reborn-e2e-rust.sh pins exact test names for the deterministic
gate. The capability-failure rehydration test moved from
ironclaw_turns::run_profile::host::capability to
ironclaw_loop_contracts::host::capability, so its selector matched zero
tests and the gate failed closed.

Swept all 10 pinned selectors in that script (4 lib + 6 integration
target); this was the only stale one. Each now resolves to exactly one
test, verified by running the selector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(collapse): reconcile the new crate's lock pins with main's dep bumps

The extension_contracts lock entry was generated before the parent branch
collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions
that no longer have [[package]] blocks. --locked lanes would have failed to
resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(arch): register the new contracts crate in the two gates that enumerate deps

The composition pub-use snapshot still carried product's PreferenceTargetCodec
re-export, and the CLI's exact-dependency allowlist did not know the extension
tier's contracts crate. Both are enumerating gates, so both failed loudly rather
than passing vacuously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(contracts): record why the extension tier's traits are not sealed

The visibility kit's sealed-strategy template exists to close a strategy set;
every trait here exists to be implemented outside the crate. State that, so the
absence reads as a decision rather than an omission.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(coverage): drop the exemption WS1.2 made stale

The changed-coverage manifest carried a WS1.1 exemption for
crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2
moved that file into ironclaw_loop_contracts, so the gate's fail-closed
path validator rejected the manifest before reaching its line-level
verdict.

Deleted rather than repointed: WS1.1 merged, so those lines are baseline
on main, and this PR's diff pairs the file as a 99%-similarity rename
whose only changed lines are imports. Repointing would re-exempt lines the
gate no longer flags. All 19 remaining entries verified to resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(coverage): merge main and derive the WS1.2 changed-coverage exemptions

Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py,
added on main after the last merge-down; the WebUI-smoke and E2E roll-up
reds were purely the missing file.

Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py
against this PR's own merged lcov artifact until it exits 0 (100% line
244/244, 100% branch 8/8) - never estimated. Three classes:

- type-path repoints on declaration/expression fragments;
- verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution:
  those files are instrumented in this lcov and partially hit (loop_exit
  195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is
  measured. The same bodies were equally unexercised by the integration tier
  before the move, when they sat in ironclaw_turns and simply were not
  changed lines;
- one crate-root inner attribute the uninstrumentable-line classifier does
  not recognise on a declaration-only facade.

Also adds the #6524 declaration-only facade entry for the new crate's
lib.rs to the informational per-crate coverage summary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): extract ironclaw_product_contracts and land the WS1.3 adapter half (WS1.4)

Carve the product tier's neutral contracts out of `ironclaw_host_api` into
`crates/ironclaw_product_contracts`, and — in the same change, because it is
what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress`
into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them.

`host_api::product_adapter` is one connected component: `channel_adapter`
names `inbound::ProductTriggerReason`, `inbound` names both
`channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`,
`projection` names inbound and outbound, `interaction_commands` names inbound,
and `product_surface` names all of them. Since `ironclaw_host_api` may hold no
internal dependency, the adapters could only leave once nothing that stays
behind names them — so the product DTO modules moved with them.

`git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move.

Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2
real gates + 5 self-tests with positive and negative fixtures) pins one home
and one import path for the product tier's ports; it fails on the four
re-export chains this change deletes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(checklist): record the hosted-MCP merge-down resolution on the WS1.4 row

The WS1.4 ruling on package_lifecycle survived the parent's #6930
reconciliation; the LifecyclePackageId split is the recorded resolution,
now stated as what happened rather than what was recommended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): consolidate sealed evidence minting behind witness grants (WS1.5)

CHECKLIST WS1's evidence-mint row: one sealed seam owns construction of
protocol-auth evidence, every other construction path is deleted, and the
refute-tests land with it.

The `host-auth-mint` cargo feature was not a seal. Cargo unifies features
across the packages selected in one invocation, so `ironclaw_webui`'s opt-in
(-> turns -> host_api) compiled `ironclaw_host_api` once with the gate ON for
every other crate in the same build. Measured before touching anything: a probe
in `ironclaw_agent_loop` — whose manifest names `ironclaw_host_api` with no
features — minted a verified bearer claim; it failed to compile alone and
passed as soon as `ironclaw_webui` joined the `cargo test`. Every workspace
build is the second case.

Replaced with the repo's existing witness-token idiom (`host_api::authorized`),
which no other crate's manifest can switch on:

- `HostAuthenticationGrant` <- `HostProtocolAuthenticator`, sole implementor
  `ironclaw_webui` (module-private `AuthLayerState`, trust stage T1).
- `VerifiedInboundGrant` <- `ChannelIngressVerifier`, sole implementor
  `ironclaw_extension_host` (`VerifiedEvidenceMint`, trust stage T2).
- Channel/webhook mint family -> `ironclaw_extension_contracts::verified_inbound`
  (§6.1.2); bearer/session family stays in `host_api` (§6.1.1). The evidence
  type does not move; `extension_contracts` reaches the private verified variant
  through the grant-gated `ProtocolAuthEvidence::seal_verified_inbound`.

Closed: the feature in 5 manifests + 1 CI recipe; four re-export chains
(`host_api::product_adapter`, `ironclaw_product` root, `ironclaw_product::auth`,
and composition's zero-consumer re-export).

Enforcement: `reborn_sealed_evidence_mint_ratchet` (10 tests, §11.2.5) plus
`host_api/tests/protocol_auth_evidence_seal.rs` (5) and
`extension_contracts/tests/verified_inbound_seal.rs` (4). +19 tests, 0 removed,
no surviving assertion edited. The production-struct dead-code baseline shrinks
81/282 -> 80/277: the five `dead_code` suppressions in `auth.rs` existed only
because the constructors were feature-gated.

* test(contracts): cover the extracted auth-prompt and lifecycle-id surface

The changed-coverage gate on #6980 flagged 134 uncovered lines and 22
uncovered branch arms, all in the two modules WS1.4 created. That was a real
regression, not an attribution artifact: the code moved out of
host_api::product_adapter::outbound and host_api::package_lifecycle, and the
owning crates' unit suites did not move with it.

Fourteen tests close every one of those lines: render_channel_auth_prompt in
both the DM and mention shapes and with/without a pairing deep link, the
AuthPromptContextView constructors and wire round-trip, each nested validator
arm rejecting independently, and the bounded-id accessor and rejection surface.
Verified by replaying reborn_changed_coverage.py against the PR's own merged
lcov (byte-identical to CI) and re-measuring with cargo llvm-cov at this head:
134 -> 0.

Three sites remain exempted with per-site evidence, all unreachable by test:
a declaration-only crate facade's inner attribute, and two pre-existing error
arms whose only change is a repointed type path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(contracts): cover the newline/tab carve-out in bounded prompt text

The changed-coverage gate reached 100% line but left three branch arms on
validate_bounded_text's control-character predicate. Newline and tab are
deliberately legal in prompt copy — channels render multi-line instructions —
and every other control character is a rejection; both halves are now driven.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(coverage): exempt the four type-position lines the gate cannot instrument

With the auth-prompt and lifecycle-id holes tested, the gate reached 100%
line and 100% branch but still failed on four files 'contributing no
instrumented lines'. Each is a single type-position line — an enum variant
field, two parameter types, a struct field — whose only change is the
repointed path for a moved contract, wrapped onto its own line by rustfmt.
LLVM emits no coverage region for a type annotation, so no test can reach them.

Replayed against this PR's own merged lcov: exit 0, 285/285 lines, 38/38
branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
BenKurrek added a commit that referenced this pull request Aug 1, 2026
…n record with shipped reality (#6995)

* docs(target-architecture): Wave 1 truth audit — reconcile the decision record with shipped reality

Wave 1 merged as seven PRs (#6967, #6975, #6977, #6979, #6980, #6981,
#6982). This audits the north-star docs against merged `main` at
`a50ad0638` and closes every gap where the decision record no longer
matches what shipped.

Docs-only: five `.md` files under `docs/reborn/target-architecture/`.
House style throughout — dated ✎ amendments, prior text quoted where a
clause is corrected, no silent rewrites (`git diff --word-diff` removes
nothing but the words each amendment quotes back).

Highlights:
- §8.3's exception-dissolution proof corrected: `conversations → turns`
  is turn admission authority, not vocabulary; the wave's "20 → 12"
  milestone was wrong by construction and the true end-state is 13.
- §6.1.1–§6.1.4 gain as-built module inventories; the #6930 amendment
  placing `hosted_mcp` in `host_api` is superseded by #6977's relocation.
- §12.1a records the evidence-mint finding: the `host-auth-mint` feature
  seal was vacuous, replaced by witness grants — plus two residuals, one
  from the slice and one this audit verified against the ratchet source.
- The coverage-governance gap (~14k lines now ungated by the floor
  file's opt-in design) moves from a sign-off comment onto the ratchet row.
- Duplicate-type findings, the `llm_costs`/`ModelCostTable` deferral, the
  surviving `product → loop_host` sites, and issues #6945/#6978 all gain
  owning rows.

Verification: docs-only diff; `cargo test -p ironclaw_architecture` green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(target-architecture): tighten the Wave 1 audit where review found it overstated

Six review findings triaged against the built tree; four were real.

- `families/contracts.md` landing marker claimed "everything else in
  this file was built as written". Three `ironclaw_loop_contracts`
  divergences contradict it and are now named at the marker and marked
  at the entry: the manifest holds `ironclaw_extension_contracts` and
  holds neither `ironclaw_common` nor `ironclaw_prompt_envelope`; the
  `tokio` carve-out; the embedded prompt asset.
- The evidence-mint guarantee said "compile-time impossibility" and
  "enforced by constructor visibility plus a workspace string-scan pin"
  in one breath. Split: the compiler enforces no-mint-without-a-grant
  (so nothing outside a workspace crate can mint at all); an
  architecture test — a line-oriented substring scan with two named
  evasions — decides which workspace crate may hold one.
- That deferral had no home. §12.1a said "hardening the scan is WS10
  work, listed there"; it was not listed. Added to the WS10 guardrail
  row with both evasions and the call-site census that backstops them.
- CHECKLIST WS8's `common/src/event.rs` row still closed with "stays in
  `common` as a deletion candidate" under an "executed by #6982"
  header. The file is deleted; the tail now says so.

Plus two clarity fixes where a reader could reach a wrong number: the
`(66)`/`(64)` figures in §9/§13 are that table's 2026-07-30 measurement
and now say so beside the live 67, and the row-1 edge count now states
that six of row 1's seven fell while the register moved by seven,
because `auth → turns` is row 9.

Dated amendments only; every replaced phrase is quoted in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
l3ocifer pushed a commit to l3ocifer/frick-ironclaw that referenced this pull request Sep 3, 2026
…adapter half (WS1.4) (nearai#6980)

* refactor(contracts): complete the turn vocabulary in host_api and retire the turns shims (WS1.1)

`ironclaw_host_api::turn` becomes the complete canonical turn vocabulary:
it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason`
gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three
`ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted —
`src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/`
module, whose `fakes.rs` moves beside the traits it implements in
`host_api::product_adapter::test_support`.

`ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a
host_api type that collided with the unrelated
`ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus
turns' bounded `gate:`-prefixed routing string). The alias is retired
rather than relocated, so the workspace now has exactly one `GateRef`.

The six vocabulary-only consumers — auth, event_streams, outbound,
telegram_extension, triggers, event_projections — import from
`ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency
entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not
waived but obsolete: the edges no longer exist. The §11.2.2 ratchet
baseline moves 20 → 15.

No behavior change. `RunOriginAdapter`'s validation error becomes
`Result<_, String>` (matching every other bounded ref in
`host_api::turn`) with a byte-identical message pinned by a test, so
both production `e.to_string()` call sites are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(target-architecture): tick WS1.1 and close PLAN decision round #1

WS1.1's box is ticked with what the change actually landed, including the
three lead-sheet corrections it turned up: the row named `TurnStatus` but
not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely
needed), `GateKind`/`BlockedReason` could not be left behind without
duplicating the single `GateKind -> TurnStatus` match table, and deleting
`ids.rs` forced retiring its `GateRef` alias rather than relocating it.

Two decisions confirmed outside the doc and never recorded:

- Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the
  owner, recorded retroactively; it was made in practice at program start.
- The `tools/` row's `default-members` trim — resolved as no trim.

Also surfaces nearai#6963 on the WS0 blocking-prerequisite row's first line
(it was already cited mid-paragraph) and records the §11.2.2 exception
ratchet moving 20 -> 15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): repoint touched imports to host_api and pin the TurnGateRef contract

CodeRabbit review round on nearai#6967.

Import repoints (accepted): every `use` line this PR already rewrote now
names `ironclaw_host_api::turn` directly instead of routing through
`ironclaw_turns`' prelude — 57 files across extension_host, product,
composition, runner, loop_host, conversations, the integration harness,
and the stress tool, plus three inside `ironclaw_turns` itself so the
crate stops consuming its own facade. Import lines this PR did not touch
are left for their consumer's own repoint slot.

TurnGateRef contract pinned (refutation): two review comments claimed
`TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes
and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail
construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty,
<= 256 bytes, no control characters); `LoopGateRef` is the prefix-
validated family via `loop_ref!(.., "gate:")`. The misreading traces to
this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing
string"), which stated a minting convention as if it were validation.
That wording is corrected and the distinction is now pinned by a test.

Also: drop a stale cross-file line reference in a product test comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(target-architecture): remove the row-97 self-contradiction

The `tools/` row resolved the `default-members` trim as "no trim" but
kept a trailing "The `tools/`/`default-members` half is still open."
from before that decision, so the row asserted both states. Drop the
stale sentence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(coverage): complete the WS1.1 changed-coverage exemptions

Finishes the remediation deferred last round, now that the settled run
(job 91246493989) provides authoritative line numbers. Manifest-only —
no .rs file changes, so the changed-line set the gate computes is
unchanged and these numbers stay valid for the next run.

Derived, not transcribed: the gate was replayed locally against its own
merged lcov from that run, reproducing CI's failure byte-identically
first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after
each entry. Final local result: 100.00% (138/138), branch 100% (4/4),
exit 0. All 45 gate self-tests pass.

Two classes, both verified rather than asserted:

- 13 files x 20 lines - declaration lines (fn params, return types,
  struct fields) whose only edit is the type NAME: GateRef ->
  TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X.
  Declarations are not executable, so these files contribute a zero
  denominator and trip the fail-closed empty_denominator branch.

- 7 lines x 3 files - executable, instrumented, and genuinely not
  exercised by the integration tier. Each checked against the base
  merged lcov (main @ 67088a4, the PR's own base sha): identical 0
  hits before and after, so no coverage was lost. approval_prompt_
  context_view is uncovered across its whole signature at base
  (lines 505-511); the background spawn-mode arm and the invalid-gate-
  ref error path likewise.

This includes the two entries I refused to guess last round -
turn_events.rs (three identical candidate lines by text; the settled
run disambiguates it as 510) and await_edge/store.rs (no verbatim twin
after the repoint; authoritatively 268-272).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): extract ironclaw_loop_contracts and flip agent_loop (WS1.2)

Carve the loop tier's neutral contracts out of the turn kernel into a new
contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): pin the loop-contract boundary and register the new crate

Enforcement, CI registration, and guidance for the WS1.2 extraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(reborn): split the loop-exit contract's ownership claim across the two crates

The claim types moved to ironclaw_loop_contracts with WS1.2; the validator
policy and the trusted applier stayed in the turn kernel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(contracts): repoint the three intra-doc links the crate split broke

The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate
links to them no longer resolve; the TurnRunId link target became redundant
when the import repoint fully qualified it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tests): repoint the loop-exit evidence imports the port rule moved

Removing the ironclaw_runner re-export (required by the new port-location
scan) left two workspace-root test-support files importing the turn kernel's
evidence types through it. They now import from ironclaw_turns::loop_exit
directly, which is the single sanctioned path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(collapse): reconcile the lock pin and the moved failure-category scan

Two artifacts of collapsing onto main:

- Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts
  entry while main's dependency bump moved the workspace to 2.0.19. The
  auto-merge kept the stale pin because the bump predates the crate, so
  --locked builds failed.
- ironclaw_product's failure-summary test reaches into another crate's
  source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2
  moved that impl to ironclaw_loop_contracts, so the include still resolved
  and matched nothing. Repointed to follow the code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): extract ironclaw_extension_contracts and close the dual import paths (WS1.3)

Carve the extension tier's neutral contracts out of the host API into a new
contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the
boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): repoint the exact-test selector WS1.2 moved

scripts/reborn-e2e-rust.sh pins exact test names for the deterministic
gate. The capability-failure rehydration test moved from
ironclaw_turns::run_profile::host::capability to
ironclaw_loop_contracts::host::capability, so its selector matched zero
tests and the gate failed closed.

Swept all 10 pinned selectors in that script (4 lib + 6 integration
target); this was the only stale one. Each now resolves to exactly one
test, verified by running the selector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(collapse): reconcile the new crate's lock pins with main's dep bumps

The extension_contracts lock entry was generated before the parent branch
collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions
that no longer have [[package]] blocks. --locked lanes would have failed to
resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(arch): register the new contracts crate in the two gates that enumerate deps

The composition pub-use snapshot still carried product's PreferenceTargetCodec
re-export, and the CLI's exact-dependency allowlist did not know the extension
tier's contracts crate. Both are enumerating gates, so both failed loudly rather
than passing vacuously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(contracts): record why the extension tier's traits are not sealed

The visibility kit's sealed-strategy template exists to close a strategy set;
every trait here exists to be implemented outside the crate. State that, so the
absence reads as a decision rather than an omission.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(coverage): drop the exemption WS1.2 made stale

The changed-coverage manifest carried a WS1.1 exemption for
crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2
moved that file into ironclaw_loop_contracts, so the gate's fail-closed
path validator rejected the manifest before reaching its line-level
verdict.

Deleted rather than repointed: WS1.1 merged, so those lines are baseline
on main, and this PR's diff pairs the file as a 99%-similarity rename
whose only changed lines are imports. Repointing would re-exempt lines the
gate no longer flags. All 19 remaining entries verified to resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(coverage): merge main and derive the WS1.2 changed-coverage exemptions

Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py,
added on main after the last merge-down; the WebUI-smoke and E2E roll-up
reds were purely the missing file.

Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py
against this PR's own merged lcov artifact until it exits 0 (100% line
244/244, 100% branch 8/8) - never estimated. Three classes:

- type-path repoints on declaration/expression fragments;
- verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution:
  those files are instrumented in this lcov and partially hit (loop_exit
  195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is
  measured. The same bodies were equally unexercised by the integration tier
  before the move, when they sat in ironclaw_turns and simply were not
  changed lines;
- one crate-root inner attribute the uninstrumentable-line classifier does
  not recognise on a declaration-only facade.

Also adds the nearai#6524 declaration-only facade entry for the new crate's
lib.rs to the informational per-crate coverage summary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): extract ironclaw_product_contracts and land the WS1.3 adapter half (WS1.4)

Carve the product tier's neutral contracts out of `ironclaw_host_api` into
`crates/ironclaw_product_contracts`, and — in the same change, because it is
what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress`
into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them.

`host_api::product_adapter` is one connected component: `channel_adapter`
names `inbound::ProductTriggerReason`, `inbound` names both
`channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`,
`projection` names inbound and outbound, `interaction_commands` names inbound,
and `product_surface` names all of them. Since `ironclaw_host_api` may hold no
internal dependency, the adapters could only leave once nothing that stays
behind names them — so the product DTO modules moved with them.

`git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move.

Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2
real gates + 5 self-tests with positive and negative fixtures) pins one home
and one import path for the product tier's ports; it fails on the four
re-export chains this change deletes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(checklist): record the hosted-MCP merge-down resolution on the WS1.4 row

The WS1.4 ruling on package_lifecycle survived the parent's nearai#6930
reconciliation; the LifecyclePackageId split is the recorded resolution,
now stated as what happened rather than what was recommended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(contracts): cover the extracted auth-prompt and lifecycle-id surface

The changed-coverage gate on nearai#6980 flagged 134 uncovered lines and 22
uncovered branch arms, all in the two modules WS1.4 created. That was a real
regression, not an attribution artifact: the code moved out of
host_api::product_adapter::outbound and host_api::package_lifecycle, and the
owning crates' unit suites did not move with it.

Fourteen tests close every one of those lines: render_channel_auth_prompt in
both the DM and mention shapes and with/without a pairing deep link, the
AuthPromptContextView constructors and wire round-trip, each nested validator
arm rejecting independently, and the bounded-id accessor and rejection surface.
Verified by replaying reborn_changed_coverage.py against the PR's own merged
lcov (byte-identical to CI) and re-measuring with cargo llvm-cov at this head:
134 -> 0.

Three sites remain exempted with per-site evidence, all unreachable by test:
a declaration-only crate facade's inner attribute, and two pre-existing error
arms whose only change is a repointed type path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(contracts): cover the newline/tab carve-out in bounded prompt text

The changed-coverage gate reached 100% line but left three branch arms on
validate_bounded_text's control-character predicate. Newline and tab are
deliberately legal in prompt copy — channels render multi-line instructions —
and every other control character is a rejection; both halves are now driven.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(coverage): exempt the four type-position lines the gate cannot instrument

With the auth-prompt and lifecycle-id holes tested, the gate reached 100%
line and 100% branch but still failed on four files 'contributing no
instrumented lines'. Each is a single type-position line — an enum variant
field, two parameter types, a struct field — whose only change is the
repointed path for a moved contract, wrapped onto its own line by rustfmt.
LLVM emits no coverage region for a type annotation, so no test can reach them.

Replayed against this PR's own merged lcov: exit 0, 285/285 lines, 38/38
branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
l3ocifer pushed a commit to l3ocifer/frick-ironclaw that referenced this pull request Sep 3, 2026
…ss grants (WS1.5) (nearai#6981)

* refactor(contracts): complete the turn vocabulary in host_api and retire the turns shims (WS1.1)

`ironclaw_host_api::turn` becomes the complete canonical turn vocabulary:
it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason`
gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three
`ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted —
`src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/`
module, whose `fakes.rs` moves beside the traits it implements in
`host_api::product_adapter::test_support`.

`ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a
host_api type that collided with the unrelated
`ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus
turns' bounded `gate:`-prefixed routing string). The alias is retired
rather than relocated, so the workspace now has exactly one `GateRef`.

The six vocabulary-only consumers — auth, event_streams, outbound,
telegram_extension, triggers, event_projections — import from
`ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency
entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not
waived but obsolete: the edges no longer exist. The §11.2.2 ratchet
baseline moves 20 → 15.

No behavior change. `RunOriginAdapter`'s validation error becomes
`Result<_, String>` (matching every other bounded ref in
`host_api::turn`) with a byte-identical message pinned by a test, so
both production `e.to_string()` call sites are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(target-architecture): tick WS1.1 and close PLAN decision round #1

WS1.1's box is ticked with what the change actually landed, including the
three lead-sheet corrections it turned up: the row named `TurnStatus` but
not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely
needed), `GateKind`/`BlockedReason` could not be left behind without
duplicating the single `GateKind -> TurnStatus` match table, and deleting
`ids.rs` forced retiring its `GateRef` alias rather than relocating it.

Two decisions confirmed outside the doc and never recorded:

- Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the
  owner, recorded retroactively; it was made in practice at program start.
- The `tools/` row's `default-members` trim — resolved as no trim.

Also surfaces nearai#6963 on the WS0 blocking-prerequisite row's first line
(it was already cited mid-paragraph) and records the §11.2.2 exception
ratchet moving 20 -> 15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): repoint touched imports to host_api and pin the TurnGateRef contract

CodeRabbit review round on nearai#6967.

Import repoints (accepted): every `use` line this PR already rewrote now
names `ironclaw_host_api::turn` directly instead of routing through
`ironclaw_turns`' prelude — 57 files across extension_host, product,
composition, runner, loop_host, conversations, the integration harness,
and the stress tool, plus three inside `ironclaw_turns` itself so the
crate stops consuming its own facade. Import lines this PR did not touch
are left for their consumer's own repoint slot.

TurnGateRef contract pinned (refutation): two review comments claimed
`TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes
and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail
construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty,
<= 256 bytes, no control characters); `LoopGateRef` is the prefix-
validated family via `loop_ref!(.., "gate:")`. The misreading traces to
this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing
string"), which stated a minting convention as if it were validation.
That wording is corrected and the distinction is now pinned by a test.

Also: drop a stale cross-file line reference in a product test comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(target-architecture): remove the row-97 self-contradiction

The `tools/` row resolved the `default-members` trim as "no trim" but
kept a trailing "The `tools/`/`default-members` half is still open."
from before that decision, so the row asserted both states. Drop the
stale sentence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(coverage): complete the WS1.1 changed-coverage exemptions

Finishes the remediation deferred last round, now that the settled run
(job 91246493989) provides authoritative line numbers. Manifest-only —
no .rs file changes, so the changed-line set the gate computes is
unchanged and these numbers stay valid for the next run.

Derived, not transcribed: the gate was replayed locally against its own
merged lcov from that run, reproducing CI's failure byte-identically
first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after
each entry. Final local result: 100.00% (138/138), branch 100% (4/4),
exit 0. All 45 gate self-tests pass.

Two classes, both verified rather than asserted:

- 13 files x 20 lines - declaration lines (fn params, return types,
  struct fields) whose only edit is the type NAME: GateRef ->
  TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X.
  Declarations are not executable, so these files contribute a zero
  denominator and trip the fail-closed empty_denominator branch.

- 7 lines x 3 files - executable, instrumented, and genuinely not
  exercised by the integration tier. Each checked against the base
  merged lcov (main @ 67088a4, the PR's own base sha): identical 0
  hits before and after, so no coverage was lost. approval_prompt_
  context_view is uncovered across its whole signature at base
  (lines 505-511); the background spawn-mode arm and the invalid-gate-
  ref error path likewise.

This includes the two entries I refused to guess last round -
turn_events.rs (three identical candidate lines by text; the settled
run disambiguates it as 510) and await_edge/store.rs (no verbatim twin
after the repoint; authoritatively 268-272).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): extract ironclaw_loop_contracts and flip agent_loop (WS1.2)

Carve the loop tier's neutral contracts out of the turn kernel into a new
contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): pin the loop-contract boundary and register the new crate

Enforcement, CI registration, and guidance for the WS1.2 extraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(reborn): split the loop-exit contract's ownership claim across the two crates

The claim types moved to ironclaw_loop_contracts with WS1.2; the validator
policy and the trusted applier stayed in the turn kernel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(contracts): repoint the three intra-doc links the crate split broke

The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate
links to them no longer resolve; the TurnRunId link target became redundant
when the import repoint fully qualified it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tests): repoint the loop-exit evidence imports the port rule moved

Removing the ironclaw_runner re-export (required by the new port-location
scan) left two workspace-root test-support files importing the turn kernel's
evidence types through it. They now import from ironclaw_turns::loop_exit
directly, which is the single sanctioned path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(collapse): reconcile the lock pin and the moved failure-category scan

Two artifacts of collapsing onto main:

- Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts
  entry while main's dependency bump moved the workspace to 2.0.19. The
  auto-merge kept the stale pin because the bump predates the crate, so
  --locked builds failed.
- ironclaw_product's failure-summary test reaches into another crate's
  source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2
  moved that impl to ironclaw_loop_contracts, so the include still resolved
  and matched nothing. Repointed to follow the code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): extract ironclaw_extension_contracts and close the dual import paths (WS1.3)

Carve the extension tier's neutral contracts out of the host API into a new
contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the
boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): repoint the exact-test selector WS1.2 moved

scripts/reborn-e2e-rust.sh pins exact test names for the deterministic
gate. The capability-failure rehydration test moved from
ironclaw_turns::run_profile::host::capability to
ironclaw_loop_contracts::host::capability, so its selector matched zero
tests and the gate failed closed.

Swept all 10 pinned selectors in that script (4 lib + 6 integration
target); this was the only stale one. Each now resolves to exactly one
test, verified by running the selector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(collapse): reconcile the new crate's lock pins with main's dep bumps

The extension_contracts lock entry was generated before the parent branch
collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions
that no longer have [[package]] blocks. --locked lanes would have failed to
resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(arch): register the new contracts crate in the two gates that enumerate deps

The composition pub-use snapshot still carried product's PreferenceTargetCodec
re-export, and the CLI's exact-dependency allowlist did not know the extension
tier's contracts crate. Both are enumerating gates, so both failed loudly rather
than passing vacuously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(contracts): record why the extension tier's traits are not sealed

The visibility kit's sealed-strategy template exists to close a strategy set;
every trait here exists to be implemented outside the crate. State that, so the
absence reads as a decision rather than an omission.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(coverage): drop the exemption WS1.2 made stale

The changed-coverage manifest carried a WS1.1 exemption for
crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2
moved that file into ironclaw_loop_contracts, so the gate's fail-closed
path validator rejected the manifest before reaching its line-level
verdict.

Deleted rather than repointed: WS1.1 merged, so those lines are baseline
on main, and this PR's diff pairs the file as a 99%-similarity rename
whose only changed lines are imports. Repointing would re-exempt lines the
gate no longer flags. All 19 remaining entries verified to resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(coverage): merge main and derive the WS1.2 changed-coverage exemptions

Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py,
added on main after the last merge-down; the WebUI-smoke and E2E roll-up
reds were purely the missing file.

Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py
against this PR's own merged lcov artifact until it exits 0 (100% line
244/244, 100% branch 8/8) - never estimated. Three classes:

- type-path repoints on declaration/expression fragments;
- verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution:
  those files are instrumented in this lcov and partially hit (loop_exit
  195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is
  measured. The same bodies were equally unexercised by the integration tier
  before the move, when they sat in ironclaw_turns and simply were not
  changed lines;
- one crate-root inner attribute the uninstrumentable-line classifier does
  not recognise on a declaration-only facade.

Also adds the nearai#6524 declaration-only facade entry for the new crate's
lib.rs to the informational per-crate coverage summary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): extract ironclaw_product_contracts and land the WS1.3 adapter half (WS1.4)

Carve the product tier's neutral contracts out of `ironclaw_host_api` into
`crates/ironclaw_product_contracts`, and — in the same change, because it is
what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress`
into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them.

`host_api::product_adapter` is one connected component: `channel_adapter`
names `inbound::ProductTriggerReason`, `inbound` names both
`channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`,
`projection` names inbound and outbound, `interaction_commands` names inbound,
and `product_surface` names all of them. Since `ironclaw_host_api` may hold no
internal dependency, the adapters could only leave once nothing that stays
behind names them — so the product DTO modules moved with them.

`git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move.

Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2
real gates + 5 self-tests with positive and negative fixtures) pins one home
and one import path for the product tier's ports; it fails on the four
re-export chains this change deletes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(checklist): record the hosted-MCP merge-down resolution on the WS1.4 row

The WS1.4 ruling on package_lifecycle survived the parent's nearai#6930
reconciliation; the LifecyclePackageId split is the recorded resolution,
now stated as what happened rather than what was recommended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(contracts): consolidate sealed evidence minting behind witness grants (WS1.5)

CHECKLIST WS1's evidence-mint row: one sealed seam owns construction of
protocol-auth evidence, every other construction path is deleted, and the
refute-tests land with it.

The `host-auth-mint` cargo feature was not a seal. Cargo unifies features
across the packages selected in one invocation, so `ironclaw_webui`'s opt-in
(-> turns -> host_api) compiled `ironclaw_host_api` once with the gate ON for
every other crate in the same build. Measured before touching anything: a probe
in `ironclaw_agent_loop` — whose manifest names `ironclaw_host_api` with no
features — minted a verified bearer claim; it failed to compile alone and
passed as soon as `ironclaw_webui` joined the `cargo test`. Every workspace
build is the second case.

Replaced with the repo's existing witness-token idiom (`host_api::authorized`),
which no other crate's manifest can switch on:

- `HostAuthenticationGrant` <- `HostProtocolAuthenticator`, sole implementor
  `ironclaw_webui` (module-private `AuthLayerState`, trust stage T1).
- `VerifiedInboundGrant` <- `ChannelIngressVerifier`, sole implementor
  `ironclaw_extension_host` (`VerifiedEvidenceMint`, trust stage T2).
- Channel/webhook mint family -> `ironclaw_extension_contracts::verified_inbound`
  (§6.1.2); bearer/session family stays in `host_api` (§6.1.1). The evidence
  type does not move; `extension_contracts` reaches the private verified variant
  through the grant-gated `ProtocolAuthEvidence::seal_verified_inbound`.

Closed: the feature in 5 manifests + 1 CI recipe; four re-export chains
(`host_api::product_adapter`, `ironclaw_product` root, `ironclaw_product::auth`,
and composition's zero-consumer re-export).

Enforcement: `reborn_sealed_evidence_mint_ratchet` (10 tests, §11.2.5) plus
`host_api/tests/protocol_auth_evidence_seal.rs` (5) and
`extension_contracts/tests/verified_inbound_seal.rs` (4). +19 tests, 0 removed,
no surviving assertion edited. The production-struct dead-code baseline shrinks
81/282 -> 80/277: the five `dead_code` suppressions in `auth.rs` existed only
because the constructors were feature-gated.

* test(contracts): cover the extracted auth-prompt and lifecycle-id surface

The changed-coverage gate on nearai#6980 flagged 134 uncovered lines and 22
uncovered branch arms, all in the two modules WS1.4 created. That was a real
regression, not an attribution artifact: the code moved out of
host_api::product_adapter::outbound and host_api::package_lifecycle, and the
owning crates' unit suites did not move with it.

Fourteen tests close every one of those lines: render_channel_auth_prompt in
both the DM and mention shapes and with/without a pairing deep link, the
AuthPromptContextView constructors and wire round-trip, each nested validator
arm rejecting independently, and the bounded-id accessor and rejection surface.
Verified by replaying reborn_changed_coverage.py against the PR's own merged
lcov (byte-identical to CI) and re-measuring with cargo llvm-cov at this head:
134 -> 0.

Three sites remain exempted with per-site evidence, all unreachable by test:
a declaration-only crate facade's inner attribute, and two pre-existing error
arms whose only change is a repointed type path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(contracts): cover the newline/tab carve-out in bounded prompt text

The changed-coverage gate reached 100% line but left three branch arms on
validate_bounded_text's control-character predicate. Newline and tab are
deliberately legal in prompt copy — channels render multi-line instructions —
and every other control character is a rejection; both halves are now driven.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(coverage): exempt the four type-position lines the gate cannot instrument

With the auth-prompt and lifecycle-id holes tested, the gate reached 100%
line and 100% branch but still failed on four files 'contributing no
instrumented lines'. Each is a single type-position line — an enum variant
field, two parameter types, a struct field — whose only change is the
repointed path for a moved contract, wrapped onto its own line by rustfmt.
LLVM emits no coverage region for a type annotation, so no test can reach them.

Replayed against this PR's own merged lcov: exit 0, 285/285 lines, 38/38
branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
l3ocifer pushed a commit to l3ocifer/frick-ironclaw that referenced this pull request Sep 3, 2026
…n record with shipped reality (nearai#6995)

* docs(target-architecture): Wave 1 truth audit — reconcile the decision record with shipped reality

Wave 1 merged as seven PRs (nearai#6967, nearai#6975, nearai#6977, nearai#6979, nearai#6980, nearai#6981,
nearai#6982). This audits the north-star docs against merged `main` at
`a50ad0638` and closes every gap where the decision record no longer
matches what shipped.

Docs-only: five `.md` files under `docs/reborn/target-architecture/`.
House style throughout — dated ✎ amendments, prior text quoted where a
clause is corrected, no silent rewrites (`git diff --word-diff` removes
nothing but the words each amendment quotes back).

Highlights:
- §8.3's exception-dissolution proof corrected: `conversations → turns`
  is turn admission authority, not vocabulary; the wave's "20 → 12"
  milestone was wrong by construction and the true end-state is 13.
- §6.1.1–§6.1.4 gain as-built module inventories; the nearai#6930 amendment
  placing `hosted_mcp` in `host_api` is superseded by nearai#6977's relocation.
- §12.1a records the evidence-mint finding: the `host-auth-mint` feature
  seal was vacuous, replaced by witness grants — plus two residuals, one
  from the slice and one this audit verified against the ratchet source.
- The coverage-governance gap (~14k lines now ungated by the floor
  file's opt-in design) moves from a sign-off comment onto the ratchet row.
- Duplicate-type findings, the `llm_costs`/`ModelCostTable` deferral, the
  surviving `product → loop_host` sites, and issues nearai#6945/nearai#6978 all gain
  owning rows.

Verification: docs-only diff; `cargo test -p ironclaw_architecture` green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(target-architecture): tighten the Wave 1 audit where review found it overstated

Six review findings triaged against the built tree; four were real.

- `families/contracts.md` landing marker claimed "everything else in
  this file was built as written". Three `ironclaw_loop_contracts`
  divergences contradict it and are now named at the marker and marked
  at the entry: the manifest holds `ironclaw_extension_contracts` and
  holds neither `ironclaw_common` nor `ironclaw_prompt_envelope`; the
  `tokio` carve-out; the embedded prompt asset.
- The evidence-mint guarantee said "compile-time impossibility" and
  "enforced by constructor visibility plus a workspace string-scan pin"
  in one breath. Split: the compiler enforces no-mint-without-a-grant
  (so nothing outside a workspace crate can mint at all); an
  architecture test — a line-oriented substring scan with two named
  evasions — decides which workspace crate may hold one.
- That deferral had no home. §12.1a said "hardening the scan is WS10
  work, listed there"; it was not listed. Added to the WS10 guardrail
  row with both evasions and the call-site census that backstops them.
- CHECKLIST WS8's `common/src/event.rs` row still closed with "stays in
  `common` as a deletion candidate" under an "executed by nearai#6982"
  header. The file is deleted; the tail now says so.

Plus two clarity fixes where a reader could reach a wrong number: the
`(66)`/`(64)` figures in §9/§13 are that table's 2026-07-30 measurement
and now say so beside the live 67, and the row-1 edge count now states
that six of row 1's seven fell while the register moved by seven,
because `auth → turns` is row 9.

Dated amendments only; every replaced phrase is quoted in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-6980 — b74e0936 Deployed Aug 1, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: dependencies Dependency updates scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant