refactor(reborn): route capability results through host_api::Resolution at the loop_host seam (§5.3) - #6245
Conversation
🔎 IronLoop Review StatusHead: Current reviewers:
Reviewer summaries
Recent activity
Available commands
Run metadataAdmission: webhook accepted the request and IronLoop persisted reviewer state before this projection. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe loop-host capability port now supports durable ChangesGate-record persistence
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant HostRuntimeLoopCapabilityPort
participant Capability
participant GateRecordStore
HostRuntimeLoopCapabilityPort->>Capability: invoke capability
Capability-->>HostRuntimeLoopCapabilityPort: blocked CapabilityOutcome
HostRuntimeLoopCapabilityPort->>GateRecordStore: save GateRecord
GateRecordStore-->>HostRuntimeLoopCapabilityPort: persistence result
HostRuntimeLoopCapabilityPort-->>HostRuntimeLoopCapabilityPort: return outcome or unavailable error
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ❌ Changes requested | 1 | 0 | 1 | e812de02dfe4 |
Head: e812de02dfe4532251668b9591fb3b15eb5217c2
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
Found one blocking idempotency issue in gate-record persistence. Static diff checks passed; Rust tests could not run because cargo is unavailable in the review environment.
Findings
Blocking: 1 / Notes: 0
Blocking findings
1. ❌ [MEDIUM] Memoize gate persistence across idempotent invocation replays
Location: crates/ironclaw_loop_host/src/capability_port.rs:1742
This mapping runs after invoke_capability_dispatch has already cached a successful gate outcome as LoopCompleted. A duplicate delivery of the same invocation therefore returns the cached CapabilityOutcome, but this call mints a new GateRef and saves another record. GateRecordStore is write-once and intentionally has no removal API, so every retry leaves an additional orphaned durable record. Persist/memoize the mapped gate record within the idempotent dispatch result (and test repeated invocation/store-retry behavior) so one logical gate produces one stable record/ref.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
| &self, | ||
| outcome: &CapabilityOutcome, | ||
| ) -> Result<(), AgentLoopHostError> { | ||
| let mapped = capability_outcome_to_resolution(outcome.clone()); |
There was a problem hiding this comment.
This executes outside the idempotent dispatch cache. Replaying the same invocation returns the cached gate outcome, then this mapping mints a fresh GateRef and persists another write-once record. Since gate records have no removal API, retries accumulate orphaned records. Please memoize/persist the mapped gate result inside the cached dispatch path and cover duplicate invocation behavior.
There was a problem hiding this comment.
Confirmed and fixed in ab6ba02. The wrapper now computes the same idempotency key the dispatch cache uses (invocation_idempotency_key is a pure function of the request, so it's derivable before dispatch) and a per-port replay guard (gate_records_persisted: Mutex<HashSet<IdempotencyKey>>) skips the persist when the key was already persisted — so a replay served from the cached dispatch record no longer mints a fresh GateRef/orphan record. The guard entry rolls back on a failed save, so a transient store fault doesn't permanently skip persistence — the next replay retries. Duplicate-invocation behavior covered by replayed_gate_invocation_does_not_persist_a_duplicate_record (ONE queued runtime outcome, two invokes through the production caller, exactly one record) and the rollback by failed_gate_record_persist_is_retried_on_replay (fail-once store: first invoke fails closed, replay lands exactly one record).
There was a problem hiding this comment.
Code Review
This pull request integrates ironclaw_run_state into the loop host capability port to persist durable gate records at the capability invocation seam. It introduces the GateRecordStore trait, implements outcome-based persistence, and adds a transitional no-op store along with corresponding unit tests. The review feedback recommends elevating the log level in gate_record_store_error from debug to error to ensure critical storage failures are visible in production environments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| fn gate_record_store_error(error: RunStateError) -> AgentLoopHostError { | ||
| tracing::debug!(error = %error, "failed to persist capability gate record at loop host seam"); | ||
| AgentLoopHostError::new( | ||
| AgentLoopHostErrorKind::Unavailable, | ||
| "failed to persist capability gate record", | ||
| ) | ||
| } |
There was a problem hiding this comment.
The helper function gate_record_store_error logs the underlying RunStateError at the debug level. Since this error represents a critical, fail-closed host storage fault that aborts the capability invocation, logging it only at debug level will make it invisible in standard production environments. This makes troubleshooting extremely difficult. Please elevate the log level to error or warn so that operators can detect and diagnose storage/filesystem failures.
| fn gate_record_store_error(error: RunStateError) -> AgentLoopHostError { | |
| tracing::debug!(error = %error, "failed to persist capability gate record at loop host seam"); | |
| AgentLoopHostError::new( | |
| AgentLoopHostErrorKind::Unavailable, | |
| "failed to persist capability gate record", | |
| ) | |
| } | |
| fn gate_record_store_error(error: RunStateError) -> AgentLoopHostError { | |
| tracing::error!(error = %error, "failed to persist capability gate record at loop host seam"); | |
| AgentLoopHostError::new( | |
| AgentLoopHostErrorKind::Unavailable, | |
| "failed to persist capability gate record", | |
| ) | |
| } |
References
- When handling errors, log detailed internal information for observability using a structured logger, while returning sanitized, generic, or typed errors to the caller to prevent leaking implementation details.
There was a problem hiding this comment.
Applied in ab6ba02 — elevated to warn! (matching the crate's existing level for host-side faults, e.g. the lease-revoke failures): a fail-closed storage fault is something operators must see at default log levels. The sanitized boundary error and the no-cause-in-model-visible-summary rule are unchanged.
…ys; warn on store faults A replayed invocation returns the CACHED gate outcome from the dispatch records, but the seam wrapper persisted unconditionally — minting a fresh GateRef and orphaning another write-once record per retry (gate records have no removal API). The wrapper now computes the same idempotency key the dispatch cache uses (a pure function of the request) and a per-port replay guard skips duplicate persists; the guard entry rolls back on a failed save so a later replay retries the persist after a transient store fault. Store-fault logging elevated debug → warn (operators must see a fail-closed storage fault; the sanitized boundary error is unchanged). Tests: replayed_gate_invocation_does_not_persist_a_duplicate_record (one queued runtime outcome, two invokes, exactly one record) and failed_gate_record_persist_is_retried_on_replay (fail-once store: first invoke fails closed, replay persists exactly one record). Reported-by: ironloopai, gemini-code-assist (PR #6245 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✅ Ready for mergeReviewed, both review comments addressed with fixes plus one CI break repaired, fully green — 19 pass / 0 fail.
Merge order: after its integration base lands (contains #6242 + #6243, which stack on #6237). 🤖 Generated with Claude Code |
…olution + GateRecordStore at the loop_host seam (§5.3) Slice C result-side collapse, seam-first (§3/§5.3 of docs/reborn/2026-07-17-architecture-simplification-dto-dyn-local.md). At the loop_host capability seam (`HostRuntimeLoopCapabilityPort::invoke_capability`), the loop-facing `CapabilityOutcome` is now mapped to a host_api `Resolution` via `capability_outcome_to_resolution` (#6242), and the derived `GateRecord` is persisted through `GateRecordStore::save` (#6243), keyed by the minted `GateRef` on the resolution channel. `DenyRecord` is terminal/same-turn and is intentionally not persisted. The batch path inherits per-outcome persistence (it calls the persisting `invoke_capability`). Store wiring: `GateRecordStore` is injected into the port and its factory via the constructor + a `with_gate_record_store` builder (mirroring the crate's existing `with_*` DI). The default is a transitional no-op (`NoopGateRecordStore`): the persisted record has no reader yet — the resume-turn render path and the loop-ref↔minted-ref association are the explicit follow-up — so skipping the write is behavior-preserving and never regresses an unwired path's gates. Dep edge: `ironclaw_loop_host` (loops) -> `ironclaw_run_state` (kernel), allowed by the layer matrix; `cargo test -p ironclaw_architecture` green. Deliberately NOT in this PR (reported as a genuine blocker / follow-up): - The port still returns `CapabilityOutcome`; the agent_loop executor and turns consumers are NOT migrated to consume bare `Resolution`. Doing so cannot preserve existing behavior: `Resolution` structurally drops the gate resume tokens (approval_resume/auth_resume), the failure `error_kind` (retry/terminal classification), and progress/terminate_hint/output_digest (G4), and mints fresh uuid refs where the evidence layer needs the loop refs. Full consumer migration needs the resume/read wiring + a loop-side carrier decision. - Durable `FilesystemGateRecordStore` composition wiring + the `/gate-records` mount alias are deferred to the resume-read follow-up (the record is orphaned until then; the mount-alias change would regress gates if wrong). Also fixes two pre-existing base-branch build breaks from the #6242+#6243 merge: `RunStateError::GateRecordAlreadyExists` left exhaustive matches in `ironclaw_capabilities` and `ironclaw_host_runtime` non-exhaustive. Tests (crate-tier, driving the production seam, red-first verified): `approval_gate_outcome_persists_gate_record_at_the_seam` (gate record round-trips via the store; outcome unchanged) and `gate_outcome_through_unwired_default_is_inert_and_non_regressing`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ys; warn on store faults A replayed invocation returns the CACHED gate outcome from the dispatch records, but the seam wrapper persisted unconditionally — minting a fresh GateRef and orphaning another write-once record per retry (gate records have no removal API). The wrapper now computes the same idempotency key the dispatch cache uses (a pure function of the request) and a per-port replay guard skips duplicate persists; the guard entry rolls back on a failed save so a later replay retries the persist after a transient store fault. Store-fault logging elevated debug → warn (operators must see a fail-closed storage fault; the sanitized boundary error is unchanged). Tests: replayed_gate_invocation_does_not_persist_a_duplicate_record (one queued runtime outcome, two invokes, exactly one record) and failed_gate_record_persist_is_retried_on_replay (fail-once store: first invoke fails closed, replay persists exactly one record). Reported-by: ironloopai, gemini-code-assist (PR #6245 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
56a3850 to
2f2d395
Compare
… gate store rides the builder The added constructor parameter broke 26 HostRuntimeLoopCapabilityPort::new call sites in ironclaw_runner's loop_driver_host tests (outside the crates the PR verified; caught by the workspace all-features clippy lane). The factory already owns the DI, so the port constructor now defaults to the transitional NoopGateRecordStore and gains a with_gate_record_store builder (matching with_execution_mounts / with_trajectory_observer); the factory forwards through it. No caller churn, same wiring semantics, seam tests unchanged and green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2f2d395 to
c7ba17e
Compare
Coverage ratchetReborn integration-tier coverageLine coverage (Reborn crates): 85.51% — 311137 / 363866 lines Per-crate breakdown (65 crates, lowest-covered first)
This table itself is informational and never gates the PR on its own — not the percentage, not the per-crate holes, not the 0-coverage callout. A separate coverage ratchet (dry-run until enforce=true; see tests/integration/coverage-floor.toml) can fail the build on specific configured floors. Exemptions (3 entry/entries excluded from the accounting above)
|
…loadStore; wire real stores (§5.3 Stage 2a-i)
Moves the raw gate/auth resume replay payload out of the untrusted loop and
into the host-private `ReplayPayloadStore`, and wires the real durable stores
in production composition. The loop-facing result stays `CapabilityOutcome`
(no `Resolution` flip — that is Stage 2a-ii).
Security fix: the loop checkpoint no longer stores raw tool args, retiring the
charter violation in `crates/ironclaw_agent_loop/CLAUDE.md` ("State stores
refs... Do not store raw prompts, raw model output, tool args ... in state").
Write (two gate-raise paths): at a FRESH runtime gate raise
(`HostRuntimeLoopCapabilityPort::invoke_capability_dispatch`, gated on
`is_fresh_dispatch` before `finish_runtime_outcome`) and at the synthetic
outbound-delivery approval-gate raise (`outbound_delivery.rs::request_approval`),
the host `save`s a `ReplayPayload{input, estimate, prior_approval:None,
input_ref, correlation_id}` keyed by `InvocationId` (write-once; a benign
same-invocation duplicate is tolerated).
Read on resume: `runtime_outcome_to_loop` no longer embeds input/estimate;
`invocation_replay_input` is replaced by `ReplayPayloadStore::load` keyed by the
`InvocationId` recovered from the resume token, in both the runtime seam
(`replay_payload_for_resume`) and the synthetic decorator/handler. Fail closed
on a miss: an absent payload (including a wrong-scope read) is a sanitized
terminal `Unavailable`, never a silent empty-input dispatch. The fingerprinted
approval lease claim ordering is preserved.
Dropped loop fields (security): `input`/`estimate` from `CapabilityApprovalResume`
and `PendingApprovalResume`; the whole `CapabilityAuthResumeReplay` type and the
`replay` field on `CapabilityAuthResume`/`PendingAuthResume`. `approval_request_id`/
`correlation_id`/`input_ref`/`prior_approval` stay on the wire (they move in 2a-ii).
Composition (real stores): `capability_wiring` builds `FilesystemGateRecordStore`
(closes the #6245 production gap where it defaulted to `NoopGateRecordStore`) and
`FilesystemReplayPayloadStore` over the shared scoped filesystem and threads both
through the refreshing capability port. Adds the `/replay-payloads` per-user mount
alias. `loop_host` gains a dependency-inversion dep on `ironclaw_capabilities` for
the port trait.
Tests (all failed first): fail-closed on missing payload (loop_host seam);
extended the #6245 approval-resume seam test to assert the store persisted the
payload and the resume redispatches the correct original input; the
`ReplayPayloadStore` contract (round-trip / write-once / cross-tenant /
within-tenant) rides the base commit. Full-infra resume edges pass through the
production-shaped harness: group_approvals (runtime approval resume),
group_journeys (approval→auth convergence), outbound_target (synthetic approval
resume).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…loadStore; wire real stores (§5.3 Stage 2a-i) (#6271) * refactor(reborn): resume replay payload moves host-side via ReplayPayloadStore; wire real stores (§5.3 Stage 2a-i) Moves the raw gate/auth resume replay payload out of the untrusted loop and into the host-private `ReplayPayloadStore`, and wires the real durable stores in production composition. The loop-facing result stays `CapabilityOutcome` (no `Resolution` flip — that is Stage 2a-ii). Security fix: the loop checkpoint no longer stores raw tool args, retiring the charter violation in `crates/ironclaw_agent_loop/CLAUDE.md` ("State stores refs... Do not store raw prompts, raw model output, tool args ... in state"). Write (two gate-raise paths): at a FRESH runtime gate raise (`HostRuntimeLoopCapabilityPort::invoke_capability_dispatch`, gated on `is_fresh_dispatch` before `finish_runtime_outcome`) and at the synthetic outbound-delivery approval-gate raise (`outbound_delivery.rs::request_approval`), the host `save`s a `ReplayPayload{input, estimate, prior_approval:None, input_ref, correlation_id}` keyed by `InvocationId` (write-once; a benign same-invocation duplicate is tolerated). Read on resume: `runtime_outcome_to_loop` no longer embeds input/estimate; `invocation_replay_input` is replaced by `ReplayPayloadStore::load` keyed by the `InvocationId` recovered from the resume token, in both the runtime seam (`replay_payload_for_resume`) and the synthetic decorator/handler. Fail closed on a miss: an absent payload (including a wrong-scope read) is a sanitized terminal `Unavailable`, never a silent empty-input dispatch. The fingerprinted approval lease claim ordering is preserved. Dropped loop fields (security): `input`/`estimate` from `CapabilityApprovalResume` and `PendingApprovalResume`; the whole `CapabilityAuthResumeReplay` type and the `replay` field on `CapabilityAuthResume`/`PendingAuthResume`. `approval_request_id`/ `correlation_id`/`input_ref`/`prior_approval` stay on the wire (they move in 2a-ii). Composition (real stores): `capability_wiring` builds `FilesystemGateRecordStore` (closes the #6245 production gap where it defaulted to `NoopGateRecordStore`) and `FilesystemReplayPayloadStore` over the shared scoped filesystem and threads both through the refreshing capability port. Adds the `/replay-payloads` per-user mount alias. `loop_host` gains a dependency-inversion dep on `ironclaw_capabilities` for the port trait. Tests (all failed first): fail-closed on missing payload (loop_host seam); extended the #6245 approval-resume seam test to assert the store persisted the payload and the resume redispatches the correct original input; the `ReplayPayloadStore` contract (round-trip / write-once / cross-tenant / within-tenant) rides the base commit. Full-infra resume edges pass through the production-shaped harness: group_approvals (runtime approval resume), group_journeys (approval→auth convergence), outbound_target (synthetic approval resume). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(turns): move the removed-replay design note out of prior_approval's rustdoc Reported-by: gemini-code-assist (PR #6271 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(runner): drop removed replay fields from planned-driver test fixtures The Pending{Approval,Auth}Resume replay-field removal missed six cfg(test) construction sites in planned_driver.rs (outside the PR's verified crates; caught by the workspace all-features clippy lane). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: cargo fmt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reborn): Resolution carries model-visible failure diagnostic + denial content (§5.3 flip prep / PR-B) (#6273) * refactor(reborn): Resolution carries model-visible failure diagnostic + denial content (§5.3 flip prep / PR-B) Additive vocabulary slice (§3/§5.2.9/§5.3): make `host_api::Resolution` carry the model-visible result content the executor's `handle_capability_outcome` reads off `CapabilityOutcome` today, so a later slice (PR-C) can flip `invoke_capability` to `Resolution` without the loop reading host storage (its charter forbids that). host_api additions (redacted vocabulary only — charter fit): - `ModelInputIssue`: redacted mirror of the loop's `CapabilityInputIssue` — `DispatchInputIssueCode` (existing host_api enum) + bounded redacted `SafeSummary` path/expected/received/schema_path. - `ModelFailureDiagnostic { InvalidInput { issues }, Diagnostic { text } }`: redacted mirror of `CapabilityFailureDetail`; rides `ToolVerdict::RecoverableFailure.diagnostic` (additive Option). - `Denial { deny, reason_kind, summary }`: `Resolution::Denied(DenyRef)` → `Resolution::Denied(Denial)`, carrying the model-visible `DenyReason` + redacted `SafeSummary` on the channel (a projection of the sibling DenyRecord). Mapping (`resolution_mapping.rs`): `failed_outcome` now redacts the loop `detail` into the diagnostic (per-field SafeSummary; path-shaped free text degrades to the placeholder); the Denied arm populates the Denial channel from the same reason/summary the DenyRecord holds. Tests (failed first, then green): structured `InvalidInput` issues and free-text `Diagnostic` round-trip through the mapping; Denied carries reason_kind + redacted summary; redaction proof — path- and secret-shaped content is redacted in the `Resolution` output, never carried raw. GateRecord/ DenyRecord persistence round-trips (ironclaw_run_state) stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(host_api): enforce the 16-issue diagnostic cap in the type, not one producer ModelFailureDiagnostic::InvalidInput now carries a bounded ModelInputIssues newtype: validated at construction, revalidated on the wire (try_from), with a truncating() producer constructor for the mapping. A 17-item payload is rejected at every entry point — pinned by model_input_issues_cap_is_enforced_at_construction_and_on_the_wire. Reported-by: ironloopai (PR #6273 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: cargo fmt (inherited from base pre-fmt fork) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reborn): complete host-side gate reconstitution — resume input_ref, local-dev gate persistence, GateRef reconcile (§5.3 flip prep / Stage 0) (#6278) * refactor(reborn): complete host-side gate reconstitution — resume input_ref, local-dev gate persistence, GateRef reconcile (§5.3 flip prep / Stage 0) Stage 0 no-flip host-reconstitution slice: closes two verified correctness hazards ahead of the atomic capability-result flip. The loop-facing trait and `CapabilityOutcome` are UNCHANGED; only host-side plumbing moves. Fix 1 — resume-time input_ref reconstitution (hazard 3). On a gate/auth resume the effective input_ref used for the idempotency key + validation is now derived from the host-private `ReplayPayload` persisted at the fresh gate raise (loaded by the resume's invocation id), not the advisory loop-supplied `approval_resume.input_ref`. A new `resume_replay_payload` helper centralizes the derivation so `invoke_capability` (seam) and `invoke_capability_dispatch` compute the SAME byte-stable key. The key is now derived lazily inside `persist_gate_record_for_outcome` (after dispatch, only for a gate outcome) so a missing/stale resume payload never pre-empts dispatch's own resume identity/activity validation — a malformed resume still surfaces `InvalidInvocation`, and a genuinely-missing payload still fails closed (2a-i missing-payload test stays green). Test proves a resume derives the same input_ref/key from the store even when the loop-supplied ref differs (a second resume differing only in that advisory ref replays the cached outcome instead of re-dispatching); failed first (re-dispatched, count 2) before the fix. Fix 2 — gate-record persistence for the local-dev synthetic approval producer (hazard 2). `OutboundDeliveryTargetSetHandler::request_approval` raises its gate OUTSIDE the loop-host persist seam, so it now persists a `GateRecord::Approval` itself at the raise (via the wired `GateRecordStore`), keyed by the canonical `GateRef::for_approval_request`, so host-side gate rendering (§5.2.9) has a record to read. Test proves the record persists and round-trips; failed first (load returned None) before the fix. Fix 3 — reconcile the two approval GateRef encodings. Canonicalized on the typed `GateRef::for_approval_request(id) = GateRef::from_uuid(id.as_uuid())` — a new `ironclaw_host_api` helper — as the GateRecordStore key. This is the host_api uuid GateRef (record key), a DIFFERENT type from the loop-facing `gate:approval-{id}` routing ref (`ironclaw_turns::GateRef`), so reconciling here touches NO persisted wire format and does not break the `is_approval_gate_ref` prefix-routing predicate. `ironclaw_capabilities::host` now uses the helper at both authorize sites. Test proves a host-persisted approval gate ref resolves through the read model: the routing ref recovers the approval id (`approval_request_id_from_gate_ref`), the canonical key re-derives, and the persisted GateRecord is found. Note: the task suggested canonicalizing on `from_uuid`; that is exactly what this does for the record-key GateRef. The RISKY direction (rewriting the `gate:approval-{id}` routing string to a bare uuid) was NOT taken — it would break the `is_approval_gate_ref` prefix predicate used in production routing, projection, and channel delivery, plus persisted adapter command strings and delivered-gate route records. Tests: loop_host 378 lib (incl. new + updated resume/seam/2a-i); host_api helper round-trip; composition 1629 (env-clean); capabilities/product_workflow/run_state green; architecture green; integration outbound_target/auth_gate/ reopen_resume_through_gate/group_approvals green; clippy -D warnings clean on all touched crates; pre-commit-safety clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(reborn): rustfmt the auto-merged local-dev gate-key test line Merge of refactor/reborn-resolution-model-visible collapsed a GateRef::for_approval_request call across two lines; rustfmt --check flagged it. Formatting-only, no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): drop stale replay/input/estimate from planned_driver resume test helpers The Pending{Auth,Approval}Resume structs migrated to the ReplayPayloadStore model — raw runtime input/estimate no longer ride the checkpoint (they are persisted host-side at the fresh gate raise and reconstituted on resume, keyed by invocation id). The structs dropped `replay`/`input`/`estimate` in favor of `provider_replay`/`input_ref`, but three test helpers in planned_driver.rs still set the removed fields. This never broke base/#6273/#6275 CI because the clippy matrix is change-scoped and those PRs did not pull ironclaw_runner into the compiled set; this stacked change does, surfacing the latent E0560. Remove the nine write-only stale field settings (3× replay, 3× input, 3× estimate) and the now-unused ResourceEstimate import. No behavior change: the fields did not exist on the structs, so no test read them back. Full runner suite passes (344+ tests) and clippy --all-features is clean. [skip-regression-check] compile-only fix in test helpers; the regression coverage is the now-compiling runner test module under the all-features lane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reborn): ResolutionBatch + parks() loop-suspension predicate + scripted-outcome fixture (§5.3 flip prep / Stage 1) (#6275) * feat(reborn): ResolutionBatch + parks() loop-suspension predicate + scripted-outcome fixture (§5.3 flip prep / Stage 1) Additive vocabulary + test-fixture slice ahead of the capability-result flip. Nothing consumes the new items yet, so the tree stays green. Closes a verified correctness hazard: `Resolution::is_suspension()` is `Suspended(_)` ONLY, but the batch loops the flip rewrites must also stop on the re-entrant gate variants (`Resolution::Blocked` — Approval/Auth/ Resource), which map to `Blocked`, not `Suspended`. A naive `is_suspension()` batch guard would silently let a gate fall through as if the call had completed (the #6137 bug class). This provides the correct predicate now so the flip has one canonical, tested site to call. - `ironclaw_host_api::Resolution::parks()`: the loop-semantics suspension predicate — true for every `Blocked` variant AND every `Suspended` variant; false for `Done` (incl. non-suspending `ChildSpawned`) and terminal `Denied`. `parks()` ⊋ `is_suspension()`: a `Blocked::Approval` parks but is not a suspension. `is_suspension()` keeps its narrow meaning unchanged. - `ironclaw_host_api::ResolutionBatch { resolutions: Vec<Resolution>, stopped_on_suspension: bool }`: the loop-facing batch result the flip adopts, mirroring `CapabilityBatchOutcome`'s shape/semantics over `Resolution`. Wire-stable serde. - `ironclaw_agent_loop::test_support::{resolution_from_scripted_outcome, resolution_batch_from_scripted}` (behind the existing `test-support` feature): convert existing `CapabilityOutcome` fixtures to `Resolution`/`ResolutionBatch` via the production mapping, for the flip's ~150 test sites. Tests (written first, watched fail): - host_api: acceptance table extended with a `parks` column across all 10 CapabilityOutcome→Resolution rows; a dedicated exhaustive `match` (compile-fails if a new `Resolution` variant is added) proving `parks()` ⊋ `is_suspension()`; a `ResolutionBatch` round-trip. Verified red: a naive `is_suspension()`-based `parks()` fails ("parks() disagrees for blocked"). - agent_loop test-support: round-trips scripted Completed/Failed/ ApprovalRequired → the right Resolution channel, and a batch preserving order + stop flag. No trait flip, no consumer change, `CapabilityOutcome` untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(host_api): Resolution::parks uses an exhaustive match, not matches! A new Resolution variant must be a compile error in parks() (§11.9 no-wildcard) so its parking behavior is decided deliberately rather than silently defaulting to false — the exact class of silent fall-through parks() exists to prevent (#6137). Reported-by: gemini-code-assist (PR #6275 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(runner): drop removed Pending{Approval,Auth}Resume replay fields (inherited base debt) The replay-field removal lands in a lower stack slice; until this branch's base picks it up, planned_driver.rs's cfg(test) fixtures still set the removed fields and fail the workspace clippy lane. Harmless once the base merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): commit the dispatch reservation only after the replay-payload write (#6271 IronLoop) `guard.commit()` ran before the fallible `persist_replay_payload_for_fresh_gate` store write. On a transient store error the `?` returned with the dispatch reservation still `InFlight` and the committed guard skipping its cleanup — stranding retries/duplicates of the same key waiting forever. Commit after the write succeeds, so a store error leaves the guard uncommitted and its drop clears the reservation and wakes waiters. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(reborn): record the drain-then-deploy rollout for pre-#6271 gates (§5.3 Stage 2a-i) IronLoop finding 2 flagged that gates raised/checkpointed before this change carried `{input, estimate}` on the loop resume, whereas the new code reads them from the host-side `ReplayPayloadStore`; a resume of a pre-deployment gate finds no store entry and fails closed as `Unavailable`. Author decision (PR #6271 thread): drain-then-deploy — no backward-compat bridge. In-flight gates from before the cutover are drained before rollout, and none exist pre-release, so a store miss in production is a genuine fault, never a migration gap. Record that rationale at the fail-closed miss site so a future reader debugging an `Unavailable` resume sees why there is no fallback. Comment-only; verified `cargo check -p ironclaw_loop_host --all-features` clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… delete CapabilityOutcome/mirror DTOs (§5.3 Slice C) (#6299) * refactor(reborn): resume replay payload moves host-side via ReplayPayloadStore; wire real stores (§5.3 Stage 2a-i) Moves the raw gate/auth resume replay payload out of the untrusted loop and into the host-private `ReplayPayloadStore`, and wires the real durable stores in production composition. The loop-facing result stays `CapabilityOutcome` (no `Resolution` flip — that is Stage 2a-ii). Security fix: the loop checkpoint no longer stores raw tool args, retiring the charter violation in `crates/ironclaw_agent_loop/CLAUDE.md` ("State stores refs... Do not store raw prompts, raw model output, tool args ... in state"). Write (two gate-raise paths): at a FRESH runtime gate raise (`HostRuntimeLoopCapabilityPort::invoke_capability_dispatch`, gated on `is_fresh_dispatch` before `finish_runtime_outcome`) and at the synthetic outbound-delivery approval-gate raise (`outbound_delivery.rs::request_approval`), the host `save`s a `ReplayPayload{input, estimate, prior_approval:None, input_ref, correlation_id}` keyed by `InvocationId` (write-once; a benign same-invocation duplicate is tolerated). Read on resume: `runtime_outcome_to_loop` no longer embeds input/estimate; `invocation_replay_input` is replaced by `ReplayPayloadStore::load` keyed by the `InvocationId` recovered from the resume token, in both the runtime seam (`replay_payload_for_resume`) and the synthetic decorator/handler. Fail closed on a miss: an absent payload (including a wrong-scope read) is a sanitized terminal `Unavailable`, never a silent empty-input dispatch. The fingerprinted approval lease claim ordering is preserved. Dropped loop fields (security): `input`/`estimate` from `CapabilityApprovalResume` and `PendingApprovalResume`; the whole `CapabilityAuthResumeReplay` type and the `replay` field on `CapabilityAuthResume`/`PendingAuthResume`. `approval_request_id`/ `correlation_id`/`input_ref`/`prior_approval` stay on the wire (they move in 2a-ii). Composition (real stores): `capability_wiring` builds `FilesystemGateRecordStore` (closes the #6245 production gap where it defaulted to `NoopGateRecordStore`) and `FilesystemReplayPayloadStore` over the shared scoped filesystem and threads both through the refreshing capability port. Adds the `/replay-payloads` per-user mount alias. `loop_host` gains a dependency-inversion dep on `ironclaw_capabilities` for the port trait. Tests (all failed first): fail-closed on missing payload (loop_host seam); extended the #6245 approval-resume seam test to assert the store persisted the payload and the resume redispatches the correct original input; the `ReplayPayloadStore` contract (round-trip / write-once / cross-tenant / within-tenant) rides the base commit. Full-infra resume edges pass through the production-shaped harness: group_approvals (runtime approval resume), group_journeys (approval→auth convergence), outbound_target (synthetic approval resume). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(turns): move the removed-replay design note out of prior_approval's rustdoc Reported-by: gemini-code-assist (PR #6271 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reborn): Resolution carries model-visible failure diagnostic + denial content (§5.3 flip prep / PR-B) Additive vocabulary slice (§3/§5.2.9/§5.3): make `host_api::Resolution` carry the model-visible result content the executor's `handle_capability_outcome` reads off `CapabilityOutcome` today, so a later slice (PR-C) can flip `invoke_capability` to `Resolution` without the loop reading host storage (its charter forbids that). host_api additions (redacted vocabulary only — charter fit): - `ModelInputIssue`: redacted mirror of the loop's `CapabilityInputIssue` — `DispatchInputIssueCode` (existing host_api enum) + bounded redacted `SafeSummary` path/expected/received/schema_path. - `ModelFailureDiagnostic { InvalidInput { issues }, Diagnostic { text } }`: redacted mirror of `CapabilityFailureDetail`; rides `ToolVerdict::RecoverableFailure.diagnostic` (additive Option). - `Denial { deny, reason_kind, summary }`: `Resolution::Denied(DenyRef)` → `Resolution::Denied(Denial)`, carrying the model-visible `DenyReason` + redacted `SafeSummary` on the channel (a projection of the sibling DenyRecord). Mapping (`resolution_mapping.rs`): `failed_outcome` now redacts the loop `detail` into the diagnostic (per-field SafeSummary; path-shaped free text degrades to the placeholder); the Denied arm populates the Denial channel from the same reason/summary the DenyRecord holds. Tests (failed first, then green): structured `InvalidInput` issues and free-text `Diagnostic` round-trip through the mapping; Denied carries reason_kind + redacted summary; redaction proof — path- and secret-shaped content is redacted in the `Resolution` output, never carried raw. GateRecord/ DenyRecord persistence round-trips (ironclaw_run_state) stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(host_api): enforce the 16-issue diagnostic cap in the type, not one producer ModelFailureDiagnostic::InvalidInput now carries a bounded ModelInputIssues newtype: validated at construction, revalidated on the wire (try_from), with a truncating() producer constructor for the mapping. A 17-item payload is rejected at every entry point — pinned by model_input_issues_cap_is_enforced_at_construction_and_on_the_wire. Reported-by: ironloopai (PR #6273 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reborn): ResolutionBatch + parks() loop-suspension predicate + scripted-outcome fixture (§5.3 flip prep / Stage 1) Additive vocabulary + test-fixture slice ahead of the capability-result flip. Nothing consumes the new items yet, so the tree stays green. Closes a verified correctness hazard: `Resolution::is_suspension()` is `Suspended(_)` ONLY, but the batch loops the flip rewrites must also stop on the re-entrant gate variants (`Resolution::Blocked` — Approval/Auth/ Resource), which map to `Blocked`, not `Suspended`. A naive `is_suspension()` batch guard would silently let a gate fall through as if the call had completed (the #6137 bug class). This provides the correct predicate now so the flip has one canonical, tested site to call. - `ironclaw_host_api::Resolution::parks()`: the loop-semantics suspension predicate — true for every `Blocked` variant AND every `Suspended` variant; false for `Done` (incl. non-suspending `ChildSpawned`) and terminal `Denied`. `parks()` ⊋ `is_suspension()`: a `Blocked::Approval` parks but is not a suspension. `is_suspension()` keeps its narrow meaning unchanged. - `ironclaw_host_api::ResolutionBatch { resolutions: Vec<Resolution>, stopped_on_suspension: bool }`: the loop-facing batch result the flip adopts, mirroring `CapabilityBatchOutcome`'s shape/semantics over `Resolution`. Wire-stable serde. - `ironclaw_agent_loop::test_support::{resolution_from_scripted_outcome, resolution_batch_from_scripted}` (behind the existing `test-support` feature): convert existing `CapabilityOutcome` fixtures to `Resolution`/`ResolutionBatch` via the production mapping, for the flip's ~150 test sites. Tests (written first, watched fail): - host_api: acceptance table extended with a `parks` column across all 10 CapabilityOutcome→Resolution rows; a dedicated exhaustive `match` (compile-fails if a new `Resolution` variant is added) proving `parks()` ⊋ `is_suspension()`; a `ResolutionBatch` round-trip. Verified red: a naive `is_suspension()`-based `parks()` fails ("parks() disagrees for blocked"). - agent_loop test-support: round-trips scripted Completed/Failed/ ApprovalRequired → the right Resolution channel, and a batch preserving order + stop flag. No trait flip, no consumer change, `CapabilityOutcome` untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(host_api): Resolution::parks uses an exhaustive match, not matches! A new Resolution variant must be a compile error in parks() (§11.9 no-wildcard) so its parking behavior is decided deliberately rather than silently defaulting to false — the exact class of silent fall-through parks() exists to prevent (#6137). Reported-by: gemini-code-assist (PR #6275 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(runner): drop removed Pending{Approval,Auth}Resume replay fields (inherited base debt) The replay-field removal lands in a lower stack slice; until this branch's base picks it up, planned_driver.rs's cfg(test) fixtures still set the removed fields and fail the workspace clippy lane. Harmless once the base merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reborn): complete host-side gate reconstitution — resume input_ref, local-dev gate persistence, GateRef reconcile (§5.3 flip prep / Stage 0) Stage 0 no-flip host-reconstitution slice: closes two verified correctness hazards ahead of the atomic capability-result flip. The loop-facing trait and `CapabilityOutcome` are UNCHANGED; only host-side plumbing moves. Fix 1 — resume-time input_ref reconstitution (hazard 3). On a gate/auth resume the effective input_ref used for the idempotency key + validation is now derived from the host-private `ReplayPayload` persisted at the fresh gate raise (loaded by the resume's invocation id), not the advisory loop-supplied `approval_resume.input_ref`. A new `resume_replay_payload` helper centralizes the derivation so `invoke_capability` (seam) and `invoke_capability_dispatch` compute the SAME byte-stable key. The key is now derived lazily inside `persist_gate_record_for_outcome` (after dispatch, only for a gate outcome) so a missing/stale resume payload never pre-empts dispatch's own resume identity/activity validation — a malformed resume still surfaces `InvalidInvocation`, and a genuinely-missing payload still fails closed (2a-i missing-payload test stays green). Test proves a resume derives the same input_ref/key from the store even when the loop-supplied ref differs (a second resume differing only in that advisory ref replays the cached outcome instead of re-dispatching); failed first (re-dispatched, count 2) before the fix. Fix 2 — gate-record persistence for the local-dev synthetic approval producer (hazard 2). `OutboundDeliveryTargetSetHandler::request_approval` raises its gate OUTSIDE the loop-host persist seam, so it now persists a `GateRecord::Approval` itself at the raise (via the wired `GateRecordStore`), keyed by the canonical `GateRef::for_approval_request`, so host-side gate rendering (§5.2.9) has a record to read. Test proves the record persists and round-trips; failed first (load returned None) before the fix. Fix 3 — reconcile the two approval GateRef encodings. Canonicalized on the typed `GateRef::for_approval_request(id) = GateRef::from_uuid(id.as_uuid())` — a new `ironclaw_host_api` helper — as the GateRecordStore key. This is the host_api uuid GateRef (record key), a DIFFERENT type from the loop-facing `gate:approval-{id}` routing ref (`ironclaw_turns::GateRef`), so reconciling here touches NO persisted wire format and does not break the `is_approval_gate_ref` prefix-routing predicate. `ironclaw_capabilities::host` now uses the helper at both authorize sites. Test proves a host-persisted approval gate ref resolves through the read model: the routing ref recovers the approval id (`approval_request_id_from_gate_ref`), the canonical key re-derives, and the persisted GateRecord is found. Note: the task suggested canonicalizing on `from_uuid`; that is exactly what this does for the record-key GateRef. The RISKY direction (rewriting the `gate:approval-{id}` routing string to a bare uuid) was NOT taken — it would break the `is_approval_gate_ref` prefix predicate used in production routing, projection, and channel delivery, plus persisted adapter command strings and delivered-gate route records. Tests: loop_host 378 lib (incl. new + updated resume/seam/2a-i); host_api helper round-trip; composition 1629 (env-clean); capabilities/product_workflow/run_state green; architecture green; integration outbound_target/auth_gate/ reopen_resume_through_gate/group_approvals green; clippy -D warnings clean on all touched crates; pre-commit-safety clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): Suspension::DependentRun carries the staged child result (§5.3 flip prep / Stage 1b) Closes a verified hazard blocking the atomic capability-result flip (§5.3): `host_api::Resolution`'s `Suspension::DependentRun` channel could not carry the dependent child's staged result, which the loop needs on resume. After the flip the executor gets only `Resolution::Suspended(Suspension::DependentRun(..))`; the mapping folded `result_ref`/`byte_len`/`safe_summary` into the host-persisted `GateRecord::DependentRun` (the loop cannot read host storage) and dropped `model_observation` entirely — so byte caps, model-visible child text, and the model observation would silently regress ("never drop model output"). This ADDITIVE slice gives `Suspension::DependentRun` an inline staged-result payload alongside its `GateWaypoint`, mirroring how `Done(Outcome)` carries a spawned child run's content: - New `DependentRunResult { byte_len, summary: SafeSummary, observation: Option<SafeSummary>, origin: Option<LoopRef> }` — plain redacted host_api vocabulary; the full child bytes stay host-owned behind the record's `ResultRef`. `Suspension::DependentRun` becomes a struct variant `{ waypoint, result }`; accessors split from `ExternalTool`, plus a `dependent_result()` accessor. - `capability_outcome_to_resolution`'s `AwaitDependentRun` arm populates it from the loop's `result_ref`/`byte_len`/`safe_summary`/`model_observation` (redaction applied), and the "model_observation has no home … dropped" behavior is deleted — it now rides the inline observation preview (the same `observation_preview` reduction Completed/SpawnedChildRun use). The durable `GateRecord::DependentRun` sidecar is still persisted (host durability); the inline payload is the loop-visible copy — the same dual pattern as PR-B. Nothing consumes the new payload yet: `LoopCapabilityPort` is not flipped, `CapabilityOutcome` is not deleted, executor consumers are untouched — the tree stays green. Test-first: extended the `resolution` + `resolution_mapping` acceptance tables and added dedicated round-trip + redaction tests — an `AwaitDependentRun` outcome round-trips its `result_ref` origin, `byte_len`, `safe_summary`, AND `model_observation` onto `Suspension::DependentRun`, and a secret/path-shaped summary/observation degrades (dropped / placeholder). These fail against the pre-change tuple variant (fields absent / model_observation dropped). The `GateRecord::DependentRun` persistence round-trip stays green. Verified: `cargo test -p ironclaw_host_api -p ironclaw_turns -p ironclaw_run_state --all-features`; `cargo clippy -p ironclaw_host_api -p ironclaw_turns --all-targets --all-features -- -D warnings`; `cargo test -p ironclaw_architecture`; `scripts/pre-commit-safety.sh`; downstream build of loop_host/agent_loop/runner/reborn_composition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): loop-facing result becomes host_api::Resolution (§5.3 Stage 2 — the flip) (#6287) * refactor(reborn): loop-facing result becomes host_api::Resolution (§5.3 Stage 2 — the flip) Collapses the loop-facing capability result — the overloaded ten-variant `CapabilityOutcome`/`CapabilityBatchOutcome` — into the five-channel `host_api::Resolution`/`ResolutionBatch` at the `LoopCapabilityPort` trait boundary. `CapabilityOutcome` is retained (Stage 2b deletes it); producers convert at their boundary via `capability_outcome_to_resolution(..).resolution`. The trait flip: `invoke_capability -> Result<Resolution, _>`, `invoke_capability_batch -> Result<ResolutionBatch, _>`. Batch loops gate on `Resolution::parks()` (a re-entrant `Blocked` gate stops the batch too, H1), replacing `CapabilityOutcome::is_suspension()`. All ~49 impls + decorators/ factories migrated in dep order (turns -> loop_host -> hooks -> composition -> runner -> agent_loop), plus every test double and the `tests/integration` doubles. Executor consumer rewrite (ironclaw_agent_loop): `handle_capability_outcome`, the batch fast-path drain, `handle_capability_error` retry, `shared_await_dependent_gate`, and `capability_batch_counts` match exhaustively over `Resolution` (no wildcard, §11.9). host_api->loop inverse reconstruction: loop refs from the channel's preserved `origin`; `Done` re-split on `ToolVerdict`; model-visible failure/denial content from PR-B (`RecoverableFailure.diagnostic`, `Denial`); the DependentRun staged result from `Suspension::dependent_result()`. Resume identity stays byte-stable: `approval_request_id` reconstructed deterministically from the `gate:approval-{id}` routing ref (fingerprinted lease byte-identical); `prior_approval` kept on the wire (host-side move deferred to §5.3 Stage 2a-ii); `input_ref` advisory on resume (host reconstitutes from `ReplayPayloadStore`, Stage 0); `correlation_id` observability-only, regenerated at the loop boundary. Stage-0 resume + missing-payload fail-closed + cross-tenant tests stay green. Auth `credential_requirements` render-from-record: moved off the loop-facing `Blocked::Auth` channel onto `GateRecord::Auth` (§5.2.9), keyed deterministically by the new name-based `GateRef::for_auth_gate` (the auth gate id is `auth-{sha256hex}`, not a uuid, so it derives a stable v5 uuid); the runner re-reads them from the durable record at the blocked-exit application into `TurnRunRecord.credential_requirements` — the auth analogue of approval's Stage-0 render-from-record. Model-visible first-look preview (#5838) fixed correctly, not dropped: the collapse mis-routed tool-result CONTENT through `SafeSummary`, a caption type that rejects `{ } [ ] /` delimiters (all structured output) and — via a substring `"secret"` match — scrubbed the ordinary word "Secretary" (the #6129 bug: an amnesia re-read loop that tanked benchmark scores). Ports the canonical word-boundary credential matcher into host_api (`credential_redaction`) and applies it to `SafeSummary` so captions like "Secretary" survive; adds `ModelResultPreview` — the correct 24 KiB content vehicle that tolerates delimiters/newlines and redacts only genuine credentials (word-boundary markers + secret-like tokens) — and re-points `Outcome.refs.preview` at it, plus `ResultPreviewMeta` carrying the truncated-preview continuation metadata (referenced ref, total bytes, next offset, item count) so `result_read` pagination survives. The executor reconstructs the `ResultReference` observation from real content, preserving the inline first-look optimization (no extra `result_read` round-trip). Resume-core completions surfaced driving the production integration tiers green (the flip drops the loop-facing `correlation_id`, so every resume consumer must reconstitute it host-side, not read it off the wire): - Loop-host runtime resume reconstitutes `correlation_id` from the persisted `ReplayPayload` (keyed by invocation id) and applies it to the invocation context before the fingerprinted approval/auth lease is matched — the loop's post-flip value is advisory. Without this the lease's correlation match fails ("approval request does not match invocation: correlation_id") and the resume terminalizes (caught by `reborn_group_approvals`, `reborn_group_journeys`). - The synthetic outbound-delivery handler (raises its OWN approval gate outside the loop-host persist seam) reconstitutes `correlation_id` from its own replay payload for the approval cross-check instead of trusting the executor's now- fresh `resume.correlation_id`, completing the same §5.3 Stage 2a-i render-from- record pattern it already applied to `{input, estimate}` (caught by `reborn_integration_outbound_target`). - Deterministic gate-record keys (`for_auth_gate`, and `for_approval_request` on the authorize path) make a re-raised gate derive the SAME content-addressed key; the write-once `GateRecordStore` reports `GateRecordAlreadyExists`, which the host now treats as benign (first-write-wins, record byte-identical), mirroring the existing `ReplayPayloadAlreadyExists` tolerance — it strengthens, not weakens, the write-once contract (caught by the auth-convergence journey). - The completed-result `ResultReference` observation's own producer-authored `summary` (the truncation/continuation hint, distinct from the generic result- message caption) is carried through the collapse on `ResultPreviewMeta.summary` so the executor rebuilds the observation with the producer's exact text rather than the caption (caught by `reborn_integration_tool_call`'s truncated-preview and array-item-count transcript assertions). Full gate green: workspace `cargo clippy --all-targets --all-features -D warnings`; `ironclaw_architecture`; the changed-crate unit suites (the only red is the 3 pre-existing `llm_admin::nearai_mcp` env-var tests, unrelated); and all 51 Reborn integration bins (706 scenarios) including every §11.9 per-channel / gate / resume / auth tier. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): gate record keyed to returned Resolution + fail unsubmittable auth block (#6287 IronLoop) Two blocking gate-record correctness fixes on the §5.3 Stage 2 flip: 1. Map once (capability_port.rs). `invoke_capability` mapped the outcome twice — once inside `persist_gate_record_for_outcome` to derive the key it saved under, and once via `capability_outcome_to_resolution` to build the return value. The approval/resource/dependent/external channels mint a fresh random `GateRef` per mapping (only auth is deterministic), so the persisted record and the returned `Resolution` pointed at different refs and a resume could never load the record. Now maps once and persists/returns the same `MappedResolution` (`persist_gate_record_for_outcome` -> `persist_gate_record_for_mapped`, no internal re-map). 2. Fail the exit on unsourceable auth requirements (turn_run_executor.rs). The flip made the durable `GateRecord::Auth` the ONLY source of `credential_requirements`, so a lookup that does not yield the record is a real regression: applying the block leaves an unsubmittable, provider-null auth gate. `enrich_auth_block_credential_requirements` now returns `Result`; the three lookup-result miss arms (store fault, `Ok(None)`, wrong kind) return `Err`, and `apply_exit` records a terminal failure (shared `record_exit_failure` helper) instead of applying the incomplete block. The two tolerant pre-conditions (no store, non-auth ref) stay warn+empty. Regression tests: extend `approval_gate_outcome_persists_gate_record_at_the_seam` to assert the returned Resolution's gate ref equals the persisted key; new `auth_block_with_unsourceable_requirements_fails_the_exit` drives `apply_exit` with a missing-record store and asserts a terminal failure is recorded (this test also caught a `&'static str` lifetime bug in the helper). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): forward dependent-run observation caption to the resumed parent (#6287 IronLoop) `dependent_run_result_message` hardcoded `model_observation: None`, so `append_capability_result_ref`'s synthesize-fallback re-dropped the child's staged observation and the resumed parent saw only a bare success sentinel — not the caption/result reference the `AwaitDependentRun` channel forwarded before the flip. The mapping already preserves the caption on `DependentRunResult.observation` ("model_observation now rides the inline observation preview (was dropped entirely)"); the consumer just dropped it. Forward it as a Success `ResultReference` observation carrying the caption as its summary and pointing at the staged child result (`result_ref` + `byte_len`), so the parent can `result_read` the child output. The inline first-look preview content stays host-owned (the completed-`Outcome` path), per the mapping's "bounded SafeSummary caption" design — so `preview` stays `None` here. Test: `await_dependent_run_preserves_model_observation_for_replay` now asserts the forwarded `ResultReference` observation instead of the previously-pinned synthesized `GenericFailure` sentinel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): replay returns the record-backed gate ref, not a fresh mint (#6287 IronLoop) The map-once fix made the first invocation's persisted key and returned gate ref agree, but the replay path still diverged: on an idempotent replay the dispatch cache returns the same CapabilityOutcome, invoke_capability re-maps it (a fresh random GateRef for the approval/resource/dependent/external channels), and the set-based guard then skipped the persist — so the replayed Resolution carried a gate ref no record was ever saved under. Turn the replay guard into a resolution cache: `persisted_gate_resolutions: HashMap<IdempotencyKey, Resolution>`. The first invocation reserves the key with its resolution (atomic check-and-insert under one lock) and persists exactly one record under that resolution's gate ref; a repeat (or concurrent duplicate) finds the reservation and returns the SAME resolution — the one whose gate ref the record is under — instead of re-minting. Rollback-on-failed-save is preserved so the next replay retries. Regression: extend `replayed_gate_invocation_does_not_persist_a_duplicate_record` to assert the replayed Resolution's gate ref equals the first invocation's and the single persisted record's key, and that the record loads by the replayed ref. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): gate resolution reservation waits for durable persist (#6287 IronLoop) The reserve-then-save replay cache had a concurrency window: a concurrent duplicate could find the reservation and return the resolution before the owner's save durably completed, so a transient store fault (owner rolls back and errors) left the waiter holding a blocked resolution whose gate record was never persisted — an unresumable gate. Make the reservation a proper in-flight/persisted state machine mirroring the crate's own DispatchRecordStore + wait_for_dispatch_completion: - persisted_gate_resolutions: HashMap<IdempotencyKey, GateResolutionState>, where GateResolutionState = InFlight(Arc<Notify>) | Persisted(Box<Resolution>). - The first caller reserves InFlight and is the sole persister. A concurrent duplicate / later replay finds InFlight and WAITS on the notify, using the lost-wakeup-safe pattern (create notified(), re-check the same reservation under the lock via gate_resolution_in_flight_matches, await only if still in-flight). - The owner publishes only AFTER the durable save resolves: Persisted on Ok/benign GateRecordAlreadyExists, cleared on a transient fault; it wakes the waiters either way, so a woken waiter receives the now-durable resolution or re-owns and retries. No waiter receives a resolution before durable persist. Covered by the existing replay/rollback regression tests (replayed_gate_invocation_does_not_persist_a_duplicate_record, failed_gate_record_persist_is_retried_on_replay), which still hold. [skip-regression-check] deterministic single-threaded coverage is unchanged; the concurrency window is a narrow race the existing tests already pin the sequential behavior of, and a hermetic multi-waiter race harness is infeasible to make reliably deterministic here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): wire durable gate/replay stores into the ProductLive capability port (#6287 IronLoop) The ProductLive planned-runtime path built its capability port with only runtime/input/result/milestone/mounts — never `.with_gate_record_store(..)` / `.with_replay_payload_store(..)` — so on that path the gate record was never persisted and the replay payload never reconstituted, breaking approval/auth resumes (the local-dev path already wires both). - `ProductLivePlannedRuntimeAdapterConfig` + `ProductLiveLoopCapabilityPortFactory` gain `gate_record_store` + `replay_payload_store`; `create_capability_port` now calls `.with_gate_record_store(..).with_replay_payload_store(..)` on the host-runtime factory, exactly as the local-dev path does. - The product-workflow planned-loop harness builds both stores over ONE in-memory filesystem (production mount view via `wrap_scoped`) so a raise and its resume round-trip through the same records; the composition adapter tests supply in-memory-backed stores through the config. - Adds `ironclaw_capabilities` to product-workflow dev-deps for the harness's `FilesystemReplayPayloadStore`. Verified: `cargo test -p ironclaw_reborn_composition --test product_live_adapters` passes; product-workflow planned-loop harness tests build; clippy --all-features clean on both crates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): RAII guard clears orphaned gate-resolution reservation on cancel/fault (#6287 IronLoop) The in-flight gate-resolution reservation cleaned up only on the paths that ran to completion. If the owning persist future was cancelled (dropped mid-`save`), or returned early before the publish block, the `InFlight` entry was orphaned and same-key replays waited on it forever; the transient-failure cleanup relied on code after the `await` running. Protect the reservation with an RAII drop guard mirroring the crate's own `DispatchReservationGuard`: - On reserving `InFlight`, the owner holds `GateResolutionReservationGuard`. Any drop without `commit()` — cancellation, transient store fault, or an early error — calls `clear_gate_resolution_reservation`, which removes the entry (only if still `InFlight`, never a published resolution) and wakes its waiters so one re-owns and retries. - The success / benign `GateRecordAlreadyExists` path publishes the durable `Persisted` resolution (`publish_gate_resolution`), wakes waiters, then `commit()`s the guard so its drop is a no-op. No waiter can be left blocked on an orphaned reservation regardless of how the owner exits. Regression: `cancelled_gate_persist_clears_reservation_so_replay_can_re_own` blocks the first `save` on a barrier, cancels the invocation while parked in `save`, and asserts a replay re-owns, completes within a timeout (no hang), and persists exactly one record. `failed_gate_record_persist_is_retried_on_replay` covers the transient-fault cleanup path through the guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): share the ProductLive gate store with the turn executor too (#6287 IronLoop) The prior ProductLive wiring fix only wired the gate-record store into the capability port (the persist side). The turn executor (`RebornTurnRunExecutor`, via `DefaultPlannedRuntimeParts.gate_record_store`) still got `None`, so its render-from-record path (`enrich_auth_block_credential_requirements`, §5.2.9) could not read the record — an auth block was applied with empty requirements / failed the exit on the ProductLive path. The harness now shares ONE gate-record store between both sides: the store built for the capability port is captured into `turn_executor_gate_store` and passed to `DefaultPlannedRuntimeParts.gate_record_store`, so a persisted auth gate record round-trips to the executor read. Stays `None` for the non-ProductLive capability fakes (which do not persist gate records), preserving the executor's tolerant "no store wired" path; `inbound_turn_contract.rs` uses `EmptyCapabilityFactory`, so its `None` is correct and unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): commit the dispatch reservation only after the replay-payload write (#6287 IronLoop) `guard.commit()` ran before the fallible `persist_replay_payload_for_fresh_gate` store write. On a transient store error the `?` returned with the dispatch reservation still `InFlight`, and the already-committed guard skipped its cleanup — stranding retries/duplicates of the same idempotency key waiting on the reservation forever. Move `guard.commit()` to AFTER the replay-payload write succeeds, so a store error leaves the guard uncommitted; its drop then clears the reservation and wakes waiters so one re-dispatches. The finish-runtime-outcome ordering is unchanged. Covered by `failed_gate_record_persist_is_retried_on_replay` and the `replayed_gate` seam tests (the reservation-clears-then-retry path). [skip-regression-check] the store-write-failure reservation-clear path is exercised by the existing retry/replay seam tests; a dedicated dispatch-cancel harness for this specific reordering is redundant with them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): producers emit Resolution directly; delete CapabilityOutcome (§5.3 Stage 2b — collapse complete) (#6293) * chore(reborn): anchor s2b worktree at flip base Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): add producer-facing Resolution constructors in ironclaw_turns (§5.3 Stage 2b) New run_profile::resolution module with completed/failed/spawned_process/ spawned_child_run (return Resolution), approval_required/auth_required/ resource_blocked/await_dependent_run/external_tool_pending (return GatedResolution), and denied (returns DeniedResolution). Moves the non-lossy redaction verbatim from resolution_mapping (ModelResultPreview 24 KiB word-boundary redaction, ModelFailureDiagnostic, deterministic GateRef::for_auth_gate auth key, resume-token carry, DependentRunResult inline staged result, DenyReason mapping). resolution_mapping:: capability_outcome_to_resolution is retained transitionally as a thin delegator so unmigrated producers keep compiling; RefBindings is now empty (loop refs ride the channel origin). 17 retargeted constructor tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): loop_host producers emit Resolution directly (§5.3 Stage 2b) capability_port.rs: invoke_capability_dispatch now returns GatedResolution; the seam persists the durable GateRecord from .gate_record (GateRef::for_auth_gate key + replay-payload persistence intact) and returns .resolution — deleting the capability_outcome_to_resolution re-map. runtime_outcome_to_loop and the synthetic/inline dispatch arms emit Resolution/GatedResolution via the new constructors; the runtime failure classifier returns a private LoopFailureClass (Failed | Denied) so the per-tool display preview still stages from the raw fields. lib.rs empty-surface denial, subagent_spawn_port (spawn gates + batch coalescing keyed on the DependentRun channel origin), and capability_surface_filter denials all emit Resolution directly. 378 loop_host tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): hooks middleware emits Resolution directly (§5.3 Stage 2b) HookedLoopCapabilityPort::decision_to_outcome and fail_closed_gate_ref_unavailable now return host_api Resolution (deny/approval/auth) via the producer constructors instead of building CapabilityOutcome and re-mapping; single and batch invoke paths consume the Resolution directly. Test double emits resolution::completed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): local_dev synthetic capabilities emit Resolution directly (§5.3 Stage 2b) SyntheticCapabilityHandler::invoke now returns host_api Resolution; the synthetic port and external-tool port delegate it straight through (no capability_outcome_to_resolution re-map). outbound_delivery (approval gate + denial/failure paths, internal ApprovedResumeDecision + approval_lease_outcome now carry Resolution), skill_activation, project_create, result_read (parse error boxed for result_large_err), and external_tool_capability all build Resolution via the producer constructors. Tests assert Resolution::Done recoverable-failure verdicts and Resolution::Denied. 230 local_dev tests green; crate clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): runner tool-disclosure + subagent flavors emit Resolution directly (§5.3 Stage 2b) tool_disclosure_port bridge helpers (invoke_bridge/tool_search/describe/ describe_first/completed_bridge_result, failed_invalid_input) and the test double now build host_api Resolution via the producer constructors; subagent flavors test double likewise. No capability_outcome_to_resolution re-map remains in runner producers. Runner tests + clippy -D warnings green (all-features). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): agent_loop executor test fixtures build Resolution via constructors (§5.3 Stage 2b) shared_await_dependent_gate fixtures (await_dependent/completed/approval) now use the producer constructors directly instead of mapping a CapabilityOutcome. 401 agent_loop tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): migrate agent_loop capability fixtures off CapabilityOutcome (§5.3 Stage 2b) MockHost stores Resolution/ResolutionBatch directly; scripted_capability_outcome maps ScriptedCapabilityOutcome -> Resolution via the producer constructors; ~90 executor test fixtures rebuilt as resolution::* + ironclaw_host_api::ResolutionBatch (transformed by a comment/brace-aware script). resolution_from_scripted_outcome deleted; resolution_batch_from_scripted takes Resolutions. 401 agent_loop tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): delete CapabilityOutcome and the transitional mapping (§5.3 Stage 2b — collapse complete) Deletes CapabilityOutcome, CapabilityBatchOutcome, CapabilityResultMessage, CapabilityFailure, CapabilityDenied, ProcessHandleSummary from run_profile::host, the resolution_mapping delegator (capability_outcome_to_resolution / MappedResolution / RefBindings), and their re-exports. Retargets the last serde-fixture tests (turn_coordinator, agent_loop_host_contract, content_digest) onto the surviving vocabulary (CapabilityDeniedReasonKind / CapabilityProgress / Resolution). CapabilityApprovalResume/AuthResume/ResumeToken kept (resume requests). Architecture ratchet trims CapabilityOutcome from FROZEN_COLLAPSE_DTOS per its own shrink-only instructions. No production code references CapabilityOutcome; turns + architecture ratchet green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): retain CapabilityResultMessage/CapabilityFailure as executor-internal vocabulary Scope correction: these two are NOT dead payloads — the agent_loop executor reconstructs them from host_api::Outcome (capability_result_from_outcome / capability_failure_from_recoverable, added by the flip) and consumes them across gates/capability_helpers/strategies. They are loop-internal working types now (no producer emits them; documented as such). The genuinely-dead CapabilityOutcome/CapabilityBatchOutcome/CapabilityDenied/ProcessHandleSummary stay deleted. Workspace builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): migrate integration harness capability double off CapabilityOutcome (§5.3 Stage 2b) RecordingTestCapabilityPort (the shared tests/integration double) builds host_api::Resolution via the producer constructors; completed_result returns Resolution, approval/echo paths emit resolution::approval_required/completed directly. Unblocks every reborn_integration_* binary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(reborn): refresh loop_host seam comments post-collapse (§5.3 Stage 2b) The seam TODO to delete CapabilityOutcome is done; dispatch emits GatedResolution directly. Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(reborn): cargo fmt the collapse tree (import ordering + line wrapping) Formatting-only. The capability-result collapse and the -s ours main merge left import ordering and long-line wrapping unformatted across the loop-host, agent-loop executor tests, composition local_dev, and turns run_profile files; `cargo fmt --all` resolves them. No logic change. Fixes the red Formatting lane on PR #6299. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): retain gate-record + replay-payload stores on ProductLivePlannedRuntimeAdapters (#6299 IronLoop) `ProductLivePlannedRuntimeAdapters::from_services` moved the host-private `gate_record_store` / `replay_payload_store` into the capability factory but did not retain them on the bundle. The bundle is the production API for composing a product-live planned runtime, so a consumer building the runner (`DefaultPlannedRuntimeParts`) from it had no way to wire the SAME store the capability port persists `GateRecord::Auth` into — it would pass `gate_record_store: None`, and on an `AuthRequired` resume the turn executor reloads an empty requirement set and applies an unactionable auth block (the §5.2.9 render-from-record seam, same class as #6287 IronLoop f6/f8). Expose both stores as `pub` fields, populated from cheap `Arc` clones taken before the originals move into the factory, so both sides share one durable store. Additive, no behavior change to existing consumers. Note: the only current bundle consumer (the product-workflow planned-loop test harness) extracts just `.capability_factory`; its runner already wires the same store via `turn_executor_gate_store`, so the end-to-end product-live auth-gate tests through that path pass unchanged. This closes the bundle's API gap so a future/production runner consumer wires the store, not None. cargo check + clippy (test-support,libsql) clean on ironclaw_reborn_composition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Slice C result-side collapse, seam-first (§3/§5.3 of
docs/reborn/2026-07-17-architecture-simplification-dto-dyn-local.md). Makeshost_api::Resolution+ theGateRecordStorelive in production at the loop_host capability seam, without a 40-file big-bang.Seam mechanics
HostRuntimeLoopCapabilityPort::invoke_capabilityis now a thin wrapper around the existing dispatch body (renamedinvoke_capability_dispatch): it produces the loop-facingCapabilityOutcome, then callspersist_gate_record_for_outcome, whichResolutionviacapability_outcome_to_resolution(feat(reborn): CapabilityOutcome → Resolution mapping in ironclaw_turns #6242), andGateRecordthroughGateRecordStore::save(feat(run_state): persistent GateRecordStore for the capability-result collapse (§5.2.9) #6243), keyed by the freshly-mintedGateRefon the resolution channel.DenyRecordis terminal/same-turn and is not persisted (per #6243).Done/Suspended(Process)carry no gate record and no-op. The batch path is unchanged — it calls the persistinginvoke_capability, so it inherits per-outcome persistence. A store write failure propagates fail-closed, consistent withrecord_loop_completed(the bound cause is logged atdebug, never interpolated into a model-visible summary).Store wiring (DI)
GateRecordStoreis injected intoHostRuntimeLoopCapabilityPortand its factory via the constructor + awith_gate_record_storebuilder (mirroring the crate's existingwith_trajectory_observerDI). The default is a transitionalNoopGateRecordStore: the persisted record has no reader yet (the resume-turn render path + the loop-ref↔minted-ref association are the follow-up), so skipping the write is behavior-preserving and never regresses an unwired path's gates.Dependency edge
ironclaw_loop_host(loops) →ironclaw_run_state(kernel), allowed by the layer matrix — no architecture exception needed;cargo test -p ironclaw_architectureis green.Consumers migrated
None — the port deliberately still returns
CapabilityOutcome, soironclaw_agent_loop(executor/capabilities.rs,executor/mapping.rs) andironclaw_turnsare untouched and every existing behavior (evidence admission, redaction, retry/terminal classification, progress/terminate_hint/output_digest derivation, gate resume) is preserved bit-for-bit.Genuine blocker — why the consumer flip is a follow-up, not this PR
Flipping the port to return bare
Resolutionand migrating the executor cannot preserve existing behavior, becauseResolutionis intentionally lossy vs. what the executor needs:CapabilityOutcome::{ApprovalRequired,AuthRequired}carryapproval_resume/auth_resumethatGateStageneeds to resume gates;Resolution::Blockedhas no such field (the mapping drops them).error_kind— the executor'scapability_error_class/capability_failure_kindneedCapabilityFailureKindfor retry/terminal classification (agent-loop-capabilities.md);ToolVerdict::RecoverableFailurecollapses all failure kinds to one.output_digest/terminate_hint/progress— G4 loop-derived signals used for output-aware progress derivation; absent fromOutcome.LoopResultRef(verified by the evidence port); the mapping mints fresh uuidResultRefs and returns the loop→minted binding separately.Preserving all of that while consuming bare
Resolutionrequires either extending the frozenhost_apivocabulary or carrying a companion (which defeats the collapse). The full consumer migration therefore needs the resume/read wiring + a loop-side carrier decision, tracked as the producer-migration follow-up (§9).Also deferred to that follow-up: durable
FilesystemGateRecordStorecomposition wiring + the/gate-recordsmount alias (the record is orphaned until the resume-read consumes it; the mount-alias change would regress gates if wrong).Incidental base-branch build fixes
The #6242+#6243 integration merge left
RunStateError::GateRecordAlreadyExistsuncovered in two exhaustive matches (ironclaw_capabilities::helpers::run_state_error_kind,ironclaw_host_runtime::production::unavailable_from_run_state). The base branch does not compile without these one-arm additions; fixed here.Tests (crate-tier, drive the production seam, red-first verified)
approval_gate_outcome_persists_gate_record_at_the_seam— a gate outcome through the seam persists aGateRecord::Approvalthat round-trips via the store (save→loadby the mintedGateRef), and the loop still receives the unchangedApprovalRequiredoutcome. Confirmed failing ("exactly one gate record persisted") with the persist call removed.gate_outcome_through_unwired_default_is_inert_and_non_regressing— a gate through the transitional no-op default still returns the gate (no regression).The durable
FilesystemGateRecordStoreround-trip itself is covered byironclaw_run_state'sgate_record_store_contract.Verification (all green)
cargo test -p ironclaw_loop_host -p ironclaw_agent_loop -p ironclaw_turns -p ironclaw_run_state --all-featurescargo clippy -p ironclaw_loop_host -p ironclaw_agent_loop -p ironclaw_turns --all-targets --all-features -- -D warnings(+ironclaw_capabilities/ironclaw_host_runtime)cargo test -p ironclaw_architecturescripts/pre-commit-safety.shNo
#[cfg(feature=...)]gates were added/moved, so the full feature matrix is not required.🤖 Generated with Claude Code