fix(workflow): complete trusted empty-scope tasks - #2776
zaxbysauce wants to merge 10 commits into
Conversation
Drift check reportFound 2 drift finding(s): 0 error, 0 warning, 2 notice. required-check-contract (2)
|
There was a problem hiding this comment.
🔵 Needs a closer look
It changes safety-critical durable task-completion, terminal WAL replay, and gate-derivation trust-boundary semantics whose fail-closed guarantees warrant final human verification.
Pull request overview
This PR fixes issue #2763, where a verification-only task that honestly declares files_touched: [] could never be marked completed because update_task_status required a pre_check gate that pre_check_batch never records for an empty file list, deadlocking the task in in_progress. It also resolves the reported disagreement between check_gate_status and update_task_status over the required-gate set by routing both (plus the durable evidence bridge and phase completion) through one shared deriveApplicableGateSet derivation. The exception is tightly scoped: it is granted only when an authoritative, generation-0 coder settlement proves an empty declared scope and no accepted mutation, and every other path (plan scope alone, null/malformed/non-empty settlement scope, accepted mutation, later generations, stale Stage A, failing secretscan) remains fail-closed.
Changes:
- Add a generation-0
noMutationSettlementmarker, bound to the trusted coder-settlement WAL, and a sharedderiveApplicableGateSet/isReadOnlyNoMutationEligiblederivation reused by completion, status, the durable bridge, andhasPassedAllGates. - Thread
readOnlyNoMutationthrough the task-terminal transition and terminal WAL schema (with cross-field validation) so a durable, non-forced empty-scope completion replays deterministically. - Add extensive positive/negative/malformed/replay/idempotence/secretscan/recovery regression tests and a release fragment.
File summaries
| File | Description |
|---|---|
| src/gate-evidence.ts | Adds the noMutationSettlement marker, deriveApplicableGateSet, WAL-bound trust adapter, reducer handling, and stricter hasPassedAllGates. |
| src/tools/check-gate-status.ts | Routes required/passed/missing gates and status through the shared derivation and readOnlyNoMutation. |
| src/tools/update-task-status.ts | Uses shared derivation in checkReviewerGate, ignores empty scope for Turbo bypass, and passes resolveTerminal with readOnlyNoMutation. |
| src/workflow/coder-settlement.ts | Emits declared-scope/transition context and hardens idempotent COMMITTED replay against advisory-gate overwrite. |
| src/workflow/task-terminal.ts | Propagates readOnlyNoMutation into the terminal event and WAL, including idempotence comparison. |
| src/workflow/workflow-wal-schema.ts | Adds the optional readOnlyNoMutation field with backward-compat parsing and cross-field validation. |
| src/evidence/gate-bridge.ts | Derives durable gate status via the shared function instead of treating empty required gates as complete. |
| scripts/retention-registry.data.ts | Updates task-workflow-evidence citation line numbers for the shifted symbols. |
| docs/releases/pending/issue-2763-empty-scope-completion.md | Adds the user-visible release fragment. |
| tests/** | Adds and updates unit/integration coverage for the empty-scope, terminal-replay, secretscan, and receiptless paths. |
Review details
- Files reviewed: 20/20 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.
Swarm PR Review — #2776 "fix(workflow): complete trusted empty-scope tasks"Scope: Verdict: BLOCKA reviewer-confirmed, critic-upheld CRITICAL finding (evidence-gate bypass) is present and is not mitigated by an unusual/adversarial precondition — it fires on the PR's own advertised happy path. Critical findingF-1 (CRITICAL, confirmed, upheld by critic) — The new "trusted no-mutation" marker is not proof of no mutationA plan task with At settlement time, the real git-diff-observed mutation set is filtered through This PR's new Net effect: a coder can be dispatched with This is not an adversarial/unusual-plan construction — it is the only way the PR's own advertised path can ever be reached (a Falsification probe: dispatch a coder for a Suggested fix: record the settlement's (Reachability/authority-chain traced independently by two separate agents — an initial reviewer and a final critic — reading Important non-blockersF-2 (HIGH, confirmed) — Legacy-migration backfill path is unreachable for its stated purpose (unwired code)
F-3 (LOW, advisory) — Undocumented
|
Keep explicit empty plan preflight fail-closed while recording a trusted no-mutation proof only after a clean baseline confirms no child ran. Preserve authoritative FILE scopes, clear stale proofs on new dispatches, and classify raw empty-scope mutations as failed rework. Review findings from PR #2776 are covered by production-path and mutation regression tests.
Refresh retention citations after the empty-scope settlement hardening. Issue: #2763
🤖 Multi-Stage PR ReviewPipeline: 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) 🔍 PR Intent
📦 Implementation SummaryThe PR adds a ✅ /
|
| Obligation | Status | Evidence (file:line) |
|---|---|---|
| O-001 | SUPPORTED |
src/gate-evidence.ts:640 — isNoMutationSettlementMetadata; src/gate-evidence.ts:600 — deriveApplicableGateSet returns empty gate set |
| O-002 | SUPPORTED |
src/tools/check-gate-status.ts:237 + src/tools/update-task-status.ts:600 — both call deriveApplicableGateSet |
| O-003 | SUPPORTED |
src/workflow/coder-settlement.ts:178 — hasUnattributedEmptyScopeMutation; src/workflow/coder-settlement.ts:216 — returns null from scopedObservedFiles |
| O-004 | SUPPORTED |
src/gate-evidence.ts:730 — dispatch_attempted clears marker; src/gate-evidence.ts:797 — accepted_mutation clears marker |
| O-005 | SUPPORTED |
src/gate-evidence.ts:730 (dispatch_attempted clears), src/gate-evidence.ts:797 (accepted_mutation clears) |
| O-006 | SUPPORTED |
src/workflow/workflow-wal-schema.ts:489 — WAL state mismatch check; src/workflow/task-terminal.ts:258 — evidenceMatchesTerminal includes readOnlyNoMutation |
| O-007 | SUPPORTED |
src/gate-evidence.ts:649 — !readOnlyNoMutation && !requiredGates.includes('pre_check') → pre_check added; src/evidence/gate-bridge.ts:62 — missingGates updated |
🚨 Confirmed Findings
[CRITICAL] reduceTaskWorkflowSnapshot task_completed branch accesses context.requiredGates which is never set on the no-mutation path
- Location:
src/gate-evidence.ts:909 - Why it matters: When
readOnlyNoMutationis true (the fix's primary feature), the reducer throwsTypeError: Cannot read properties of undefined (reading 'every')becausecontext.requiredGatesis never populated by thedispatch_no_mutationevent orbindTrustedNoMutationSettlement. This permanently blocks the exact task the PR intends to unblock. - Evidence:
dispatch_no_mutationevents (fromsrc/workflow/coder-settlement.ts:238) construct context as{ declaredFiles, settlementTransitionId }— norequiredGatesorgates.bindTrustedNoMutationSettlementat:1001only addsdeclaredFilesfrom the WAL andsettlementTransitionId.reduceTaskWorkflowSnapshotat:622defaultscontextto{}, socontext.requiredGatesisundefined. - Fix direction: In the
task_completedreducer, replacecontext.requiredGates.every(...)withderivedGates.requiredGates.every(...)wherederivedGates = deriveApplicableGateSet(/* reconstruct from evidence */). Alternatively, construct theapplicableGatesobject inupdateEvidenceForTransitionbefore callingreduceTaskWorkflowSnapshotand pass it in.
[MEDIUM] continue in malformed-settlement test case skips the failure assertion
- Location:
tests/unit/tools/empty-scope-completion-2763.test.ts:193 - Why it matters: The
malformed-settlementcase callsexpect(...).rejects.toThrow(...)thencontinues before theexpect(result.success, candidate.label).toBe(false)assertion at line 265. IfsettleNoMutationdoes NOT throw (e.g., WAL schema acceptsscope: 'not-an-array'), the test silently passes with no assertion covering that case. - Evidence:
continueat:193skips the only assertion in the loop body that checksresult.success. - Fix direction: After the
rejects.toThrowassertion, either assertresult.success === falsedirectly for that case, or restructure the loop to have a dedicated assertion for each case type.
🔬 Unverified but Plausible Risks
- Risk:
isNoMutationSettlementMetadataat line 651 checksworkflow.state === 'idle' || workflow.state === 'complete'. A task that somehow reachescompletestate through another path (e.g., forced completion) and then re-entersdispatch_no_mutationwould pass the state check. Whilerepair_idleopens a new generation, the interaction surface between forced completion and the read-only path is complex and deserves a dedicated regression test.- What would verify it: A test exercising forced completion followed by a no-mutation dispatch on the same taskId.
🧪 Test / Coverage Gaps
- Gap:
deriveApplicableGateSetandisNoMutationSettlementMetadatahave no unit tests in isolation — coverage is through integration tests. Thetask_completedreducer path forreadOnlyNoMutationis exercised by the integration repro scripts but not by a fast unit test.- Evidence:
tests/unit/evidence/gate-evidence-2763.test.tsandtests/unit/evidence/gate-bridge-2763.test.tscover gate derivation and bridge but not the full reducer path includingtask_completed.
- Evidence:
📋 Shipped-vs-Claimed Gaps
- Gap: None. All four acceptance criteria have corresponding test coverage (C1/C2/C3/C4).
📝 Merge Recommendation
[BLOCK]
Two defects: one is a guaranteed TypeError crash on the primary feature path (CRITICAL), one is a silent test gap (MEDIUM).
| Check | Result |
|---|---|
| No CRITICAL findings | ❌ reduceTaskWorkflowSnapshot TypeError on task_completed with readOnlyNoMutation |
| No unresolved STEALTH_CHANGE | ✅ |
| No UNSUPPORTED obligations | ✅ |
| Test coverage adequate | task_completed reducer path; malformed-settlement case assertion gap |
| No hardcoded secrets | ✅ |
| All async errors handled | ✅ |
| Input validation present | ✅ |
| No broken agent role boundaries | ✅ |
| Prompt format contracts intact | ✅ |
| Lockfile consistent | ✅ |
🔁 Validation Provenance
| Finding | Source | Outcome |
|---|---|---|
context.requiredGates TypeError |
Reviewer confirmed, verified | KEPT (CRITICAL) — traced through bindTrustedNoMutationSettlement and settlementTransitionEvent; context is { declaredFiles, settlementTransitionId }, never requiredGates |
accepted: true && settlementFailed: true contradiction |
Reviewer confirmed | DROPPED — accepted means "mutation occurred"; settlementFailed means "settlement broke"; both true for out-of-scope mutation is by design per PR docs and test at line 95 |
String match error.message.includes(...) |
Reviewer confirmed | DROPPED — maintenance risk, not a functional defect; other structural guards (WAL assertion, generation check) provide the authorization |
continue skips assertion |
Reviewer confirmed | KEPT (MEDIUM) — malformed-settlement case skips expect(result.success).toBe(false) |
| Loop cleanup leak | Reviewer confirmed | DROPPED — resetSwarmState() before cleanup() in each iteration; stale directory reference is recovered on next iteration |
result.success not checked by reason |
Reviewer confirmed | DROPPED — all 4 cases fail identically (no settlement → blocked by pre_check); error reason is deterministic |
Unused settlement variable |
Reviewer confirmed | DROPPED — settlement.evidence is used via transitionTaskWorkflowEvidence; beginCoderSettlement → settleCoderDispatch order is tested in other cases |
Blind spot: task_completed reducer context access |
Blind-spot pass | ADDED as CRITICAL above |
🔒 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.
Harden recovery and current-scope validation after review. Issue: #2763
🤖 Multi-Stage PR ReviewPipeline: 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) PR Reviewer — opencode-swarm🔍 PR Intent
📦 Implementation SummaryThe PR introduces a generation-0 no-mutation settlement path: when a coder dispatch with an empty declared scope produces no observed mutations, a trusted ✅ /
|
| Obligation | Status | Evidence |
|---|---|---|
| O-001 | SUPPORTED |
src/gate-evidence.ts:653–688 — deriveApplicableGateSet skips pre_check for trusted no-mutation evidence |
| O-002 | SUPPORTED |
src/evidence/gate-bridge.ts:61, src/tools/check-gate-status.ts:250–268 — both call deriveApplicableGateSet |
| O-003 | SUPPORTED |
src/workflow/coder-settlement.ts:165–182 — hasUnattributedEmptyScopeMutation forces accepted=true / settlementFailed=true for raw mutations on empty scope |
| O-004 | SUPPORTED |
src/gate-evidence.ts:619–660 — isNoMutationSettlementMetadata requires exact generation-0 + empty scope + idle/complete state |
🚨 Confirmed Findings
None. All three reviewer-confirmed findings were false positives; see §🔁 Validation Provenance.
🔬 Unverified but Plausible Risks
None. The empty-scope path is gated by five independent conditions (WAL trust-boundary, generation, settlement metadata, current plan scope, workflow state) and the recovery path also carries the settlementFailed flag.
🧪 Test / Coverage Gaps
- Gap: No test covers the case where a no-mutation settlement is recorded, then the plan's
files_touchedis expanded mid-session before completion, and completion is subsequently attempted.- Evidence: The test
'revokes the no-mutation exception when the current plan later gains scope'coverscheckReviewerGatebut notexecuteUpdateTaskStatuswith the terminal-WAL path. - Severity: Low — the
readCurrentTaskDeclaredFilescall inupdate-task-status.ts:604–608is present; this is a missing regression test rather than a structural gap.
- Evidence: The test
📋 Shipped-vs-Claimed Gaps
None.
📝 Merge Recommendation
[APPROVE]
The PR correctly implements all four obligations. The fix is precise, fail-closed by construction, and the five-condition gate (WAL trust-boundary + generation + settlement metadata + current plan scope + workflow state) provides defence-in-depth against false-positive read-only completions.
| 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
Confirmed findings CHALLENGED and DROPPED:
| Finding | File:Line | Drop reason |
|---|---|---|
Idempotence asymmetry (undefined vs false) |
task-terminal.ts:313 |
Both existingWal.readOnlyNoMutation === true and terminal.readOnlyNoMutation === true evaluate to false when the field is undefined or false. undefined !== true is false, which equals false !== true — idempotence holds. The WAL omit-when-false pattern is also intentional (optional boolean). FALSE POSITIVE. |
| Idempotence asymmetry (WAL write omit-when-false) | task-terminal.ts:386 |
Same root cause as above. The omit-when-false pattern is intentional and correct for an optional boolean field; it does not create a mismatch with the idempotence check. FALSE POSITIVE. |
| Empty declared scope skips scope validation | workflow-wal-schema.ts:277 |
When declaredFiles === [], the filter condition short-circuits and passes all candidate paths. This is correct by design: (a) scopedObservedFiles returns null for empty scopes in normal flow, so raw paths never reach this validator; (b) the recovery path intentionally preserves raw observed files (for audit) and the hasUnattributedEmptyScopeMutation guard in settleCoderDispatch already forces accepted=true + settlementFailed=true, routing the workflow to rework_required. The validation bypass is harmless here. FALSE POSITIVE. |
Blind-spot findings added: None — all obligations are verified by the existing test matrix (104 focused tests + 39 compatibility tests + 117 gate-status assertions + 4 acceptance criteria fixtures).
🔒 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.
Closes #2763
Summary
files_touched: []to complete without inventing apre_check_batchreceipt.check_gate_status.Invariant audit
bun run build, Node ESM import ofdist/index.js, and the gate-portability check passed..swarmpaths; focused settlement and recovery tests pass.test_runnercall.bun:testregressions cover empty, omitted, and expanded scope plus legacy gate parity; no newmock.module. No new test file exceeds 500 lines; the existing 522-line gate-status test shrank to 521 under the shrink-only FR-006 ratchet.Test plan
bun run typecheck;bun run lint:ci(exit 0; four non-blocking Biome warnings); targeted Biome;git diff --check;bun run build; Node ESM import.Acceptance criteria → evidence
files_touched: []with a trusted no-mutation settlement completes after applicable task gates without a fabricated file or Stage A scan.tests/unit/tools/empty-scope-completion-2763.test.ts;tests/unit/evidence/gate-evidence-2763.test.ts.tests/unit/tools/legacy-gate-parity-2763.test.tscompares required, satisfied, and missing arrays, including a satisfied reviewer receipt and missingpre_check.pre_check.tests/unit/tools/phase-complete-doc-only-evidence.test.tsproves empty scope passes while omitted and expanded scope fail.Recurrence prevention
Risk and rollback
pre_checkby design.Environment notes
01c13fd57d0de2d7babeb877059cb4a701edc2aehad green CI and passed the focused security/adversarial/smoke checks; exact-head checks for this follow-up are pending.skill-optimizer-e2eproduction-store tripwire failure also reproduced on cleanorigin/main([Observability PR 05/23] Isolate hive stores and add exact-ID quarantine with rollback #2033). The restricted-home swarm-model F-07 case likewise failed on clean main; its isolated writable-runtime run passed 19/19.Waivers
None.
Merge status
Awaiting explicit user approval; not merged. The PR remains draft.
PR head:
2d40c56f640b573aa868f5e25fd379d3c57be940