Skip to content

fix(workflow): architect-only audited exit from rework_required (#2755) - #2760

Merged
zaxbysauce merged 3 commits into
mainfrom
fix/issue-2755-rework-recovery-escape-hatch
Sep 14, 2026
Merged

zaxbysauce merged 3 commits into
mainfrom
fix/issue-2755-rework-recovery-escape-hatch

Conversation

@zaxbysauce

@zaxbysauce zaxbysauce commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Closes #2755

PR head: c826e08

Summary

A task stranded at rework_required by a Stage B verdict that did not require
a code change had no architect-legal exit: the mechanical stage_a_passed
transition intentionally throws TASK_WORKFLOW_CODER_MUTATION_REQUIRED from
that state, /swarm recover skips non-coder_delegated states, and the
dispatch refusal recommended dead ends — unattended runs hard-stopped with
correct, reviewed, gate-green code. This PR adds the audited escape hatch the
repo already uses for this class (#2703 precedent): the architect-only
recover_rework_task tool.

Root Cause

src/gate-evidence.ts — the stage_a_passed reducer case (around line 647)
throws from any state other than coder_delegated/pre_check_passed, which is
intentional (a genuine defect must go back through the coder; sibling #2758
documents the same). But nothing represented "Stage B failed WITHOUT a code
defect": the repair machinery behind /swarm recover
(src/workflow/stage-a-repair.ts scanStageATask) only handles
coder_delegated, the repair_idle force-repair requires a settled plan
status, and every non-terminal exit presupposed an accepted coder mutation.
The dispatch refusal at src/hooks/delegation-gate.ts:4458-4465 then advised
pre_check_batch (throws) and the human-only /swarm recover (skips this
state).

Fix

  • src/gate-evidence.ts — the stage_a_passed event union gains an optional
    supervisedRecovery flag; the reducer admits rework_required ONLY when it
    is set. Mechanical emitters (guardrails recorder, stage-a-repair) never set
    it, so they still fail closed exactly as before.
  • src/workflow/rework-recovery.ts (new) — forceRecoverReworkTask, the
    fail-closed ladder mirroring forceRecordRetrySoundingBoardApproval:
    architect session, exact plan task, durable state exactly
    rework_required, green pre-check proof for the wedged generation (the
    [Workstream A] PR 10 of 12: Connect existing task recovery and status to authoritative transition receipts #2665 secretscan+SAST bar, anchored at the wedge transition timestamp),
    generation CAS, then the supervised stage_a_passed plus a
    stage_a_repair audit event (action rework_recovered) through the shared
    wrapper. Every refusal is a distinct typed error.
  • src/tools/recover-rework-task.ts (new) — the registered tool
    (task_id + audited reason, zod-strict, JSON contract).
  • Registration per AGENTS.md invariant 11: TOOL_METADATA (architect-only),
    TOOL_MANIFEST thunk, barrel export; derived maps regenerate
    (check-tool-registration.ts: 134 tools coherent).
  • src/hooks/delegation-gate.ts — the TASK_WORKFLOW_STAGE_A_REQUIRED
    remediation now branches on state: rework_required names
    recover_rework_task (and the coder repair loop for real defects); the
    attribution-wedge branch keeps /swarm recover where it actually applies.
  • src/workflow/stage-a-repair.ts — additive export on
    hasGreenPostSettlementPreCheck / appendStageARepairEvent for reuse.

Recurrence Prevention (defect class)

  • Defect class: agent-reachable workflow failure states whose only exits
    require work the triggering condition did not produce, with no audited
    supervisor override and remediation naming agent-uninvocable surfaces.
  • Sweep result: 47 + 9 + 9 hits across three predicates, every hit
    dispositioned (by-design human surfaces; settlement-wedge policy with
    documented agent self-heal; correct state-gated texts; two follow-up rows
    owned by open sibling Guardrails label TASK_WORKFLOW_CODER_MUTATION_REQUIRED as an attribution miss and advise the human-only /swarm recover #2758; the fixed state itself).
  • Guardrail: frozen PRESERVING check C3 + reducer regression tests, with
    executed revert probe (supervised branch removed → C1 FAIL) and widening
    probe (flag check dropped → C3 FAIL).

Regression Protection

  • rework-recovery-supervised.test.ts: supervised admission (state flip,
    generation unchanged, retry history preserved) vs mechanical refusal vs
    terminal-state refusal vs plain-path preservation.
  • rework-recovery.test.ts: the full fail-closed ladder incl. the
    SAST-disabled shape, audit event fields, transition-id prefix, CAS, and
    repeat-call refusal.
  • delegation-gate-rework-remediation.test.ts: both Stage B agents get the
    recover_rework_task remediation from rework_required; the
    coder_delegated branch keeps /swarm recover.

Acceptance Criteria -> Evidence

Acceptance criterion (from intake) Evidence (command + output, or test name)
AC1 autonomous architect-legal move to a Stage-B-dispatchable state through the registered tool C1: ERROR→GREEN (tool-driven rework_requiredpre_check_passed, durable re-read); dispatch gate accepts pre_check_passed (delegation-gate.ts:4409-4415)
AC2 audited + distinguishable + fail-closed C2: ERROR→GREEN (audit event rework_recovered, rework-recovery: transition id, session/state/greenness refusals leave state untouched)
AC3 guardrail intent preserved C3: GREEN→GREEN (mechanical throw retained, auto-scan skip retained, terminal refusal retained) + widening mutation probe flipped C3 RED
AC4 truthful remediation text C4: RED→GREEN (rework branch names the tool, no /swarm recover there)
AC5 registration surfaces complete C5: RED→GREEN + check-tool-registration.ts "134 tools, coherent"

Invariant audit

  • 1 (plugin init): not touched — no init-path code changed; the tool/helper run only on explicit invocation.
  • 2 (runtime portability): touched — new tool/module in the bundle; bun run build + bundle-portability 10/10 + plugin-shape 2/2 + packaging smoke 10/10 + Node ESM import → plugin id: opencode-swarm | has server: true; no bun: resolution, no Bun.* calls.
  • 3 (subprocesses): not touched — no spawn sites added or modified.
  • 4 (.swarm containment): touched — all writes go through existing transitionTaskWorkflowEvidence / appendCoreEventSync seams under .swarm/.
  • 5 (plan durability): not touched — plan files/ledger untouched (read-only loadPlanJsonOnly).
  • 6 (test_runner safety): not touched.
  • 7 (test writing): touched — four new bun:test files, real fixtures on temp dirs, zero mock.module, each < 500 lines (check:test-file-cap 0 violations, check:mock-cleanup clean).
  • 8 (session state): touched (read-only) — architect check via the existing ensureAgentSession seam; no new session-keyed state.
  • 9 (guardrails/retry): touched — new audited escape hatch; the mechanical stage_a_passed path and the retry circuit semantics are unchanged (frozen C3 + mutation probe).
  • 10 (chat/system msg): not touched.
  • 11 (tool registration): touched — full chain (metadata + manifest + barrel + release fragment + tests); check-tool-registration.ts passes, tests/unit/config 2166/0, architect-tool-lists 13/13.
  • 12 (release/cache): touched (docs only) — docs/releases/pending/rework-recovery-escape-hatch-2755.md fragment; version files untouched.

Test plan

  • New regression tests (per file): bun test tests/unit/gate-evidence/rework-recovery-supervised.test.ts 6/0;
    tests/unit/workflow/rework-recovery.test.ts 6/0;
    tests/unit/tools/recover-rework-task.test.ts 5/0;
    tests/unit/hooks/delegation-gate-rework-remediation.test.ts 3/0.
  • Frozen acceptance checks C1-C5: ERROR/RED at base → GREEN at head
    (replayed via repro-check.sh run; verify-checkpoint 5/5 OK).
  • Impacted suites: tests/unit/gate-evidence (21/0), tests/unit/workflow
    (all files green incl. stage-a-repair 15/0), delegation-gate-reentry-bypass
    5/0, stage-a-error-classification 3/0, acceptance-injection 18/0,
    guardrails-durable-stage-a-attribution 10/0, manifest-parity 9/0,
    registration-fix.adversarial 37/0, architect-tool-lists 13/13,
    tests/unit/config 2166/0.
  • Lint/type/build: biome check clean on touched files; bun run typecheck
    clean; bun run build ok; bundle-portability 10/10; plugin-shape 2/2;
    node --input-type=module -e "await import('./dist/index.js')"
    plugin id: opencode-swarm | has server: true; packaging smoke 10/10
    (9,465,417 B < 9.5 MiB cap); drift:check --enforce clean;
    check:invariants all passed; check:test-file-cap 0 violations;
    check:mock-cleanup clean.
  • Deferred-work scan: scan-deferred.sh → clean.

Risk and Rollback

Waivers (or none)

none

Merge status

Awaiting explicit user approval; not merged. Independent implementation
review (Kimi K2.7) and final critic (Kimi K3) both APPROVE at head
c826e08; plan critic APPROVE at base
d40e934.

CI-round note

One CI round after initial publication: the drift gate flagged the new tool's
user-facing strings that mention events.jsonl (the #2039 literal-mention
ratchet); fixed with the sanctioned prompt-doc allowlist entry in
scripts/check-core-events-usage.ts (the same class the
approve-retry-sounding-board.ts precedent uses — the append itself already
routes through the appendCoreEventSync seam), and the PR body sections were
reordered to satisfy the pr-standards contract. Head re-stamped to
c826e08; all three gates re-approved at the
new head.

Pre-existing local-only artifact (not introduced by this PR): on this Windows
checkout tests/unit/scripts/drift-check.test.ts ("no false positives on the
real repository") reports a [WORKFLOW_CHANGED_AFTER_CAPTURE] finding for
.github/workflows/pr-standards.yml — proven present on the unmodified base
tree via a stash probe and absent from the CI drift run for this PR (known
captured-evidence local divergence, same class as the documented
schema-serializer local/CI split). The related
tests/unit/events/check-core-events-usage.test.ts passes 15/15 with this
PR's allowlist entry.

CI-round note (round 2)

A second CI round fixed the quality job: the registry-citation gate flagged
one anchor my reducer edit shifted (src/gate-evidence.ts getEvidencePath
:786 → :798 — re-pinned at source in scripts/retention-registry.data.ts, the
sanctioned arm), and check:test-clock flagged two new test fixtures using the
wall clock (timestamps now derived from the durable workflow anchor —
deterministic and strictly newer than the wedge by construction). The unit
matrix had been skipped only as quality's dependent. Final head:
c826e08; all three review gates re-approved
at this head (Round 3).

CI-round note (round 3)

A third CI round reworded the recover_rework_task TOOL_METADATA description
to drop the literal "Stage B": the architect prompt embeds the full tool list
on a single line, where a pre-existing "skip" token elsewhere in that line
combined with the new "Stage B" token and tripped architect-council-prompt's
no-skip-Stage-B assertion. That suite is 53/53 and the whole
tests/unit/agents directory 1949/0 after the reword; registration stays
coherent (134 tools). Final head: c826e08;
all three review gates re-approved at this head (Round 4).

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Drift check report

Found 2 drift finding(s): 0 error, 0 warning, 2 notice.

required-check-contract (2)

  • 🔵 notice scripts/required-check-contract.json: [RULESET_DIVERGENCE] intended-required context "drift" is not yet required by the captured ruleset
  • 🔵 notice scripts/required-check-contract.json: [RULESET_DIVERGENCE] intended-required context "drift" is not present for every expected event in captured external workflow evidence

@zaxbysauce
zaxbysauce force-pushed the fix/issue-2755-rework-recovery-escape-hatch branch 2 times, most recently from 512a994 to a639ae1 Compare September 14, 2026 02:44
@zaxbysauce
zaxbysauce force-pushed the fix/issue-2755-rework-recovery-escape-hatch branch from a639ae1 to c826e08 Compare September 14, 2026 03:00
@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

🤖 Multi-Stage PR Review

Pipeline: MiniMax-M2.7-highspeed (orientation) (context pack) → MiniMax-M2.7-highspeed (explorer) + MiniMax-M2.7-highspeed (explorer B) (parallel explore, distinct lenses) → GLM-5-turbo (critique) ↔ GLM-5-turbo (critique) (cross-critique) → MiniMax-M2.7-highspeed (fallback arbiter) (arbiter: blind-spot + synthesize)
Commit reviewed: a639ae157753


🔍 PR Intent

Reconstructed from PR description, linked issue #2755, and the diff.

  • O-001: Add architect-only recover_rework_task tool that writes a supervised stage_a_passed from rework_required (the mechanical path intentionally refuses this transition)
  • O-002: Add the supervisedRecovery event flag to the stage_a_passed reducer case; admit rework_required only when the flag is set
  • O-003: Update the TASK_WORKFLOW_STAGE_A_REQUIRED error message from rework_required to name recover_rework_task instead of the human-only /swarm recover
  • O-004: Wire the new tool into the registration chain (metadata, manifest, barrel export) and add the drift-gate allowlist entry for its user-facing strings
  • O-005: Expose appendStageARepairEvent and hasGreenPostSettlementPreCheck from stage-a-repair.ts for reuse

📦 Implementation Summary

The PR adds a new architect-only tool recover_rework_task that acts as a supervised escape hatch from the rework_required workflow state. When Stage B (reviewer or test_engineer) fails a task without requiring a code change (e.g. a SKIPPED verdict from a tool-argument error), the task is stranded: the mechanical stage_a_passed intentionally throws from rework_required, and the only previously offered remedy (/swarm recover) skips non-coder_delegated tasks.

The tool is fail-closed: it requires an active architect session, the exact plan task id, durable workflow state exactly rework_required, and green secretscan+sast_scan pre-check bundles newer than the wedge transition. On success it writes a supervised stage_a_passed (admitted by the new supervisedRecovery flag in the reducer) and a stage_a_repair audit event with action rework_recovered. The dispatch refusal error now branches on state, naming recover_rework_task from rework_required.


✅ / ⚠️ / ❌ Intended vs Actual

Obligation Status Evidence (file:line)
O-001 SUPPORTED src/tools/recover-rework-task.ts:10-82 — full tool with zod validation, architect session check, all preconditions, supervised stage_a_passed + audit event
O-002 SUPPORTED src/gate-evidence.ts:655-663 — reducer admits rework_required only when event.supervisedRecovery === true; mechanical emitters never set it
O-003 SUPPORTED src/hooks/delegation-gate.ts:4458-4471 — state-branching remediation: rework_required names recover_rework_task; other states keep /swarm recover
O-004 SUPPORTED src/tools/tool-metadata.ts:258-263, src/tools/manifest.ts:201, src/tools/index.ts:248-251, scripts/check-core-events-usage.ts:97-100 — full registration chain + drift-gate allowlist
O-005 SUPPORTED src/workflow/stage-a-repair.ts:54,108 — both functions promoted from async function to export async function

🚨 Confirmed Findings

None — all 25 reviewer-confirmed findings were challenged and refuted (see §🔁 Validation provenance). The implementation is structurally sound.


🔬 Unverified but Plausible Risks

None that meet the structural-proof bar. The acknowledged concurrency window between greenness check and state write (concurrent pre_check_batch after greenness pass, before transition) is documented as a known trade-off identical to issue #2665; it is not a defect introduced by this PR.


🧪 Test / Coverage Gaps

  • Gap: No test verifies that calling forceRecoverReworkTask twice for the same task fails closed on the second call.
    • Evidence: The happy-path test at tests/unit/workflow/rework-recovery.test.ts:249-256 does include a repeat-call assertion, but only from the workflow-level test. No standalone tool-level test exercises this.
    • Severity: Low — covered by the integration test; the tool-level test validates the JSON contract on success and invalid args, and the repeat-call behavior is exercised at the workflow layer.
  • Gap: The SAST-disabled documented trap (projects with SAST disabled can never satisfy the greenness precondition) is tested for failure but has no explicit documentation in the tool description or the release notes.
    • Evidence: src/workflow/rework-recovery.ts:126-128 comment + src/tools/recover-rework-task.ts:50-52 description; docs/releases/pending/rework-recovery-escape-hatch-2755.md:9-10 does not mention the constraint.
    • Severity: Low — the PR comment documents it, and the failure message RECOVER_REWORK_GREEN_PRECHECK_REQUIRED with the underlying reason is surfaced to the architect.

📋 Shipped-vs-Claimed Gaps

None. The release notes accurately describe the tool, its preconditions, the guardrail preservation, and the remediation text change.


📝 Merge Recommendation

[APPROVE]

The PR delivers exactly what issue #2755 requires: an architect-only, audited, fail-closed escape hatch from rework_required. The supervisedRecovery flag is admitted only from rework_required; all other emitters (mechanical stage_a_passed, guardrails recorder, stage-a-repair wedge scan) remain unchanged. The dispatch refusal now tells the architect the truth instead of the human-only command. The registration chain is complete, the drift gate allowlist entry is correct, and the four regression test files cover the full state machine surface.

Check Result
No CRITICAL findings
No unresolved STEALTH_CHANGE
No UNSUPPORTED obligations
Test coverage adequate
No hardcoded secrets
All async errors handled
Input validation present
No broken agent role boundaries
Prompt format contracts intact
Lockfile consistent ✅ (no lockfile changes)

🔁 Validation Provenance

Findings challenged and DROPPED (one-line reason):

# File:line Claim Why dropped
1 gate-evidence.ts:655 Implicit-negation guard is fragile for future states Established codebase pattern; terminal states are separately guarded at reducer entry
2 delegation-gate.ts:4458 Future enum state silently falls to else Same pattern used throughout the file; enum additions require explicit code changes
3 delegation-gate-rework-remediation.test.ts:71 Hardcoded session ID may cause unexpected errors Test setup uses ensureAgentSession explicitly in other tests; hardcoded string is correct test fixture
4 recover-rework-task.test.ts:159 Session context not established before tool call Tool's own !context.sessionID?.trim() guard handles this; execution flow is correct
5 rework-recovery.test.ts:112 updatedAt may be invalid for Date.parse updatedAt is always set by the reducer on every transition; never undefined on authoritative workflow
6 rework-recovery.test.ts:292 readEvents() returning '' causes split('') edge case existsSync guard ensures file existence is checked; split behavior on empty string is intentional and handled by toBeDefined() assertion
7 rework-recovery.test.ts:120 Invalid timestamp causes updated_at to be invalid Timestamp is always derived from a real workflow.updatedAt (ISO string from toISOString()); never NaN
8 gate-evidence.ts:659 supervisedRecovery not authenticated; any caller can set it The only public entry point is the architect-only registered tool, which enforces ensureAgentSession → architect internally; forceRecoverReworkTask is not exported
9 delegation-gate.ts:4467 taskId log injection taskId is validated by isStrictTaskId before use; not user-supplied free text
10 gate-evidence.ts:659 Three-way OR future-state maintenance burden Intentional by-design; future states require explicit code updates, which is standard
11 rework-recovery.ts:139 Task ID not validated against workflow evidence knownTaskIds.has(taskId) against the plan's flat task list is the correct check; the plan IS the authoritative scope
12 rework-recovery.ts:153 transitionId prefix constructed from user-supplied taskId transitionId is a data field in the audit event JSON, not a file path; taskId comes from validated plan
13 rework-recovery.ts:151 Best-effort audit event leaves forensic gap on failure Explicitly documented as best-effort in code comment; criticalWarn provides observability; the durable state transition is authoritative
14 rework-recovery.ts:52 ensureAgentSession third arg undefined may bypass validation The third arg is callID, irrelevant to authorization; agentName is explicitly checked on the returned session
15 rework-recovery.ts:119 Concurrency gap between greenness check and transition Acknowledged trade-off in code comment, identical to issue #2665; pre_check_batch is the project's own agent tool
16 recover-rework-task.ts:58 Whitespace-only sessionID guard inconsistency !context.sessionID?.trim() correctly rejects ' '''; consistent with codebase trim patterns
17 rework-recovery.ts:124 wedgeMs parameter type mismatch risk wedgeMs is derived from Date.parse(workflow.updatedAt) which is always a valid ISO string; Number.isFinite guard is defensive
18 rework-recovery.ts:119 SAST-disabled project trapped in retry loop Explicitly documented in code comment as a known trade-off; not a bug
19 stage-a-repair.ts:54 Exported functions can be called independently of formal workflow Export enables reuse; appendStageARepairEvent requires a valid action string; authorization is at the tool registration level
20 recover-rework-task.ts:72 Error serialization leaks internal state Standard pattern throughout the codebase (error instanceof Error ? error.message : String(error))
21 rework-recovery.test.ts:54 readFileSync without explicit encoding Node.js defaults to 'utf-8'; the test fixture is always UTF-8; this is correct
22 rework-recovery.test.ts:136 writeGreenBundles not awaited It IS awaited in the test body (await writeGreenBundles('1.4')); the claim is false
23 delegation-gate-rework-remediation.test.ts:31 tmpDir cleanup if beforeEach throws createSafeTestDir/canonicalMkdtemp always return a cleanup function; not a real issue
24 recover-rework-task.test.ts:127 JSON.parse without try/catch on test executeRecoverReworkTask always returns a JSON string; the test would fail on parse error which is correct behavior
25 rework-recovery.test.ts:278 split('\n').find() returns undefined silently expect(auditLine).toBeDefined() fails explicitly on undefined; the test asserts presence, not absence
26 rework-recovery.test.ts:284 Test passes if SAST is not required The test correctly verifies the guard DOES require SAST (it fails with the message); the comment describes the test's intent, not a bug

Blind-spot findings added: 0 — no new real defects found beyond those already documented (SAST-disabled constraint, concurrency window — both are acknowledged trade-offs, not defects).

Final verdict: APPROVE. No confirmed findings survive the challenge. The implementation correctly delivers the escape hatch with appropriate fail-closed preconditions, preserves the mechanical guardrail unchanged, and updates the dispatch refusal to give the architect a legal move.


🔒 Reviewed by a 3-model cross-family adversarial debate (architect → dual-lens parallel explorers → cross-critique → arbiter) for high recall with low false-positive noise. Findings are advisory — verify before acting.

@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

🤖 Multi-Stage PR Review

Pipeline: MiniMax-M2.7-highspeed (orientation) (context pack) → MiniMax-M2.7-highspeed (explorer) + MiniMax-M2.7-highspeed (explorer B) (parallel explore, distinct lenses) → GLM-5-turbo (critique) ↔ GLM-5-turbo (critique) (cross-critique) → MiniMax-M2.7-highspeed (fallback arbiter) (arbiter: blind-spot + synthesize)
Commit reviewed: c826e0882454


🔍 PR Intent

Reconstructed obligation list (from PR text, linked issue #2755, commit messages, changed tests, changed docs, changed interfaces — not from priors).

  • O-001 Add recover_rework_task architect-only tool that writes a supervised stage_a_passed from rework_required state
  • O-002 The supervised path writes a distinguishable stage_a_repair audit event (action rework_recovered) to events.jsonl
  • O-003 All preconditions fail closed: architect session, exact plan task id, durable state exactly rework_required, green pre-check evidence (secretscan + sast_scan bundles newer than wedge transition)
  • O-004 The mechanical stage_a_passed path from rework_required remains blocked (guarded by supervisedRecovery flag only this tool sets)
  • O-005 Dispatch refusal for Stage B from rework_required names recover_rework_task (not the human-only /swarm recover)
  • O-006 Full registration chain: TOOL_METADATA (architect), TOOL_MANIFEST thunk, barrel export, derived maps coherent

📦 Implementation Summary

The PR adds a new recover_rework_task tool (src/tools/recover-rework-task.ts) and its underlying workflow helper (src/workflow/rework-recovery.ts). The tool is architect-only, validates all preconditions (session, task id in plan, durable rework_required state, green pre-check bundles), then calls transitionTaskWorkflowEvidence with supervisedRecovery: true to push the state to pre_check_passed without rotating generation, plus a best-effort stage_a_repair audit event. The reducer in gate-evidence.ts accepts stage_a_passed from rework_required only when the flag is set. The delegation-gate error message branches on rework_required state to name the new tool instead of /swarm recover. Four new test files cover the reducer guard, the workflow function, the tool wrapper, and the delegation-gate error text.


✅ / ⚠️ / ❌ Intended vs Actual

Obligation Status Evidence (file:line)
O-001 SUPPORTED src/tools/recover-rework-task.ts:33forceRecoverReworkTask called with taskId + reason; src/workflow/rework-recovery.ts:156supervisedRecovery: true set on the transition event
O-002 SUPPORTED src/workflow/rework-recovery.ts:164appendStageARepairEvent({ action: 'rework_recovered', ... }) called; transitionId prefixed rework-recovery:
O-003 SUPPORTED src/workflow/rework-recovery.ts:42-76 — architect session check (:44), plan+taskId check (:61-77), rework_required state check (:96), green pre-check check (:120); every refusal is a distinct typed error
O-004 SUPPORTED src/gate-evidence.ts:662 — reducer guard event.supervisedRecovery === true required; tests/unit/gate-evidence/rework-recovery-supervised.test.ts:77 — mechanical path test confirms throw
O-005 SUPPORTED src/hooks/delegation-gate.ts:4460-4466stageARemediation branches on workflow.state === 'rework_required'; tests/unit/hooks/delegation-gate-rework-remediation.test.ts:53-65 — asserts /swarm recover absent from rework branch
O-006 SUPPORTED src/tools/tool-metadata.ts:258agents: ['architect']; src/tools/manifest.ts:202 — thunk registered; src/tools/index.ts:248 — barrel export; scripts/check-core-events-usage.ts:97 — allowlist entry for prompt-doc class

🚨 Confirmed Findings

One finding carried forward after challenge.

[MEDIUM] UTF-16 slicing corrupts reason for multi-byte characters

  • Location: src/workflow/rework-recovery.ts:137
  • Why it matters: sanitizedReason (up to 500 code units) is audited to events.jsonl via appendStageARepairEvent. A reason containing emoji, CJK, or accented characters can be sliced mid-codepoint, producing a corrupted audit string.
  • Evidence: src/workflow/rework-recovery.ts:137:
    ? options.reason.trim().slice(0, 500)
    JavaScript .slice() counts UTF-16 code units, not grapheme clusters or code points. "👍🏾".slice(0, 1)"👍" is intact (surrogate pair); but "a👍🏾".slice(0, 2)"a👍" (trailing modifier severed) and "a👍🏾".slice(0, 3)"a👍�" (half a surrogate).
  • Fix direction: Replace .slice(0, 500) with [...str].slice(0, 500).join('') (iterates grapheme clusters) or use a code-point-aware utility. The same pattern also exists in src/tools/recover-rework-task.ts at the Zod layer (.max(500)), which will reject valid multi-byte strings earlier than intended; that layer should use a custom validator that counts code points.

🔬 Unverified but Plausible Risks

None at structural-confidence threshold.


🧪 Test / Coverage Gaps

Minor items collapsed into single list per rubric.

  • Gap: tests/unit/tools/recover-rework-task.test.ts — no test at the 500-character boundary (501-char reason to verify truncation, 500-char reason to verify acceptance). The happy-path test uses a short ASCII string.
    • Evidence: tests/unit/tools/recover-rework-task.test.ts — all reason values are ≤ ~80 chars.
  • Gap: tests/unit/workflow/rework-recovery.test.ts — no concurrent-call race probe; the expectedGeneration CAS is exercised only by the repeat-call test (single-threaded).
    • Evidence: tests/unit/workflow/rework-recovery.test.ts:180 — no concurrency test.
  • Gap: tests/unit/hooks/delegation-gate-rework-remediation.test.tscoder_delegated branch tested only for reviewer dispatch; test_engineer from coder_delegated not verified to retain /swarm recover (only the rework_required branch tested both agents).
    • Evidence: tests/unit/hooks/delegation-gate-rework-remediation.test.ts:97 — one seedCoderDelegated test with reviewer only.

📋 Shipped-vs-Claimed Gaps

None.


📝 Merge Recommendation

[APPROVE]

The PR fully delivers its six obligations. One confirmed medium finding (UTF-16 slicing) is a real but narrow defect: it affects only multi-byte-character reasons and does not block any other precondition. Four test-coverage gaps are noted as minor.

Check Result
No CRITICAL findings
No unresolved STEALTH_CHANGE
No UNSUPPORTED obligations
Test coverage adequate ✅ (minor gaps noted)
No hardcoded secrets
All async errors handled ✅ (best-effort audit is intentional/design)
Input validation present
No broken agent role boundaries
Prompt format contracts intact
Lockfile consistent ✅ (no lockfile touched)

🔁 Validation provenance

Confirmed finding carried (1):

  • UTF-16 slicing in rework-recovery.ts:137 — structurally provable, real behavior change, not pre-existing at this call site.

Dropped findings and why:

  • gate-evidence.ts:241 (optional boolean type) — === true strict guard is the correct runtime enforcement; type system is compile-time; no path bypasses it.
  • gate-evidence.ts:655 (supervised flag from non-rework states) — workflow.state is a TaskWorkflowState enum (never null/undefined); the guard is correct as written; only forceRecoverReworkTask sets the flag.
  • delegation-gate.ts:4460 (null check on workflow.state) — workflow.state is a typed enum; cannot be null at that call site; not a defect.
  • delegation-gate.ts:4463 (null taskId in error) — taskId is a validated non-empty string at that point; not a defect.
  • recover-rework-task.ts:53 (sessionID guard allows undefined) — ?.trim() on undefined returns undefined; falsy check correctly rejects it; guard is sound.
  • recover-rework-task.ts:45 (UTF-16 slicing in tool) — Zod .max(500) is code-unit-based (same as the helper), so the schema rejects valid multi-byte strings earlier than intended, but this is a pre-existing pattern not introduced by this PR; the real slice bug is in rework-recovery.ts.
  • rework-recovery.ts:148 (unchecked updated return) — transitionTaskWorkflowEvidence returns Promise<TaskEvidence>; updated is TaskEvidence (never null); getTaskWorkflowSnapshot handles it safely.
  • rework-recovery.ts:65 (stripKnownSwarmPrefix empty-string) — stripKnownSwarmPrefix returns string; the || guard before it handles the falsy case including empty string; guard is required (not dead code).
  • rework-recovery.ts:164 (unawaited appendStageARepairEvent) — documented as best-effort audit; internal try/catch + criticalWarn; not a defect.
  • gate-evidence.ts:662 (strict-equals bypass) — TypeScript enforces boolean; === true enforces at runtime; no path deserializes events back through the reducer.
  • rework-recovery.ts:59 (dead code on session guard) — ensureAgentSession can return null/undefined; the !session || guard is required.
  • recover-rework-task.ts:29 (whitespace-only reason) — intentional; sanitized to undefined which the audit event handles as absent field.
  • rework-recovery.ts:109 (TOCTOU race) — expectedGeneration CAS handles it; not a defect.
  • delegation-gate.ts:4470 (taskId in error disclosure) — task IDs are not secrets; not a security defect.
  • All test-coverage findings (missing boundary test, missing concurrent test, missing audit schema validation, missing negative assertion) — confirmed as gaps but below MEDIUM threshold; collapsed into minor list.

🔒 Reviewed by a 3-model cross-family adversarial debate (architect → dual-lens parallel explorers → cross-critique → arbiter) for high recall with low false-positive noise. Findings are advisory — verify before acting.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It modifies the safety-critical task-workflow reducer and guardrail/retry semantics to add a supervised bypass of a deliberate fail-closed transition, which warrants final human verification despite the well-tested, precedent-aligned implementation.

Pull request overview

This PR adds an architect-only audited escape hatch for tasks stranded at rework_required when a Stage B verdict did not require a code change (e.g. a SKIPPED verdict scored from a tool-argument error). The mechanical stage_a_passed transition intentionally throws TASK_WORKFLOW_CODER_MUTATION_REQUIRED from rework_required, /swarm recover skips this state, and the dispatch refusal previously recommended dead ends — hard-stopping unattended runs on correct, reviewed, gate-green code. The fix introduces a new recover_rework_task tool that writes a supervised stage_a_passed (generation preserved) plus a distinguishable stage_a_repair audit event, gated behind a fail-closed precondition ladder, and corrects the dispatch remediation text to name an architect-invocable remedy.

Changes:

  • Adds an optional supervisedRecovery flag to the stage_a_passed event; the reducer admits rework_required → pre_check_passed only when set, so mechanical emitters still fail closed exactly as before.
  • Adds forceRecoverReworkTask (new src/workflow/rework-recovery.ts) and the registered recover_rework_task tool, with architect-session, plan/task, state, green pre-check, and generation-CAS preconditions each surfacing a distinct typed error.
  • Makes the TASK_WORKFLOW_STAGE_A_REQUIRED remediation state-specific (names recover_rework_task from rework_required; keeps /swarm recover for the attribution-wedge case).
File summaries
File Description
src/gate-evidence.ts Adds supervisedRecovery event flag and the guarded rework_required admission in the stage_a_passed reducer case.
src/workflow/rework-recovery.ts New fail-closed ladder writing the supervised transition + audit event; mirrors the #2703 precedent.
src/tools/recover-rework-task.ts New architect-only tool: zod-strict args, JSON success/refusal contract, delegates to the workflow helper.
src/tools/tool-metadata.ts Registers recover_rework_task metadata (architect-only).
src/tools/manifest.ts Adds the manifest handler thunk.
src/tools/index.ts Barrel export for tool + execute function.
src/hooks/delegation-gate.ts State-specific remediation text for the Stage-A-required refusal.
src/workflow/stage-a-repair.ts Additive export on appendStageARepairEvent / hasGreenPostSettlementPreCheck for reuse.
scripts/retention-registry.data.ts Re-pins the getEvidencePath anchor shifted by the reducer edit (:786→:798, verified).
scripts/check-core-events-usage.ts Sanctioned prompt-doc allowlist entry for the tool's events.jsonl mentions.
docs/releases/pending/rework-recovery-escape-hatch-2755.md User-visible release fragment.
tests/unit/gate-evidence/rework-recovery-supervised.test.ts Reducer admission/refusal matrix for the supervised flag.
tests/unit/workflow/rework-recovery.test.ts Full fail-closed ladder + audit/CAS/repeat-call coverage.
tests/unit/tools/recover-rework-task.test.ts Tool argument validation, session requirement, and JSON contract.
tests/unit/hooks/delegation-gate-rework-remediation.test.ts State-specific remediation-text assertions for both branches.
Review details
  • Files reviewed: 15/15 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

Swarm PR Review — PR #2760

Scope: d40e9341611401e53e42d448e372f5bea3ed5e57...c826e08824548f06a91e8edf415b198db34b9787 (15 files, +1172/-5). Depth tier M. 4 explorer lanes → 17 raw candidates → 2 independent opus reviewers → 1 opus critic pass on the surviving MEDIUM findings.

Obligations

Issue #2755 is OPEN and correctly linked only to this PR (no obligation drift). All 5 acceptance criteria (AC1–AC5) map cleanly to the diff. All 7 quantitative test-count/registration claims in the PR body were independently re-run and matched exactly (e.g. rework-recovery.test.ts 6/0, check-tool-registration.ts "134 tools, coherent", tests/unit/config 2166 total).

Confirmed findings (all MEDIUM/LOW — no CRITICAL, no HIGH)

F-1 (MEDIUM) — The pre-check "proof" gating this permission-bypass tool is not bound to the wedged task, and the gated actor can self-manufacture it.
src/workflow/rework-recovery.ts:113-134 calls hasGreenPostSettlementPreCheck (src/workflow/stage-a-repair.ts:108-164), which reads global secretscan/sast_scan evidence buckets with only a recency check — no task/file scoping (the helper's own comment concedes "Pre-check bundles are global (not task-scoped)"). The same architect role authorized to call recover_rework_task is also authorized to call pre_check_batch with optional/no file scope (src/tools/tool-metadata.ts:148-151, src/tools/pre-check-batch.ts:91-92), so the evidentiary gate can be self-satisfied in one call rather than only by coincidental concurrent-task timing.
Additional aggravating fact: src/gate-evidence.ts:885-894 makes the supervised stage_a_passed synthesize a gates.pre_check entry — the same entry src/gate-evidence.ts:896-898 deletes on a genuine stage_a_failed (real secretscan/SAST failure). The remediation steer in src/hooks/delegation-gate.ts:4459-4466 fires on workflow.state === 'rework_required' alone, regardless of whether the wedge came from a spurious verdict or a real scan failure.
Not HIGH: downstream completion is still gated (src/tools/update-task-status.ts:594-601), stage_b_failed is still accepted after recovery (re-opens review, doesn't skip it), and the cited sibling precedent (forceRecordRetrySoundingBoardApproval, src/hooks/delegation-gate.ts:2055-2215) has no evidentiary gate at all — this tool is strictly more gated than the pattern it follows. Same-run manufacture is also blocked by the recency check.
Suggested fix: bind the pre-check bundle to the task's changed files, or refuse recovery when lastOutcome === 'stage_a_failed'.

F-2 (MEDIUM) — The tool's audit claim is unconditional even though the actual audit-log append is best-effort and untested on failure.
src/tools/recover-rework-task.ts:63 unconditionally states the recovery "is audited to .swarm/events.jsonl," but the append (appendStageARepairEvent, src/workflow/stage-a-repair.ts:54-71) swallows failures into criticalWarn only. This diverges from the explicit precedent this PR's own docstring cites (approve_retry_sounding_board, src/tools/approve-retry-sounding-board.ts:64-78), which returns an auditEventRecorded boolean and branches its message on failure. No test in the four new test files exercises the append-failure path. Not HIGH: grep shows no programmatic consumer of the rework_recovered event anywhere in src//scripts/, so a lost line degrades forensics only, and the durable transition itself still stamps a self-identifying transitionId (rework-recovery:{taskId}:gen{N}) as a partial fallback trail. Relatedly, two genuine recoveries at the same generation (recover → re-dispatch → stage_b_failed → recover again) produce byte-identical transitionIds, so the audit trail can't distinguish them even when the append succeeds.
Suggested fix: thread a boolean out of appendStageARepairEvent into ReworkRecoverySummary.auditEventRecorded and branch the tool's message exactly as the #2703 sibling does; add a test that forces the append to fail.

F-3 (MEDIUM) — This PR creates the first exit from rework_required that never passes the coder-retry circuit breaker.
enforceCoderRetryEscalation only gates coder-dispatch preflight (src/hooks/delegation-gate.ts:4559,4632). Before this PR, accepted_mutation was the only exit from rework_required, and it necessarily traverses coder dispatch. recover_rework_task re-enables reviewer/test_engineer dispatch without ever passing that chokepoint, so recover → reviewer dispatch → stage_b_failed → recover can repeat with no automatic block — though retryCount/retryEpoch do still increment (saturating at 3) and stay visible, and each cycle needs a fresh green pre-check bundle plus an architect-supplied reason. Not HIGH: cost is token burn and audit noise, not a correctness break.
Suggested fix: count/cap supervised recoveries per generation, or route this exit through the same retry-escalation check.

F-4 (LOW) — Mislabeled test, not test theater on a live invariant.
tests/unit/tools/recover-rework-task.test.ts:218-222, named "is registered with an architect-only grant... (invariant 11)," only asserts description text and function arity — never the actual agents: ['architect'] grant. A reviewer literally widened the grant to ['architect', 'coder'] and reran all four new test files (20/0) plus the broader registration/manifest suites (63/0) — all still passed. Downgraded from an initial MEDIUM/HIGH read because the runtime guard (RECOVER_REWORK_ARCHITECT_REQUIRED, src/workflow/rework-recovery.ts:52-62) is separately and correctly tested and fails closed regardless of the metadata grant — so widening the grant would only change tool visibility, not actual capability.
Suggested fix: one line — expect(TOOL_METADATA.recover_rework_task.agents).toEqual(['architect']).

Advisory / no action needed

  • Pre-existing, gate-tolerated documentation drift: scripts/retention-registry.data.ts fixed one stale line-citation but left six sibling citations in the same row stale (70–170 lines); check-registry-citations.ts passes because these sit in an explicitly tolerated 95-entry baseline. Not introduced by this PR.
  • The release note's "every refusal is a distinct typed error" claim was checked and holds — every refusal path does throw a typed RECOVER_REWORK_*/PLAN_* error, even though two of those codes each cover two distinct preconditions with different messages. Disproved as a defect.
  • rework_required can be reached via a genuine settlement failure (accepted_mutation + settlementFailed:true) as well as a spurious verdict (stage_b_failed), and forceRecoverReworkTask doesn't distinguish them — but the discriminator (workflow.lastOutcome) already exists in the snapshot the function reads; it's just unused. LOW, easy follow-up.
  • Several fail-closed-ladder guards (generation-mismatch, PLAN_CORRUPT, RECOVER_REWORK_NO_WORKFLOW, unparseable-timestamp) are real and reachable but untested — a reviewer proved this by deleting the generation-CAS line and rerunning all four new test files with no failures. Coverage gap, not a functional defect.
  • Two candidates were disproved outright: a claimed fail-open on malformed pre-check timestamps is actually caught by upstream schema validation (z.string().datetime() + zod parse failure → fail-closed); and the supervisedRecovery flag being reducer-trusted "unconditionally" is by design (matches every other transition type in this reducer) and has exactly one guarded call site today.

Verdict: APPROVE_WITH_NOTES

No CRITICAL or HIGH findings survived independent review + critic challenge. The four confirmed MEDIUM/LOW findings (F-1..F-4) are real but each has a documented mitigating factor (defense-in-depth elsewhere, no downstream consumer, bounded blast radius, or a runtime guard covering the gap). None block merge; F-2 and F-4 are one-function/one-line fixes worth doing before or shortly after merge, and F-1/F-3 warrant a tracked follow-up given this tool's role as a permission-boundary escape hatch.

Review method: 4 parallel explorer lanes (Claude Sonnet) → 2 independent reviewer passes (Claude Opus, one with executed mutation tests) → 1 critic challenge pass (Claude Opus) on all surviving MEDIUM findings. Full validation provenance (raw candidates, reviewer verdicts, critic verdicts) available on request.

@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

PR #2760 feedback closure ledger

Review run: swarm-pr-review (Profile B, tier L, head c826e08) → 36 raw candidates → 4 reviewer lanes → 2 critic lanes. Fix commit: c1d7801 (this push). Gates: verification lane 19/19 CONFIRMED → Stage A green (typecheck, biome, registry-citations, core-events, tool-registration, test-clock, test-file-cap, invariants, git diff --check) → Stage B reviewer 19/19 APPROVE + test_engineer 19/19 PASS → closeout reviewer 19/19 APPROVE + closeout critic 19/19 APPROVE.

Item Outcome Evidence
FB-001 audit overclaim (MEDIUM, upheld) fixed appendStageARepairEvent → boolean; audit_event_recorded + WARNING branch; audit-failure test (EISDIR probe) — stage-a-repair.ts:57, recover-rework-task.ts:51-70
FB-002 no persisted supervision marker (MEDIUM, upheld) fixed supervisedRecovery persisted in TaskWorkflowMetadata mirroring forcedCompletion; cleared on generation rotation/repair_idle — gate-evidence.ts:109-115,624,667-698,787
FB-003 reviewer_run admission unpinned (MEDIUM, upheld) fixed seeded rejection test — rework-recovery-supervised.test.ts
FB-004 corrupt-evidence misdiagnosis fixed readTaskEvidenceState swap + RECOVER_REWORK_EVIDENCE_CORRUPT — rework-recovery.ts:106-118
FB-005 unsanitized reason fixed sanitizeDiagnosticText(reason, 500) — rework-recovery.ts:177-181
FB-006 green-bar wording vs enforced semantics fixed description/remediation/refusal aligned to global-bundle recency
FB-007 SAST-disabled dead end fixed refusal hint (config + coder repair loop) + fragment Known-limitation line
FB-008 command-surface discoverability fixed /swarm recover rework_required pointer + reset-session guidance line
FB-009 registry pin rot in edited row (pre-existing, PR-worsened) fixed row-only re-pin at fresh anchors + baseline shrink; check:registry-citations passes
FB-010 recency floor unpinned fixed stale-bundle refusal test (anchor−60s → pre_check_failed_or_stale)
FB-011 grant unpinned fixed TOOL_METADATA.recover_rework_task.agents toEqual(['architect'])
FB-012 prefixed-architect acceptance untested fixed local_architect accepted / mega_coder refused test
FB-013 untested refusal branches fixed PLAN_CORRUPT / NO_WORKFLOW / NaN-updatedAt tests + NaN message now points at evidence repair
FB-014 task_id echo fixed zod .max(64) + no-echo test
FB-015 duplicate-audit race advisory (critic-downgraded INFO) race-only, state-idempotent; no change (documented)
FB-016 re-wedge TOCTOU advisory (critic-downgraded INFO) millisecond window, CAS-bounded; no change
FB-017 UTF-16 slicing / LLM exposure / flag layering / NaN bundle ts disproved execution probes: zod counts UTF-16 units (slice unreachable); no LLM consumer; layering matches #2703; zod4 datetime rejects NaN candidates
FB-018 architect trust root pre-existing byte-identical to #2703 sibling; delegation-tracker.ts untouched
FB-019 tool-level repeat test / supervised-CAS variant advisory (INFO) covered at owning layers

Regression scan (Stage B): mechanical reducer paths byte-identical; appendStageARepairEvent return-type widening ripples nowhere; EISDIR probe deterministic cross-platform; no sibling asserts the old refusal texts.

Sibling scope note unchanged: #2756/#2757/#2758 remain open and untouched.

@zaxbysauce
zaxbysauce added this pull request to the merge queue Sep 14, 2026
@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

🤖 Multi-Stage PR Review

Pipeline: MiniMax-M2.7-highspeed (orientation) (context pack) → MiniMax-M2.7-highspeed (explorer) + MiniMax-M2.7-highspeed (explorer B) (parallel explore, distinct lenses) → GLM-5-turbo (critique) ↔ GLM-5-turbo (critique) (cross-critique) → MiniMax-M2.7-highspeed (fallback arbiter) (arbiter: blind-spot + synthesize)
Commit reviewed: c1d78017ef3e


🔍 PR Intent

Reconstructed obligation list (from PR text, issue #2755, commits, changed tests, changed docs, changed interfaces).

  • O-001 Add architect-only recover_rework_task tool that writes a supervised stage_a_passed from rework_required without a coder mutation (issue rework_required has no autonomous exit: pre_check_batch passes but stage_a_passed never fires, and the only offered remedy is the human-only /swarm recover #2755)
  • O-002 The reducer admits stage_a_passed from rework_required ONLY when the supervisedRecovery flag is set; mechanical emitters never set it
  • O-003 Persist supervisedRecovery marker in durable evidence so a supervised recovery stays distinguishable from a mechanical pass after later transitions
  • O-004 Clear supervisedRecovery when repair_idle opens a new generation
  • O-005 Add stage_a_repair audit event (action rework_recovered) to .swarm/events.jsonl with rework-recovery: transition id prefix
  • O-006 Fail-closed preconditions: architect session, exact plan task, durable state exactly rework_required, green pre-check proof newer than the wedge
  • O-007 Update TASK_WORKFLOW_STAGE_A_REQUIRED remediation text to name recover_rework_task for rework_required state
  • O-008 /swarm recover wedge scan intentionally skips rework_required; updated messaging points to recover_rework_task instead
  • O-009 Register tool (TOOL_METADATA architect-only, TOOL_MANIFEST, barrel export)
  • O-010 Export hasGreenPostSettlementPreCheck and appendStageARepairEvent from stage-a-repair.ts for reuse; update return type of latter

📦 Implementation Summary

The PR adds a new architect-only recover_rework_task tool (src/tools/recover-rework-task.ts) backed by forceRecoverReworkTask (src/workflow/rework-recovery.ts, new). The core mechanic: src/gate-evidence.ts gains a supervisedRecovery flag on TaskWorkflowMetadata and TaskWorkflowTransitionEvent; the stage_a_passed reducer case admits rework_required ONLY when event.supervisedRecovery === true, which only the new tool sets. The reducer preserves the marker across same-generation transitions and clears it on repair_idle. Precondition ladder (architect session → plan task lookup → evidence state check → green pre-check proof → generation CAS) fails closed at each step with distinct typed errors. Audit event stage_a_repair with action rework_recovered is appended via the shared appendStageARepairEvent seam; its success/failure is surfaced as audit_event_recorded in the tool result. Dispatch refusal at delegation-gate.ts now branches on state: rework_required names recover_rework_task, other states keep /swarm recover. Four new test files cover reducer behavior, tool wrapper, recovery function, and delegation-gate remediation text.


✅ / ⚠️ / ❌ Intended vs Actual

Obligation Status Evidence (file:line)
O-001 SUPPORTED src/tools/recover-rework-task.ts:46executeRecoverReworkTask calls forceRecoverReworkTask; src/workflow/rework-recovery.ts:62 — supervised stage_a_passed with supervisedRecovery: true
O-002 SUPPORTED src/gate-evidence.ts:673!(current.state === 'rework_required' && event.supervisedRecovery === true) gate; src/workflow/rework-recovery.ts:175 — only path setting the flag
O-003 SUPPORTED src/gate-evidence.ts:690{ supervisedRecovery: true as const } in return; src/gate-evidence.ts:618-624 — preserved across transitions via ...(current.supervisedRecovery === true ? { supervisedRecovery: true } : {})
O-004 SUPPORTED src/gate-evidence.ts:779-785repair_idle destructuring strips supervisedRecovery; src/gate-evidence.ts:663-665 — retry case also clears via supervisedRecovery: undefined
O-005 SUPPORTED src/workflow/rework-recovery.ts:188-200appendStageARepairEvent({ action: 'rework_recovered', ... }); src/workflow/rework-recovery.ts:168 — transition id rework-recovery:${taskId}:gen${workflow.generation}
O-006 SUPPORTED src/workflow/rework-recovery.ts:67-74 — architect session check; src/workflow/rework-recovery.ts:85-99 — plan task existence; src/workflow/rework-recovery.ts:108-118rework_required state check; src/workflow/rework-recovery.ts:128-147 — green pre-check proof
O-007 SUPPORTED src/hooks/delegation-gate.ts:4458-4468stageARemediation branches on workflow.state === 'rework_required' to name recover_rework_task
O-008 SUPPORTED src/commands/recover.ts:59-65skipped_not_wedged branch checks outcome.state === 'rework_required' and names the tool
O-009 SUPPORTED src/tools/tool-metadata.ts:255-259 — architect-only grant; src/tools/manifest.ts:202 — manifest entry; src/tools/index.ts:248-251 — barrel export
O-010 SUPPORTED src/workflow/stage-a-repair.ts:52export async function appendStageARepairEvent returns Promise<boolean>; src/workflow/stage-a-repair.ts:113export async function hasGreenPostSettlementPreCheck

🚨 Confirmed Findings

None. Every confirmed finding from the prior review was either pre-existing, grounded in incorrect assumptions about types/contract, or already addressed by the PR's own review rounds. No real, PR-introduced defects remain.


🔬 Unverified but Plausible Risks

None that meet the structural-evidence bar. The SAST-disabled unavailability is documented and intentional (docs acknowledge it; the error message names the cause and alternative). The append-only audit event outcome surfacing (audit_event_recorded: false with WARNING) is the correct behavior per the review finding it addresses (FB-001).


🧪 Test / Coverage Gaps

None identified. The PR adds 4 test files (20 test cases total) covering: reducer state-machine behavior (6 cases including generation rotation, marker persistence/clearing, terminal-state rejection, widening probe), recovery function preconditions (13 cases including SAST-disabled shape, corrupt evidence/plan, stale bundles, prefixed agent names), tool wrapper contract (7 cases including audit append failure), and delegation-gate remediation text (3 cases). Each bun test entry point is exercised at the acceptance level.


📋 Shipped-vs-Claimed Gaps

None. The supervisedRecovery marker is correctly typed as boolean | undefined (not true | undefined), so event.supervisedRecovery === true in the reducer is precise — false is excluded by the strict equality check, matching the forcedCompletion precedent. The true as const literal in the reducer return is correct TypeScript narrowing. All acceptance criteria have traceable code evidence.


📝 Merge Recommendation

[APPROVE]

The PR fully delivers its obligations. The supervisedRecovery guardrail is structurally sound, the preconditions fail closed at each step with distinct errors, the audit trail is surfaced honestly, and the mechanical path is provably unchanged by the flag check (widening-probe mutation confirmed to flip the guardrail test RED in the PR's own review). No real defects survive this pass.

Check Result
No CRITICAL findings
No unresolved STEALTH_CHANGE
No UNSUPPORTED obligations
Test coverage adequate
No hardcoded secrets
All async errors handled
Input validation present
No broken agent role boundaries
Prompt format contracts intact
Lockfile consistent ✅ (only metadata changes to existing files)

🔁 Validation Provenance

Confirmed findings reviewed (all DROP'd):

Finding File:Line Drop Reason
outcome.state undefined access recover.ts:60 StageARepairOutcome.skipped_not_wedged carries state — TypeScript would reject missing it
Architect check missing in tool recover-rework-task.ts:46 forceRecoverReworkTask:67-74 calls ensureAgentSession and validates agentName; tool is architect-only in TOOL_METADATA
Whitespace sessionID bypass recover-rework-task.ts:44 !context.sessionID?.trim() correctly blocks whitespace-only strings
Directory traversal in tool recover-rework-task.ts:31 directory is framework-validated plugin context, not untrusted user input
updated uninitialized after throw rework-recovery.ts:156 transitionTaskWorkflowEvidence always returns; any exception would propagate before the getTaskWorkflowSnapshot call
Oversized task_id not enforced recover-rework-task.test.ts:258 Zod schema z.string().max(64) enforces it; test passes on schema rejection
Retry idempotency — no duplicate audit rework-recovery.test.ts:157 State precondition fails closed on second call; evidence unchanged by design
greenness.reason fragile string rework-recovery.ts:127 Discriminant is a string enum in the same module; intentional API design
taskId unsanitized in recover.ts recover.ts:64 outcome.taskId comes from the typed StageARepairOutcome union
Architect check only in workflow recover-rework-task.ts:44 Redundant with forceRecoverReworkTask; architect-only TOOL_METADATA is the intended primary enforcement
SAST-disabled hard failure recover-rework-task.ts:73 Documented as intentional design limitation; error message names the cause and alternative
task_id 64-char bound not enforced recover-rework-task.ts:34 Zod schema enforces it; the claim of no enforcement is incorrect
supervisedRecovery unconditional clear gate-evidence.ts:663 ...(undefined) is a no-op in spread; harmless asymmetry with forcedCompletion
taskId unsanitized in delegation-gate delegation-gate.ts:4463 taskId sourced from validated workflow state enum
workflow null before .authoritative rework-recovery.ts:128 getTaskWorkflowSnapshot throws RECOVER_REWORK_NO_WORKFLOW when !evidence || !workflow.authoritative — guard precedes the access
loadPlanJsonOnly unhandled throw rework-recovery.ts:74 Return value checked for null; throws for corrupt files — same pattern as forceRecordPlanCriticApproval
Date.parse(workflow.updatedAt) undefined rework-recovery.ts:128 TaskWorkflowMetadataSchema requires updatedAt: z.string() — property always exists
appendStageARepairEvent return type change stage-a-repair.ts:52 New callers (forceRecoverReworkTask) use the boolean; existing caller (scanStageATask) ignores the return — no functional break
hasGreenPostSettlementPreCheck newly exported stage-a-repair.ts:67 Intentional: sibling #2758 uses it; export is the required seam
FB-014 test doesn't assert error value recover-rework-task.test.ts:176 Test validates the call is rejected; echoing the oversized string is not a security concern for a JSON error message
supervisedRecovery type inconsistency gate-evidence.ts:663 true as const vs true in two return sites — harmless; TS narrows both to true

Blind-spot findings (none): No additional real defects found. The code is well-structured with clear defensive checks, and the test coverage is comprehensive for the new surface area.


🔒 Reviewed by a 3-model cross-family adversarial debate (architect → dual-lens parallel explorers → cross-critique → arbiter) for high recall with low false-positive noise. Findings are advisory — verify before acting.

Merged via the queue into main with commit c49125a Sep 14, 2026
45 checks passed
@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

🤖 Multi-Stage PR Review

Pipeline: MiniMax-M2.7-highspeed (orientation) (context pack) → MiniMax-M2.7-highspeed (explorer) + MiniMax-M2.7-highspeed (explorer B) (parallel explore, distinct lenses) → GLM-5-turbo (critique) ↔ GLM-5-turbo (critique) (cross-critique) → MiniMax-M2.7-highspeed (fallback arbiter) (arbiter: blind-spot + synthesize)
Commit reviewed: 67431fc21f32


Phase 0 — Reconstruct Intent

Obligations from issue #2755 and PR description:

  • O-001: Add architect-only recover_rework_task tool as the legal autonomous exit from rework_required
  • O-002: Tool writes supervised stage_a_passed + stage_a_repair audit event; reducer admits rework_required only with new supervisedRecovery flag
  • O-003: Require green secretscan+SAST evidence newer than the wedge transition; SAST-disabled config is a known permanent refusal
  • O-004: Require active architect session (defense-in-depth beyond manifest registration)
  • O-005: Update TASK_WORKFLOW_STAGE_A_REQUIRED error to name recover_rework_task from rework_required; keep /swarm recover guidance for coder_delegated attribution wedge
  • O-006: Persist supervisedRecovery marker in evidence; distinguish from mechanical pass via rework-recovery: prefix and action: 'rework_recovered' audit event
  • O-007: Mechanical guardrail untouched (C3 frozen check + widening probe)
  • O-008: Export hasGreenPostSettlementPreCheck and appendStageARepairEvent from stage-a-repair.ts for reuse

Phase 1 — Summarise Actual Behaviour

The PR adds a complete new escape hatch:

  1. src/workflow/rework-recovery.ts (forceRecoverReworkTask): fail-closed ladder — architect session check → task-in-plan check → evidence read → state exactly rework_required → green pre-check proof (both bundles, newer than wedge timestamp) → CAS-guarded supervised stage_a_passed + audit event
  2. src/tools/recover-rework-task.ts: registered tool, Zod-strict args, sessionID gate, calls forceRecoverReworkTask, surfaces audit_event_recorded honestly
  3. src/gate-evidence.ts: reducer admits stage_a_passed from rework_required ONLY when event.supervisedRecovery === true; supervisedRecovery field added to metadata and schema, preserved across same-generation transitions, cleared on generation rotation
  4. src/hooks/delegation-gate.ts: error message branches on workflow.state === 'rework_required' to name recover_rework_task; non-rework_required states keep /swarm recover guidance
  5. src/workflow/stage-a-repair.ts: appendStageARepairEvent returns boolean (was void); both functions exported for reuse
  6. src/commands/recover.ts and reset-session.ts: user-facing dead-end messages now reference the tool

Phase 2 — Intended vs Actual

Obligation Status Evidence
O-001 SUPPORTED src/tools/recover-rework-task.ts — full registered tool
O-002 SUPPORTED src/gate-evidence.ts:673-691 — reducer gate + metadata field
O-003 SUPPORTED src/workflow/rework-recovery.ts:141-163 — greenness check + SAST-disabled hint
O-004 SUPPORTED src/workflow/rework-recovery.ts:76 — explicit architect role check after ensureAgentSession
O-005 SUPPORTED src/hooks/delegation-gate.ts:4464-4474 — state-branching error text
O-006 SUPPORTED src/workflow/rework-recovery.ts:183rework-recovery: prefix; src/gate-evidence.ts:690-694 — marker persistence
O-007 SUPPORTED reducer guardrail unchanged; widening probe (flag dropped) flips C3 RED (documented in PR)
O-008 SUPPORTED src/workflow/stage-a-repair.ts:52,110 — both functions exported
AC1-AC5 SUPPORTED C1: tool-driven rework_required→pre_check_passed; C2: audit event + prefix + refusals; C3: mechanical throw retained; C4: error names tool; C5: 134 tools coherent

Phase 3 — Confirmed Findings: Challenge Results

Keeping: NONE — all confirmed findings are either false positives or intentional design

Dropped findings (with rationale):

Finding Why DROPPED
registry-citation-baseline.json 5-entry removal The PR added supervisedRecovery to the schema (~15 new lines in gate-evidence.ts), naturally shifting every cited line number. The PR legitimately re-pinned citations in retention-registry.data.ts for the same row. The baseline entries were tracking pre-existing out-of-range citations on the old line numbers — removing them when the PR re-pins the row is correct behavior, not silent drift acceptance.
recover.ts:61 outcome.state type safety The StageARepairOutcome discriminated union has state on skipped_not_wedged. The switch cases are exhaustive and TypeScript narrows correctly inside each case. No crash path.
recover.ts:62,63 hardcoded tool name / null check The tool name in user guidance is intentional — it must tell the architect what to invoke. The state field is on the discriminated union variant; no null check gap.
rework-recovery.ts:59 loadPlanJsonOnly no try-catch loadPlanJsonOnly returns null on parse failure, never throws — so the !plan guard catches both null and undefined. The fs.existsSync check below is dead code for the corrupt-plan path but is not a defect.
rework-recovery.ts:76 ensureAgentSession(undefined) The PR adds an explicit stripKnownSwarmPrefix + 'architect' check at line 76 immediately after the call. The undefined is the default for the sessionAgent param (not the session check). The defense-in-depth architect role check is present.
rework-recovery.ts:141 concurrency The generation CAS (expectedGeneration) correctly fails closed on concurrent mutation — not a defect, a designed safety property. The PR documents the race; the error is informative.
rework-recovery.ts:178 audit integrity The transition ID has rework-recovery: prefix, the evidence has supervisedRecovery: true, AND the audit event has action: 'rework_recovered' — three independent distinguishing factors. The "indistinguishable" claim is refuted.
rework-recovery.ts:109 missing/unparseable conflation The PR adds an explicit kind === 'unparseable' check before the !evidence guard (lines 112-119), which is the exact FB-004 fix — the finding predates this addition.
stage-a-repair.ts:52 return type void→boolean Intentional documented breaking change. appendCoreEventSync never partially writes (atomic seam). The return value is consumed by the one new caller. No caller that expected void silently breaks.
stage-a-repair.ts:65 partial write appendCoreEventSync is an atomic seam (lock + torn-tail re-framing). If it throws, no partial line is written. The try/catch is defensive.
gate-evidence.ts:678 bypass risk The supervisedRecovery flag can only be set by calling transitionTaskWorkflowEvidence with the event — a deliberate internal API call, not a direct bypass. The claim conflates "internal callers" with "arbitrary callers."
gate-evidence.ts:693 schema implicitness The schema describes what CAN be written by the internal reducer. Direct file writes bypass the reducer regardless of field presence; this is not a defect introduced by this PR.
delegation-gate.ts:4465 taskId injection taskId is shape-validated (assertStrictTaskId) before reaching this line. The error string is logged as a throw new Error, not rendered as HTML. No injection risk.
recover-rework-task.ts:48 no role check The PR adds an explicit architect role check at rework-recovery.ts:76. The sessionID check is a separate layer (presence guard). The finding predates this addition.
recover-rework-task.ts:54 info disclosure Fields returned (taskId, generation, transitionId, recordedAt) are all task-scoped identifiers already derivable from the workflow evidence file. The audit_event_recorded boolean is the honest-surfacing design decision. Not a disclosure defect.
tests/unit/gate-evidence/rework-recovery-supervised.test.ts:272 missing edge case The reducer explicitly rejects supervisedRecovery from pre_check_passed (current.state !== 'pre_check_passed' check). The test named "still admits plain stage_a_passed" tests the pre-existing path, not the new supervised path. The absence of a supervised-from-pre_check_passed test is a minor coverage gap, not a defect — the reducer logic is correct by design.

Phase 4 — Runtime-Aware False-Positive Control

No findings survive to this phase. All concerns were either:

  • Already addressed in the diff (FB-004 unparseable check, architect role check)
  • Intentional design documented in the PR
  • Incorrect assumptions about TypeScript narrowing or function behavior

Phase 5 — Blind-Spot Pass

Blind-spot: supervisedRecovery is preserved but never read by any consumer

Location: src/gate-evidence.ts:690-694supervisedRecovery is written to evidence but no code reads it back. The field exists solely to make the evidence file byte-distinguishable from a mechanical pass after lastTransitionId is overwritten. No downstream consumer acts on it. This is correct by design (the reducer consumes the transient event flag, not the persisted marker), but it means the field is write-only: a future consumer reading the evidence file would not know whether Stage A was passed mechanically or supervised. The PR explicitly accepts this — the distinguishing factors are the rework-recovery: transition prefix + audit event + supervisedRecovery marker together. Not a defect, but worth noting as a maintenance invariant.

Blind-spot: appendStageARepairEvent API contract change (void→boolean) — external callers

Location: src/workflow/stage-a-repair.ts:52

The function's return type changed from Promise<void> to Promise<boolean>. The PR correctly updated the single internal caller at line 188 of rework-recovery.ts. However, if any external or future caller (outside this PR's scope) imports and calls appendStageARepairEvent expecting void, the returned boolean would be silently discarded — valid JS, but the caller's assumption would be violated. This is a low-confidence structural concern: the function was not previously exported (async function without export), and the PR explicitly exports it. The change is intentional and correct. No fix required now; if the function gains external callers, they must handle boolean.

Blind-spot: rework-recovery.ts audit event action string 'rework_recovered'

Location: src/workflow/rework-recovery.ts:193

The action string 'rework_recovered' is a new string literal used as action: 'rework_recovered' in the audit event. No schema validation or allowlist enforces this string — a typo would silently write a different action value to the audit log. The same risk exists for all existing audit event action values in this codebase, so this is not a PR-specific defect. Advisory only.


🔍 PR Intent

  • O-001: Architect-only recover_rework_task tool as legal autonomous exit from rework_required
  • O-002: Supervised stage_a_passed + audit event; reducer admits rework_required only with supervisedRecovery flag
  • O-003: Green pre-check proof required (secretscan + SAST); SAST-disabled = permanent refusal by design
  • O-004: Active architect session required (defense-in-depth beyond manifest registration)
  • O-005: TASK_WORKFLOW_STAGE_A_REQUIRED error branches: rework_requiredrecover_rework_task; coder_delegated/swarm recover
  • O-006: supervisedRecovery marker persisted; rework-recovery: prefix + rework_recovered audit action for distinguishability
  • O-007: Mechanical guardrail unchanged; C3 frozen check + widening probe
  • O-008: Export hasGreenPostSettlementPreCheck and appendStageARepairEvent for reuse

📦 Implementation Summary

The PR adds a complete architect-only escape hatch from rework_required. The core is forceRecoverReworkTask in a new rework-recovery.ts, which runs a fail-closed precondition ladder (architect session → task in plan → evidence readable → state exactly rework_required → green secretscan+SAST newer than wedge → CAS-guarded supervised stage_a_passed + audit event). The reducer in gate-evidence.ts gains a supervisedRecovery flag on both the event union and metadata schema; it admits stage_a_passed from rework_required only when the flag is set. The delegation-gate.ts error branches on state to give truthful remediation. The stage-a-repair.ts exports two reused functions with an intentional void→boolean return type change on appendStageARepairEvent. Four new test files cover the reducer, the workflow, the tool wrapper, and the error text. Tool registration is complete (metadata + manifest + barrel + TOOL_METADATA).


✅ / ⚠️ / ❌ Intended vs Actual

Obligation Status Evidence
O-001 SUPPORTED src/tools/recover-rework-task.ts
O-002 SUPPORTED src/gate-evidence.ts:673-691
O-003 SUPPORTED src/workflow/rework-recovery.ts:141-163
O-004 SUPPORTED src/workflow/rework-recovery.ts:76-85
O-005 SUPPORTED src/hooks/delegation-gate.ts:4464-4474
O-006 SUPPORTED src/gate-evidence.ts:690-694 + rework-recovery.ts:183
O-007 SUPPORTED mechanical path unchanged; widening probe documented
O-008 SUPPORTED src/workflow/stage-a-repair.ts:52,110

🚨 Confirmed Findings

None — all 18 reviewer-confirmed findings were either false positives, already-addressed-by-this-PR, or intentional design. The implementation is correct and complete.


🔬 Unverified but Plausible Risks

  • Risk: appendStageARepairEvent return type changed void→boolean. If the function gains external callers (it was not previously exported), callers expecting void will silently discard the boolean.

    • Why suspicious: Explicit export added in this PR; void→boolean is a documented intentional change.
    • What would verify it: Audit of all imports of appendStageARepairEvent across the codebase (currently only rework-recovery.ts).
    • Confidence: Low — the function was not exported before; no external callers exist today.
  • Risk: The supervisedRecovery evidence marker is write-only from the perspective of any future consumer of the evidence file. The marker is never read back by any code path.

    • Why suspicious: Write-only fields are code smells; a future consumer could misinterpret the evidence.
    • What would verify it: Full-text search for any readTaskEvidence caller that reads .workflow.supervisedRecovery (currently none).
    • Confidence: Low — this is by design; the three distinguishing signals (prefix + audit event + marker) work together.

🧪 Test / Coverage Gaps

  • Minor gap: No explicit test for supervisedRecovery: true from pre_check_passed (which the reducer correctly rejects). The test named "still admits plain stage_a_passed from coder_delegated and pre_check_passed" covers the pre-existing mechanical path. The supervised-path rejection is implicitly tested by the reducer's conditional logic and explicitly by the widening probe documented in the PR.
  • Adequacy: 4 new test files (20 test cases total) + frozen C1-C5 checks + widening probe. Coverage is sufficient.

📋 Shipped-vs-Claimed Gaps

  • None. All five acceptance criteria are substantiated in the diff.

📝 Merge Recommendation

[APPROVE]

The implementation correctly addresses issue #2755. All 18 confirmed findings were refuted as false positives or intentional design. The two plausible risks in the blind-spot pass are low-confidence, not fixable without changing intended behavior, and do not affect correctness of the primary fix.

Check Result
No CRITICAL findings
No unresolved STEALTH_CHANGE
No UNSUPPORTED obligations
Test coverage adequate
No hardcoded secrets
All async errors handled
Input validation present
No broken agent role boundaries
Prompt format contracts intact
Lockfile consistent

🔁 Validation provenance

  • Survived (0): No findings survived the challenge pass.
  • Dropped (18): All 18 reviewer-confirmed findings were dropped — each was either already fixed in the diff, an intentional design documented in the PR, an incorrect assumption about TypeScript narrowing, or a misreading of the code flow.
  • Blind-spot additions (2): appendStageARepairEvent return-type change (low confidence, no external callers today); supervisedRecovery write-only marker (by design, not a defect).

🔒 Reviewed by a 3-model cross-family adversarial debate (architect → dual-lens parallel explorers → cross-critique → arbiter) for high recall with low false-positive noise. Findings are advisory — verify before acting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants