Files
ironclaw/README.md
Benjamin Kurrek 80af97390f WS7 (1/2): family directory moves — text-only (#7206)
* refactor(contracts): move extension runtime descriptors to a neutral contract (WS3)

Deletes the two `-> ironclaw_extensions` layer-matrix exceptions
(`ironclaw_mcp`, `ironclaw_scripts`) by giving the runtimes-layer lanes a
contracts home for the descriptors they read, instead of the registry crate
they may not depend on. Exceptions 13 -> 11; baseline lowered in the same
change.

Moved to `ironclaw_extension_contracts`:
- `runtime::{ExtensionRuntime, ExtensionAssetPath, ExtensionAssetPathError}`
- `hosted_mcp::{HostedMcpDiscoveredTool, HostedMcpDiscoveredToolAnnotations}`

`ExtensionPackage`/`ExtensionManifest` deliberately stay in
`ironclaw_extensions`: they carry the whole parsed manifest tree and a
`PackageRootBinding` typed on `ironclaw_filesystem::VirtualPath`, which the
§11.2.3 contracts-purity allowlist (`{ironclaw_host_api}` only) forbids the
contracts crate from naming. Measured instead: both lanes read exactly three
things off the package — `id`, `capabilities`, `manifest.runtime` — so the
lane request structs now take those three and the caller (which owns the
package) projects them.

Also repointed `ResourceReceipt` to its real owner: `ironclaw_resources`
only re-exports `ironclaw_host_api::resource::ResourceReceipt`, so the lanes'
import was a §11.2.4 two-import-paths hop, not a dependency.

No `pub use` shims (§11.3): every consumer is repointed in this change, and
`resolve_under` becomes the free function `ironclaw_extensions::resolve_asset_under`
because the orphan rule forbids an inherent impl on the moved type.

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

* refactor(sandbox): merge the sandbox lane into one crate (WS3)

Creates `ironclaw_sandbox` (runtimes) from the three halves of "run an
already-authorized command away from the host", and deletes the two crates
PROPOSAL §6.6.4 marks for merge:

- `ironclaw_process_sandbox` (plan contract)      -> `src/plan.rs`, `src/validation.rs`
- `ironclaw_host_runtime::sandbox_process`        -> `src/sandbox_process/**`
- `ironclaw_scripts` (script lane + Docker path)  -> `src/script.rs`

The kernel sheds the Docker/CA cone: `bollard`, `rcgen`, `x509-parser` and
`time` are gone from `ironclaw_host_runtime`'s manifest, and `bollard`/`rcgen`
are now declared by exactly one crate in the workspace.

Two migration details PROPOSAL §6.6.4 and CHECKLIST WS10 call load-bearing:
- `PROCESS_SANDBOX_CAPABILITY_ID` -> `ironclaw_host_api::capability`, so
  `ironclaw_loop_host` drops its lane dependency (production dep gone; a
  dev-dep remains for the tests that build plans).
- `SandboxCommandTransport` -> `ironclaw_host_api::process`, with the shapes
  it names (`CommandExecutionRequest`/`Output`, `RuntimeProcessError`,
  `SavedCommandOutput`, `SavedCommandOutputSanitization`). Without this the
  runtimes-layer lane could not implement what the kernel consumes.

Enumerating gates were repointed, never relaxed: the specificity carve-outs and
the struct/test-support ratchet entries moved with their files (both baselines
unchanged at 129 and their prior values), the panic-gate baseline row moved,
`reborn-crate-test-buckets.sh` registers the new crate, and the three
`reborn-e2e-rust.sh` script selectors follow the tests (plus `docker_security`,
which had no selector before).

One gate would have gone silently vacuous and was fixed rather than moved: the
script-lane surface scan in `reborn_dependency_boundaries.rs` read a hardcoded
`src/lib.rs`, which after the merge no longer holds the lane. It now scans the
whole crate source tree with a fatal-read walk and a non-vacuity assertion.

One deletion, recorded: `RebornScopedSandboxCommandTransport::into_process_port`
returned a kernel type a runtimes crate may not name. It had zero callers
workspace-wide; the kernel wraps the transport, which is the direction the port
inversion requires.

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

* docs(target-architecture): record the WS3 corrections with their evidence

Three dated amendments, each quoting the text it replaces:

1. CHECKLIST WS3 sandbox row + PROPOSAL §6.6.4 — "all pieces currently
   unwired/test-only" is REFUTED. Three production paths cross the merged
   crate (spawn-path plan validation, the process_executor routing check, and
   the saved-command-output scope digest). The accurate claim is narrower:
   no production *execution backend*. Behavior preservation is therefore
   argued at the diff (11 of 26 moved files byte-identical, 9 more differing
   by one import line, +63/-36 overall), not inferred from deadness.

2. CHECKLIST WS3 mcp row + PROPOSAL §6.6.3 — the prior wave's "structurally
   blocked" finding is half right, and the wrong half is load-bearing: only
   `ExtensionPackage` is un-absorbable, and no lane ever needed it (both read
   `id`, `capabilities`, `manifest.runtime` and nothing else). The registry
   half of the flip is done; the `resources` half is refuted as phrased —
   the estimate/usage vocabulary the row asks about is already in
   `host_api::resource` and already imported from there, while the real
   blocker is the `ResourceGovernor` authority port and `ResourceError`'s
   denial cone.

3. Recorded as a structural finding, not a note: the sandbox row and the mcp
   row are ONE problem. `ironclaw_scripts` imports the identical DTO set, so
   the merge alone deletes zero exceptions and only the mcp carve-out lets
   either lane shed the registry edge.

Also reconciled: PROPOSAL §6.1.2's as-built inventory gains the two modules
WS3 landed (and states why `ExtensionPackage` stayed); §2's package count
66 -> 65; the §9 disposition rows for `ironclaw_scripts`/`ironclaw_process_sandbox`/
`ironclaw_mcp`; the §11.2.2 ratchet rows (13 -> 11); the WS3 verify row; the
stale WS1.3 sentence asserting the blocker as settled fact; and
`reborn_restructure_baselines.rs`'s doc table, which still read 15.

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

* chore(sandbox): drop imports the merge left unused

`process_port.rs` no longer names `MountView` or `thiserror::Error` (both went
to `host_api::process` with the types that used them), and `sandbox_process.rs`
no longer needs `sync::Arc` after `into_process_port` was deleted. Found by
per-crate `clippy --all-targets --all-features -D warnings`.

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

* fix(ci): let the Reborn PR planner plan guidance edits and crate deletions

Three fail-closed gaps in `reborn_pr_test_plan.py`, all hit by this PR and all
live on `main` today — any PR with the same change shape is unplannable.

1. `.claude/**` was unclassified, so the planner refused outright. It is agent
   guidance in exactly the sense `docs/**` is human guidance: no Rust test
   reads either as data (the only in-tree references are prose citations in
   test doc comments). Added to `IGNORED_PREFIXES`. Without this, "guidance
   travels with the change" — the restructure's own discipline — cannot be
   satisfied in a single PR.

2. `crates/AGENTS.md`, `crates/README.md`, `crates/Architecture.md` raised
   "unmapped crate path": they sit under `crates/` but belong to no package.
   Now classified as crate-tree prose, matched by "Markdown no package
   directory owns" so a genuinely unmapped crate path is unaffected.

3. An unmapped crate path used to raise. `git diff` reports a deleted crate's
   old paths and CI feeds the planner that diff, so **every crate deletion or
   rename was unplannable** — including the six deletions PROPOSAL §2 plans.
   It now widens to the exhaustive plan. This is a semantic change and it is
   the safe direction: the full plan is a superset of any narrowing, so an
   unattributable path can never cause under-selection, whereas refusing to
   plan blocks the PR instead of protecting it. Malformed input is still
   rejected by the unclassified-path branch.

Each lands with fixtures per WS10's rule, positive and negative: guidance
paths select nothing while non-guidance paths still fail closed; crate-tree
prose selects nothing while crate *code* under the same unmapped directory
widens to `full` (so the Markdown carve-out cannot swallow code). The
pre-existing `test_unmapped_crate_path_fails_fast` is renamed and rewritten to
pin the new contract rather than deleted.

Verified against this PR's real 130-path diff: the planner returns `mode:
full`, and the workflow's own exhaustiveness guard passes on that output.

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

* fix(arch): give the retained resource exceptions an owning issue, not a wave

Review (#7065) caught that both surviving `-> ironclaw_resources` exceptions
declared `removes_in = "WS3"` — the wave this PR *is*, which does not remove
them. That is precisely the defect §11.2.2 already records against
`conversations -> turns` ("`removes_in = "WS5"` and WS5 has partly shipped
without it falling"), and it would have been repeated here.

Both now point at issue #7067, which owns the design work that actually clears
them: replacing the `ResourceGovernor` dependency with a narrow
reserve/reconcile/release port. The issue carries the measurements — 3 of 10
methods used, zero implementors, and the `ResourceError` denial cone — plus the
two open questions (error shape, port home) that make it a design slice rather
than a move.

An owning issue is also what §11.2.2 asks for and what the ratchet still cannot
enforce (there is no `owning_issue` field yet), so this is the strongest form
currently expressible.

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

* test(contracts): pin the asset-path validator that moved into extension_contracts

`validate_asset_path` moved here with `ExtensionAssetPath`, the type it
constructs. In `ironclaw_extensions` it was only ever reached indirectly
through manifest parsing, so its six rejection branches had no direct test —
and a contracts crate that carries validation owes that validation one.

Two tests: every reject branch with its exact reason and `Display` output
(empty, NUL/control, URL, absolute, Windows drive and backslash, and the
empty/`.`/`..` segment cases) plus the manifest-relative shapes that must keep
being accepted; and `ExtensionRuntime::kind()` over all five variants, since
that projection is what every lane uses to reject a runtime it does not serve.

Also removes a changed-line coverage risk this PR would otherwise carry into
the merge queue: the gate does not run on ordinary PRs (#7036), so ~100
newly-added lines of validator would first be measured where a failure is
expensive to diagnose.

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

* test(coverage): re-capture the host_runtime floor and floor the new sandbox lane

`RATCHET FAIL: ironclaw_host_runtime` — observed 18854 covered vs a
`floor_covered_lines` of 20538. This is the shrinkage case the ratchet's own
"To fix" text describes, not a coverage regression: `sandbox_process/**` moved
to `ironclaw_sandbox`, so the crate's denominator fell 23277 -> 21267 (-2010
instrumented lines) and its covered lines fell with it.

The percentage floor is **raised, not lowered**: observed 88.65% against an old
floor of 88.23%, so the entry now reads 88.65. Only the absolute line count
moves down, and it must — those lines are no longer in this crate.

To keep that from being a net loss of protection, `ironclaw_sandbox` is floored
on arrival at its observed 87.09% (3185 / 3657). This is a net *increase* in
ratchet coverage: neither `ironclaw_scripts` nor `ironclaw_process_sandbox` was
ever floored, and the `sandbox_process` half was protected only as part of
host_runtime's line count, which this PR necessarily reduces. Floored crates
16 -> 17.

Verified by replaying the ratchet arithmetic against CI's observed numbers:
both crates pass on percentage and on covered lines. Numbers taken from the
failing run's own report (job 91740733521), which is the authority for this
gate.

The `Tests (Reborn)` roll-up failed solely on this sub-job
("coverage-report result 'failure' did not match planned=true"); no other lane
failed — 50 pass, 2 fail, both this root cause and its roll-up.

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

* docs(target-architecture): record the coverage ratchet as a move-sensitive gate

WS3 hit a gate no move row had named. `tests/integration/coverage-floor.toml`
is keyed on crate identity plus absolute covered-line counts, so it is
invisible to WS10's path-keyed gate audit and yet it fails on every crate move,
merge, rename, or family `git mv` that shifts instrumented lines between
crates — as it did here, while the percentage floor was *improving*.

Recorded on WS10 with the three rules WS7 will need: re-capture in the same PR,
raise the percentage floor rather than leaving it, and floor the destination
crate or the move silently drops that code out of the ratchet.

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

* fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath

A semantic conflict the merge could not see: #6780 landed
`ironhub/{package,catalog}.rs` importing `ExtensionAssetPath` from
`ironclaw_extensions`, while this branch moved that type to
`ironclaw_extension_contracts::runtime`. Different files, so git auto-merged
cleanly and the breakage surfaced only at `cargo check`.

Repointed both sites to the contracts crate (no shim, per §11.3). The manifest
already named `ironclaw_extension_contracts`, so this is imports only.

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

* test(coverage): exempt the WS3 move's no-region lines and record the gate

The changed-lines coverage gate went red on four files while changed-line
coverage was 95.35% against a 90% floor: the failure was its two fail-closed
STRUCTURAL assertions, not any percentage.

Every line below was derived by replaying scripts/ci/reborn_changed_coverage.py
against this PR's own merged lcov (run 30831658659) with the base lcov the gate
itself resolved (run 30828540055 @ b89fcd3575), until the replay reproduced the
CI verdict byte-identically. Line numbers come from the gate's own
`candidate_lines - mechanically_uninstrumentable_lines()`, not from the log.

- host_api/src/process.rs (31 lines): new placement-neutral process vocabulary
  with no function body anywhere in the file; rustc emits no LCOV record for it
  at all. Same shape already exempted for product_contracts/loop_contracts.
- extension_contracts/src/hosted_mcp.rs (12): field declarations of the two new
  tools/list descriptor structs. The file is plainly instrumented (191 DA, 164
  hit), so this is a no-region artifact, not an instrumentation gap.
- host_runtime/src/services/runtime_adapters.rs (13): continuation lines of
  three rewritten calls, all PROVEN EXECUTING by their region-start heads
  (lines 380/434/977 score 24/16/63 hits). The four genuinely-uncovered lines
  in the same rewrite are deliberately NOT exempted -- the gate already
  subtracts them as pre-existing debt inherited from base.
- composition capability_host_tests/approval_gates.rs (6): type positions in a
  test double whose body region scores 1 hit.

The last one is a finding, not just a waiver: that file is 100% test code
behind `#[cfg(test)] mod capability_host_tests;`, but the gate's
test_only_path() recognises /tests/, /test_support/, */tests.rs and *_tests.rs
and NOT a cfg(test) module DIRECTORY, so it measures it as production. It is
the only such directory in crates/ today.

Docs (target-architecture, same PR per the docs-truth rule):
- CHECKLIST WS10 gains the changed-lines gate beside the ratchet row, cross-
  referencing the WS2.1 note rather than restating it: percentages are not what
  fail a move; derive lines by byte-identical replay (--fetch-base-coverage
  silently degrades without --github-repo); and a stranded exemption path is an
  ABORT with no verdict, not a loud failure.
- CHECKLIST WS10 exception-ratchet row: the constant was cited at :4063 and
  sits at :4164 -- corrected by removing the line pin, since the file is edited
  every wave. Records that the baseline is a UNION across parallel WS3 lanes.
- families/contracts.md: records extension_contracts' new ownership of the
  runtime descriptor vocabulary -- the carve-out that let BOTH lanes drop the
  registry edge -- and the orphan-rule seam that keeps resolve_asset_under in
  the registry crate.
- families/lanes.md: two "Never" claims were reading as satisfied when they are
  not. ironclaw_mcp's "never depends on the resource-governor crate directly"
  is refuted (the compiled edge survives; #7067 tracks the narrow port), and
  ironclaw_sandbox's "no direct process spawning outside the transport seam" is
  aspirational -- script.rs:454 still builds Command::new("docker").

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

* docs(sandbox,mcp): correct the wiring inventory and record the projection cost

Two review findings verified against the tree; three refuted with evidence in
the PR threads.

Valid — the sandbox wiring inventory was self-contradictory. `CLAUDE.md` said
"Two production call paths ... and both are plan validation" directly above a
list of THREE bullets, and `lib.rs` omitted the third entirely. The third is
real and is not validation: `host_runtime/src/process_output.rs:482` derives the
scoped saved-output directory through `RebornSandboxScopeKey::from_scope`. That
inventory is what tells a future agent which paths are live, so an undercount
invites deleting a production path as dead code. Both surfaces now say three and
no longer claim they are all plan validation (the `loop_host` capability-id
comparison never was either).

Valid, and recorded rather than redesigned — the registry carve-out cost a
type-level invariant. Replacing `package: &ExtensionPackage` with independent
`extension` / `capabilities` / `runtime` borrows is what deleted the
`mcp -> extensions` and `scripts -> extensions` exceptions, but it also means
the type no longer guarantees the three came from one package.
`execute_extension_json` re-checks the descriptor half
(`descriptor.provider == extension`); the runtime half cannot be re-derived,
because nothing in an `&ExtensionRuntime` names its owning extension. No caller
can trip it today -- there is exactly one production caller
(`runtime_adapters`) and it projects all three from one package in one
expression -- so this is a latent structural weakening, not a live defect.
Restoring the compile-time binding needs a sealed projection minted by the
package owner; a check inside the lane cannot express it, and re-taking the
registry edge would undo the carve-out. Both request types now carry the caller
obligation in their field docs.

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

* refactor(extensions): move the skill-install executor to extension_support (WS3)

WS3's first-party-tools row, family 1 of 6: skill management / URL install.

`skill_url_install.rs` and its `bundle`/`github`/`zip_bundle` submodules,
plus the install-input normalizer, move out of
`ironclaw_host_runtime::first_party_tools` into
`ironclaw_extension_support::skills::{url_install, resolve_install_input}`,
where the skill executor half already lived. Move-only: no behavior change,
no test edited for content.

`ironclaw_host_runtime -> ironclaw_skills` is deleted from
LAYER_MATRIX_EXCEPTIONS — the edge is gone, not waived (exceptions 13 -> 12,
WS0_LAYER_MATRIX_EXCEPTION_BASELINE drops with it). `ironclaw_skills` and
`zip` survive as dev-dependencies for host_runtime's own tests; dev edges are
outside the matrix by construction.

Two doc ambiguities are resolved in the same diff, as dated PROPOSAL
amendments quoting the text they replace:

- §6.8.4's "the builtin first-party tool handlers absorbed from
  host_runtime/first_party_tools" contradicted §8.2's "kernel: ✗ (ports only)"
  row and the enforced BoundaryRule. Resolution: the seam splits executor from
  adapter — the executor moves behind a neutral request/error pair, the
  FirstPartyCapabilityHandler / CapabilityManifest / registry wiring stay
  host-side. Same shape the groupware and web-access tools already ship.
- §8.2's "ports only" cell now says what it means: contracts-layer ports the
  kernel also consumes, not permission to name a kernel trait.

Two cost corrections recorded for the remaining families:
`host_runtime -> extension_support` is not divisible family-by-family (mod.rs
holds it via `extension_support::coding`), and
`host_runtime -> ironclaw_extensions` is not reachable by this row at all.

PATH_TERM_COLLISIONS shrinks by two: the installer's github carve-outs now sit
inside a scan-exempt crate.

Test accounting (un-masking discipline), unfiltered `--list` over both crates:
1398 -> 1398, with exactly two tests renamed by module path and none lost.

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

* docs(sandbox): record that the Docker fail-closed switch is wired to nothing

Review asked why the migrated docker_security test can pass with no daemon.
The skip is pre-existing (the file differs from its pre-merge original by one
import line); WS3 only enrolled it in the required Rust e2e lane, where it was
not run at all before.

The real defect the question surfaced is worse and also pre-existing: this
crate's tests/support/docker_gate.rs states that IRONCLAW_REQUIRE_DOCKER_TESTS=1
makes a missing daemon a hard failure and that "CI sets this" -- and nothing
sets it. Repo-wide the name occurs only in docker_gate.rs and
attribution_tests.rs, here and on main. So every real-Docker test in the crate
skips-and-passes everywhere, which is exactly the gap the gate's own comment
says let sandbox security bugs ship unnoticed. docker_security.rs additionally
open-codes its own check rather than using the gate, so it would stay fail-open
even once something did set the variable.

Recorded rather than fixed: setting the variable is a CI-behavior change that
would hard-fail any lane without a daemon or the ironclaw-worker image, which
is not verifiable from inside a move PR whose evidence claim is behavior
preservation. Filed as the #6945 guardrail-claim-vs-reality class with the
two-part fix stated.

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

* docs(host_runtime): record the executor/adapter seam in crate guidance

The crate's CLAUDE.md said "first-party runtime tools belong under
`first_party_tools/`" without saying that only the host half does. WS3 moves
each tool's executor into `ironclaw_extension_support`, which may not name this
crate, so the rule now names both halves and points at the skill-install family
as the worked example.

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

* refactor(host_runtime): keep the install-input error path log-free

The moved executor returns `SkillManagementCapabilityError`, and routing it
through `skill_management_error` would have added a `debug!` line to a path
that had none before the move. A move-only change must not add one, so the
install-input arm maps the kind directly and the `dispatch` arm keeps the
record it already had.

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

* ci(coverage): re-capture the host_runtime floor for the WS3 executor move

The ratchet does not run on `pull_request` (`reborn_pr_test_plan.py:21`; issue
#7036), so this PR's green checks were not evidence on this axis. A full-plan
`workflow_dispatch` run on this exact head reported:

  RATCHET FAIL: ironclaw_host_runtime
    observed: 88.59% (20485 / 23124 lines)
    floor:    88.23% ... floor_covered_lines: 20538 (effective floor 20518)

The percentage went UP while `floor_covered_lines` went DOWN — shedding
well-covered code lowers the absolute numerator, which is a separate assertion
from the percentage one. Re-captured to the observed numbers (floor raised
88.23 -> 88.59, not merely held). Verified locally against that run's own merged
lcov artifact: ENFORCING mode, 17 PASS / 0 FAIL, exit 0.

  run: https://github.com/nearai/ironclaw/actions/runs/30858257594
  head: e07b3b0299

The destination crate is deliberately not floored, because it cannot be: every
crate under `crates/extensions/` is invisible to the coverage tooling —
`reborn_coverage_lcov.py:19`'s CRATE_RE still requires a crate directory
directly under `crates/`, which #7037's colocation broke. Filed as #7083 with
the measurement; the global floor is left alone rather than re-captured onto
that hole.

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

* refactor(wasm): move wit/ inside its owning crate (Wave 3)

CHECKLIST WS4 + WS10 `wit/` rows. `wit/{tool,channel}.wit` moves from the
repo root to `crates/ironclaw_wasm/wit/` — the crate that owns the ABI —
per PROPOSAL §6.6.1. Behavior-free: same bytes, same generated bindings.

Wave-3 coordinates: the docs write the destination as
`crates/lanes/ironclaw_wasm/wit/`, but `crates/lanes/` does not exist until
WS7. Because the files now sit *inside* the crate, the WS7 family move
carries them with no further path edit anywhere — which is the whole point
of putting them there.

Ten wit-bindgen `path:` args repointed (the host plus nine guests: six under
`crates/extensions/packages/*/wasm-src/`, three under `test-tools/*/wasm-src/`
— the CHECKLIST row said six). All nine guests verified building against the
moved WIT on wasm32-wasip2.

The four `include_str!` readers of the ABI text do NOT get repointed
literals. Doing that would turn the two `ironclaw_host_runtime` sites from
repo-root reach-ins into *cross-crate* ones — §11.2.7's strict class, the
one WS2 turns into hard failures — taking the scan from 19 to 21 while
ticking a box that says "§11.2.7 scan passes". Instead the ABI text gets one
owner, `ironclaw_wasm::TOOL_WIT` (`src/config.rs`, beside `WIT_TOOL_VERSION`),
and all four sites read the const over cargo edges that already exist.
Measured with the scan: 133 -> 129 escaping sites, cross-crate 19 -> 19,
zero `wit/` entries remaining.

Path-keyed gates repointed: `scripts/check-version-bumps.sh` (both ABI
paths), `.githooks/pre-commit`, and `platform-and-compat.yml`'s
`has_direct_wasm_abi_risk` filter — where the bare `wit/` alternative is
*deleted* rather than rewritten, because the filter's existing
`crates/([^/]+/)*ironclaw_wasm/` alternative already matches both the
Wave-3 and the WS7 location. `scripts/ci/ws12_workflow_contracts.py`
anchored on that deleted string, so its anchor moves to
`build-wasm-extensions` and its in-scope probe now pins both locations.

`Dockerfile` loses two `COPY wit/ wit/` lines in the planner and builder
stages: both already run `COPY crates/ crates/`, so the files arrive with
the crate and the old line would COPY a path that no longer exists.

Docs: the WS4 row's `crates/lanes/wit/` destination was the only doc site
placing the directory beside the crate rather than inside it; corrected
there and in README's tree, with dated amendments in CHECKLIST, PROPOSAL
§6.6.1 and PLAN Wave 3 recording what the move found.

Test accounting (unfiltered `--list`, name-by-name, quiescent tree):
ironclaw_wasm 51 -> 51, ironclaw_host_runtime 1246 -> 1246,
ironclaw_architecture 198 -> 198. Zero diff, no test edited for content.

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

* build(wasm): rebuild first-party artifacts for the moved wit/ path

Forced by the previous commit, not incidental to it.
`scripts/ci/check-wasm-artifact-freshness.py` keys each package's committed
`wasm/<name>.wasm` to a digest of the `wasm-src/` tree that produced it, so
editing a guest's `wit_bindgen::generate!` `path:` — which the `wit/` move
requires in all six shipped guests — invalidates the recorded digest and
fails the gate.

The gate's own contract forbids the shortcut: "Re-record only after
`./scripts/build-wasm-extensions.sh --first-party` and committing the rebuilt
artifact — the digest asserts a claim about the artifact, and updating it
without rebuilding launders a stale one." So the artifacts are genuinely
rebuilt (`--first-party`, exit 0, 6 OK / 2 host-native SKIP), not re-recorded
in place.

Byte sizes move by more than the source change accounts for because these
builds are not reproducible by design — the guests pin no toolchain and
resolve their own `Cargo.lock` at build time, which is the documented reason
the gate hashes sources rather than artifact bytes.

Verified: `check-wasm-artifact-freshness.py` OK (6 packages), and
`cargo test -p ironclaw_extension_support` green (102/46/4) — that crate
`include_bytes!`s these artifacts, so it exercises the rebuilt components.

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

* docs(target-arch): record the WS7 artifact-rebuild cost of guest path edits

The `wit/` move had to rebuild six shipped WASM binaries because
`check-wasm-artifact-freshness.py` digests each guest's whole `wasm-src/`
tree. WS7 hits the same wall from the other direction: the six package
guests reach the ABI across two trees, so moving either `ironclaw_wasm` or
`extensions/packages` rewrites all six `path:` literals and forces the same
rebuild. Recorded on CHECKLIST WS10's `wit/` row (point 6), on the
loud-path-pattern row that owns the WS7 repoint (also corrected six -> nine
guests there), and on PLAN's Wave 5 block with the cheap mitigation: move
the two crates in one PR and pay it once.

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

* ci(planner): classify the path classes that blocked the wit/ move

`Detect Reborn test scope` exits 1 on any pull request whose diff holds a
path `reborn_pr_test_plan.py` has no rule for, which made this PR
unmergeable: it must edit `Dockerfile` (the moved directory's
`COPY wit/ wit/` no longer resolves) and `scripts/check-version-bumps.sh`
(the ABI gate would otherwise grep dead paths and silently stop
enforcing). 18 of its 46 paths were unclassified.

Same class as the `.claude/` gap #7064 fixed, and classified the same
way — one rule per class, recorded beside the constant:

  * `Dockerfile` / `.dockerignore` — `platform-and-compat.yml` keys
    `has_docker_risk` off exactly this pair and owns the image build.
  * `.githooks/**` — Code Style triggers on the tree and lints its
    contents (`test-ci-comm-locale-pin.sh`); no Reborn lane runs a hook.
  * `scripts/{build-wasm-extensions,check-version-bumps}.sh` —
    `platform-and-compat.yml`'s `has_direct_wasm_abi_risk` classifier
    both scopes and runs them.
  * markdown owned by no crate (`crates/AGENTS.md`,
    `test-tools/README.md`) — prose, like `docs/` and `.claude/`. A
    crate-resident doc still selects its own crate's lane.

The first-party extension package assets are deliberately NOT ignored.
`crates/extensions/packages/*/wasm/*.wasm` is a shipped artifact that
`ironclaw_extension_support` embeds with `include_bytes!`, and
`test-tools/*/manifest.toml` is `include_str!`d by
`ironclaw_extension_host`. Calling either prose would convert today's
loud failure into a silent under-schedule of a change to production
output — the WS10 failure mode. `EMBEDDED_ASSET_OWNERS` routes each tree
to the crate that compiles it instead, so this PR now additionally
schedules `ironclaw_extension_{support,host,manager}`: the crates that
consume the six rebuilt WASM artifacts.

Also fixes #7085 in a file this PR already touches. The WIT version
extractors used the GNU-only BRE `\+`, so on BSD sed (macOS) they matched
nothing, and because the `WIT_TOOL_VERSION` cross-check is guarded on a
non-empty version the hook printed "All version checks passed" having
compared nothing. `[[:space:]][[:space:]]*` is identical under GNU sed,
so the enforced Linux CI lane is unchanged; verified on BSD sed that both
`wit/tool.wit` (0.3.0) and `wit/channel.wit` (0.3.1) now extract.

Regression tests: every classified class gets a case in
`test_reborn_pr_test_plan.py`, including the paired assertion that the
embedded assets *select a lane* rather than merely being accepted (the
inverse of the `.claude/` prose test), and a staleness pin that fails if
an asset tree or its owning crate moves. All ten new cases fail against
the planner on `main`. `test_unclassified_build_input_fails_fast` moves
off `Dockerfile` onto a still-undecided input so the fail-closed arm
stays exercised.

Refs #7087, #7085

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

* refactor(host-runtime): split obligations into its three chartered owners (WS3)

`crates/ironclaw_host_runtime/src/obligations.rs` was 3,122 lines fusing the
three owners PROPOSAL §6.5.9 charters separately, held apart only by an
`// arch-exempt: large_file` waiver. It is now one module per owner:

- `obligations::handler` — which obligations apply and what each does
  before/after dispatch, plus the audit/redaction/ceiling/mount validation.
- `obligations::staged_handoffs` — material staged for a later consumer:
  the runtime-secret and network-policy stores and the credential-account
  resolver port.
- `obligations::process_store` — post-start handoff discard and reservation
  reconciliation.
- `obligations::mod` — only `BuiltinObligationServices`, the assembly seam,
  and deliberately the one place naming all three at once.

Every module is under the 1,500-line gate, so the waiver is deleted rather
than carried forward: re-fusing the owners now trips `pre-commit-safety.sh`.
`mod obligations;` stays private and the crate's `pub use obligations::{…}`
names are unchanged, so no consumer outside the crate sees this.

Behavior-free. Cross-owner access is `pub(super)` (three methods), not
`pub(crate)`. The split revealed one narrowing in the other direction:
`secret_present` was `pub(crate)` with no caller outside its own file and is
now private.

Also from the same CHECKLIST row, the bounded half of "shrink
`services/builder.rs` toward composition-facing factories": three builder
methods whose only callers are inside the crate's `src` narrow to
`pub(crate)`. The rest of that clause is measured and deferred in the
CHECKLIST amendment — 17 methods need a `test-support` cargo feature, three
are callerless and belong to WS8, and the remaining 33 are a redesign of the
fluent surface rather than a shrink of it. `+production_wiring` is refuted
there: it is readiness diagnostics, not assembly.

Two loud path-keyed gates fired and were repointed, not relaxed:
`reborn_host_runtime_services_do_not_expose_lower_substrate_handles` now
scans the whole `obligations/` directory and asserts it read ≥ 4 files
(`collect_runtime_rs` returns a count; both its callers now assert non-zero),
and `reborn_struct_test_support_ratchet`'s frozen per-file count moves to
`staged_handoffs.rs` with its count unchanged at 1.

Test accounting (un-masking discipline): `cargo test -p ironclaw_host_runtime
--all-targets -- --list` is 1,246 before and 1,246 after, name-by-name
identical — zero added, removed or renamed. `LAYER_MATRIX_EXCEPTIONS` is 10
before and after; an intra-crate split cannot move the register.

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

* refactor(operator,contracts): route operator secrets through a product_contracts port (WS3)

`ironclaw_operator` is a products-tier crate and held `ironclaw_secrets`, the
substrate that owns CAS one-shot leases, AAD/crypto and the OS keychain master
key. PROPOSAL §8.2's product row says the products tier loses that edge, and
§12.1b requires the port replacement to land before the edge is removed. Both
happen here, in that order.

- Port: `ironclaw_product_contracts::operator_secrets::OperatorSecretValueStore`.
- Implementor: `ironclaw_reborn_composition::RuntimeOperatorSecretValueStore`,
  the same placement as `OperatorStatusService` — assembly is the only layer
  that may name both a products-tier port and a substrate. Registered in
  `INVERTED_PORTS` beside it.
- `ironclaw_secrets` is gone from the operator manifest under every dependency
  kind, and `"ironclaw_secrets"` is now in the crate's `boundary_rules()`
  forbidden list. That gate's comment previously said the entry was
  deliberately absent because "the row owns it"; the row now owns it.

The port is deliberately narrower than the substrate, so this is a tightening
rather than a relocation: it takes no `ResourceScope` (the implementor fixes
the operator scope, where the caller used to pass one), exposes no
lease/consume protocol, and carries only a `&'static str` classification
instead of the substrate's error `Display` — asserted, including that the
backend message and the handle name are both absent from what crosses.

Two tests travelled with the behavior rather than being pointed at a fake:
`read_is_repeatable_across_reloads` (repeatability is a property of the lease
protocol) and the #4673 production-store reproduction (its value is wiring the
store exactly as production does, which now means the real store *behind the
adapter*). Two `FaultInjecting`-over-real-store fixtures became per-operation
port fakes, with the substrate error mapping re-pinned at the adapter; a third
assertion got stronger — batched-vs-N+1 stored-key lookup is now observed at
the port rather than by counting filesystem ops.

Test accounting: operator 154 -> 153, product_contracts 142 -> 143,
composition 937 -> 942 with zero removed; name-by-name diffs on a quiescent
tree.

Two findings the row could not have anticipated, both recorded in the
CHECKLIST amendment:

- The `webui` half of the row was already closed and was never a production
  edge. `ironclaw_secrets` has been a dev-dependency of `ironclaw_webui` since
  the commit that added it (#6619), both src mentions are `#[cfg(test)]`, and
  webui's boundary rule already forbade it.
- `ironclaw_extension_manager` (layer `products`) still holds a normal
  `ironclaw_secrets` edge in `admin_configuration.rs`. §8.2 covers it; the row
  does not, because the crate landed with WS2.4 after the row was written, and
  the substrate sits in the service's type parameters so it is not a
  like-for-like swap. Filed as #7095.

`LAYER_MATRIX_EXCEPTIONS` is 10 before and after: `products -> substrates` is
matrix-legal, so this edge was always an §8.2 rule and never a layer exception.

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

* test(sandbox): put the Docker security check behind the fail-closed gate

Review asked why the required Rust e2e lane can report `docker_security` as
passing with no daemon. Half of that is #7081 (nothing sets
IRONCLAW_REQUIRE_DOCKER_TESTS=1, so the switch is inert) and is not fixable
from here -- arming it hard-fails any lane lacking a daemon or the worker
image, which needs a runner guaranteed to have both.

The other half is fixable here and is fixed: docker_security.rs open-coded its
own `docker version` / `image inspect` checks with three bare `return`s, so it
sat entirely outside docker_gate and would have stayed fail-open even once
something did set the variable. It now takes both preconditions from
docker_gate::{docker_available, docker_image_available} and skips with the
visible `SKIP:` line that gate's module doc requires.

Measured, same machine, image absent:

  before, IRONCLAW_REQUIRE_DOCKER_TESTS=1 -> "skipping ..." / 1 passed
  after,  IRONCLAW_REQUIRE_DOCKER_TESTS=1 -> panic at docker_gate.rs:74 / FAILED
  after,  variable unset                  -> "SKIP: ..." / 1 passed

The third line is the no-op proof: the variable is set nowhere in this tree or
on main, so no lane's behavior changes today. The daemon-down path already
reached the image check and skipped there, so the outcome is identical; only
the branch it takes differs.

Two stale comments in docker_gate.rs corrected with it (they claimed
docker_security used its own gate, and that docker_image_available had no
consumer), and the crate's Known debt entry now splits the done half from the
#7081 half instead of describing both as open.

cargo test -p ironclaw_sandbox: 193 passed, 0 failed
cargo clippy -p ironclaw_sandbox --tests --all-features -- -D warnings: exit 0

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

* docs(reborn): stop calling the unwired script lane an execution lane

Two review findings, both correct, both artifacts of this PR's own renames.

1. engine-v2-to-reborn-parity.md note 4 read "a native script/software
   execution lane (`ironclaw_sandbox`, `RuntimeKind::Script`) sandboxed via
   `ironclaw_sandbox`" -- self-referential after the merge collapsed
   ironclaw_scripts and ironclaw_process_sandbox into one crate, and it
   contradicts note 5 four paragraphs down ("no production execution backend
   is wired for it"). Re-stated as the typed runtime contract it is, citing
   the measurement: `with_script_runtime` has zero production callers
   (`rg` finds only the builder itself, docs, and 30 test call sites).

2. CHECKLIST WS10 ratchet note 2 said "raise the percentage floor ...; only
   the line count should fall". That generalises WS3's sandbox merge, where
   observed coverage happened to rise. It is wrong as guidance for WS7, and
   the counterexample is in this same file: the 2026-08-03 entry from #7064
   records ironclaw_runner falling 85.55% -> 82.53% because the shed removed
   the crate's better-covered half, holding the floor, and RATCHET FAILing in
   the merge queue. Note 2 now says re-capture from the merged artifact, and
   lower only with that entry's move-not-regression counterfactual (add the
   moved files back, confirm the union clears the old floor, plus a zero-tests-
   lost name set-diff).

cargo test -p ironclaw_architecture: 32 targets, 206 passed, 0 failed

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

* fix(ci): pin the WIT scope probes and the embedded-asset owner pairing

Three review findings on the `wit/` move, each verified before it was acted on.

1. `ws12_workflow_contracts.py` probed `crates/ironclaw_wasm/wit/host.wit` and
   its nested twin. No `host.wit` exists in this repository — `git ls-files
   '*.wit'` returns only `tool.wit` and `channel.wit` — so both probes sat
   under the `crates/([^/]+/)*ironclaw_wasm/` alternative and re-asserted the
   crate-name term while saying nothing about the canonical ABI contracts. In
   a validator whose stated design is "probe derived from reality rather than
   from a guessed layout", a fabricated filename is a defect on its own terms.
   Replaced with a `crate_globs` entry, `("ironclaw_wasm", "wit/*.wit")`, which
   discovers the contracts on disk, requires each in scope, and synthesises the
   nested WS7 form — so a third contract, or the directory leaving the crate,
   fails the pin instead of passing on a stale name. Verified non-vacuous:
   narrowing the workflow alternative to `.../ironclaw_wasm/src/` now reports
   `tool.wit`, `channel.wit` and the nested probe as out of scope.

2. The embedded-asset routing test substituted `alpha`/`beta` owners so it
   could reuse the synthetic workspace. That exercised the real prefix strings
   through the real routing, but left the prefix->owner *pairing* — the table's
   entire semantic content — asserted nowhere: swapping
   `ironclaw_extension_support` and `ironclaw_extension_host` passed. Fixed in
   two halves. The routing test now drives the real `EMBEDDED_ASSET_OWNERS`
   against a workspace carrying the real owners' names and real manifest paths
   (the synthetic one could not: `build_plan` rejects a changed package outside
   the canonical set), asserting the real owner is selected. And the not-stale
   test now derives the same pairing from the tree instead of restating the
   constant: it resolves every literal `include_str!`/`include_bytes!` in every
   workspace crate through `crate_tree`, keeps the targets no crate owns — the
   ones that actually reach the table — and asserts that every crate compiling
   one of them is the routed owner or a dependent of it.

   That surfaced a property worth pinning: `crates/extensions/packages/` is
   embedded by four crates, not one. `ironclaw_extension_host`,
   `ironclaw_extension_manager` and `ironclaw_reborn_composition` reach into it
   alongside `ironclaw_extension_support`, and routing to the support crate
   covers them only because each depends on it. If that edge goes, a shipped
   artifact change stops scheduling a crate that embeds it — the silent
   under-schedule the table exists to prevent.

   Regression coverage verified red by sabotage, all three wrong tables:
   owners swapped (7 failures), `packages/` -> `ironclaw_llm` ("embeds nothing
   from it"), and the hardest case, `packages/` -> `ironclaw_reborn_composition`
   — a real embedder that the other embedders do not depend on
   ("...does not depend on..., so routing there never schedules it").

3. CHECKLIST WS10 claimed each of the nine `wit_bindgen` guest edits forces a
   committed WASM artifact rebuild. Only six do:
   `scripts/ci/check-wasm-artifact-freshness.py` scans
   `crates/extensions/packages/*/wasm-src` alone, `wasm-src-digests.toml` holds
   exactly six entries, and `git ls-files '*.wasm'` returns exactly those six.
   The three `test-tools/*/wasm-src/` guests commit no artifact; the tenth site
   is the host's `bindings.rs`, not a guest. Corrected, and the `wit/` row now
   states the boundary rather than implying it.

Guest paths, `wit/` contents and the six rebuilt artifacts are untouched.

Verified: `test_reborn_pr_test_plan.py` 46/46, `test_ws12_workflow_contracts.py`
25/25, `ws12_workflow_contracts.py` green on the real tree,
`cargo test -p ironclaw_architecture` 206/206 across 32 binaries.

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

* docs(host-runtime): state the obligation visibility rule as it holds

Review catch (#7090): the guardrail sentence promised "cross-owner access is
`pub(super)`, never `pub(crate)`", which is stronger than the code. Verified:
`RuntimeSecretInjectionStore::{insert, take, clone_material,
discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take,
discard_for_capability}` and both constructors are `pub(crate)` and must stay
so — `src/egress/{mod,host_port,credential}.rs` call them, and that is
host-runtime composition outside `obligations/`.

The rule is restated as the property that actually holds: a method whose only
callers are inside `obligations/` is `pub(super)` (the three that are), and
`pub(crate)` is what the stores expose to the egress pipeline they exist to
serve. A future agent reading the old sentence would have read the existing
`pub(crate)` methods as violations.

Guidance-only; no code change.

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

* fix(architecture): put the operator secrets boundary entry on the right rule

Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"`
entry landed in `ironclaw_extension_contracts`'s forbidden vector, not
`ironclaw_operator`'s. The suite still passed, because `extension_contracts`
has no such dependency and `ironclaw_operator` then had no entry at all — so
the guard this row exists to add was inert, and a green architecture suite was
evidence of nothing. Reintroducing the edge would have passed every check.

Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its
`origin/main` content byte-for-byte.

Negative-probed rather than assumed. With `ironclaw_secrets` temporarily
re-added to `crates/ironclaw_operator/Cargo.toml`:

    reborn_crate_dependency_boundaries_hold ... FAILED
    ironclaw_operator must not have a normal dependency on ironclaw_secrets

and with the manifest restored, 35/35 pass.

Two further review findings, both verified before being accepted:

- `ironclaw_extension_manager` **does** have a `boundary_rules()` entry
  (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL
  §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale
  and is marked superseded. The real gap is narrower and now stated: the rule
  exists and simply does not forbid `ironclaw_secrets` (#7095).
- `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules".
  Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated
  `test_support`), and the table was missing `ironhub` **before** this branch
  touched it. Count corrected to twenty-six and the missing `ironhub` row
  added, so the inventory matches `lib.rs`.

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

* docs(sandbox): state the Docker-gate claim as the search that checks it

Review caught a false inventory in the Known debt entry, and the previous
commit is what made it false: "the name appears only in docker_gate.rs and
attribution_tests.rs" stopped holding the moment docker_security.rs gained a
module doc naming the variable, and CLAUDE.md itself was already a third
counterexample.

The narrower claim is the one that was always meant and is the one that
matters, so it now carries its own reproduction: no workflow, script, env file
or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/
*.toml/*.py/*.json/.env* is empty here and on main -- and the sole code
reference is a read, std::env::var(...) at docker_gate.rs:23. Every other
occurrence is a doc comment or a panic message.

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

* refactor(triggers,conversations): scan trusted trigger prompts at the mint (WS6)

PROPOSAL §6.4.2 asked for the trusted-trigger prompt safety scan to move
"behind the triggers/kernel seam it guards". It was not a module: it was
three lines inside `ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire`
— one of the two implementations of `ironclaw_triggers::TrustedTriggerFireSubmitter`
— holding its own `Arc<dyn InjectionScanner>` from `Sanitizer::new()`.

That placement is a fail-open: a guard that lives inside one implementation
of a port is lost the moment a second implementation exists, and nothing in
the tree forced a new submitter to re-run it.

The seam is `TrustedTriggerFireSubmitter`, whose only input is the sealed
`TrustedTriggerSubmitRequest`, which `ironclaw_triggers` is the sole minter
of. So the scan moved to the mint: `TrustedTriggerSubmitRequest::new` is now
fallible and calls the new `ironclaw_triggers::prompt_safety` first, making
"this prompt passed the trusted-prompt scan" an invariant of the type rather
than a step some submitter performs. `new_for_test` delegates to `new`, so
the test-support seal bypasses visibility only, never the scan.

Behaviour at the fire level is unchanged — same rejection point, same
`TriggerError::InvalidMaterialization`, same permanent disposition — and
composition's pre-materialization scan is untouched, so defence in depth
survives with the second scan relocated and now covering every submitter.

`ironclaw_conversations` drops `ironclaw_safety` entirely (the scan was its
only use). Enforcement: triggers' boundary rule stops forbidding
`ironclaw_safety` (a same-layer, I/O-free `substrates` leaf — a peer edge,
not a reach upward), and a NEW `BoundaryRule` for `ironclaw_conversations`
forbids it, plus `ironclaw_threads` (§6.4.2's "Never: transcript content"),
a crate that was unruled until now.

Regression coverage at the caller tier, not on the helper:
`tick_rejects_injection_prompt_before_any_trusted_submitter_is_reached`
drives the real `TriggerPollerWorker::tick_once` with a materializer that
does NOT scan and a submitter configured to accept, and asserts the
submitter is never reached. A companion pins that a medium-severity-only
prompt still submits, so the mint cannot drift into a blanket filter.

Tests: conversations 97 -> 97 (name-identical), triggers 169 -> 173
(+2 worker, +2 prompt_safety unit), architecture 206 -> 206.
LAYER_MATRIX_EXCEPTIONS unchanged at 10.

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

* fix(coverage): re-anchor the exemptions the merge shifted

tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and
auto-merges silently. #7096's additions to ironclaw_reborn_composition moved
four entries' subject lines by +2 without anything flagging it; a stranded
entry makes the changed-coverage validator abort with no verdict at all.

Re-anchored by content (difflib line map from the #7065 tree, which the file
was validated against, to the union) rather than by arithmetic:
  runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085]
  runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435]
  lib.rs     [616]  -> [618]
All 142 entries / 1124 line references re-verified against the merged tree:
0 drift, 0 out-of-bounds, 0 missing paths.

* refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4)

Two CHECKLIST rows, both of which were a one-line manifest correction rather
than a code move: the family docs already placed both crates where the rows
want them and only `Cargo.toml`'s `layer =` disagreed.

processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes
among the kernel crates. The re-layer makes processes -> resources a
kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate
said so itself:

  Stale IronClaw crate layer matrix exceptions:
  ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed
  in W7: runtime process management still depends on resource contracts
  currently classed with kernel behavior

That is the gate's verdict, not a judgement call - deleting the entry is the
only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list).
Checked the direction both ways: all nine crates that take a normal dependency
on processes (capabilities, turns, host_runtime, extension_host, loop_host,
extension_manager, runner, reborn_composition, stress) are kernel or above, so
the move legalizes an edge without forbidding an existing one.

skills -> substrates (WS4 SS3.D). families/domains.md already lists
ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies
are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both
at or below substrates, and its six consumers are all loops or above. No
exception moves in either direction.

cargo test -p ironclaw_architecture: 206 passed, 0 failed.
cargo check --workspace --all-targets: clean.

* docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence

Every tick was verified against the merged tree, never against a PR title.

TICKED:
- sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and
  ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one
  manifest in the workspace.
- mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only,
  0 production ironclaw_extensions:: refs in src/.
- skills -> substrates: landed here.
- hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four
  rejected alternatives and the evidence they are already converged on one
  trait plus a shared conformance suite. #6945 read first as the row demands,
  and explicitly NOT discharged: this PR changes nothing in the dispatch path.
- WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10
  exceptions carried removes_in = W7). Corrected with the replaced text
  quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder
  named with its owning field value. Ticked on the corrected condition.

LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave:
- first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime.
  Ticking would be false.
- processes/capabilities row: re-layer DONE; the capabilities/host.rs split is
  deferred with every module boundary already computed (4,560 lines, the six
  workflow ranges, and the arch-exempt waiver that must be deleted with it).
- host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs
  RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same
  section's Keeps clause; zero external references to either). Catalog half
  cannot go to extension_host at all - host_runtime is itself a production
  consumer at memory_native_extension.rs:96,101, so the move is a
  kernel -> products edge and a Cargo cycle. Correct destination is downward.
- network test_rewrite: NOT executed. Recorded the security shape (production
  binaries compile the seam and honour the rewrite env var at runtime) and the
  full 6-step plan, because the env var is how the entire E2E suite redirects
  vendor traffic through the production binary and the change needs feature
  forwarding into CI lanes I cannot verify here.

cargo test -p ironclaw_architecture: 206 passed, 0 failed.

* refactor(traces): drop the boundary-laundering re-export modules (WS6)

PROPOSAL §6.4.14: "drop the boundary-laundering re-export modules
(`recording`, `paths`) — consumers import the owners".

`ironclaw_reborn_traces::{recording, paths}` were two `pub use <other
crate>::*` passthroughs whose own doc comments stated their purpose
plainly: "so reborn-cli does not need a direct `ironclaw_llm`
dependency, preserving the architectural boundary". They preserved
nothing — the edge existed either way; the wildcard only hid which crate
owned the type, so the dependency graph read as a lie.

All three call sites were in `ironclaw_reborn_cli`. Note the literal
reading of "consumers import the owners" is not available here: the CLI's
dependency allowlist (`reborn_cli_binary_crate_stays_separate_from_v1_root`)
deliberately excludes `ironclaw_llm`, so importing the owner would have
traded a laundered re-export for a breached, tested boundary. Satisfied
instead by giving the owning crate the operation, which is what the
laundering was standing in for:

- `onboarding::onboard_instance(invite, consents)` — resolves the
  contribution root itself. Path layout under the base dir is this
  crate's own knowledge; the CLI no longer needs base-dir vocabulary.
- `TraceClientHost::build_envelope_from_recorded_trace_json(json, opts)`
  — parses `ironclaw_llm::recording::TraceFile` inside the crate that
  already depends on `ironclaw_llm`. The CLI hands over raw JSON.
- the CLI's private `trace_contribution_dir()` now delegates to
  `contribution::trace_contribution_dir_for_scope(None)` instead of
  re-deriving `<base>/trace_contributions`. Verified byte-identical:
  `trace_contribution_dir_for_scope(None)` is
  `trace_contribution_dir_for_scope_at(&ironclaw_base_dir(), None)`,
  whose `None` arm returns `base.join("trace_contributions")`.

No dependency was added to any crate. Semantics unchanged.

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

* refactor(llm): make providers.json a crate asset with a boundary rule (WS6)

CHECKLIST WS6: "`llm` `providers.json` becomes a crate asset/composition
input + boundary rule added".

The provider catalog sat at the **repository root**. A root-level data
file has no owning crate, so no boundary rule could govern who edits it,
and every consumer compiled it in behind Cargo's back with an escaping
`include_str!` — the "repo-root asset reach-in" shape §11.2.7's scanner
inventories. `git mv`'d to `crates/ironclaw_llm/assets/providers.json`
and the 20 `include_str!("../../../providers.json")` sites in
`registry.rs` become in-crate `../assets/providers.json`.

⚠ Correcting the row's inherited premise: a prior lane recorded the
"load-bearing include site is in `ironclaw_reborn_cli`" and judged the
item "needs a new mechanism, not a new path". Measured on main: the
load-bearing site is `crates/ironclaw_llm/src/registry.rs:383`
(`builtin_provider_definitions`), inside the owning crate. No new
mechanism was needed — only the path.

**Path-keyed gates rewritten in the same commit** (WS10: these fail
*silently* under a move):
- `Dockerfile` — both `COPY providers.json providers.json` lines deleted;
  `COPY crates/ crates/` already covers the new location in both stages.
  Verified by `scripts/ci/check-include-str-paths.sh` (OK, 119 refs).
- `.github/workflows/reborn-e2e.yml` — the literal `providers.json` path
  filter and its regex alternative removed; the depth-independent
  `crates/**` entry already matches. `ws12_workflow_contracts.py` passes.
- `scripts/ci/classify-test-scope.sh` — kept at its **shared** (both
  lanes) classification under the new path rather than letting it fall
  through to crate scope, so CI breadth does not silently narrow; the
  now-redundant entry in the reborn-only branch is dropped.

**The one consumer that could not simply be repointed.** The CLI's
`default_llm_consts_match_the_real_providers_json_nearai_entry` embedded
the catalog from five directories up to check its mirrored `DEFAULT_LLM_*`
constants. Repointing it would have turned a repo-root reach-in into a
*cross-crate* reach-in — the category §11.2.7 turns into a hard failure —
and the CLI may not depend on `ironclaw_llm`. A cross-crate consistency
rule belongs in the cross-crate suite, so the assertions moved into
`ironclaw_architecture` and read both files from disk at runtime, needing
no compile-time coupling at all.

Test accounting: `ironclaw_reborn_cli` config-init tests 2 -> 1; the
removed one is reborn as `reborn_provider_catalog_is_owned_by_its_crate`
in `reborn_dependency_boundaries.rs`, strictly stronger (it also pins the
asset's location, the repo root's emptiness, and single-embedder
ownership). Net test count +0.

**The new rule is sabotage-tested** — five cases, each red with the right
message, each restored to green:
1. catalog copied back to the repo root -> "must not sit at the
   repository root"
2. a foreign crate `include_str!`s it -> names the offending file
3. catalog `default_model` drifts from the CLI mirror -> names the const,
   the field and both files
4. walker pointed at a non-existent dir -> "walked only 0 Rust files ...
   would pass no matter what the tree contained" (reachability)
5. mirrored const renamed -> "no longer declared as a plain const ...
   update the extraction rather than deleting the drift check"

Case 2 caught a real false positive in the first draft of the guard: a
file-level `include_str!` AND `providers.json` conjunction flagged
`cli/tests/smoke.rs`, which names the *runtime*
`$IRONCLAW_REBORN_HOME/providers.json` and separately embeds something
else. The matcher now inspects the macro argument, not the file.

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

* ci(coverage): recapture the two composed floors from a real measurement

The provisional values were arithmetic - the sum of the two slices' recorded
deltas - and the dispatch caught them, which is the whole reason the brief
demanded a measurement rather than a reconciliation.

Dispatch run 30907774036 at 4512e03e28:
26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one
skip is the pull_request-gated mutation gate; the two failures are the coverage
report and the roll-up it drags down, i.e. this file doing its job.

ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63%
(17562 / 19814). The composition was wrong by 1300 denominator lines because
both slices measured their delta under the pre-#7083 aggregator, which could
not see crates/extensions/** at all - lines leaving host_runtime for
extension_support vanished from the tree it could measure, so neither branch's
recorded delta describes the post-#7094 world.

ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's
82.64% (6826 / 8260), captured before #7080's executor lines arrived.
floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye
rather than written quietly. Evidence it is composition and not lost tests:
floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more
absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398
with zero test names lost. Same shape as #7094's own ironclaw_runner recapture.

ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657).
The [global] entry is untouched: both moves are crate-to-crate inside the set
the fixed aggregator sees.

* docs(skills): rewrite the stale v1 lib.rs charter note (WS6)

CHECKLIST WS6 domain-internal cleanups: "`skills` stale v1 lib.rs doc
rewritten".

The crate doc claimed "In v1, trust-based tool filtering happens via
`src/skills/attenuation.rs`. In v2, the Python orchestrator handles trust
labels and the policy engine controls tool access via capability leases."
Both halves are dead vocabulary: there is no `src/` monolith on this tree
and no Python orchestrator anywhere in Reborn.

Replaced with what is true and checkable — this crate owns the trust
*label* and none of its enforcement; the ceiling is applied at the
capability tier (`host_api` capability/invocation attenuation via
`first_party_extension_ports`' activation and execution paths) and the
decision belongs to `ironclaw_authorization`. Also points at the existing
`SkillTrust` `Ord` safety note, which the old text left unconnected.

Doc-only; no code change.

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

* fix(network): compile the test rewrite seam out of production builds (WS3)

Closes the WS3 network row. Also RETRACTS an overstatement I made in this
row's earlier annotation.

CORRECTION FIRST. The earlier note claimed production binaries compile the
seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone
able to set it could redirect all credentialed vendor egress. That was WRONG.
RewriteNetworkTransport::from_env_value already returned UnavailableInRelease
when !cfg!(debug_assertions) (test_rewrite.rs:150), and neither
[profile.release] nor [profile.dist] sets debug-assertions, so a shipped
binary with the variable set REFUSES TO BOOT. It was fail-closed before this
PR. I had read the ungated `mod test_rewrite;` declaration as an ungated runtime
path.

What was genuinely wrong, and is fixed:
1. The guard was a RUNTIME check keyed on cfg!(debug_assertions) - a profile
   proxy, not a build-kind guarantee. A release profile with debug-assertions
   turned on (normal when chasing a production bug) silently re-arms it.
2. The refusal arm had NO TEST. The one guard between a shipped binary and
   redirectable vendor egress was unpinned.

Fix: compile-time exclusion instead of a runtime check. mod test_rewrite and
its four re-exports are now cfg(any(debug_assertions, feature=test-support)),
and default_host_http_egress is a compile-time pair - production builds
PolicyNetworkHttpEgress<ReqwestNetworkTransport> directly, with the rewrite
wrapper absent from the binary. The runtime check stays as defence in depth.

E2E needs no change: those harnesses build DEBUG binaries, so they satisfy
debug_assertions and keep redirecting with no feature flag and no workflow
edit. The feature-forwarding-into-CI risk I flagged earlier does not arise.
test-support is still forwarded composition -> network for a release-PROFILE
build that needs the seam.

Both halves proven rather than assumed:
(a) release refuses - new regression test
    a_set_rewrite_map_activates_only_in_debug_and_is_refused_in_release feeds
    a well-formed map and asserts on profile. Under
    'cargo test --release -p ironclaw_network --features test-support' it
    passes on the UnavailableInRelease branch; under debug 'cargo test -p
    ironclaw_network' it passes on the active branch. 56 passed, 0 failed.
(b) production compiles without the seam -
    'cargo check --release -p ironclaw_reborn_composition' (no test-support)
    is clean, which only compiles if the cfg(not(..)) arm is right.

Also: WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE 129 -> 127. The constant
had drifted ABOVE the real list length; the ratchet is shrink-only so it
passed silently while buying back two unearned slots. Measured off the
compiler (set baseline to 0, read the reported length), identical on main and
on every slice, so pre-existing drift rather than something this PR caused.

cargo test -p ironclaw_architecture: 206 passed, 0 failed.
cargo check --workspace --all-targets: clean.

* refactor(crates): execute the WS6 crate renames, no shims (WS6)

Three CHECKLIST WS6 rename rows, executed together as one pure rename.
No compatibility re-export shims (WS6 discipline); every consumer, doc,
CI script and snapshot repointed in this commit.

**Row 1 — stutter kills (decided 2026-07-29):**
- `ironclaw_events`             -> `ironclaw_event_log`
- `ironclaw_extensions`         -> `ironclaw_extension_registry`
- `ironclaw_product`            -> `ironclaw_assistant`

**Row 2 — naming audit (decided 2026-07-30):**
- `ironclaw_architecture`       -> `ironclaw_architecture_tests`
- `ironclaw_runner`             -> `ironclaw_turn_runner`
(`ironclaw_first_party_extensions` -> `ironclaw_extension_support` landed
early with WS2.6 and is already ticked.)

**Row 3 — the `reborn_` batch (decided 2026-07-30):**
- `ironclaw_reborn_composition`   -> `ironclaw_composition`
- `ironclaw_reborn_config`        -> `ironclaw_config`
- `ironclaw_reborn_event_store`   -> `ironclaw_event_store`
- `ironclaw_reborn_identity`      -> `ironclaw_identity`
- `ironclaw_reborn_openai_compat` -> `ironclaw_openai_compat`
- `ironclaw_reborn_traces`        -> `ironclaw_trace_commons` (§6.4.14:
  the crate is the Trace Commons client, not trace machinery)
- root package `ironclaw_reborn_integration_tests` -> `ironclaw_integration_tests`

4,806 occurrences rewritten across 901 files, plus 11 `git mv`'d crate
directories (`git diff -M` reports them as renames). Replacement used
word-boundary matching, which is what keeps `ironclaw_product` from
touching `ironclaw_product_contracts` and `ironclaw_extensions` from
touching the four `ironclaw_extension_*` siblings.

**Semantics: none.** No type was renamed, no module moved, no signature
changed. `cargo check --workspace --all-targets` is clean.

**Path-keyed gates rewritten in the same commit** — WS10 lists these as
the ones that fail *silently* under a rename, and each was re-run to
prove it still scans a non-zero tree rather than merely passing:
- `scripts/no_panics_reborn_baseline.txt` — 3 entries repointed, 0 stale
  names left; `--reborn-baseline` reports "OK ... (1203 files, 51
  reviewed invariant(s))" and `--self-test` passes 34 tests.
- `docs/plans/composition-pubuse.snapshot` — 5 entries. This one is not
  documentation despite its path: `composition_public_pub_use_surface_matches_snapshot`
  compares against it byte-for-byte, and it failed loudly when the rename
  first landed without it. Caught by running the suite, not by inspection.
- `scripts/ci/classify-test-scope.sh`, `scripts/ci/reborn-crate-test-buckets.sh`
  (+ its self-test), `scripts/ci/discover-reborn-package-crates.sh`,
  `scripts/ci/package-feature-flags.sh`,
  `scripts/ci/check-generic-without-concrete.sh`,
  `scripts/ci/ws12_workflow_contracts.py`, `scripts/dev_metrics.py`,
  `scripts/reborn-e2e-rust.sh`, `scripts/pre-commit-safety.sh`.
- **CI lane names**, which the `ironclaw_architecture` row calls out
  explicitly: `.github/workflows/code_style.yml`'s `cargo test -p
  ironclaw_architecture reborn` step and its changed-paths regex.

Verification: `cargo check --workspace --all-targets` clean;
`ironclaw_architecture_tests` 32/32 suites green; `ws12_workflow_contracts.py`,
`test-classify-test-scope.sh`, `test-reborn-crate-test-buckets.sh`,
`check-include-str-paths.sh` all pass. `LAYER_MATRIX_EXCEPTIONS` counted
with Python between the const and its `];` — **6**, unchanged.

Deliberately not rewritten: `docs/reborn/subagent-spawn/diagrams/*.{d2,svg}`
and the historical prose in `docs/`. Those describe an unlanded design
authored against the old tree; renaming inside them would misrepresent
what was designed, and the `.svg`s are generated artifacts.

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

* docs(coverage): verify the extension_support floor drop is composition, independently

The 82.64 -> 75.31 recapture carried a rationale that was recorded but
explicitly NOT verified. Re-derived it from scratch between the two capture
refs (f946a93fae -> 939af4847d) rather than inheriting the claim:

- 0 test names lost in the crate (158 -> 160 test fns; both new names belong
  to the arriving executor).
- 0 test names lost WORKSPACE-WIDE (13836 -> 13843 test fns, 13752 -> 13759
  unique). This is the check that separates a relocation from a deletion:
  host_runtime's roster drops 156 names over the same range and every one
  reappears in another crate.
- Exactly four files arrived, 1367 source lines, all of them the family-1
  skill-install executor (src/skills/url_install.rs + url_install/{github,
  zip_bundle,bundle}.rs). No pre-existing file left the crate.
- The arithmetic closes with the pre-existing numerator held CONSTANT:
  (6826+316)/(8260+1224) = 75.31% exactly, so the pre-existing code lost zero
  covered lines. The arriving block's own coverage is 316/1224 = 25.82%.

Composition, confirmed rather than assumed. No test regression to fix; the
25.82% arrival is what earns the follow-up already recorded above the entry.

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

* fix(host_runtime): collapse a duplicated obligation predicate and quiet a background warn!

Three verified review findings from the #7141 round. Each was confirmed
against the code before being acted on; nothing was changed on assertion alone.

1. obligations/handler.rs — `obligation_supported_before_dispatch` and
   `obligation_supported_after_dispatch` had BYTE-IDENTICAL 19-line bodies
   (verified by exact line-by-line comparison). Both were private, each called
   exactly once, both taking the same `phase` argument. The two names asserted
   a pre/post-dispatch distinction the code never implemented, while the pair
   gates admission of RedactOutput, EnforceOutputLimit and
   EnforceResourceCeiling — so editing one copy alone would have left the other
   stage accepting an obligation the host cannot honour (a fail-open).
   Collapsed to one `obligation_supported`, with the reasoning recorded so the
   pair is not reintroduced.

2. obligations/process_store.rs — `cleanup_terminal` is reached from
   `observe_process_commit` (an async background journal callback, call sites
   at :363/:379/:394), so its `tracing::warn!` violates the repo rule that
   background tasks never use info!/warn! — they corrupt the REPL/TUI display.
   Lowered to `debug!`; the error is still returned to the caller on the next
   line, so nothing is swallowed.

3. reborn_restructure_baselines.rs — the doc table said the
   LAYER_MATRIX_EXCEPTIONS count was "now 11". Recomputed on this ref by
   anchoring on the `= &[` of the value (the `&[LayerMatrixException]` type
   annotation opens a bracket on the same line and silently yields 0): the real
   count is 4, matching WS0_LAYER_MATRIX_EXCEPTION_BASELINE = 4. Corrected.

Verification: cargo check --all-targets -p ironclaw_host_runtime exit 0;
obligation tests 13+26 passed, 0 failed; reborn_restructure_baselines 1 passed.

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

* fix(ci): a shipped package prompt is an asset, not prose — it was selecting no lane

Review finding on #7141, confirmed empirically before acting. The Markdown
prose carve-out in the planner ran BEFORE the `EMBEDDED_ASSET_OWNERS` lookup.
A prompt is a `.md` file that no package *directory* owns, so a change to
`crates/extensions/packages/*/prompts/**.md` took the prose arm and planned:

    mode=none   crate_buckets=[]   "crate-tree guidance changed: ..."

while its sibling `manifest.toml` in the same package planned `mode=selected`
onto ironclaw_extension_support + ironclaw_extension_host. Prompts are shipped
production output that `ironclaw_extension_support` compiles in, and the
comment above `EMBEDDED_ASSET_OWNERS` names "manifests, prompts, schemas and
built wasm/*.wasm" as exactly what that table owns — so this was the "silent
under-schedule of a change to production output" that comment forbids. 145 of
the 149 `.md` files under `packages/` are prompts.

The rule is keyed on the `prompts/` path segment, not on the asset prefixes.
That distinction is load-bearing: the first attempt yielded to the asset
prefixes wholesale and broke `test-tools/README.md`, which is documentation of
the fixture bundles and is deliberately pinned as prose. Of the four asset
kinds the table owns, only a prompt is Markdown (manifests are .toml, schemas
.json, wasm .wasm), so `.md` asset <=> prompt is exact.

Sabotage-tested in both directions:
  * `_is_package_prompt` -> False (reinstates the bug): RED,
    "AssertionError: 'none' != 'selected'".
  * `_is_package_prompt` -> any .md under an asset prefix (over-broad): RED on
    both the new test and the pre-existing
    `test_markdown_owned_by_no_crate_is_prose`, at `test-tools/README.md`.
  * restored: 52 passed, 51 subtests, green.

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

* refactor(cli): move the binary crate to crates/app/ironclaw_cli (WS6)

Last clause of the WS6 `reborn_` rename row: "cli directory ->
`app/ironclaw_cli`". Package name stays `ironclaw` (unchanged, as the row
requires); this is a directory move plus the crate-directory rename.

82 path references rewritten across 39 files, plus the crate's own 18
`path = "../X"` dependencies re-based to `../../X` now that it sits one
level deeper. `cargo check --workspace --all-targets` clean.

This is the first crate to live at a nested family path, which is exactly
the shape WS10 warns about: a gate keyed to the flat `crates/<name>/`
layout stops matching and goes green having scanned nothing. Two gates
were found by running them, not by reading them:

1. **`scripts/ci/ws12_workflow_contracts.py` failed loudly and correctly** —
   `.github/workflows/code_style.yml`'s `has_reborn_cli` filter named the
   crate `ironclaw_reborn_cli`, which the crate inventory could no longer
   resolve: "expected exactly one crate directory named
   'ironclaw_reborn_cli' under crates/, found 0 ... repoint the gate that
   names it rather than letting it measure an empty tree." Repointed
   there, in ws12's own probe table, and in
   `check-generic-without-concrete.sh`. The workflow regex already used
   the depth-independent `crates/([^/]+/)*` form, so the nesting itself
   was safe — only the crate *name* needed repointing.

2. **`docs/plans/composition-pubuse.snapshot` regenerated after `cargo
   fmt`**, not before. The rename lengthened a `pub use` line past the
   width limit, so fmt rewrapped it and the snapshot went stale a second
   time. Diff is exactly one alphabetical re-sort
   (`ironclaw_product`->`ironclaw_assistant`) and one rewrap; no symbol
   added or removed.

**Pre-existing bug fixed in passing, with evidence it predates this PR.**
`check-generic-without-concrete.sh` listed `"ironclaw_reborn_cli"` among
its sanctioned assemblers, but that set is matched against cargo
*package* names and the CLI package is `ironclaw`. The exemption
therefore matched nothing and the gate was **already red on clean
`origin/main` @ 283e1f6b7c**, reporting the two concrete extension crates
DEL-7 explicitly allows the binary to link:

    ironclaw: dependency graph contains concrete extension crate ironclaw_slack_extension
    ironclaw: dependency graph contains concrete extension crate ironclaw_telegram_extension

Reproduced on a clean checkout before assuming this PR caused it. Fixed
by naming the package, with a comment recording that these are package
names — the same directory-vs-package confusion that
`boundary_rule_names_are_package_names_not_crate_directories` exists to
catch on the dependency-boundary rules.

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

* fix(harness): refresh the latency-runner lockfile after the sandbox consolidation

Review finding on #7141, reproduced before fixing. The latency harness keeps
its own committed `Cargo.lock`, separate from the workspace lockfile, and the
crate consolidation that replaced `ironclaw_scripts` + `ironclaw_process_sandbox`
with `ironclaw_sandbox` never regenerated it. It still carried entries for both
removed packages (lines 3244 and 3602) and the old host-runtime/loop-host
dependency graphs.

Reproduced exactly as reported:

    $ cargo metadata --locked --manifest-path harness/latency/runner/Cargo.toml
    error: cannot update the lock file ... because --locked was passed
    exit 101

so any reproducible invocation of the harness was broken, while the documented
unlocked command silently rewrote the lockfile as a side effect of running.

Regenerated with `cargo update --workspace`, which re-resolves the path
dependencies. Verified after: `--locked` exits 0, the two removed packages are
gone (0 entries), and `ironclaw_sandbox` is present (1 entry).

Note: the re-resolve also carried three registry deps forward
(wasmtime-wasi 46.0.1 -> 47.0.3, wasmtime-wasi-io likewise, wit-parser
0.251.0 -> 0.252.0). That is contained — this lockfile governs only the
standalone benchmark harness and is not the workspace lockfile, and it was
already unusable under `--locked` before this change.

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

* docs(target-arch): tick the three WS6 rename rows, amend four others (WS6)

Dated amendments, each quoting or naming the text it replaces.

**Ticked (condition verified on the merged tree):**
- the three `Renames executed` rows — stutter kills, naming audit, and
  the `reborn_` batch. All 14 clauses across them are done.

**Amended without ticking, because a clause is genuinely unmet:**
- `Domain-internal cleanups` — three of six clauses done (traces
  re-export modules, `llm providers.json`, `skills` lib.rs doc), one
  refuted (`identity` absorbing `host_api::user_identity`), two open
  (`triggers` SQL ADR, `projects` composition adapter).
- `Retire the local_dev misnomer` — the row stays ticked; its *residue
  clause* is re-scoped with measurements.

**Two row texts were wrong and are corrected rather than executed:**
1. The `traces` `ScopedFilesystem` clause says the type is "dropped".
   §6.4.14 says the crate should *take* one. §6.4.14 is right and the
   row is the error — the type exists (`ironclaw_filesystem::ScopedFilesystem`)
   and is absent from the traces crate, so this is adoption, not removal.
   Also corrects "~91 raw `fs` call sites" (that counted test code; the
   production surface is 11 in `contribution.rs` plus ~7 in
   `device_key.rs`).
2. The `local_dev` residue said "the local variable at
   `composition/src/runtime.rs:3016`". It is not one variable — it is 14
   distinct identifiers; #7098's "public type" claim is wrong
   (`RebornLocalRuntimeIdentity` is `pub(crate)`); and #7098's
   explanation for why the ratchet missed it is wrong, because a
   *second* ratchet (`reborn_deployment_mode_typename_ratchet`) already
   inventories the name and records that the sanctioned exit is Slice B,
   not a rename. Every obvious rename target is also already taken by a
   different concept.

**One clause refuted with measurements (delegated authority).** "`identity`
absorbs `host_api::user_identity` ports" would move a ports module out of
the neutral contracts crate into a crate that neither implements nor
consumes it — the sole production implementor is
`extension_host::channel_identity_store::FilesystemChannelIdentityStore`
— and, because `ironclaw_identity` depends on `ironclaw_host_api` and not
the reverse, would force `extension_host` to take a new dependency to
name a port it implements. The ports stay in `host_api`. The dual
binding-store ambiguity is resolved as nominal, not structural: principal
identity (`ironclaw_identity::identity_store`) and post-OAuth channel
binding (`extension_host::channel_identity_store`) are distinct concerns
and neither subsumes the other.

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

* fix(skills): stop rejecting inline bundle installs and stop dropping url conflicts

Review finding on #7141, verified against `dispatch_install` before acting.
Two defects in `resolve_install_input`, in opposite directions:

1. Inline installs lost their bundle. The inline arm required `files`,
   `source` and `source_url` to be ABSENT, so `{name, content, files}` fell
   through to `InputEncode`. That shape is fully supported downstream —
   `dispatch_install` reads `content` and then `parse_install_files`,
   `parse_install_source` and `source_url` off the same object — so a valid
   bundle install was rejected before it ever reached the dispatcher. Those
   three keys conflict with `url`, not with `content`.

2. URL installs silently discarded conflicts. The url arm accepted `url`
   even when `files`/`source`/`source_url` were present, then rebuilt a fresh
   object from the fetched payload — so those fields vanished without a word
   and the caller saw a successful install of something it had not asked for.
   The function's own contract already called that combination an input error
   ("`url` combined with `files`/`source`/`source_url`"); now the code agrees.

Sabotage-tested both guards, and the second round caught a defect in the TEST
rather than the code — worth recording, because it is the failure mode this
program keeps hitting:

  * inline arm made over-strict again: RED on
    `inline_install_keeps_its_bundle_files_source_and_source_url`.
  * url conflict guard removed: initially STILL GREEN. The test used
    `https://example.test/...`, an unroutable host that `validate_skill_url`
    rejects with the SAME `InputEncode` kind — so it passed whether or not the
    guard existed. Rewritten against an allowed `raw.githubusercontent.com`
    URL, where removing the guard now reaches the fetch and fails
    `NetworkDenied`: RED, "left: NetworkDenied, right: InputEncode". The test
    also asserts `usage() == None`, since the guard must reject before any
    egress is consumed.
  * restored: 112 passed, 0 failed.

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

* fix(ci): repoint the release-cut scripts at the moved CLI manifest

`origin/main` added `scripts/ci/cut_ironclaw_release.py` and its
self-test while this branch was in flight; both locate the version to cut
via `crates/ironclaw_reborn_cli/Cargo.toml`, which this PR moved to
`crates/app/ironclaw_cli/Cargo.toml`.

Caught by re-scanning the merge for reintroduced old crate names rather
than trusting a clean `git merge` — the merge was conflict-free precisely
because these files are new on main and touch nothing this branch edited,
which is the shape that reintroduces a stale path silently.

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

* refactor(capabilities): split host.rs along its six workflows (WS3 Row 2)

`crates/ironclaw_capabilities/src/host.rs` was 4,560 lines — the capability
membrane, where every privileged effect in the stack crosses — fusing all six
caller-facing workflows into one 3,048-line `impl CapabilityHost` block and
held together only by an `// arch-exempt: large_file` waiver on line 1.

It is now the directory module `src/host/`, one file per workflow:

- `invoke`           — workflow 1, `invoke_json`
- `approval_resume`  — workflow 2, `resume_json`
- `auth_resume`      — workflows 3 and 4, `auth_resume_json` / `decline_auth_json`
- `spawn_resume`     — workflow 5, `resume_spawn_json`
- `spawn`            — workflow 6, `spawn_json` + its private `authorize_spawn` fold
- `authorize`        — the one authorization fold all six funnel through
- `resume_support`   — the preflight/authorize/dispatch tail the three resume
                       workflows converge on
- `obligation_seams` — prepare/complete/abort around dispatch
- `error_mapping`    — foreign errors and verdicts renamed into this vocabulary
- `mod`              — the struct, the `CapabilityAuthorizer` seal, the
                       cross-workflow types, the constructors, and the charter
                       table saying which file a new item belongs to

The charter does not follow the CHECKLIST's ranges blindly. Those filed
`evaluate_trust`, `enforce_runtime_policy`, `apply_persistent_approval` and
`seal_authorization` under `invoke_json`, but the call graph shows
`authorize_spawn` and `authorize_resumed` call them too, so they belong with
the fold in `authorize`, not with one workflow. Layering is downward-only: no
module calls a workflow entry point.

Every module clears the 1,500-line gate on its own — largest production file
612, largest of all 910 (`tests.rs`) — so the waiver is **deleted** rather than
carried, and no new waiver is added anywhere. Re-fusing them now trips
`scripts/pre-commit-safety.sh`.

Behavior-free, and no consumer edits: `mod host;` stays private, every workflow
stays an inherent method on `CapabilityHost`, `lib.rs`'s
`pub use host::CapabilityHost;` is untouched, and the 11 unit tests keep their
exact `host::tests::*` paths. Cross-module access is `pub(super)` — 11 methods
and 12 free items, enumerated, never `pub(crate)` and never `pub`. Those 23
signature lines are the only in-body change in the whole split.

Proven no-loss rather than assumed, because a sibling split silently deleted
four tests and five helpers and still went green:

- Bodies sliced by computed item spans and verified byte-verbatim against the
  pre-edit file; all 4,560 lines accounted for (3,040 impl body + 223
  vocabulary + 321 free helpers + 900 tests + imports/headers).
- Item-roster diff vs the pre-edit ref: zero items missing; the only additions
  are the 9 `mod X;` declarations.
- Unfiltered `--list`: 158 tests before, 158 after, names identical; all pass.

One path-keyed gate fired and was repointed, not relaxed:
`scripts/no_panics_reborn_baseline.txt` pinned
`enrich_dispatch_error_credential_requirements`'s `unreachable!` to the old
whole-file path; it now resolves to `src/host/error_mapping.rs`, and
`check_no_panics.py --reborn-baseline` is green.

Guidance travels with the change: the crate's `AGENTS.md` and `CLAUDE.md` now
point at the charter, PROPOSAL §6.5.6 records the split as done, and the
CHECKLIST row is ticked with the per-module line counts.

Verification: `cargo check --all-targets` (workspace) clean; `cargo clippy -p
ironclaw_capabilities --benches --tests --examples --all-features` clean;
`cargo test -p ironclaw_capabilities` 158/158; `cargo test -p
ironclaw_architecture` 130/130; `cargo fmt --check` clean.

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

* docs(target-arch): retract the "W7 is Wave 5" premise and tighten the ALLOWLIST baseline

Three doc-truth defects found by audit, each verified against the source of
truth before being rewritten.

1. RETRACTED: "W7 is Wave 5". The WS3 verify-row correction on this branch
   justified its tick by claiming nine of ten exceptions carried
   `removes_in = "W7"` and that "W7 is Wave 5". That is false. `W7` is a
   retired July-train milestone label (#5852, 2026-07-09) — one of the dated
   target milestones the exception register stamps on its own entries beside
   `W4.3` and `W6`, as §2.2 states outright. §8.3's dissolution table resolves
   every W7 edge through WS2/WS3/WS4 actions (re-layering, contract moves,
   package moves) and not one through a WS7 physical move, so the label
   carries no wave assignment at all.

   The tick STANDS: it was already earned on the corrected edge-by-edge scope,
   which was derived by reading LAYER_MATRIX_EXCEPTIONS and each edge's real
   owner, not by reading the label. Only the justification was wrong — but it
   was wrong in a way that made Wave 3's remaining scope look smaller than it
   is, so it is retracted in full rather than quietly amended, and the
   surviving W7-labelled entry (`host_runtime → ironclaw_extension_support`)
   now names its real owner: this checklist's own first_party_tools row.

2. The branch contradicted itself: the WS3 heading still read "kills the
   remaining W7 exceptions", restating the same label-as-wave confusion while
   the row below it retracted that reading. Heading reconciled.

3. §8.3's lane-edge row still carried a proof §6.6.3 refuted on 2026-08-03 —
   that the blocker is "the estimate/usage vocabulary … it already does".
   #7067 measured the real blocker as `ResourceGovernor` (10 methods, the lane
   calls 3 and implements none) plus `ResourceError`'s denial cone: a kernel
   carve-out, not a vocabulary move. §8.3 now matches §6.6.3 instead of
   leaving a live false premise for whoever plans that slice.

Also: WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE 127 -> 126, the live count.
Read back off the ratchet by setting the baseline to 0 and letting it report
(126 entries), rather than counted by eye. The branch was carrying one slot of
slack; #7147 tracks the union recount across the sibling PRs.

Verification: cargo test -p ironclaw_architecture — 32 binaries, 0 failed.

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

* fix(ci): classify the Dockerfile in the Reborn PR test planner

`Detect Reborn test scope` failed on this PR with:

    Reborn PR test planner failed: unclassified pull-request path: Dockerfile

and took `Tests (Reborn)` down with it ("changes failed: failure").

`scripts/ci/reborn_pr_test_plan.py` classifies every changed path and its
fail-closed arm raises on anything no rule claims. `PR_STATIC_CONTROL_PATHS`
held `Cargo.toml`, the toolchain files and the coverage manifests, but not
`Dockerfile` — so **any** PR editing the container build context aborted
the planner. This PR is simply the first to do so: moving `providers.json`
into its owning crate made the two `COPY providers.json` lines redundant.

The Dockerfile is owned by the `Docker` workflow (its own trigger on this
path) and its COPY coverage by `check-include-str-paths.sh` under Code
Style. No Reborn test lane reads it, so it belongs with the other
de-escalating static-control paths: `mode: none`, `coverage_mode: none`,
no buckets selected.

The existing `test_unclassified_build_input_fails_fast` used `Dockerfile`
as its *example* of an unclassified path. The invariant it protects is the
fail-closed arm, not the filename, so it keeps that arm with a genuinely
unowned fixture (`unowned-root-input.mk`, fictional and never touched on
disk — same convention as `test_unmapped_crate_path_fails_fast`), and a new
`test_dockerfile_is_static_control_not_a_planner_abort` pins the new
decision by asserting the mode, the coverage mode, the empty bucket list
and the reason string.

Sabotage-tested: removing `"Dockerfile"` from the set turns the new test
red; restoring it returns 44/44 green.

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

* test(architecture): fix drifted ratchet baselines and fail on slack (#7147)

Two shrink-only ratchets carried untracked slack, and a `<=` ratchet cannot
see it: a baseline sitting ABOVE the live list is an unclaimed budget for
exactly the growth the ratchet exists to refuse.

- `WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE`: 129 recorded, 126 live —
  three free vendor carve-out slots.
- `reborn_struct_test_support_ratchet.rs`: 80/277 recorded, 79/276 live —
  one free frozen dead-code path carrying one suppressed member.

Both baselines are set to the live counts, read off the compiler (zero the
constant, run the gate, read the panic) rather than counted by eye, and both
checks become equalities with a distinct message per direction, so a deletion
that forgets to lower the constant is red instead of silently banked.

Sabotage evidence (each restored to green afterwards):
- allowlist growth: 127 entries vs baseline 126 -> "ALLOWLIST grew to 127".
- allowlist slack: baseline 127 vs 126 live -> "1 entries of UNTRACKED SLACK".
- allowlist negative: entry + baseline raised together (the sanctioned
  carve-out path the message documents) -> green.
- struct growth: a real `#[allow(dead_code)]` field in a new production file
  plus its frozen entry -> "inventory grew to 80 paths / 277 members". With
  the OLD 80/277 baselines that identical input passes green — the defect.
- struct slack: baselines 80/277 vs 79/276 live -> "UNTRACKED SLACK of 1
  paths / 1 members".
- struct negative: an ordinary new production struct with no suppressions ->
  green.

Both gates also now assert they measured something non-zero, so a truncated
const cannot read as success. The WS0 summary table in
`reborn_restructure_baselines.rs` is refreshed: all three of its numbers were
the WS0 capture and every constant they describe had since moved.

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

* docs(checklist): strike the egress-threat text the same row already retracted

Review finding on #7141, verified in place. The WS4 egress row contradicted
itself: one bullet retracted the claim that "production binaries compile the
seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone
able to set it can redirect all credentialed vendor egress", and a later
bullet in the SAME row still asserted it verbatim, with a sized remediation
plan premised on it.

The retraction is the correct half: `RewriteNetworkTransport::from_env_value`
returns `HostRewriteMapError::UnavailableInRelease` whenever
`!cfg!(debug_assertions)`, and neither `[profile.release]` nor `[profile.dist]`
enables debug-assertions, so a release binary with the variable set refuses to
boot. Compiling the seam is not honouring it.

Kept as struck history rather than deleted — these rows are append-only — with
the accurate wiring facts preserved and the unsupported conclusion marked as
the thing not to act on. The remediation plan stays (a dev-only seam still
should not compile into production, which is exactly what
.claude/rules/cargo-features.md's `test-support` shape is for) but is re-framed
as hygiene rather than a vulnerability fix, since scheduling it as an open hole
would be acting on the withdrawn premise.

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

* ci(composition): bound composition's absolute production LOC (#7151)

The composition mass gate was share-based and therefore inert twice over.

Poisoned denominator: the metric is composition's fraction of ALL production
crate code, so feature inflow anywhere else improves composition's score while
composition itself grows. Measured on main across two days, composition took
+619 lines of feature inflow against -23 from an entire eviction wave, and its
share still FELL (658 bp -> 634 bp) because the workspace grew faster.

Inert ceiling: 634 bp observed against a 2398 bp ceiling is ~17.4pp of slack —
composition could roughly quadruple untouched. CHECKLIST WS0 records that slack
itself ("constrains nothing").

`[gate].loc_ceiling` bounds composition's production `.rs` LOC directly, on the
same numerator the share metric already computes (one definition, two bounds).
Baseline 44021, a real count on origin/main @ 676d86ce02, cross-checked two
ways that agree exactly: the gate's own `find`-based counter and a
git-tracked-only count, so a stray working-tree file cannot have set it.
Tolerance 150 — deliberately below the +619 inflow this exists to catch.
`loc_nudge_slack = 200` prints the re-ratchet reminder at every wave close.

The keys are REQUIRED, not optional-with-a-default, in both the shell schema
check and `reborn_restructure_baselines.rs`, so the binding metric cannot be
disarmed by deleting three TOML lines. The Rust record also asserts the ceiling
BINDS — a ceiling more than one nudge window above the recorded count fails,
which is the specific way the share ceiling went inert.

Sabotage evidence (all restored to green):
- +619 LOC into the real composition crate -> gate exit 1, "ABSOLUTE MASS
  EXCEEDED: composition holds 44640 production LOC, 469 over the effective
  ceiling of 44171" — while the share metric printed "NUDGE: mass is 17.56pp
  below ceiling", i.e. nowhere near firing. That contrast is the defect.
- delete `loc_ceiling` -> shell exit 1 "[gate].loc_ceiling must be an integer,
  got '<missing>'"; Rust test panics in `integer()`.
- `loc_ceiling = 0` -> exit 1, "must be greater than 0 — a zero absolute
  ceiling is a disarmed gate, not a bound".
- `loc_ceiling = 60000` -> Rust test red, "15979 LOC of unclaimed headroom,
  more than the 200-LOC nudge window".
Negative cases (must NOT trip, and do not):
- +619 LOC into ironclaw_webui (feature inflow elsewhere) -> exit 0.
- +120 LOC of routine wiring in composition (inside tolerance) -> exit 0.

Self-test grows 66 -> 76 assertions; L2 pins the poisoned-denominator scenario
end to end (share improves 30.00% -> 26.57% while the absolute bound fires),
and C11 pins that the committed ceiling itself is not slack.

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

* refactor(host_runtime): shed the catalog defaults downward (WS3 row 3)

CHECKLIST WS3 row 3 / PROPOSAL §6.5.9 asked for "extension
binding/catalog defaults → `extension_host`". That destination is
structurally impossible for the catalog half and the binding half is
refuted outright; both docs are corrected in this commit and the row is
closed against the corrected condition.

Catalog defaults — moved DOWN, not up. `ironclaw_host_runtime` is itself
a production consumer of both defaults (memory_native_extension.rs:96
and :101, inside the bundled-memory package builder §6.5.9 keeps), and
`ironclaw_extension_host` is layer `products` already depending on
`host_runtime` (`kernel`), so moving up would create an illegal
kernel→products edge and a Cargo cycle. Each default goes instead to the
crate that owns the vocabulary it enumerates:

  * `default_host_port_catalog` → `ironclaw_host_api::host_port`, beside
    the three port constants it lists. Its unit test moves with it.
  * `default_host_api_contract_registry` → `ironclaw_extensions::host_api`,
    beside the one contract it registers.

89 references across 30 files repointed; no `pub use` shim left in
`ironclaw_host_runtime` (§11.3), which keeps only the RootFilesystem-bound
`discover_extensions_*` fns that apply the defaults (extension_contracts.rs
151 → 99 lines). No crate gained a dependency, so LAYER_MATRIX_EXCEPTIONS
is unchanged at 4.

Binding — REFUTED and struck, not deferred. `RuntimeLaneExecutor`
(`pub(super)`) and `RuntimeLaneRequest` (`pub(crate)`) have zero
references in any .rs file outside `crates/ironclaw_host_runtime/`;
shedding `services/extension_tool_binder.rs` requires widening both to
`pub`, contradicting §6.5.9's own Keeps clause ("the closed
RuntimeLaneExecutor + lane adapters"). The binder's `Arc<dyn
LanePackageBinder>` handle already delivers the encapsulation the shed
was meant to buy.

Regression coverage: the moved
`default_catalog_registers_egress_storage_and_audit_ports` guard pins the
port set at its new home, and the host_runtime
`host_api_contract_composition` suite pins the contract registry through
production discovery. Both sabotage-verified — dropping the audit port
fails with "default catalog must contain host.events.audit"; dropping the
contract registration fails with UnknownHostApi
{ id: "ironclaw.capability_provider/v1" }.

Guidance travels with the change: the three crate AGENTS.md files, ADR
0002, and the memory-profiles contract doc all name the new homes.

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

* fix(operator): name the port call in LlmKeyStoreError::Store

Review finding on #7141. All five `OperatorSecretValueStore` calls — put,
contains, handles, read, delete — collapsed into one bare
`Store(OperatorSecretValueStoreError)`, so a store failure kept its stable
reason but lost which operation produced it. Carries a `&'static str`
operation name beside the source now; the delete-path log line in
`llm_config_service` emits it as `secret_store_operation`.

`&'static str` rather than an enum on purpose: it is diagnostic only, nothing
branches on it, and a caller that needs to branch should match the source.

The existing five-operation test was updated rather than replaced, and
STRENGTHENED — it now zips each error with the port call that produced it and
asserts the name, which is the property the variant exists to provide.

Sabotage-tested, and the first attempt was a false pass worth recording:
mislabelling `read` as `put` appeared green because `cargo fmt` had reflowed
the struct literal across four lines, so the single-line search string
silently matched nothing. Re-applied against the real text: RED,
"assertion `left == right` failed: store failure must name the port call it
came from, left: \"put\", right: \"read\"". Restored: 153 passed, 0 failed.

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

* refactor(cli): keep the rename flat; sever the app/ relocation to WS7 (WS6)

**Reverts the `crates/app/` family directory this branch created.** The
crate keeps its WS6 **rename** — `ironclaw_reborn_cli` -> `ironclaw_cli`,
package name `ironclaw` unchanged — at the flat path
`crates/ironclaw_cli`.

The defect was in the row, not in executing it. CHECKLIST WS6's CLI row
names `app/ironclaw_cli` as its rename target, and PROPOSAL §5's tree
confirms that destination — but family directories are WS7 (Wave 5), so a
Wave-4 row named a Wave-5 path. The row's own `[decision — severable]`
tag shows the authors knew a call was owed; it was never made, so
following the row literally does both halves at once. **Owner ruling
2026-08-04: Waves 0–4 close before anything touches Wave 5.** Severed.

This matters beyond tidiness: PLAN marks the WS10 nested-tree-safe gate
rewrites a hard prerequisite *before the first family `git mv`*, because
path-keyed gates fail **silently** under family directories rather than
loudly — #7083 (a coverage regex that blinded 11 crates the moment
`crates/extensions/` appeared) is the worked example. WS10 still has open
rows.

`crates/app/` was the **only** family directory this branch created;
`crates/extensions/` pre-exists on `main`.

**Recorded as a class, not an instance** (docs commit alongside): any
pre-WS7 row quoting PROPOSAL §5 inherits the same collision. The
established precedent is to land flat — WS1's three `contracts/ironclaw_*`
rows all say `contracts/` and all landed at `crates/ironclaw_*`; there is
no `crates/contracts/` directory. Two sibling rows carry the same defect
and are now flagged not-to-execute-as-written: WS3's
`lanes/ironclaw_sandbox` and WS4's `crates/lanes/wit/`.

**Also: the Reborn PR test planner could not classify a rename PR at all.**
`Detect Reborn test scope` failed the whole run — first on `Dockerfile`,
then on `clippy.toml` — and each fix surfaced the next, because
`reborn_pr_test_plan.py` fails closed on any unclassified path and had
never seen a diff of this shape. Fixed as a class:
- root workspace policy files decided: `clippy.toml`, `deny.toml`,
  `release-plz.toml` (beside the already-classified `Cargo.toml`);
- root scripts decided per-file as that set requires:
  `check_no_panics.py`, `dev_metrics.py`, `pre-commit-safety.sh`,
  `test-mutation-audit.sh`;
- prose/standalone trees ignored: `openwiki/` (generated wiki),
  `test-tools/`, `harness/` (standalone cargo project, own Cargo.lock);
- **`scripts/live_canary/`** added to the QA harness prefixes — the set
  listed only `scripts/live-canary/` and **both directories exist**,
  differing by hyphen-vs-underscore, so the underscore one fell through;
- files sitting directly in `crates/` (`crates/AGENTS.md`) classified as
  tree-wide prose — they belong to no package, so the crate arm raised;
- **paths removed by the diff** classified instead of fatal. This is the
  one that matters for the programme: renaming 11 crates puts ~600 deleted
  paths in the diff, none of which map to a package. Without it every WS6
  rename PR and every WS7 family move fails closed here.
- the shared-E2E-harness wall is kept but made *satisfiable*: a
  `DECIDED_E2E_HARNESS_PATHS` set records a decision. The guard's purpose
  is "changing a shared fixture must be deliberate"; as written it had no
  way to record a decision, so it blocked even a mechanical rename with no
  route forward. `tests/e2e/reborn_webui_harness.py` is decided (the E2E
  workflow owns it); everything else still raises, on both fail-closed
  arms.

Its self-test goes 43 -> 49. Two existing tests used as their *example* a
path this commit classifies; both keep their invariant with an undecided
fixture instead. **Sabotage-tested each new arm**: disabling the
removed-path arm, emptying the decided set, and disabling the `crates/`
prose arm each turn the suite red; restoring returns green. The prose arm
initially passed while sabotaged — it had no test — which is precisely the
green-while-checking-nothing shape, so a test was added and the sabotage
re-run to confirm it now fails.

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

* fix: repoint crate names reintroduced by the merge-down from main

`git merge origin/main` (fb776f3c62) was conflict-free — main's new work
touches files this branch had not edited — which is exactly the shape that
reintroduces stale crate names silently. 77 occurrences across 33 files,
found by re-scanning for every old name after the merge rather than
trusting the clean merge.

Dated historical prose under `docs/reborn/target-architecture/` is
deliberately excluded: those rows record what was true when they were
written, and rewriting them would misrepresent the record.

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

* test(architecture): inventory same-layer dependency edges (#7149)

`layer_allows_dependency` is reflexive, so an edge between two crates in the
same layer is legal by construction: it never reaches the violation branch, no
`LAYER_MATRIX_EXCEPTION` can exist for one, and the matrix cannot see it.
PROPOSAL §8.1's 2026-08-02 amendment records the hole and measured 72 such
edges; WS10 has no gate for it.

Measured on origin/main @ 676d86ce02: 391 workspace normal edges, 73 of them
same-layer (34 substrates, 15 kernel, 10 products, 7 loops, 5 contracts, 1
runtimes, 1 app). Recounted, not inherited — #7149 quotes 68 and the amendment
72, from earlier trees. Counting method: deduplicated (crate, dependency) pairs
from `cargo metadata --no-deps` where both ends declare the same layer and the
dependency kind is `normal` — the same filter the layer-matrix gate applies, so
the two measure one graph.

`SAME_LAYER_EDGE_INVENTORY` is the missing default guard, shaped like
`LAYER_MATRIX_EXCEPTIONS`: complete (a 74th edge is red), non-stale (a deleted
edge is red), shrink-only in BOTH directions (growth is new coupling, slack is
an unclaimed budget for it — #7147's lesson applied from the start), and
tracked (owner = the consumer's §5 family, `decided_in` = the CHECKLIST
workstream that owns it; placeholders count as missing). The doc comment is
explicit that `decided_in` is not a deletion promise: some same-layer edges are
permanent by charter.

Second rule: a downward re-layer must land with a consumer-side pin.
`CRATE_LAYER_ORIGINS` freezes each crate's FIRST declared layer, derived from
`git log` over all 67 layered crates rather than assumed — exactly one downward
re-layer has ever happened (`ironclaw_extensions` loops -> substrates, #7094),
alongside two promotions (`hooks`, `runner`) which need no pin because moving up
narrows reach. A live layer below the origin is therefore a permanent,
detectable demotion, and the gate then demands a `DowngradePin` whose frozen
consumer set is enforced on every commit. A layer ceiling would not bite:
`extensions` moved down precisely so kernel/runtimes could reach it, so only an
explicit consumer set constrains anything.

Sabotage evidence (each restored to green):
- NEW same-layer edge `slack_extension -> host_ingress` (products->products):
  this gate RED with "NEW SAME-LAYER DEPENDENCY EDGE(S)" and the ready-to-paste
  row, while `reborn_workspace_crates_declare_layers_and_follow_layer_matrix`
  on the IDENTICAL input stayed GREEN. That contrast is the defect.
- stale row (drop `threads -> safety`) -> "names edges that no longer exist".
- slack (baseline 74 vs 73) -> "1 entries of UNTRACKED SLACK".
- growth (baseline 72 vs 73) -> "inventory grew to 73 (baseline 72)".
- untracked entry (`decided_in: "TBD"`) -> "missing `decided_in`".
- demote `host_ingress` products -> substrates, reproducing #7143 ->
  "DOWNWARD RE-LAYER WITHOUT A CONSUMER-SIDE PIN".
- new consumer of the demoted `extensions` -> "reach taken after the loops ->
  substrates demotion without review".
- a permitted consumer that stops depending on it -> stale-pin failure.
Negative cases (must NOT trip, and do not):
- a legitimate CROSS-layer edge (operator products -> threads substrates).
- a PROMOTION (host_ingress products -> app) demands no pin.
- the sanctioned deletion: drop the edge, its row, and the baseline together.

Scanned-something guards throughout: floors on layered-crate and edge counts,
a non-empty live set, non-empty inventory, duplicate-row rejection, unknown
declared layers fail loudly, and every pinned consumer must resolve to a real
layered package.

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

* revert(skills): restore the hidden-field install guards — the review finding was wrong

Reverts the resolver change from b57ac8e59f. That commit acted on a review
comment claiming `resolve_install_input` wrongly rejected inline bundle
installs and wrongly dropped url-path conflicts. Both halves are REFUTED by
pre-existing integration tests I failed to consult before changing behaviour,
and CI caught it: `first_party_builtin_tools` went 205 passed / 2 failed.

  * `builtin_skill_install_rejects_hidden_url_install_fields` asserts inline
    `content` + `files` / `source` / `source_url` is REJECTED with InputEncode
    and nothing is written to disk. My change accepted it.
  * `builtin_skill_install_url_path_ignores_caller_supplied_hidden_bundle_files`
    asserts url + caller `files` SUCCEEDS with `files_installed == 0` — the
    caller's files silently dropped. My change rejected it.

The asymmetry is deliberate, not a defect. `files`, `source` and `source_url`
are PROVENANCE fields the resolver sets itself on the url path; a caller may
never supply them. Accepting them inline would let a caller forge provenance —
claim an inline skill came from a trusted URL — or smuggle bundle files past
the fetch. `dispatch_install` reading `files` is not evidence a *caller* may
send it: that support exists for the rewritten payload this resolver builds.

My two unit tests encoded the wrong contract and are removed rather than
adjusted. The reasoning is now a comment on the match itself, naming both
integration tests, so the next reader does not re-propose either change.

After: first_party_builtin_tools 206 passed, 0 failed.

Lesson recorded because it is the general one: "verify first" means checking
for existing tests that pin the behaviour, not only reading the downstream
function's shape. I checked `dispatch_install` and stopped too early.

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

* test(architecture): census LLM-vendor names in the contracts family (#7150)

§12.11 D-E amended §8.2 to sanction LLM-vendor administration vocabulary in
`ironclaw_product_contracts::operator_llm` — "that module and nowhere else in
the contracts family" — and owed a vendor-name census with the amendment,
because `reborn_extension_specificity.rs` cannot see this surface at all:
`nearai` is removed globally by its TERM_COLLISIONS and `codex`/`openai`/
`anthropic`/`claude`/`gpt` are not derived terms in any package manifest. D-E
says so itself: without the census "the bound is review discipline rather than
enforcement". The census existed on no ref. This is it.

Scope is the whole contracts family, not one file: "nowhere else in the
contracts family" is a claim about the family, and a census scoped to
`operator_llm.rs` cannot check it. Roots resolve through `cargo metadata`
manifest paths, so the WS7 family move cannot take it dark.

⚠ FINDING — D-E's "nowhere else" is not true today. The census turns up a
second LLM-vendor surface D-E did not know about: `ironclaw_common::llm_costs`,
a per-model price table naming 9 distinct vendors across 91 occurrences
(claude, gpt, sonnet, opus, haiku, codex, mistral, deepseek, llama), invisible
to the specificity scanner for exactly the same reason `operator_llm` is. The
gate does not delete it — that is a product decision — but it names it, freezes
it, and refuses to let it grow, which the honour-system could not. Two further
matches are classified rather than waved through: `prompt_envelope`'s
"you are chatgpt" is a safety DENYLIST (removing the term weakens the
detector), and `attachment_format`'s `opus` is the Opus AUDIO CODEC, handled by
a path-scoped term-collision carve-out that itself fails the day it stops
matching.

D-E's three bounds are enforced as numbers AND as an exact roster, so a rename
that swaps one vendor for another cannot pass with the counts unchanged:
6 vendor-named DTOs, 3 vendor-named methods, 2 distinct vendors. Extraction
finds exactly D-E's stated 3 methods + 6 DTOs.

Baselines measured by the gate's own scanner on origin/main @ 676d86ce02, so
the baseline and the measurement can never disagree about method: operator_llm
16 occurrences / 2 vendors; llm_costs 91 / 9; prompt_envelope 1 / 1. Counts are
equalities — growth is new coupling, slack is an unclaimed budget for it
(#7147).

The comment/`#[cfg(test)]` strippers are LOCAL, not added to `ratchet_support`:
the shared `strip_comments_and_strings` blanks string CONTENTS, which a vendor
census must not do (a provider id hides in a string literal), and changing the
shared lexer would put a behaviour change under thirty other ratchets to serve
one caller. Both have fixtures.

Sabotage evidence (each restored to green):
- a SEVENTH vendor DTO (`AnthropicLoginStart`) -> RED "NEW VENDOR-NAMED ITEM";
  the specificity scanner on the IDENTICAL input stayed GREEN.
- a FOURTH provider login (`start_gemini_login`) -> RED.
- a vendor name in an un-censused family file (`host_api`) -> RED "LLM-VENDOR
  NAME IN AN UN-CENSUSED CONTRACTS-FAMILY FILE"; specificity scanner GREEN.
- growth inside a censused scope (one more model row) -> RED census drift.
- slack (census records 95 against 91 live) -> RED census drift.
- a RENAME `CodexLoginStart` -> `GeminiLoginStart`, counts unchanged -> RED.
- a narrowing that forgets to lower the ceiling -> RED "defines 5 vendor-named
  DTOs; §12.11 D-E bounds it at 6".
- removing the Opus MIME alias -> RED stale carve-out.
- emptying LLM_VENDOR_TERMS -> RED "would pass having looked for nothing".
Negative cases (must NOT trip, and do not):
- a non-vendor production addition to the contracts family.
- a vendor name added inside a `#[cfg(test)]` block and a doc comment.

A matcher bug was caught by writing the fixtures first: `_` had been treated as
identifier-internal, so `start_nearai_login` did not match `nearai` and the
surface read as six items instead of nine. `_` is a word separator; `llama`
still does not fire inside `ollama`. Both directions are pinned in the
self-test.

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

* test(architecture): make the two new gates visible to CI's test-name filter

Both gates added in this PR were INERT in one of the two lanes that run them,
and the sabotage suites did not catch it because they invoke cargo directly.

`code_style.yml` runs `cargo test -p ironclaw_architecture reborn`. That
argument is a **test name** filter, not a path filter — the file being called
`reborn_same_layer_edge_inventory.rs` selects nothing. Under the exact command
CI uses, both binaries reported `running 0 tests`. Measured, then fixed, then
re-measured: 0 -> 6 and 0 -> 5.

Every test function now carries the `reborn_` prefix the crate's other 45
filter-visible tests already use, and both module docs record the trap so the
next gate added here does not repeat it. The test roster was diffed before and
after the rename: 11 functions, 11 functions, none lost.

Context for reviewers, measured while diagnosing: the crate has 217 `#[test]`
functions and that filtered step runs 45 of them. The other 172 are NOT dark —
`reborn-tests.yml`'s crate-bucket lane runs `cargo test -p ironclaw_architecture
--all-targets` with no filter, so they execute there. The filtered step is a
narrower smoke, not the only lane. Naming these gates to the convention means
they run in both.

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

* docs(target-architecture): record the four enforcement additions and two findings

Target-architecture docs are the single source of truth, so each gate and each
measurement in this PR lands here rather than only in a PR body.

CHECKLIST WS10 gains three rows — the same-layer inventory, the downward
re-layer pin (#7149), and D-E's vendor census (#7150) — each carrying its
baseline and counting method.

CHECKLIST's WS10 composition-ratchet row is answered rather than left standing:
"the composition-mass ceiling is already ~17.4pp slack and constrains nothing"
could never be fixed by re-capturing `ceiling_bp`, because the share metric's
denominator is every other crate's production code. The original sentence is
kept as the record of why; the note adds the absolute bound (#7151) and the
+619/-23 measurement that motivated it.

PROPOSAL §8.1 rule 1's amendment is annotated: the plane it measured is now
inventoried and enforced, and the recount is 73, not 72 — the kernel and loops
buckets moved.

PROPOSAL §8.2's amendment and §12.11 D-E both carry the census result, including
the part that contradicts the ruling: "nowhere else in the contracts family" is
not true today, because `ironclaw_common::llm_costs` names 9 vendors across 91
occurrences and was invisible for exactly the reason D-E gives for
`operator_llm`. Recorded as a frozen residue with the obvious candidate fix
(move the cost table beside the `llm` providers, which §8.2 already sanctions),
not silently corrected.

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

* ci(test-plan): classify the whole repo-root metadata class, not one file per red run

`.gitattributes` is touched by this PR (the rename left its `wix/main.wxs`
rule pointing at `crates/ironclaw_reborn_cli/`, a path that no longer
exists), and the planner fails closed on unclassified paths — so it aborted
`Tests (Reborn)` with "unclassified pull-request path: .gitattributes".

Every entry already in this set was added the same way: a rename-shaped diff
touches root files a feature PR never touches, the planner dies on the first
one, and the next only appears after that one is fixed — Dockerfile, then
clippy.toml, then six more. Rather than add a ninth, this enumerates the
remaining class: all 19 unclassified root paths were found by driving the
planner over every tracked root file, and 17 are listed.

The two that are NOT listed are the point. Membership requires that no
Reborn test lane reads the file, checked per file against `crates/**/*.rs`
and `tests/**`. That check found real readers for `.dockerignore`
(`tests/dockerfile_runtime_home.rs`) and `.env.example` (`ironclaw_cli`,
`ironclaw_host_runtime`), so both stay fail-closed. Classifying a file a
test depends on would silently skip that test — worse than an aborted
planner.

Verified: planner self-test 48/48; every tracked root file except those two
now classifies; the full PR diff plans without error.

* fix(ci): repoint the test-scope classifier off the dead `ironclaw_reborn_*` glob

`Fast deterministic checks` failed on `test-classify-test-scope.sh`:

    FAIL reborn binary crate
    Expected: has_legacy_tests=false has_reborn_tests=true
    Actual:   has_legacy_tests=true  has_reborn_tests=false

`is_reborn_test_path` matched the CLI through `crates/ironclaw_reborn_*/*`.
The WS6 renames dropped that prefix from all seven crates that carried it, so
the glob now matches **nothing** and every one of them silently reclassified
as legacy. Enumerated the seven new names instead of re-globbing: they share
no prefix, and this is the second time a prefix glob has rotted here.

Fixed the classifier, not the fixture. The self-test's expectations describe
the intended behaviour; flipping them to match the break is how a gate goes
quiet.

**This class fails OPEN**, which is why only one crate's assertion caught it —
the classifier keeps answering, just wrongly. Added a guard asserting every
`crates/…` pattern in the classifier matches at least one real path, the same
shape as `sanctioned_paths_all_match_real_files`: an exemption may not outlive
the code it exempts. Two pre-existing dead arms
(`crates/ironclaw_extension_support/`, `crates/ironclaw_oauth/`) are listed
known-dead and shrink-only rather than repointed — both match nothing today,
so neither is load-bearing, and repointing them would change which tests those
crates select. That is a behaviour change, not this PR's business.

Swept the siblings: every `crates/<name>` literal and glob stem across
`scripts/`, `.github/`, and the architecture tests was checked against the
real tree. The only dead reference attributable to the 13 WS6 renames is the
one fixed here; the rest are synthetic self-test fixtures or crates deleted
long before this branch.

Sabotage-tested both, confirming red with the RIGHT message and green after
restore: (1) restoring the dead glob reproduces `FAIL reborn binary crate`;
(2) adding `crates/ironclaw_totally_invented/*` trips the new guard with
`classifier pattern matches no real path`.

Also recorded the ALLOWLIST union recount in the constant's own doc comment:
this branch carried 129, `main` 125, and the merge inherited 125 without
measuring. Recounted off the compiler (constant → 0, read `ALLOWLIST grew to
125 entries`): 125 is the live count with zero slack (#7147).

* fix(capabilities): make the auth-required enrichment total, dropping its unreachable!

The host.rs split moved `enrich_dispatch_error_credential_requirements` into
`host/error_mapping.rs`. The code was byte-identical to its pre-split form
(`host.rs:3649` at the merge base), but the move made the file a *changed*
file, so the changed-lines panic scanner
(`check_no_panics.py --base <base> --head HEAD`) scanned it for the first time
and flagged the `unreachable!("matched AuthRequired above")`.

The scanner was right that the panic was there, and the honest fix is to remove
it rather than annotate it. The function destructured `error` twice: once by
`ref` to inspect, then again by value to take ownership, with an `unreachable!`
covering the second match that the first had already proven. `AuthRequired` has
exactly three fields, so a single by-value `match` with a guard is total: the
guard only borrows, so a non-enriching outcome falls through to `other` with
`error` un-moved, and the enriching arm rebuilds the variant from parts it
already owns. No branch is left to assert.

Behavior is unchanged and pinned: 158/158 `ironclaw_capabilities` tests pass,
including the six `enrich_*` unit tests and the caller-level
`invoke_json_*`/`auth_resume_json_*` contract tests. Sabotage-tested — dropping
the derived requirement from the enriching arm fails
`enrich_fills_empty_from_single_credential_obligation` with `left: 0, right: 1`,
so the guard checks what it claims.

Both scanner modes verified, because they disagree by design: the changed-lines
mode honors only inline `// safety:` comments and never reads the baseline,
while `--reborn-baseline` rejects stale entries as well as new ones. Removing
the panic therefore made the baseline row stale, so it is deleted in the same
commit — a real downward ratchet, 51 -> 50 reviewed invariants, not a repoint.

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

* refactor(capabilities): return the authorization policy helpers to authorize

Two review findings on the host.rs split, both confirmed against the code.

`error_mapping`'s module doc says outright that nothing in it may make a policy
decision — "it only renames one that was already made". Three items contradicted
that: `WITNESS_DEFAULT_TTL` and `witness_deadline` decide how long a sealed
authorization witness stays valid, and `permission_mode_allows_persistent_approval`
classifies which permission modes an "always allow" decision may upgrade. Both
are authorization policy. They move to `authorize.rs`, which already owns the
verdict, leaving `error_mapping` as the translation-and-cleanup seam it claims to
be. Their only callers were `authorize.rs` and the test module, so this is a
visibility-neutral move: still `pub(super)`, no widening.

Verifying that finding surfaced a second defect the review did not name, in the
same class as the `authorize`/`evaluate_trust` doc slip reported beside it. The
split had fused two doc comments onto one item: the ten-line paragraph describing
`permission_mode_allows_persistent_approval` sat directly above
`WITNESS_DEFAULT_TTL`, so the constant carried someone else's documentation and
the function it described had none at all. Each doc is reattached to its own item.

The reported slip is fixed the same way: the pre-dispatch authority-fold paragraph
was left on `evaluate_trust` while `authorize` — the function it describes — had
no doc comment. Moved onto `authorize`.

Text is carried verbatim in every case; no doc was reworded, and no behavior
changed. `ironclaw_capabilities` 158/158 pass, clippy clean.

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

* fix(docs,ci): correct the guest WIT path and delete a test that never ran

Two confirmed review findings, both verified before acting.

`building-a-channel.mdx` told channel authors to point `wit_bindgen::generate!`
at `../../crates/ironclaw_wasm/wit/channel.wit`. From a guest crate at
`crates/extensions/packages/<name>/wasm-src` — the layout the page describes and
the one the Slack package uses — that resolves nowhere. The correct relative path
is four levels up, `../../../../ironclaw_wasm/wit/channel.wit`, confirmed with
`os.path.relpath` against the real tree. The trailing "Adjust path as needed"
hint is replaced by a comment naming the directory the path is relative to, so
the reader can tell when it needs adjusting rather than guessing.

`test_reborn_pr_test_plan.py` defined
`test_shared_e2e_harness_remains_an_explicit_mapping_error` twice in one class,
at lines 368 and 546, with byte-identical bodies. Python keeps the last binding,
so the first never ran — a test present in the file and absent from the suite.
Removed the shadowed copy and kept the live one.

Proven rather than assumed: the suite reports 52 passed / 51 subtests both before
and after the deletion, which is what confirms the removed definition was
contributing nothing. No assertion was dropped.

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

* fix(ci): restore the composition-budget negative case the rename collapsed

T4 asserts the budget gate fails LOUDLY when the composition crate is absent.
It builds a fixture under the crate's real name and renames it away so the
gate cannot find it. The destination was hard-coded `ironclaw_composition` —
which is exactly what the WS6 rename turned the crate's real name into, so
both sides of the `mv` became the same path.

`mv X X` does not rename; it tries to nest a directory inside itself and dies
with "Invalid argument". The negative case stopped running.

Renamed the destination to `composition_renamed_away` — deliberately
synthetic, so no future crate rename can collide with it again — and wrote the
reason into the test.

Found by running the nine `Static-check self-tests` scripts that CI never
reached: that step stops at the first failure, so fixing the classifier only
uncovered what was behind it. Ran all of them, plus the nine skipped steps
after it, rather than discovering them one CI cycle at a time. This was the
only other failure; the other seventeen checks pass.

Sabotage-tested: skipping the rename (so the crate is present) makes T4 fail
with `expected exit 1, got 0` and the missing-message assertion — 49 passed,
2 failed. Restored: 51 passed, 0 failed. The case genuinely exercises the
absence again rather than passing because it never ran.

* test(host-api): pin the process-sandbox capability literal as a valid id

Partly accepts a review finding. The reviewer asked for a typed
`CapabilityId` accessor beside `PROCESS_SANDBOX_CAPABILITY_ID`, on two grounds:
the comparison sites are stringly, and the literal is never validated by
`CapabilityId::new`.

The second ground is real and is the one worth closing. The constant is compared
as a `&str` on two *gating* paths — the kernel spawn check
(`production.rs:1580`) and the process executor's routing check
(`process_executor.rs:185`) — and a malformed literal would not fail there: the
comparison would simply never match, so sandbox plans would quietly stop being
recognised. That is a fail-open, and nothing in the tree pinned the literal's
validity.

The proposed accessor is declined, with the reason. `CapabilityId::new` is
fallible, so the accessor must return a `Result`, which puts error handling on
two hot gating comparisons to re-derive a fact that is fixed at compile time —
and it would not make those sites typed anyway, since both compare against a
value they already hold as `&str`. A test costs nothing at those call sites and
closes the same gap: the literal is now checked to parse, and to round-trip
through `CapabilityId::as_str` unchanged.

Sabotage-tested: mutating the literal to `"system.process sandbox.run!"` fails
the guard, so it checks what it claims.

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

* test(ci): pin the pre-commit staged-path selector after the WIT move

Wave 3 moved the WIT directory into its owning crate, which changed
`.githooks/pre-commit`'s staged-path selector from `^wit/` to
`^crates/ironclaw_wasm/wit/`. A path-literal gate fails silently: move the
directory it names and the hook keeps exiting 0, so version-bump checks stop
running and nothing reports it. Repo guidance requires a behavior-changing hook
to land with a regression test; there was none.

The test matches through `grep -E` so it sees the hook's own regex dialect
rather than Python's, and it extracts the pattern from the hook instead of
restating it, so a restructured selector fails loudly rather than leaving the
test asserting a copy of itself. Wired into the reborn-tests step that already
runs `test_reborn_pr_test_plan.py` — `scripts/test-pre-commit-safety.sh`, the
existing precedent for a hook self-test, is referenced only in a comment and is
run by no workflow, so following it would have added a test nothing executes.

Writing it surfaced a pre-existing finding: the hook also gates `channels-src/`
and `tools-src/`, and neither directory exists — here or on `origin/main`
(`git ls-tree origin/main` returns neither), so they are dead literals this
branch did not create. `check-version-bumps.sh` carries the same two prefixes.
Asserting them away would make this branch red for someone else's debt, so they
are pinned as a known-missing set instead: a *new* dead prefix fails the test,
while the existing two are recorded where the next reader will see them.

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

* style: cargo fmt after the #7155/#7062 merge

`Check formatting` (step 6 of Fast deterministic checks) went red on
cca5884b47: the merge was pushed under time pressure without running fmt.

Only the two files whose crate references I rewrote by hand are affected —
`ironclaw_reborn_composition` -> `ironclaw_composition` is 9 characters
shorter, so call sites that were wrapped at the old width now fit on one line.
No semantic change.

* chore(ci): re-seed composition loc_ceiling at the merged-tree count (44392)

Merging main @ be33ae138f into this branch brought #7062's +371 production
LOC of composition wiring, and the new absolute-mass gate correctly went
red against its own merge context (44392 observed vs 44021+150 effective
ceiling — the exact failure CI showed). Re-measured on the merged tree with
the gate's own counter and re-seeded to current, not padded, per the
manifest's ratchet convention. Gate + its 76-case self-test green locally;
both new architecture gates (same-layer inventory, vendor census) pass on
the merged tree.

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

* chore(ci): move the absolute-mass record with its re-seeded ceiling (44392)

The nudge-window assertion refused a ceiling that moved without its
record (44392 - 44021 = 371 > 200) — which is precisely the binding
property this PR adds; the previous commit re-seeded the manifest and
left the test's record behind. Full ironclaw_architecture suite green
on this tree.

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

* WS5: repoint conversations' turn vocabulary to host_api; record the sever fork

The `conversations -> turns` sever cannot land as specified. CHECKLIST WS5 and
PROPOSAL §6.4.2/§8.3 all name "the product tier" as the destination for the
inbound submit orchestration; §8.2's own retained named rule
("untrusted-ingress paths never construct trusted trigger submitters") and the
two gates that implement it forbid exactly that. §6.4.2 also contradicts itself
in one paragraph: its charter retains the trusted-trigger submitter while its
Deps clause drops the coordinator that submitter holds.

Landed here — the half that is fork-independent and required by every
resolution: the ten `host_api`-owned turn names this crate uses now import from
`ironclaw_host_api::turn` instead of travelling through the `ironclaw_turns`
re-export hop (§11.2.4 two-import-paths, the same repoint the WS3 mcp row took
for free on `ResourceReceipt`). No manifest change, no behaviour change; the
residual is now exactly two turn-crate-owned names (`SubmitTurnResponse`,
`TurnError`) plus the orchestration.

Recorded — measurements, sizing, the destination refutation and both candidate
resolutions with their costs, on the CHECKLIST WS5 row, in PROPOSAL §6.4.2, and
in the exception entry's own `reason`. The register is unchanged at 4: the edge
still exists, so deleting its entry would fail the staleness gate and lie.

Verification: cargo test -p ironclaw_conversations --no-fail-fast 97/97;
cargo test -p ironclaw_architecture --no-fail-fast 211/211;
clippy --all-targets --all-features -D warnings clean on both;
cargo check --workspace --all-targets clean (one pre-existing dead_code warning
in ironclaw_extension_support, present on the base).

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

* WS5: record the trigger-poller bound mapping and the step-1 blocker

Fork resolved by the coordinator under delegated authority: the "product tier"
prescription is struck (THE CODE WINS over §6.4.2/§8.3), and the resolution is
delete-the-dead-half + move-the-live-half to composition. Executing it stops at
step 1.

Bound mapping (the review-critical artefact): production wiring instantiates C
as RebornFilesystemConversationServices. ConversationContentRefMaterializer
needs only ConversationBindingService and invokes exactly one method
(resolve_or_create_binding_with_trusted_scope). The InboundConversationService
bound exists solely for trusted_trigger_fire_submitter -> InboundTurnService,
which invokes all six of its methods -- so the trait is not dead and the
submitter cannot move without the orchestration it wraps.

STOP at step 1, per the resolution's own stop condition. handle_inbound_turn is
production-uncalled but not dead: deleting it and running the unfiltered suite
surfaced 37 E0599 across 22 test functions (33 in tests/inbound_contract.rs, 4
in inbound.rs's module) plus the compiler's own "variant Untrusted is never
constructed". Among them,
untrusted_trigger_adapter_records_product_inbound_not_scheduled_trigger is the
sole executable proof that an untrusted adapter cannot spoof TrustedTrigger
classification. Deletion refused; no test weakened. Deletion reverted, tree
byte-identical, 97/97 green.

Also recorded: the workable shape (move both entry points + all 22 tests, gate
the untrusted entry behind composition's existing test-support feature) at its
true cost of ~540 production + ~2,224 test lines, against the ~62-100 the move
was scoped at; and the one residue that must be settled first, SubmitTurnResponse,
which sits in the RETAINED ledger contract rather than in the moved code and so
needs to descend to host_api::turn before the manifest dep can drop.

Verification: cargo test -p ironclaw_conversations --no-fail-fast 97/97;
cargo test -p ironclaw_architecture --no-fail-fast 32/32 binaries green.

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

* WS3: lanes consume a narrow reserve/reconcile/release port (#7067)

Dissolve the last two `runtimes -> kernel` layer-matrix exceptions,
`ironclaw_mcp -> ironclaw_resources` and `ironclaw_sandbox ->
ironclaw_resources`, by inverting the seam rather than relocating the
kernel's budget authority (PROPOSAL 8.3 row 7's 2026-08-04 amendment
rules the relocation out).

`ironclaw_host_api::resource` declares `RuntimeResourceBudget` — reserve
/ reconcile / release only, typed on shapes that crate already owned —
plus a narrow classified error (`RuntimeResourceError` +
`RuntimeResourceErrorKind`). `ironclaw_resources` implements it over any
`ResourceGovernor` as `GovernorRuntimeBudget` and owns the
`ResourceError` projection, which is subtractive by design: the
classification survives whole (LimitExceeded and RequiresApproval stay
distinct) while account/limit/dimension values stop in the kernel. Both
lanes drop `ironclaw_resources` from `[dependencies]`; it stays a
dev-dependency so the lane suites keep driving the port over the real
governor.

Behavior-free at the effect level: same authority calls in the same
order, and `model_visible_cause` is byte-identical because the
projection carries the authority's own rendering.

Regression coverage at the lane seam: the existing budget-denial tests
now assert classification and preserved wording; new tests pin that an
approval pause stays distinct from a hard denial, and that the
prepared-reservation path reuses a matching hold and rejects a
mismatched one before any side effect (that path had no lane-seam
coverage before).

LAYER_MATRIX_EXCEPTIONS 4 -> 2 and WS0_LAYER_MATRIX_EXCEPTION_BASELINE
lowered by 2 in the same change. Closes #7067.

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

* WS5: descend SubmitTurnResponse to host_api::turn; record the port-inversion shape

Coordinator decision: NOT relocation. Orchestration stays in
ironclaw_conversations; the crate will declare a narrow submission port that
composition implements with the coordinator handle it already constructs
(dependency inversion, type-placement rule 2). Both earlier candidates struck.

Pre-build gate verification (ordered before any code) - BOTH PASS:
(a) trusted_trigger_submit_request_minting_stays_worker_owned polices the string
    "TrustedTriggerSubmitRequest {" - the triggers-owned fire request - and says
    nothing about SubmitTurnRequest. No refutation.
(b) Six-method bound mapping re-run against the port surface: the coordinator
    handle is touched at exactly ONE call site (submit_turn, inside
    submit_or_replay), so the port is a one-method trait. TurnErrorCategory and
    adapter_status_code are named only in this crate's TESTS, never in
    production, so the port error needs three equivalence classes, not the
    kernel denial cone: rotate+retryable {ThreadBusy, Unavailable,
    AdmissionRejected(TenantLimit|Unavailable)}; keep+retryable
    {CapacityExceeded, Conflict}; keep+rejected {everything else}.

Landed here - the precondition: SubmitTurnResponse descends from
ironclaw_turns::response to ironclaw_host_api::turn. Every field type was
already that module's, so zero new dependencies; re-exported through
ironclaw_turns' already-documented host_api::turn facade, so no call site
outside the two crates changes (no-shim rule satisfied via a sanctioned facade).

Effect: traits.rs, types.rs, memory.rs and conversation_state_store.rs are now
completely free of ironclaw_turns - the retained ledger contract no longer names
the kernel. Production residue is exactly the orchestration in three files
(inbound.rs, trusted_trigger.rs, error.rs), which the port removes.

Also recorded for the port build: product_context::{InboundClassification,
resolve_inbound} is turns-owned and must become a conversations-declared typed
classification (it is the trust distinction the spoof-proof test pins); and the
crate's AGENTS.md/CLAUDE.md invariant naming ironclaw_turns::TurnError must be
amended in the port change rather than silently contradicted.

Verification: conversations+turns+host_api 553/553; ironclaw_architecture
207/207; clippy --all-targets --all-features -D warnings clean on all four;
cargo check --workspace --all-targets clean; fmt clean.

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

* WS10: convert the loud path-keyed gates to inventory keying before the family moves

Executes the WS10 CHECKLIST row "Loud path-pattern inventory updated with the
moves". #6946/#6996 fixed the SILENT path-keyed gates; the loud ones were
deferred because they fail visibly at the `git mv` — but only by demanding a
lockstep sweep of ~450 literals in the same commit that moves 65 crates.

Gates keep their readable flat `crates/ironclaw_x/...` spelling and now RESOLVE
it through the crate inventory: the literal is a crate NAME plus an in-crate
remainder, not a directory path. On today's tree resolution is the identity
(the behavior-free proof); after Wave 5 the same literal resolves to the new
directory with no edit.

- ratchet_support gains the Rust half of scripts/ci/lib/crate_tree.py's rule
  (crate_directories / crate_directory / crate_dir / crate_path /
  resolve_crate_relative / owning_crate_name), pinned equal to the Python
  inventory by the new reborn_crate_inventory.rs.
- Converted: ~108 literals in reborn_dependency_boundaries.rs, ~215 in
  reborn_extension_specificity.rs, 79 FROZEN_PATH_COUNTS in
  reborn_struct_test_support_ratchet.rs, plus the single-site gates and
  reborn_sealed_evidence_mint_ratchet's owning_crate.
- Scripts and workflows: 28 WebUI-frontend sites, docker.yml's VERSION
  extraction, nightly-deep-ci's mutation target, check-version-bumps.sh,
  reborn_pr_test_plan.py, classify-test-scope.sh, cut_ironclaw_release.py,
  quality_gate_strict.sh, run-hermetic-deterministic-suite.sh,
  run-reborn-webui.sh, scrub-artifacts.sh, audit_surface_inventory.py,
  slack_helpers.py — all via the new scripts/ci/crate-dir.sh, and every
  rewrite pinned in scripts/ci/ws12_workflow_contracts.py.

Four defects surfaced, all live on the flat tree, none needing Wave 5:
1. reborn_extension_specificity.rs's fail-open registration guard joined
   crates/<package name>/ and so has been checking ZERO crates since WS2
   colocation renamed the directories.
2. reborn_dependency_boundaries.rs:37/:89 would have skipped every crate under
   a move, both behind a `continue`.
3. reborn_sealed_evidence_mint_ratchet::owning_crate took the first component
   under crates/, mis-attributing mint sites in a security-critical census.
4. Production: ironclaw_extension_host/build.rs derived the repo root with two
   .parent() hops, then read <root>/skills. One family level deeper that root
   is crates/, and the script writes [] for both bundles and returns Ok(()) —
   a green build shipping a binary with no bundled Reborn skills. Fixed, and
   reborn_build_script_roots.rs now bans the counted-hop idiom.

Evidence, both directions on the same tree (crates/substrates/{ironclaw_llm,
ironclaw_webui}, manifests repointed): base main 200 passed / 7 failed;
this change 219 / 0; back on the flat tree 219 / 0. cargo fmt --check and
clippy clean; eleven script self-tests green.

The CHECKLIST row is amended in the same diff and stays OPEN — the residue that
must travel with the move (Cargo manifests, wit_bindgen paths, include_str!,
the panic baseline, the Dockerfile) is listed there verbatim.

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

* WS10: pin the hermetic suite's WebUI frontend resolution

`scripts/ci/run-hermetic-deterministic-suite.sh` resolves the WebUI frontend
directory through `scripts/ci/crate-dir.sh`; without a pin, a literal
`crates/ironclaw_webui/frontend` regressing back in is a silent break — the
suite would `cd` into a directory that used to exist and report nothing wrong
until the frontend build actually runs.

The assertion matches the exact removed literal (with the `/frontend` suffix)
rather than the bare crate name, so it does not trip on its own explanatory
prose, and it also requires `resolve_webui_frontend_dir` to still be present.

Regression test: `bash scripts/ci/test-hermetic-test-process.sh` -> OK.

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

* fix(ci): restore the entry tail the exemptions-union resolution dropped

Git kept the shared issue/review_after tail of both sides' final entries
outside the conflict markers; the union reorder handed it to the wrong
block, leaving the tool_payloads.rs entry (#166) without its policy
fields. Validated with CI's own invocation this time
(--validate-manifest-only), not just a TOML parse.

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

* WS10: classify the repo-root scripts this PR touches in the test planner

`Detect Reborn test scope` failed on this branch:

    Reborn PR test planner failed: unmapped test or CI path: scripts/check-version-bumps.sh

Same shape as the two planner gaps the WS10 CHECKLIST row already records:
`scripts/ci/reborn_pr_test_plan.py` fails closed on any path it has no rule
for, so an unclassified class makes "never edit this file" the only satisfiable
behaviour — and the failure takes `Tests (Reborn)` down with it, since every
downstream lane reports `skipping` when the scope job is red.

Repo-root `scripts/` is deliberately not prefix-classified, so each file needs
a decision recorded beside the constant. Four were missing:

- `scripts/check-version-bumps.sh` -> PR_STATIC_CONTROL_PATHS. Invoked only by
  `platform-and-compat.yml`, behind that workflow's own `has_direct_wasm_abi_risk`
  filter (which already names the script). No `Tests (Reborn)` lane runs it.
- `scripts/run-reborn-webui.sh` -> PR_STATIC_CONTROL_PATHS. A local developer
  launcher referenced by no workflow at all, so no lane can be selected for it.
- `scripts/reborn_qa_matrix/` -> QA_HARNESS_PREFIXES, beside `live-canary/` and
  `reborn_webui_v2_live_qa/`. Offline QA tooling over the route descriptors.

The fail-closed arm is untouched: an undecided repo-root script still refuses,
pinned by the existing second half of
`test_decided_repo_root_script_paths_are_owned_by_other_workflows`.

Regression tests: the two existing classification tests are extended to cover
all four paths. Sabotage-verified by removing the classifications and observing
4 errors (`ERROR: ... (path='scripts/check-version-bumps.sh')` and the three
siblings), then restoring -> 45 tests OK. The planner also now runs clean over
this PR's exact 45-path changed set.

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

* WS10: name the new gates so the Code Style lane actually runs them

`code_style.yml`'s architecture step is `cargo test -p ironclaw_architecture
reborn` — a NAME filter, not a binary filter. None of the twelve new test
functions matched it, so all twelve of this PR's guardrails were invisible in
that lane: green, and checking nothing there.

`cargo test -p ironclaw_architecture reborn -- --list` counted 45 before this
change and 57 after, with every new gate now named:

    reborn_crate_inventory_measures_the_real_tree
    reborn_rust_and_python_crate_inventories_agree
    reborn_logical_spellings_resolve_to_each_crates_real_directory
    reborn_resolution_is_the_identity_on_a_flat_fixture_tree
    reborn_crate_moved_into_a_family_directory_still_resolves
    reborn_crate_that_no_longer_exists_is_refused_not_answered
    reborn_ambiguous_crate_name_is_refused_not_picked
    reborn_truncated_tree_refuses_rather_than_reporting_an_empty_inventory
    reborn_separate_workspaces_nested_manifests_and_build_output_are_excluded
    reborn_allowlist_entries_follow_a_crate_into_its_family_directory
    reborn_build_scripts_do_not_derive_the_repo_root_by_counted_parent_hops
    reborn_fixed_depth_matcher_catches_the_banned_shapes_and_ignores_prose

Rename only; no assertion changed. Full suite still 219 passed / 0 failed,
fmt clean, clippy zero warnings.

Note for the WS10 "guardrails must fail loudly on their own regressions" row:
that filter means Code Style runs 57 of the crate's 219 architecture tests. The
`Tests (Reborn)` bucket lane runs the crate unfiltered (`cargo test -p <pkg>
--all-targets`), so nothing is unrun overall — but a gate whose name misses
`reborn` is absent from the lane most reviewers read.

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

* docs(ws10): record the two gate defects this PR's own CI surfaced

The row's amendment listed four defects found while converting. Two more turned
up afterwards, from the PR's own CI run, and belong on the same row because
both are the fail-closed-with-no-rule / guardrail-that-checks-nothing shape it
already documents twice:

- `reborn_pr_test_plan.py` had no rule for four repo-root `scripts/` files the
  conversion touched, failing `Detect Reborn test scope` outright and skipping
  every downstream Reborn lane.
- `code_style.yml`'s architecture step filters on the test NAME `reborn`, so the
  twelve new gates were absent from it (45 -> 57 listed after the rename), and
  the lane as a whole runs 57 of the crate's 219 architecture tests.

Docs-only; the code changes both landed in earlier commits on this branch.

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

* WS5: sever conversations -> turns by port inversion; register 4 -> 3

ironclaw_conversations drops ironclaw_turns from [dependencies] and declares
the one coordinator call its inbound orchestration makes as a port. Zero
production behaviour moved: the orchestration, the trusted-trigger submitter
and every one of their tests stay in the crate that owned them.

The port (src/turn_submission.rs): ConversationTurnSubmitter, one method
submit_conversation_turn; ConversationTurnSubmission carrying only
host_api::turn vocabulary plus ConversationInboundClassification, the trust
value the orchestration derives from its own binding policy and never from the
adapter string; TurnSubmissionError with retry() and category()/
adapter_status_code() over the host's verbatim rendered cause.

The adapter (composition, automation/conversation_turn_submitter.rs, +158 net
production lines): holds the TurnCoordinator handle composition already
constructed for the trigger poller, calls product_context::resolve_inbound, and
maps TurnError -> port error totally (no wildcard arm).

CORRECTION to the pre-build analysis: the retry class is NOT derivable from the
category. The Conflict category straddles retryable TurnError::Conflict and
permanent LeaseMismatch/InvalidTransition/RunNotRetryable, so the port error
carries two independent axes, not one three-valued one. Same branches, same
ordering, same user-visible messages at every effect.

Invariants amended in the same diff, not silently contradicted: both
ironclaw_conversations/AGENTS.md and CLAUDE.md now name the port error and its
class partition where they named ironclaw_turns::TurnError, and both gained the
standing rule that a TurnCoordinator handle or an ironclaw_turns normal
dependency must not come back.

untrusted_trigger_adapter_records_product_inbound_not_scheduled_trigger is
byte-identical (verified) and still in inbound.rs. It asserts on the
SubmitTurnRequest a coordinator receives, so the fakes swapped to the port and
gained a documented mirror of the production adapter; ironclaw_turns is
retained as a DEV-dependency for that, with the reason in the manifest.
Dev-deps are not layer-matrix edges (is_normal_dependency filters them), and
cargo metadata confirms kind = dev with normal deps exactly
{extension_contracts, filesystem, host_api, safety, triggers} -- PROPOSAL
6.4.2's Deps clause, literally.

New seam coverage at the real adapter:
conversation_turn_submitter_maps_every_turn_error_to_its_class (16 rows: all 12
TurnError variants, AdmissionRejected once per reason; asserts category, retry,
that the port status equals the kernel's, and that the cause is verbatim);
conversation_turn_submitter_covers_every_turn_error_variant (discriminant
census); conversation_turn_submitter_mints_scheduled_trigger_only_for_trusted_trigger
(the composition half of the spoof guard). Composition's five
classify_materializer_inbound_error submission tests now build inputs through
the production mapping instead of a stand-in.

One consumer arm changed shape and is provably unreachable: ironclaw_product's
map_conversation_error only ever sees ConversationBindingService failures, which
never submit a turn (product has its own DefaultInboundTurnService). It now
yields TurnSubmissionRejected carrying the port error's rendering rather than
fabricating a TurnError to satisfy a variant no caller can reach. Recorded in
the CHECKLIST row rather than hidden.

Register: the conversations -> turns entry is deleted and
WS0_LAYER_MATRIX_EXCEPTION_BASELINE lowered 4 -> 3. No other entry touched.
Docs in the same diff: CHECKLIST WS5 row ticked with the as-built shape, WS1's
"count <= 12" verify row ticked (its enumerated clause is now fully true -- no
*->turns exception remains), PROPOSAL 6.4.2 amended with the built shape.
docs/plans/composition-pubuse.snapshot 131 -> 132 for the one deliberate
export, the module-owned adapter factory the integration harness uses instead
of hand-mirroring the wiring.

Verification (all unfiltered, none piped through head/tail):
  cargo fmt --all                                        clean
  clippy (6 crates, --all-targets --all-features -Dwarn) zero warnings
  cargo test -p ironclaw_conversations                   99 passed / 0 failed
  cargo test -p ironclaw_product                       1050 passed / 0 failed
  cargo test -p ironclaw_reborn_composition             945 passed / 0 failed
  cargo test -p ironclaw_architecture                    207 passed / 0 failed
  cargo test --test reborn_group_triggers                 15 passed / 0 failed
  cargo test --test reborn_group_journeys                 16 passed / 0 failed
  cargo check --workspace --all-targets                  clean (one
    pre-existing dead_code warning, unused_fetch_context in
    extension_support/src/skills.rs:572, confirmed on the base via git stash)
Register reads 3 entries against baseline 3; the ratchet and the staleness
check both pass.

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

* chore(ci): exempt the consolidation's internal-move re-attributions that failed changed-coverage

The full-mode PR run failed the changed-line gate two ways: 74.74% vs
the 90% floor (1,080 misses — 1,065 of them the capabilities host.rs
six-workflow split, the obligations three-owner split, and the
first-party-tools move re-attributed as new code) and the generated
wasm bindings.rs tripping the empty-denominator fail-closed rule on its
single changed line (the wit path arg). Same-run proof of no real
loss: the global floor and every configured per-crate floor PASSED in
the failing run. Exact-line exemptions per manifest policy (#6963
class); the 15 uncovered lines in other crates stay measured.
Offline arithmetic on the gate's own numbers: 3,195/3,210 = 99.53%
post-exemption. Validated with --validate-manifest-only (191 entries).

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

* chore(arch): reconcile the same-layer inventory and downgrade pins with the batch's re-layers

The #7156 gates met the batch's real movement and demanded the full
delta: ironclaw_sandbox's layer-origin row; five new same-layer edges
(four kernel edges made same-layer by the processes re-layer, one
substrates edge by the skills re-layer) with the baseline raised
70->75 then banked back to 72 as three stale skills edges deleted;
the skills DowngradePin freezing its six consumers at the move; and
two stale rows (deleted crates' origins, mcp's dead extensions
consumer entry). Every finding a real batch effect, none suppressed.
Composition absolute ceiling re-seeded to the batch tree's measured
45127 with the test record moved in lockstep.

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

* WS2: clear the extension_host->product vocabulary residue (ports 4->1, ledger 9->5)

Three of the four frozen ports and four of the nine reference-ledger rows fall
by one move: the port-facing vocabulary is declared where it already lives, and
product maps at its boundary.

- `ExternalActorBindingEpoch` moves `ironclaw_conversations` ->
  `ironclaw_extension_contracts::external`, beside the `ExternalActorRef` whose
  binding it versions. Zero new crate edges (conversations already depends on
  extension_contracts). Its constructor error becomes
  `ProductAdapterError::InvalidIdentifier`, matching its siblings in that module
  byte-for-byte on the three validation rules.
- `ProductActorUserResolver` + `ProductActorUserResolutionRequest` +
  `ResolvedProductActorUser` invert into
  `ironclaw_product_contracts::actor_identity`, error swapped to
  `ProductOperationFailure` (product absorbs it with the existing total `From`,
  discriminants preserved).
- `AuthChallengeProvider`, `BlockedAuthFlowCanceller`, `AuthChallengeView`,
  `PairingAuthChallengeView` and `auth_prompt_view_for_blocked_auth` move to
  `ironclaw_auth::product_prompt`; `ChannelConnectionService` and
  `ChannelAuthAccountState` to `ironclaw_auth::channel_connection`, beside
  `project_auth_account_state` whose argument pair the latter is. Zero
  vocabulary narrowing. `ironclaw_auth` gains a `product_contracts` dependency
  (substrates -> contracts, the same downward edge and rationale
  `ironclaw_attachments` already carries).
- `ExtensionAccountSetupRegistry` stays product-owned state; extension_host now
  holds the two-method read port `ExtensionAccountSetupReader` declared in
  `product_contracts::account_setup`. `None` == empty registry.
- The approval-prompt projection, gate-ref parse and lookup scope move to
  `ironclaw_product_contracts::approval_prompt`, collapsing product's two copies
  and letting the extension host read the approval store itself instead of
  reaching up into `ironclaw_product::projection`. The scope derivation's
  equivalence with `ApprovalInteractionScope` is pinned in product.

Gate updated in the same change: residue 4 -> 1, baseline 4 -> 1, ledger 9 -> 5,
workflow-error residue 2 -> 1, `ProductActorUserResolver` added to
`INVERTED_PORT_IMPLEMENTORS`.

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

* WS2.5: gate + CHECKLIST reconciliation, and two pre-existing clippy reds

- `reborn_extension_host_port_inversion.rs`: `channel_host.rs`'s ledger reason
  loses its stale `ProductActorUserResolver` half (that port is inverted now).
- `reborn_extension_specificity.rs`: the moved `ChannelConnectionService` doc
  carried a `slack` example into `ironclaw_auth`. Reworded generically rather
  than carved, which also made the product entry stale — deleted, allowlist
  baseline 123 -> 122. The gate reported both directions; neither was allowlisted.
- Two clippy reds that pre-exist on this base and bite a `-D warnings` bar: an
  empty line splitting a doc-comment run in the specificity gate, and a
  never-used negative-control fixture in `ironclaw_extension_support`. The
  fixture is `#[allow(dead_code)]`-ed rather than deleted, with the reason.
- CHECKLIST WS2 re-layer row, blockers half: dated and measured annotation of
  what fell, why the "narrow the vocabulary out" framing was only half right,
  and that §12.11 D-A's factory port is unstarted.

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

* WS2: invert channel_host's product-stack construction behind the D-A factory port

§12.11 D-A's factory port, built. `ChannelWorkflowFactory` is declared in
`ironclaw_product_contracts::channel_workflow`, implemented by
`ironclaw_product::RebornChannelWorkflowFactory`, and injected through the
`GenericChannelHostDeps` bundle composition already builds — so
`channel_host.rs` states the shape of the per-extension product cone and
consumes the result instead of inline-constructing product's concrete stack.

`channel_triggered_delivery.rs` sheds through the same seam, but its port
could not live in contracts: it drives the driver with
`TriggerCommunicationContext`, which `ironclaw_outbound` owns and a contracts
crate may not name. So `TriggeredRunDelivery` and `TriggeredRunDeliveryRequest`
are declared in `ironclaw_outbound` beside that vocabulary — the same placement
rule WS2.5 applied to the auth ports, and zero new crate edges. Composition
builds one driver per codec-bearing binding through the same factory; routing
policy stays in the host.

The conversations wrinkle resolved as sanctioned, with no mirror type.
`RebornFilesystemConversationServices` is constructed, consumed and dropped
inside product's factory. What crosses the port is `ChannelWorkflowStorageRoots`
(a `VirtualPath` pair — placement is host policy) in, and the surface, the
binding resolver and the run-delivery observer out.

The last residue port had to be renamed, not just moved:
`ConversationBindingService` is now `ironclaw_product_contracts::binding::
ProductBindingResolver`, because `ironclaw_conversations` already defines a
trait by the old name and §11.2.4's one-home rule refuses two definitions of a
contracts name. The boundary error grew `BindingRequired`,
`UnknownInstallation` and `TurnSubmissionRejected` rather than weakening: all
three are constructed by the port's implementor, `BindingRequired` is what an
unpaired external actor is told, and every one carries `String`/nothing so the
contracts ceiling is untouched.

Gates:
  EXTENSION_HOST_PRODUCTION_FILES_STILL_NAMING_PRODUCT  5 -> 3
  EXTENSION_HOST_PRODUCT_REFERENCE_FILE_BASELINE        5 -> 3
  PRODUCT_DEFINED_TRAITS_EXTENSION_HOST_STILL_IMPLEMENTS 1 -> 0
  WS2_PRODUCT_DEFINED_TRAIT_RESIDUE_BASELINE            1 -> 0
  EXTENSION_HOST_FILES_STILL_NAMING_THE_WORKFLOW_ERROR  1 -> 0

`the_extension_host_manifest_names_product_only_while_a_residue_needs_it` is
re-keyed on the trait residue OR the reference ledger. That is a correction,
not a relaxation: keyed on the trait list alone it would now demand the
manifest edge be deleted while three adapter-registry rows still name the
crate — failing a correct tree and passing an impossible one. Both directions
stay enforced against the union.

Regression coverage: the ingress/delivery/trigger integration suites are
unchanged in behaviour and green; the only edits to them are import repoints
for the renamed port. `unknown_manifest_command_fails_generic_graph_assembly`
still pins that an undeclarable command fails the whole graph build.

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

* WS2 flip: extension_host products -> loops — manifest edge deleted, ledger/residue 0/0, DowngradePin armed

The batch-2 union (via #7181) and the D-A factory port each discharged
exactly the rows the other left, so the port-inversion biconditional
demanded the flip: layer line + manifest edge in one change. Same-layer
inventory 74 -> 72 net (+1 loops edge extension_host->loop_host, -3
products rows), pin frozen at the four normal-dep consumers. Two typed
ExtensionId seams reconciled between batch-2 and the D-A branch.

* fix(arch): equality-assert the zeroed reference ledger; fmt

* review(7181): architecture-gate hardening from CodeRabbit round 1

Three armed gates were reporting on shapes they could not actually see.

- `reborn_composition_boundaries.rs`: the consumer-annotation scan walked
  back over the attribute block by line prefix, so a multiline
  `#[cfg(any(...))]` between the annotation and the `pub use` stopped the
  walk and rejected a correctly annotated re-export. The walk is now
  bracket-aware and extracted into `pub_use_consumer_annotations` so it is
  testable on synthetic input; the new fixture covers the multiline shape,
  the single-line shape, a bracketed comment, and the unannotated
  sabotage case.
- `reborn_dependency_boundaries.rs`: the MCP/sandbox lane-existence probes
  searched raw concatenated source, so a comment, doc example, string
  literal, or `#[cfg(test)]` fixture naming `McpRuntime<C>` would have kept
  them green after the production runtime was gone. They now scan
  production tokens only (`production_rust_files` +
  `strip_comments_and_strings`), with a regression fixture that plants the
  marker in each of those non-production forms.
- `ironclaw_webui/tests/handlers_module_charter.rs`: `top_level_items`
  stripped only `pub `/`pub(crate) `, so a `pub(super)`/`pub(in ...)` item
  was silently excluded from `charted_surface()` and therefore never
  registered as unassigned. `strip_visibility` now handles every
  visibility form.
- `ironclaw_auth/tests/module_charter.rs`: the two-engine severance scan
  dropped only lines beginning `//`, so a block comment, a trailing
  comment, or a string literal naming the other engine reached the probes
  and a documentation edit could fail the charter gate. A lexical stripper
  replaces the prefix filter, with fixtures for each shape plus a
  must-still-be-seen `use` case.
- `reborn_extension_host_port_inversion.rs`: the reference-ledger history
  still described a 9 -> 5 reduction with five survivors; the live ledger
  has two rows and the baseline is 2. Corrected to the actual 9 -> 5 -> 2.

Every strengthened scanner was sabotage-tested (broken, watched fail,
restored). `cargo test -p ironclaw_architecture` is green across all 37
binaries with no new violations surfaced.

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

* review(7181): MCP lane — arm the charter's failure-string rule, close its exceptions

The crate charter's load-bearing clause — "no module builds a failure
string of its own" — was stated in three files and enforced in none, and
the crate carried live exceptions.

- `egress.rs` minted `"runtime_http_egress_panicked"` inline and forwarded
  `stable_runtime_reason()` verbatim into `McpClientError`. Both are now
  `diagnostics::McpEgressCause` variants named through `egress_failure`.
- `impl From<String> for McpClientError` was the implicit bypass: any `?`
  in the crate could turn an arbitrary String into a model-visible
  reason. It had exactly one user (`client.rs`'s credential-injection
  check, whose reason already came from `diagnostics`), now an explicit
  `map_err(McpClientError::client)`. The impl is deleted.
- `diagnostics.rs` claimed "every reason is capped here" but appended the
  server-supplied `JsonRpcError.message` verbatim. The only production
  producer bounds it upstream, but the cap is this module's invariant,
  not the caller's, so it now goes through `bound_mcp_reason_detail`.
- New `tests/module_charter.rs` arms the rule: a new `reason: "..."` /
  `reason: format!(...)` outside `diagnostics.rs` fails, a re-added
  `From<String>` fails, and the charter text in `lib.rs` + `CLAUDE.md`
  must keep naming the rule and its gate. The rule's one remaining
  carve-out — `runtime.rs`'s two `McpError` descriptor/invocation reasons,
  which echo manifest ids rather than classify a failure — is an
  enumerated list, not a wildcard, and both docs now say so.

Also in `runtime.rs`: the `transport == "stdio"` process-count branch is
unreachable (`prepare_client_request` rejects stdio and everything that
is not http/sse before it), so it is replaced by a comment saying why no
process accounting happens here; and `release_after_failure`'s discarded
`Result` gets the required `// silent-ok:` annotation plus a `debug!` so a
leaked reservation leaves a trace without masking the caller-facing error.

Sabotage-tested: re-inlining the egress reason makes the new gate fail
with 3 inline reasons instead of the 2 grandfathered rows.

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

* review(7181): type the channel-connection port, and fix the trace-prune lock

Two Major findings with real failure modes.

**Typed channel identifier at the `ChannelConnectionService` boundary.**
The port exchanged channel package ids as `String` map keys and a `&str`
disconnect argument, so a malformed or non-canonical id could become a
key no lookup would ever match — a channel that silently reads as "not
connected" instead of failing. The sibling map on the very same product
call (`installed_activation_errors`) was already keyed by `ExtensionId`,
so the untyped half was the odd one out. All three signatures now use
`ironclaw_host_api::ids::ExtensionId`; the generic service applies the
same skip-invalid-vocabulary rule its own discovery walk already used,
and `extension_info` resolves the id once for all three lookups.

**`std::sync::Mutex` held across filesystem I/O in the trace prune step.**
`trace_scope_has_pending_queue` is a synchronous `read_dir` per scope and
was called from inside `observed_scopes.retain`, under the guard, on the
runtime worker thread — while `record_observed_scope` takes the same lock
from the capture path, so a stalled filesystem blocked capture-time scope
recording. The probe now runs on the blocking pool against a snapshot and
the guard is re-acquired only to apply the result, which also leaves
scopes recorded mid-probe alone. (This pattern predates the WS6 move —
it was introduced 2026-06-15 in 410db7720 and relocated verbatim by this
batch — but it is contained enough to fix here.)

**Fire-access unavailable-precedence coverage.** New WS6 policy code
decided what a transient backend fault becomes (retryable `Err` when the
final answer is a denial, but never over a grant) with no test driving a
failing checker at all. Added, test-first: breaking the precedence branch
makes it fail with `Denied` where `Unavailable` is required. Also pins the
last-position fault, which the other two cases never reach.

**Product-adapter section invariants.** `DuplicateCredentialHandle`,
`DuplicateEgressTarget`, and the RFC 7230 token rule (including
`auth.timestamp_header_name`, the optional field a rename could quietly
drop from validation) came over from `ironclaw_product::adapter_registry`
with WS5 and had no assertion anywhere. Covered through the real
deserialize + resolve + validate path.

**Smaller items.** The relocated trigger-fire contract no longer keeps a
second import path through composition (`runtime_input`'s `pub use` and
the four names in the lib.rs surface are gone, consumers repointed at
`ironclaw_triggers`, snapshot recaptured); `repository_contract.rs` uses
`var_os` for presence so a non-UTF-8 `IRONCLAW_REQUIRE_POSTGRES` cannot
silently disarm the parity guard, with a regression fixture;
`ironclaw_reborn_identity`'s stale `Self::bind` rustdoc link, the
`ironclaw_auth` AGENTS.md `loopback_oauth` contradiction, and the
`ironclaw_extension_contracts` charter row missing
`ExternalActorBindingEpoch` are corrected.

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

* Re-arm the union's ratchets: recount, swap one same-layer edge, repoint a coverage exemption

Three gates fired on the merged tree; each is fixed by measurement, not by
lowering a bar.

**Extension-specificity baseline: 125 (ours) / 122 (batch) -> 122.** Neither
side's number is evidence for the union, so the constant was set to `0` and the
true length read out of the ratchet's own panic. The batch's three vendor-pair
removals are the only entries either side removed and this branch's renames
repoint entries in place without adding any, so the union is the batch's number.

**SAME_LAYER_EDGE_BASELINE stays 72 -- one row moved, the count did not.** The
gate found both halves by itself: `triggers -> safety` tripped the
not-inventoried arm and `conversations -> safety` tripped the stale-row arm.
They are the two sides of one swap -- the trusted-trigger prompt scan moved
behind the seam into `TrustedTriggerSubmitRequest::new`, so the edge changed
crate rather than appeared. This merge is the first tree where both halves
exist, which is why nothing had inventoried it before. The equality is what made
the second half loud: under a `<=` ratchet the stale row would have sat green as
one entry of slack.

**changed-coverage exemption #113 repointed 1276 -> 975.** Inherited red, not
caused here: reproduced on a pristine `git archive` of `ws2/da-factory-port`
with the same message. The extension_host products -> loops flip shrank
`channel_host.rs` from 1405 to 1098 lines and left the exemption past EOF.
Repointed to the same construct rather than deleted -- `observe_error`'s `error`
parameter is the only `product_adapter_error::ProductAdapterError` in the file,
so the exemption still names exactly what it always named.

* ci(test-plan): classify the two path classes a rename PR reaches and the planner did not

`Detect Reborn test scope` aborts on the first path no rule claims, and the
nine steps after it are then skipped — so the set gets discovered one CI red at
a time. Both gaps below are the shape #7152 already records for `Dockerfile`
and `clippy.toml`: fail-closed with no rule, surfaced only because a rename
diff touches files a feature PR never touches.

Found as a class rather than one-per-red-run: `build_plan` was driven over all
1,250 paths in this PR's diff with `cargo metadata` resolved once. Two came
back unclassified; after the fix the sweep reports **0**, and the planner
produces a real plan for the actual diff.

- **`openwiki/**`** — the auto-generated wiki, regenerated by
  `openwiki-update.yml` and explicitly not hand-edited. No build or test
  surface, so it joins `docs/` in `IGNORED_PREFIXES`. A crate rename touches it
  by construction: its prose names crate directories.
- **`scripts/live_canary/**`** (UNDERSCORE) — a *second* real directory beside
  the already-classified `scripts/live-canary/` (hyphen), differing only by
  that character. It is the canary's importable Python package; the rename
  reaches it through a `RUST_LOG` string naming a crate. The ⚠ note about the
  two directories is restored beside the constant.

Both are pinned: the wiki test asserts the plan is *equal* to a `docs/` plan
(so a later change that escalates it to a lane fails here too) and that a real
change riding along still selects its lane; the canary paths join the existing
QA-harness subTest list.

* fix(ci): repoint changed-coverage exemption #113 past the flip's channel_host shrink (1276 -> 975)

* test(triggers): hold the workspace env mutex across the non-UTF-8 presence fixture

The hermetic env-mutation guard rejects raw set_var/remove_var without
lock_env(); the fixture now holds the guard across both mutations.

* refactor(crates): move every crate into its §5 family directory (text-only)

Wave 5 / WS7, PR 1 of 2. Creates the ten family directories PROPOSAL §5
specifies — contracts/ substrates/ events/ domains/ kernel/ lanes/ loop/
extensions/ product/ app/ — and `git mv`s 56 crates into them. No crate is
renamed, no code moves between crates, no behavior changes: every diff outside
a manifest, a path literal, or a gate's path resolution is a pure rename.

What moved, and what deliberately did not:

  * 56 of the 58 §5 rows. `ironclaw_extension_support` was already at
    `crates/extensions/`; `ironclaw_wasm` is WS7 2/2's (its `wit/` travels with
    it and forces the guest components' `wit-bindgen` paths plus a rebuild of
    the committed `.wasm` binaries — a binary change that does not belong in a
    text-only move).
  * `tools/` is untouched per the owner ruling, so `ironclaw_stress` stays at
    `tools/ironclaw_stress`.
  * `ironclaw_projects`, `ironclaw_first_party_extension_ports` and the
    workspace-excluded `ironclaw_silk_decoder` stay flat under `crates/`; each
    has an open disposition of its own and is listed in the PR's exceptions
    table.

Fail-open gates hardened BEFORE the first `git mv` (all three were already
inventory-resolved by WS10; each was sabotage-tested here to prove it goes red
rather than silently passing on an unresolvable tree):

  * `reborn_boundary_rules_active_crates_are_workspace_members` — forcing
    resolution to fail drops `checked` to 1 and the `>= 30` floor fires.
  * `boundary_rule_names_are_package_names_not_crate_directories` — adding
    `"ironclaw_cli"` (a directory, package `ironclaw`) to a forbidden list
    produces the directory-vs-package violation.
  * `concrete_extension_crates_link_only_from_the_binary_and_tests` — a fake
    `CONCRETE_EXTENSION_CRATES` resolves nothing and the non-vacuity assert
    fires.

Loud path-inventory repoints (every one of these FAILED first and was fixed by
resolving through the crate inventory — no gate was weakened, no scope
narrowed):

  * 12 architecture gates: composition-boundaries walk root; conversations /
    extension-manager-split / operator-port-inversion scan roots; the three
    contract-location scans' owner attribution (first-path-component under
    `crates/` now answers the FAMILY name, so it moved to
    `ratchet_support::owning_crate_name`); the persistence-driver walk; the
    vendor-census CENSUS/carve-out keys and its sanctioned module; the
    manifest-reparse ALLOWLIST keys; the service-method-freeze sources; the
    provider-catalog ownership test.
  * `scripts/ci/ws12_workflow_contracts.py`: the WebUI lockfile's "one level
    deeper" cache-dependency-path sibling is now `crates/*/*/…`, because the
    single-`*` form matches the crate's real location post-move and the gate
    correctly rejects a probe that is broad rather than depth-tolerant.
  * `scripts/ci/test-classify-test-scope.sh`: the "every arm names a real
    crate" check now resolves through the inventory instead of globbing the
    filesystem — the arms are keyed to the classifier's NORMALIZED
    `crates/<crate>/…` identity, which is not a path on disk.
  * `tests/integration/changed-coverage-exemptions.toml` and the changed-
    coverage self-test fixtures.
  * `Dockerfile`, `.dockerignore`, `.gitattributes`, `.coderabbit.yaml`, the
    seven workflows carrying WebUI/stress path literals, and `README.md`'s
    `cargo install --path`.
  * 39 cross-crate `include_str!`/`include_bytes!` literals and eight
    `CARGO_MANIFEST_DIR`-relative test helpers; the four that resolved the repo
    root by counted `..` hops now search upward for the nearest ancestor
    holding both `crates/` and `Cargo.toml`, because their wrong answer
    (`crates/`) is a directory that exists.

Guidance: one `AGENTS.md` per family directory (charter, member list with each
crate's enforced layer, link to `families/<name>.md`), plus a family index in
`crates/AGENTS.md`. Live agent-facing docs were repointed; generated
(`openwiki/`) and historical (`docs/plans/`, `docs/superpowers/`, `docs/adr/`,
`CHANGELOG.md`, the target-architecture docs) were not.

Measurements unchanged by the move, which is the evidence it is text-only:
composition budget 40582 / 691597 LOC (identical to the base branch — the
ratchet followed the crate by name), specificity allowlist 122, same-layer edge
inventory 72, architecture suite 36 test binaries / 261 tests green.

The projects→identity merge (§12.10) was measured and SKIPPED — see the PR
body's finding: its consumers are two crates and five files, not the single
wiring site the audit counted.

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

* fix(ci): repoint the four path-keyed baselines onto the family tree

Four data files key their entries on a repository path rather than on a crate
name, so the family move leaves every row naming a directory that no longer
exists. Each fails loudly, which is how they were found:

  * `scripts/no_panics_reborn_baseline.txt` — `check_no_panics.py
    --reborn-baseline` reported all 50 audited invariants as *stale* and the
    same 50 as *new*, because the fingerprint's first field is the file path.
  * `tests/integration/coverage-exemptions.toml`,
    `tests/integration/coverage-floor.toml`,
    `tests/integration/critical-mutation-functions.toml` — same shape; the
    critical-mutation manifest validator resolves each row against the live
    crate tree and refuses a row it cannot attribute.

Paths only: no entry added, removed, or re-justified, so every floor,
exemption and reviewed invariant keeps exactly the scope it had.

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

* style(arch): drop two needless borrows the family-move repoint introduced

`crate_dir(root, OWNER)` inside the two `fn …(root: &Path)` helpers took
`&root`, which clippy's `needless_borrow` rejects under `-D warnings`. Found by
the workspace clippy lane, not by review.

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

* docs(crates): sharpen three family AGENTS.md entries

`extensions/` now says what `packages/` holds beyond its four crates (the
data-only packages) and the rule for when a package earns a crate. `app/` names
the one directory whose name and package name differ, and replaces two
descriptions that read as tautologies. `domains/` records why
`ironclaw_projects` is not in the table.

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

* docs(ci): correct the cache-dependency-path comment for the landed move

The comment described the pair as "flat line + one family directory down".
Post-WS7 the first line IS the family path, and the spare is one level below
that — so the comment now says which is which, and names the rule that forces
them not to overlap (`ws12_workflow_contracts.py` rejects a spare that already
matches the real location).

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

* fix(ci): repoint two path-keyed self-tests the family move took dark

Both fired in CI, not in review.

`crates/app/ironclaw_cli/tests/smoke.rs` asserts against *repository paths*
written into the Dockerfile and the release workflows, and those carry the
crate's family. Three assertions read a flat `crates/ironclaw_*` path: the
Dockerfile's WebUI frontend install, and two reads of the CLI manifest / WiX
manifest — the latter two failed with `NotFound`, the first with a
"Dockerfile must install WebUI frontend dependencies" message that pointed at
the Dockerfile rather than at the test. They now derive the crate directory by
walking `crates/` for the outermost directory owning a `Cargo.toml`, and panic
on absent-or-ambiguous rather than answering an empty path.

`scripts/check_no_panics.py`'s `test_test_only_path_detection` names a REAL
repository file, because that branch of `is_test_only_path` reads the file to
confirm its `#[cfg(test)] mod` declaration. Pointed at the crate's family path
so the assertion measures the file it claims to.

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

* ci(classify): attribute family-level guidance files (crates/<family>/AGENTS.md)

The WS7 family dirs each carry an AGENTS.md that sits inside no crate;
the classifier's fail-closed refusal correctly caught the new shape on
CI. Recognized structurally (parent dir is a prefix of a discovered
crate dir), bucketed exactly like crates/AGENTS.md. Fixture red-verified
before the fix.

* test(e2e): repoint journey-evidence and inventory paths to the family tree

The journey-coverage suite asserts each case's evidence file exists;
four e2e data/scenario files still carried flat crates/<crate> paths.
Repointed through the family map; every referenced .rs path verified
resolving on this tree.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:33:28 +00:00

14 KiB

IronClaw

IronClaw

Your secure personal AI assistant, always on your side

License: MIT OR Apache-2.0 Telegram: @ironclawAI Reddit: r/ironclawAI gitcgr

English | 简体中文 | Русский | 日本語 | 한국어

Quick StartPhilosophyFeaturesInstallationConfigurationSecurityArchitecture


Quick Start

Choose an ironclaw-v* tag from the Releases page, then install it on macOS, Linux, or Windows/WSL. Replace X.Y.Z with the selected version, including any prerelease suffix:

IRONCLAW_RELEASE_TAG=ironclaw-vX.Y.Z
curl --proto '=https' --tlsv1.2 -LsSf \
  "https://github.com/nearai/ironclaw/releases/download/${IRONCLAW_RELEASE_TAG}/ironclaw-installer.sh" | sh

Then run the guided setup:

ironclaw onboard

Choose an LLM provider, enter its API key in the hidden prompt, and accept the default model or enter another one. IronClaw provisions its local configuration, encrypted credential store, and WebUI login token. On macOS and Linux it also installs and starts the background service, then prints a link that opens the WebUI.

Use ironclaw status to check the service and print the login link again. Windows users can start the WebUI in the foreground with ironclaw serve. See Installation for Windows installers and source builds.

Philosophy

IronClaw is built on a simple principle: your AI assistant should work for you, not against you.

In a world where AI systems are increasingly opaque about data handling and aligned with corporate interests, IronClaw takes a different approach:

  • Your data stays yours - All information is stored locally, encrypted, and never leaves your control
  • Transparency by design - Open source, auditable, no hidden telemetry or data harvesting
  • Self-expanding capabilities - Build new tools on the fly without waiting for vendor updates
  • Defense in depth - Multiple security layers protect against prompt injection and data exfiltration

IronClaw is the AI assistant you can actually trust with your personal and professional life.

Features

Security First

  • WASM Sandbox - Untrusted tools run in isolated WebAssembly containers with capability-based permissions
  • Credential Protection - Secrets are never exposed to tools; injected at the host boundary with leak detection
  • Prompt Injection Defense - Pattern detection, content sanitization, and policy enforcement
  • Endpoint Allowlisting - HTTP requests only to explicitly approved hosts and paths

Always Available

  • Multi-channel - REPL, HTTP webhooks, WASM channels (Telegram, Slack), and web gateway
  • Docker Sandbox - Isolated container execution with per-job tokens and orchestrator/worker pattern
  • Web Gateway - Browser UI with real-time SSE/WebSocket streaming
  • Routines - Cron schedules, event triggers, webhook handlers for background automation
  • Heartbeat System - Proactive background execution for monitoring and maintenance tasks
  • Parallel Jobs - Handle multiple requests concurrently with isolated contexts
  • Self-repair - Automatic detection and recovery of stuck operations

Self-Expanding

  • Dynamic Tool Building - Describe what you need, and IronClaw builds it as a WASM tool
  • MCP Protocol - Connect to Model Context Protocol servers for additional capabilities
  • Plugin Architecture - Drop in new WASM tools and channels without restarting

Persistent Memory

  • Hybrid Search - Full-text + vector search using Reciprocal Rank Fusion
  • Workspace Filesystem - Flexible path-based storage for notes, logs, and context
  • Identity Files - Maintain consistent personality and preferences across sessions

Installation

The Releases page provides pre-built binaries and installers.

Install via Windows Installer (Windows)

Open the selected ironclaw-v* release, download ironclaw-x86_64-pc-windows-msvc.msi, and run it.

Install via PowerShell script (Windows)
$IronClawReleaseTag = "ironclaw-vX.Y.Z"
irm "https://github.com/nearai/ironclaw/releases/download/$IronClawReleaseTag/ironclaw-installer.ps1" | iex
Install via shell script (macOS, Linux, Windows/WSL)
IRONCLAW_RELEASE_TAG=ironclaw-vX.Y.Z
curl --proto '=https' --tlsv1.2 -LsSf \
  "https://github.com/nearai/ironclaw/releases/download/${IRONCLAW_RELEASE_TAG}/ironclaw-installer.sh" | sh
Build and install from source

Source builds require Rust 1.96+ and Node.js 22+ with Corepack/pnpm.

git clone https://github.com/nearai/ironclaw.git
cd ironclaw
corepack enable pnpm
cargo install --locked --path crates/app/ironclaw_cli

Configuration

ironclaw onboard is the primary configuration path. It writes Reborn state under $HOME/.ironclaw/reborn by default, stores the selected LLM credential in the encrypted local secret store, and preserves existing configuration when it is run again.

Inspect the current setup with:

ironclaw status
ironclaw models status
ironclaw config list

To switch providers after onboarding, select the route and then store its API key using the hidden prompt:

ironclaw models set-provider openai --model gpt-5-mini
ironclaw config set openai.api_key

Additional settings use the same command. For example:

ironclaw config set google.client_id YOUR_CLIENT_ID
ironclaw config set google.client_secret
ironclaw config set google.redirect_uri YOUR_REDIRECT_URI
ironclaw config set webui.token --rotate

Secret values never accept a positional argument; IronClaw prompts for them without echoing the value. Channels such as Slack and Telegram have no configuration-file settings and no CLI enablement key: install the extension and complete its setup on the WebUI Extensions page, which is what makes the route serve.

Configuration writes never restart the service automatically. Run ironclaw service restart after a change that affects the running service, and use ironclaw config set --help for the complete list of supported keys.

Security

IronClaw implements defense in depth to protect your data and prevent misuse.

WASM Sandbox

All untrusted tools run in isolated WebAssembly containers:

  • Capability-based permissions - Explicit opt-in for HTTP, secrets, tool invocation
  • Endpoint allowlisting - HTTP requests only to approved hosts/paths
  • Credential injection - Secrets injected at host boundary, never exposed to WASM code
  • Leak detection - Scans requests and responses for secret exfiltration attempts
  • Rate limiting - Per-tool request limits to prevent abuse
  • Resource limits - Memory, CPU, and execution time constraints
WASM ──► Allowlist ──► Leak Scan ──► Credential ──► Execute ──► Leak Scan ──► WASM
         Validator     (request)     Injector       Request     (response)

Prompt Injection Defense

External content passes through multiple security layers:

  • Pattern-based detection of injection attempts
  • Content sanitization and escaping
  • Policy rules with severity levels (Block/Warn/Review/Sanitize)
  • Tool output wrapping for safe LLM context injection

Data Protection

  • All data stored locally in IronClaw's application state
  • Secrets encrypted with AES-256-GCM
  • No telemetry, analytics, or data sharing
  • Full audit log of all tool executions

Architecture

┌────────────────────────────────────────────────────────────────┐
│                          Channels                              │
│  ┌──────┐  ┌──────┐   ┌─────────────┐  ┌─────────────┐         │
│  │ REPL │  │ HTTP │   │WASM Channels│  │ Web Gateway │         │
│  └──┬───┘  └──┬───┘   └──────┬──────┘  │ (SSE + WS)  │         │
│     │         │              │         └──────┬──────┘         │
│     └─────────┴──────────────┴────────────────┘                │
│                              │                                 │
│                    ┌─────────▼─────────┐                       │
│                    │    Agent Loop     │  Intent routing       │
│                    └────┬──────────┬───┘                       │
│                         │          │                           │
│              ┌──────────▼────┐  ┌──▼───────────────┐           │
│              │  Scheduler    │  │ Routines Engine  │           │
│              │(parallel jobs)│  │(cron, event, wh) │           │
│              └──────┬────────┘  └────────┬─────────┘           │
│                     │                    │                     │
│       ┌─────────────┼────────────────────┘                     │
│       │             │                                          │
│   ┌───▼─────┐  ┌────▼────────────────┐                         │
│   │ Local   │  │    Orchestrator     │                         │
│   │Workers  │  │  ┌───────────────┐  │                         │
│   │(in-proc)│  │  │ Docker Sandbox│  │                         │
│   └───┬─────┘  │  │   Containers  │  │                         │
│       │        │  │ ┌───────────┐ │  │                         │
│       │        │  │ │Worker / CC│ │  │                         │
│       │        │  │ └───────────┘ │  │                         │
│       │        │  └───────────────┘  │                         │
│       │        └─────────┬───────────┘                         │
│       └──────────────────┤                                     │
│                          │                                     │
│              ┌───────────▼──────────┐                          │
│              │    Tool Registry     │                          │
│              │  Built-in, MCP, WASM │                          │
│              └──────────────────────┘                          │
└────────────────────────────────────────────────────────────────┘

Core Components

Component Purpose
Agent Loop Main message handling and job coordination
Router Classifies user intent (command, query, task)
Scheduler Manages parallel job execution with priorities
Worker Executes jobs with LLM reasoning and tool calls
Orchestrator Container lifecycle, LLM proxying, per-job auth
Web Gateway Browser UI with chat, memory, jobs, logs, extensions, routines
Routines Engine Scheduled (cron) and reactive (event, webhook) background tasks
Workspace Persistent memory with hybrid search
Safety Layer Prompt injection defense and content sanitization

Usage

# Check the background service and print the WebUI login link
ironclaw status

# Start an interactive terminal session
ironclaw repl

# Run one turn
ironclaw run --message "hello"

Development

# Format code
cargo fmt

# Lint
cargo clippy --all --benches --tests --examples --all-features

# Run tests
createdb ironclaw_test
cargo test

# Run specific test
cargo test test_name

OpenClaw Heritage

IronClaw is a Rust reimplementation inspired by OpenClaw. See FEATURE_PARITY.md for the complete tracking matrix.

Key differences:

  • Rust vs TypeScript - Native performance, memory safety, single binary
  • WASM sandbox vs Docker - Lightweight, capability-based security
  • PostgreSQL vs SQLite - Production-ready persistence
  • Security-first design - Multiple defense layers, credential protection

License

Licensed under either of:

at your option.