refactor(contracts): extract ironclaw_product_contracts and land the adapter half (WS1.4) - #6980
Conversation
…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>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR adds ChangesContract extraction and migration
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
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. Comment |
|
🚅 Deployed to the ironclaw-pr-6980 environment in ironclaw-ci-preview
|
…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>
|
Coordinator sign-off (Wave 1 slot 4 — WS1.4
Ready to merge. Stack note: #6981 (evidence-mint) syncs this branch down next; its collapse follows this merge. |
…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>
…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>
…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>
…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>
…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>
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_apiintocrates/ironclaw_product_contracts, and land the WS1.3 adapter half it exists to unblock —ChannelAdapter,ToolAdapter, andRestrictedEgressnow live inironclaw_extension_contractswhere §6.1.2 assigns them. Flat crate directory today; thecontracts/family directory arrives in Wave 5.git mvmoved 15 modules at 83–100% similarity, so the diff reads as a move, not a rewrite:ironclaw_product_contractssurface(fromhost_api::product_surface)ProductSurface,BoundProductSurface,ProductSurfaceCaller, the invoke/query/stream DTOs,ChannelInboundProductSurface, theProductSurfaceErrorfamilyinboundoutboundProjectionCursorprojectionProjectionStream+ the read/subscribe request DTOsinteraction_commandsoperator_llmpackage_lifecycle(fromextension_contracts)Lifecycle*,ChannelConnectStrategy,ChannelConfigFieldironclaw_extension_contractschannel_adapterChannelAdapter+ its whole DTO family, incl.ProductTriggerReasontool_adapterToolAdapter+RestrictedEgressegressProtocolHttpEgress, theEgress*types,DeliveryStatus/OutboundDeliverySinkexternalExternalActorRef/ExternalConversationRef/ExternalEventId/ProductAttachmentDescriptor/Kindauth_promptrender_channel_auth_prompttest_supportThe adapter-move mechanics
PR #6977's headline finding was that a type cannot leave
host_apiwhile a type staying inhost_apinames it, becausehost_api's allowlist is "noironclaw_*". The corollary nobody had measured is thathost_api::product_adapteris one connected component:channel_adapternamesinbound::ProductTriggerReason;inboundnameschannel_adapter::ChannelAttachmentRefandoutbound::ProjectionCursor;projectionnames inbound and outbound;interaction_commandsnames inbound;product_surfacenames all of them. So the adapters could only move onceinbound,outbound,projection,interaction_commands, andproduct_surfacemoved too. That co-movement, not theProductSurfacetrait 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):ProductTriggerReasonwent to the extension tier. It is stamped by the adapter on everyNormalizedInboundMessage; leaving it ininboundwould have inverted the dependency. It sits inchannel_adapterbeside the field that carries it.externalandegresswent 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 fiveexternaltypes — includingProductAttachmentDescriptor/Kind— are named byChannelAdapter's own cone, so there is no product half to keep back.identityandproduct_adapter_errorcould not leavehost_apiat all.host_api::user_identitynamesAdapterInstallationId;host_api::product_adapter::auth(which stays until WS1's sealed-evidence row moves it) namesProductAdapterError. Both contracts tiers reach them downward, which is the only placement serving both. §6.1.3's "fed by" namingproduct_adapter_erroris not achievable whileauthstays — 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 ownOutboundPart::AuthPromptcarriesBox<AuthPromptView>, and both shipped channel packages callrender_channel_auth_promptfromdeliver(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 nowextension_contracts::auth_prompt. The approval half (ApprovalPrompt*View), reached only by product and WebUI, stayed inproduct_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 fromhost_api" row already ownsrender_channel_auth_promptand is where they converge.2.
package_lifecyclecame back toproduct_contracts— the §6.1.3 ruling. WS1.3 placed it inextension_contractsas 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_lifecycleUI projections"; §6.1.2's Owns does not), and the code agrees:LifecycleProductAction/LifecycleProductResponseare the vocabulary ofLifecycleProductService, 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 onextension_contracts, so the four §6.1.2 types it is typed on stay reachable from below.3.
AppEventdid not come, because it is dead. §6.1.3 and the WS1.4 row both assign "the 43-variantAppEventwire enum (todaycommon::event)" here, on the premise that a transport streams it to a client. Re-measured on this base:crates/ironclaw_common/src/event.rsis 1,234 lines with zero consumers outsideironclaw_commonitself — the only other workspace reference isreborn_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 incommon, flagged on the "Narrowironclaw_common" row, which ownsevent.rsanyway. Inventory correction, not a scope cut.4. Four re-export chains gave one trait two import paths, and all four are deleted.
ChannelAdapterthroughironclaw_productand throughironclaw_reborn_composition(a re-export of a re-export);OutboundDeliverySink+ProtocolHttpEgressthroughironclaw_product;ProjectionStreamandChannelInboundProductSurfacethroughironclaw_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 layerrather than duplicate definitions, both recorded inCOLLISION_EXEMPTwith the colliding definition named:ProjectionCursor(ironclaw_event_projections/src/lib.rs:116is the structured internal position{ runtime: EventCursor, scope: ProjectionScope }with rebase semantics; ours is the opaqueserde(transparent)wire token it stands for) andProjectionSubscriptionRequest(ironclaw_outbound/src/types.rs:157is 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_adapterarrived fromhost_apiwhere it could not see them.ToolCall/ToolResultvsironclaw_llm::providerare not the same concept (the LLM pair isSerializewire vocabulary a model emitted; the extension pair is the in-process invocation envelope whose module doc says "nothing here serializes").ExternalActorRef/ExternalConversationRefvsironclaw_conversations::idsare the same concept declared twice — a real duplicate-surface finding, sinceconversations::idscarries a parallel copy of the adapter-identity vocabulary includingAdapterInstallationIdandExternalEventIdthat the walk cannot even see (bounded_string_id!expansions, the same blind spot slot 3 recorded forLifecyclePackageRef). 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 fromironclaw_host_api, and the CHECKLIST assigns them elsewhere by name: WS2's first row ("flipextension_host's implemented ports toproduct_contracts/extension_contractsdefinitions — delivery resolver, reply-context source, admission, pairing sources, account-status") and WS6'soperatorrow ("implementsproduct_contractsports; 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 externalsecrecyon the manifest);OperatorStatusService,OperatorLogsService,OperatorServiceLifecycleService+ their DTOs (carry or inlineoperator_surface_unavailable());ChannelConfigProductService;AccountConnectionStatusSource;ProductSurfaceCommandDescriptor,ProductCapabilityDescriptor,ProductView.Blocked, with the blocker (3):
ProductOutboundTargetResolver— needsValidatedReplyTargetBindingfromironclaw_outboundandProductSurfaceFailure(product/src/error.rs, whose cone pullsironclaw_turns::{TurnError, TurnErrorCategory}); both are forbidden deps, so it cannot move without type surgery.ProductCommandAdmissionService— needsProductActionId/ActionFingerprintKey(product::action, itself needingironclaw_turns) andProductCommand(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; andextension_host's impl additionally importsProductSurfaceFailure+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_EXCEPTIONSwas re-read against this branch's base. Not one is ahost_api,product, orproduct_contractsedge, 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 fivemcp/scriptslane exceptions still wait on the registry DTOs inironclaw_extensions(CHECKLIST WS3'smcprow);conversations → turnsis 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
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.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_productitself,ironclaw_operatorandironclaw_extension_host— the two crates whose ports it declares — andironclaw_webui/ironclaw_host_ingress, since a transport edge would mean HTTP reached contracts).untrusted_ingress_paths_cannot_submit_host_trusted_inbound's roots;reborn_service_method_freeze_ratchet'sHOST_PRODUCT_SURFACE_SOURCEconstant now points atproduct_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 fouroutbound.rsentries were repointed toauth_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).members,[package.metadata.ironclaw] layer = "contracts",classify-test-scope.sh's shared arm, andreborn-crate-test-buckets.sh'sproduct-workflowbucket (besideironclaw_product— the bucket groups by what a change can break). Verified, not assumed:discover-reborn-package-crates.shresolves it through the shipped-binary closure; both CI self-tests pass; the bash/python crate inventories agree at 66.CLAUDE.md(admission test, module table, the three rulings above, the deferred-by-design section), acrates/AGENTS.mdrow,ironclaw_extension_contracts/CLAUDE.mdrewritten for its four arrivals and one departure, and the livedocs/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_api335 → 243 (−92, gained 0).ironclaw_extension_contracts44 → 65 (+24 arrived, −3 departed).ironclaw_product_contractshas 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 threepackage_lifecycle::tests::*that came fromextension_contracts. No surviving test was edited for content — the moved files are 83–100% similar and every other test diff is an import line. Twohost_api_contract.rsintegration tests moved verbatim with the types they pin, intoproduct_contracts/tests/product_contract.rs.Verification
Lean gauntlet, all local (see the CI note below):
cargo fmt --all -- --checkclean.cargo clippy --all-targets --all-features -- -D warningsover all 15 touched crates: zero warnings. This is where a real masking bug surfaced —product_contracts' tests useProtocolAuthEvidence::test_verified, which is behindhost_api'stest-supportseam; it only compiled while host_api was in the same multi--pinvocation (feature unification). Fixed with an explicit dev-dependency, not by widening a feature.cargo test -p <crate> --all-features: green. One observed flake, not a defect:ironclaw'ssmoke::serve_mounts_cli_login_route_without_ssofailed once under parallel load and passed in isolation plus two consecutive full re-runs (145/145 each).ironclaw_architecturesuite 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.snapshotline 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-featuresclean, run last — workspace-roottests/targets are invisible to-planes, and 3 of them needed repointing.cargo metadata --lockedresolves;git statusre-checked after every cargo invocation.scripts/reborn-e2e-rust.shexecuted, 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 becausecargowas consuming the loop's stdin — re-run underbashwith</dev/null.)include_str!/include_bytes!reaches into any file this PR moved code out of;scripts/ci/check-include-str-paths.shpasses (119 references).scripts/ci/critical_mutation_gate.py --selection-onlyagainst this diff printsfalse— 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 fourpull_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 head12fcc339a.Judge that run by its per-job tally, not the roll-up. Per #6978,
workflow_dispatchruns ofreborn-tests.ymlalways fail theTests (Reborn)roll-up structurally: the critical-mutation gate ispull_request-gated, so it skips under dispatch, and the roll-up disallows skipped for that job. The--selection-onlyresult 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.tomlhas noironclaw_host_apientry, butironclaw_product's numerator is untouched by this PR (no product source moved out of it) — only its import lines changed.