@gobing-ai/spur 0.4.11 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +22 -0
  3. package/config/rules/boundary/config-loading-ownership.yaml +1 -1
  4. package/config/rules/quality/coverage-gate.yaml +2 -2
  5. package/config/rules/strict/runtime-boundaries.yaml +2 -2
  6. package/config/rules/surface/check-cli-surface.yaml +4 -0
  7. package/config/rules/typescript/output-boundaries.yaml +4 -10
  8. package/config/workflows/feature-verification.yaml +14 -5
  9. package/config/workflows/idea-pipeline.yaml +92 -12
  10. package/config/workflows/task-pipeline.yaml +456 -830
  11. package/config/workflows/wayfinder-resolution.yaml +20 -3
  12. package/config/workflows/wrapup-pipeline.yaml +22 -9
  13. package/package.json +1 -1
  14. package/plugins/sp/README.md +2 -2
  15. package/plugins/sp/agents/super-planner.md +2 -2
  16. package/plugins/sp/agents/super-reviewer.md +17 -8
  17. package/plugins/sp/bunfig.toml +3 -3
  18. package/plugins/sp/commands/dev-changelog.md +1 -1
  19. package/plugins/sp/commands/dev-gitmsg.md +2 -2
  20. package/plugins/sp/commands/dev-handover.md +1 -1
  21. package/plugins/sp/commands/dev-idea.md +23 -13
  22. package/plugins/sp/commands/dev-job-dump.md +1 -1
  23. package/plugins/sp/commands/dev-plan.md +17 -6
  24. package/plugins/sp/commands/dev-review.md +1 -1
  25. package/plugins/sp/commands/dev-run.md +8 -7
  26. package/plugins/sp/commands/dev-runall.md +1 -1
  27. package/plugins/sp/plugin.json +1 -1
  28. package/plugins/sp/skills/code-implementation/SKILL.md +7 -7
  29. package/plugins/sp/skills/code-refactoring/SKILL.md +1 -1
  30. package/plugins/sp/skills/code-verification/SKILL.md +1 -1
  31. package/plugins/sp/skills/code-verification/references/secu-review.md +1 -1
  32. package/plugins/sp/skills/code-verification/references/verdict-schema.md +23 -10
  33. package/plugins/sp/skills/doc-evolve/SKILL.md +1 -1
  34. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  35. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +7 -7
  36. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +4 -4
  37. package/plugins/sp/skills/spur-check/SKILL.md +9 -8
  38. package/plugins/sp/skills/spur-cli/SKILL.md +2 -2
  39. package/plugins/sp/skills/spur-cli/references/self.md +37 -2
  40. package/plugins/sp/skills/spur-cli/references/tasks/l3-guard-cheatsheet.md +1 -1
  41. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -3
  42. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
  43. package/plugins/sp/skills/spur-cli/references/workflows.md +5 -3
  44. package/plugins/sp/skills/spur-dev/SKILL.md +3 -3
  45. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +12 -11
  46. package/plugins/sp/skills/spur-dev/references/dev-operations.md +15 -15
  47. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +4 -3
  48. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +4 -4
  49. package/plugins/sp/skills/spur-dev/references/execution-batch.md +13 -11
  50. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +32 -23
  51. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +5 -5
  52. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +4 -4
  53. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +4 -4
  54. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +1 -1
  55. package/schemas/spur-config.schema.json +19 -1
  56. package/spur.js +31459 -31868
  57. package/web/_astro/{BoardApp.BMprmPRc.js → BoardApp.Bjdp3XWV.js} +36 -36
  58. package/web/_astro/{BoardApp.B5rTVNd_.js → BoardApp.DCDZxLlQ.js} +1 -1
  59. package/web/_astro/{TaskDetail.Ddsur8qd.js → TaskDetail.B3vBxynO.js} +1 -1
  60. package/web/_astro/{arc.DLqVVBxP.js → arc.DcuHTB_T.js} +1 -1
  61. package/web/_astro/{architectureDiagram-3BPJPVTR.C-Pk3uAi.js → architectureDiagram-3BPJPVTR.ClOFWBON.js} +1 -1
  62. package/web/_astro/{blockDiagram-GPEHLZMM.BRO1i_Nm.js → blockDiagram-GPEHLZMM.otYyr7Zb.js} +1 -1
  63. package/web/_astro/{c4Diagram-AAUBKEIU.BU_cWu6e.js → c4Diagram-AAUBKEIU.BCjpc9LP.js} +1 -1
  64. package/web/_astro/channel.qgOLXjI_.js +1 -0
  65. package/web/_astro/{chunk-2J33WTMH.D2VXiQ2t.js → chunk-2J33WTMH.D5Wpnpt_.js} +1 -1
  66. package/web/_astro/{chunk-4BX2VUAB.BcHT772J.js → chunk-4BX2VUAB.DkQGtWNz.js} +1 -1
  67. package/web/_astro/{chunk-55IACEB6.DhDtICmo.js → chunk-55IACEB6.B7dqjAip.js} +1 -1
  68. package/web/_astro/{chunk-727SXJPM.BoKHp7gV.js → chunk-727SXJPM.BDQvnKE2.js} +1 -1
  69. package/web/_astro/{chunk-AQP2D5EJ.Chu4JRpE.js → chunk-AQP2D5EJ.D92doBX5.js} +1 -1
  70. package/web/_astro/{chunk-FMBD7UC4.wHdwKPlq.js → chunk-FMBD7UC4.C968oI5G.js} +1 -1
  71. package/web/_astro/{chunk-ND2GUHAM.ffN-M1JB.js → chunk-ND2GUHAM.AkkYC1bY.js} +1 -1
  72. package/web/_astro/{chunk-QZHKN3VN.FRubwg4S.js → chunk-QZHKN3VN.sCc-rKKo.js} +1 -1
  73. package/web/_astro/{classDiagram-4FO5ZUOK.c22tQsc_.js → classDiagram-4FO5ZUOK.pOQ9u0mY.js} +1 -1
  74. package/web/_astro/{classDiagram-v2-Q7XG4LA2.c22tQsc_.js → classDiagram-v2-Q7XG4LA2.pOQ9u0mY.js} +1 -1
  75. package/web/_astro/{cose-bilkent-S5V4N54A.B9c6fiSj.js → cose-bilkent-S5V4N54A.CRaeD6FY.js} +1 -1
  76. package/web/_astro/{cynefin-OW5HDTMX.DrZsrUHl.js → cynefin-OW5HDTMX.BiCPBu54.js} +1 -1
  77. package/web/_astro/{dagre-BM42HDAG.DG1U4b3y.js → dagre-BM42HDAG.C1-q3vkT.js} +1 -1
  78. package/web/_astro/{diagram-2AECGRRQ.CH15b-K3.js → diagram-2AECGRRQ.BSVOv30O.js} +1 -1
  79. package/web/_astro/{diagram-5GNKFQAL.BH1j7twR.js → diagram-5GNKFQAL.CHMkjJ-V.js} +1 -1
  80. package/web/_astro/{diagram-KO2AKTUF.q2zCkbAj.js → diagram-KO2AKTUF.CKY9M564.js} +1 -1
  81. package/web/_astro/{diagram-LMA3HP47.BLaaZnHV.js → diagram-LMA3HP47.B9yNJiTP.js} +1 -1
  82. package/web/_astro/{diagram-OG6HWLK6.BcpRdQwI.js → diagram-OG6HWLK6.Dk8AUGsk.js} +1 -1
  83. package/web/_astro/{erDiagram-TEJ5UH35.D7KBlqfM.js → erDiagram-TEJ5UH35.BQHR89TZ.js} +1 -1
  84. package/web/_astro/{flowDiagram-I6XJVG4X.D_gmuRqg.js → flowDiagram-I6XJVG4X.oNkWVgoK.js} +1 -1
  85. package/web/_astro/{ganttDiagram-6RSMTGT7.DzgWYniD.js → ganttDiagram-6RSMTGT7.Kz2U2nu9.js} +1 -1
  86. package/web/_astro/{gitGraphDiagram-PVQCEYII.576i3hm3.js → gitGraphDiagram-PVQCEYII.DHIZ_wPq.js} +1 -1
  87. package/web/_astro/{infoDiagram-5YYISTIA.DMCclBiz.js → infoDiagram-5YYISTIA.D0NJLcv9.js} +1 -1
  88. package/web/_astro/{ishikawaDiagram-YF4QCWOH.9Kyca4SH.js → ishikawaDiagram-YF4QCWOH.CFEfTWRG.js} +1 -1
  89. package/web/_astro/{journeyDiagram-JHISSGLW.chbdG-8q.js → journeyDiagram-JHISSGLW.Dc7IE7I4.js} +1 -1
  90. package/web/_astro/{kanban-definition-UN3LZRKU.Bds54-e0.js → kanban-definition-UN3LZRKU.D5v0xO9k.js} +1 -1
  91. package/web/_astro/{linear.D5ND9GN_.js → linear.wR5KzXnE.js} +1 -1
  92. package/web/_astro/{mermaid.core.D9N_PGbj.js → mermaid.core.BDQ2FoPd.js} +4 -4
  93. package/web/_astro/{mindmap-definition-RKZ34NQL.t0xop5vp.js → mindmap-definition-RKZ34NQL.W6_INsia.js} +1 -1
  94. package/web/_astro/{pieDiagram-4H26LBE5.DqgKh6hM.js → pieDiagram-4H26LBE5.61ihbgkK.js} +1 -1
  95. package/web/_astro/{quadrantDiagram-W4KKPZXB.DeuHVi2t.js → quadrantDiagram-W4KKPZXB.B5lEpFLo.js} +1 -1
  96. package/web/_astro/{requirementDiagram-4Y6WPE33.BdYFSG4W.js → requirementDiagram-4Y6WPE33.CmhVBVX0.js} +1 -1
  97. package/web/_astro/{sankeyDiagram-5OEKKPKP.k1LPPETJ.js → sankeyDiagram-5OEKKPKP.D7Vvj9Wf.js} +1 -1
  98. package/web/_astro/{sequenceDiagram-3UESZ5HK.DdyfpzoG.js → sequenceDiagram-3UESZ5HK.B1vPLixM.js} +1 -1
  99. package/web/_astro/{stateDiagram-AJRCARHV.DSEI8il0.js → stateDiagram-AJRCARHV.BnACmrGi.js} +1 -1
  100. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CoMnQRKo.js → stateDiagram-v2-BHNVJYJU.CW4pnmc3.js} +1 -1
  101. package/web/_astro/{timeline-definition-PNZ67QCA.DM1AHNm-.js → timeline-definition-PNZ67QCA.CkzMCPnC.js} +1 -1
  102. package/web/_astro/{vennDiagram-CIIHVFJN.C-cJmVtl.js → vennDiagram-CIIHVFJN.CY4aNdAR.js} +1 -1
  103. package/web/_astro/{wardleyDiagram-YWT4CUSO.D0xMzKPb.js → wardleyDiagram-YWT4CUSO.BtoU9UFk.js} +1 -1
  104. package/web/_astro/{xychartDiagram-2RQKCTM6.BH5xJcEq.js → xychartDiagram-2RQKCTM6._9ijUUAs.js} +1 -1
  105. package/web/index.html +1 -1
  106. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +0 -997
  107. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +0 -178
  108. package/web/_astro/channel.Bs3Illhy.js +0 -1
@@ -1,38 +1,45 @@
1
1
  # Task execution pipeline — one run per execution attempt of a task (design §6).
2
2
  #
3
- # Orchestration is configuration (ADR-022 / §3.2): this is YAML over the existing
4
- # dual-workflow engine — zero new engine code. The pipeline NEVER touches files
5
- # directly — `record` writes via `spur task update --section`, and status moves go
6
- # through the normal `spur task update <wbs> <status>` verb so the lifecycle guards
7
- # (0055) apply identically. Run linkage is written to `task_run_links` (kind=pipeline).
3
+ # Orchestration is configuration (ADR-022 / §3.2): this is YAML over the engine — zero new
4
+ # engine code. The pipeline NEVER touches files directly: task-file writes go through
5
+ # `spur task update --section` / `spur task record`, and status moves go through the normal
6
+ # `spur task update <wbs> <status>` verb so the lifecycle guards (0055) apply identically.
7
+ # Run linkage is written to `task_run_links` (kind=pipeline).
8
8
  #
9
- # Shape: precheck → implement[→escalate] → test[→test-fix↔test-recheck] → review → approve(HITL)
10
- # → verify → record → done
11
- # (precheck failure short-circuits to `failed`; approve routes to `failed` on
12
- # operator rejection or `cancelled` on operator cancel — R1, bug-750).
13
- # `test` is the project quality gate (shell + bounded /sp:dev-fixall), not
14
- # /sp:dev-unit (coverage gap-fill; router C3/C5).
9
+ # Shape (D65 1239, ADR-140): precheck → implement[⇄ escalate] → gate[⇄ remediate] → review
10
+ # → done | failed.
11
+ # Six working states — `precheck`, `implement`, `escalate`, `gate`, `remediate`,
12
+ # `review` — and two terminals. There is no `test`/`test-recheck` pair (`gate` owns
13
+ # the quality gate, its mode chosen per visit), no `triage`/`test-fail-triage`/
14
+ # `review-fail-triage` states (the same catalog decisions run inside the state that
15
+ # produced the failure and their rows drive the transitions), no `approve` state (the
16
+ # operator approval is a decision point at the end of `review`, 1236 R1/R2), no
17
+ # `verify` state (`review` certifies: `spur task verdict --from-answer` derives the
18
+ # verdict from the reviewer's own requirement traceability), and no `record` state
19
+ # (`done`'s onEnter performs the deterministic close).
15
20
  #
16
- # F96 residual sweep (0950, reordered 0983): precheck captures the resume-safe base
17
- # commit; record scans and folds residuals AFTER `task record` flips the verdict-proven
18
- # boxes (a post-record blocking leftover downgrades PASS → PARTIAL, fails the record →
19
- # done guard closed, and re-records Testing before the failed edge); done settles
20
- # deferrals; failed renders the recovery report. No new state, edge, or model query.
21
+ # Evidence model (D65 R3/R11): the run's evidence is the tree-hash gate receipt
22
+ # (`.spur/run/<wbs>-check-receipt.json`, keyed by the git tree the gate captures itself),
23
+ # the review verdict artifact written by `spur task verdict`, and the gate/review status
24
+ # tokens the transitions read. The former proof chain — `proofDigest`, `proof.fingerprint`
25
+ # captures and compares, digest-bound receipt preconditions, `run.artifact` verdict
26
+ # registration and the `verifiedOutcome` it fed — is gone; a PASS receipt is reused while
27
+ # the tree hash holds and invalidated when the tree moves. `done` closes on the verb's own
28
+ # target-aware `task check` plus the residual sweep, not on a re-asserted digest.
21
29
  #
22
- # Review-failure routing (session finding after 1088): `review` had no outgoing edge for a
23
- # FAIL verdict, so a failing review could only be remediated by driving the graph by hand
24
- # (re-entering `test` and hand-emitting rows). The review stage now derives
25
- # `.spur/run/<wbs>-review.status` from its own answer file (fail-closed to FAIL), and a FAIL
26
- # routes through `review-fail-triage` — the review-side twin of `test-fail-triage` (0943) —
27
- # into the SAME bounded fix budget, so remediation stays on-graph and bounded.
30
+ # No operator pause lives in this file (D65 R10): the only pause is the host executor's
31
+ # `agent.run` transform (1232). `escalate` asks its question through the responder and the
32
+ # approval gate answers through the decision-point responder, so an interactive run is
33
+ # answered in-session and a headless run without `--auto` answers with the responder default
34
+ # (deny / empty) instead of pausing for a later `spur workflow continue`.
28
35
  #
29
36
  # Vars (passed as a JSON object via `--vars`):
30
37
  # wbs — task WBS (required)
31
- # profile — "auto" skips HITL approve (R4)
38
+ # profile — "auto" answers the approval gate from its declared binding (R4)
32
39
  # agent / spurBin — executor + spur binary (CLI overrides spurBin)
33
- # stepTimeoutMs — agent.run budget for review/verify/test-fix (ms)
40
+ # stepTimeoutMs — agent.run budget for review/remediate (ms)
34
41
  # implementTimeoutMs — implement agent.run budget (ms)
35
- # qualityGateCmd — project gate (default: bun run spur-check)
42
+ # qualityGateCmd — project gate (default: ${spurBin:-spur} self exec check)
36
43
  # qualityGateMaxFixAttempts — max /sp:dev-fixall hops after a red gate (default: 2)
37
44
  # maxEscalations — max operator-question pauses per implement chain (default: 2)
38
45
  #
@@ -41,34 +48,29 @@
41
48
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
42
49
  kind: state-machine
43
50
  name: task-pipeline
44
- # 0772 R2: explicit identity tag. Set only after the proof failure-path tests
45
- # passed; the engine treats unversioned external definitions as supported.
46
- # v4 (session finding after 1088): the graph routes a non-PASS review into bounded remediation —
47
- # a contract change, so the identity moves with it (the confidence half of v4 was retired at v7).
51
+ # 0772 R2: explicit identity tag. Set only after the failure-path tests passed; the engine
52
+ # treats unversioned external definitions as supported.
53
+ # v4 (session finding after 1088): the graph routes a non-PASS review into bounded remediation.
48
54
  # v5 (task 1077's inline run, 2026-10-06): the verify verdict action is explicitly `onError:
49
- # continue`. `task verdict` exits non-zero on any non-PASS verdict BY DESIGN (1003 R2) — that is
50
- # data for the verify guards, not a state failure. Under the engine's default 'fail' policy
51
- # (`resolveOnErrorPolicy: action ?? workflowDefault ?? runOption ?? 'fail'`) the state sequence
52
- # halted on that exit code and `lifecycle.fail('verify')` finalized the run before any transition
53
- # guard was evaluated, which made the declared `verify → test-fix` "repairable non-PASS" lane
54
- # UNREACHABLE: a non-PASS verify could only end the run failed. `test-fix` also gained the
55
- # review-lane evidence projection (see its onEnter) so the repair hop is not fed an empty findings
56
- # list when the failing hop was `review-fail-triage` rather than the quality gate.
57
- # v7 (task 1207, F511): the asserted `Confidence: HIGH|MEDIUM|LOW` machinery is retired — the
58
- # verifier's and reviewer's self-graded levels (re-imported from the cc anti-hallucination skill
59
- # by task 1068) are no longer written, checked, or gated on; spur certifies on deterministic
60
- # evidence (rows MET, proof digests, findings dispositioned). The `confidence` check row, the
61
- # `ackLowConfidence` var, and the guards' LOW/absent clauses are gone. The derived
62
- # feature-receipt confidence (1196) followed in task 1221 (ADR-138 amendment).
63
- version: "7"
64
- description: "Standard task pipeline: precheck → implement → quality-gate (test/fix/recheck) → review → approve → verify → record → done"
55
+ # continue` — a non-PASS verdict is data for the guards, not a state failure.
56
+ # v7 (task 1207, F511): the asserted `Confidence: HIGH|MEDIUM|LOW` machinery is retired.
57
+ # v9 (task 1239, D65 R10): the seven-state rewrite. `test`/`test-recheck` collapse into `gate`
58
+ # (one typed `command.gate` over `spur task gate`, mode by visit, tree-hash receipt reuse under
59
+ # the host-wide full-gate lock); `verify` merges into `review`, which certifies through
60
+ # `spur task verdict --from-answer` over the reviewer's requirement/AC traceability; the three
61
+ # routing states become catalog decision calls inside `gate` and `review`; `approve` becomes the
62
+ # review's operator decision point; `record` becomes `done`'s deterministic close; and the whole
63
+ # proof chain (proofDigest, `proof.fingerprint`, receipt preconditions, `run.artifact` verdict
64
+ # registration) is removed with the ADR-071 machinery (ADR-140 item 7). A contract change.
65
+ version: "9"
66
+ description: "Standard task pipeline: precheck → implement → gate (typed quality gate, tree-hash receipt) → review (verdict + R/AC traceability + operator decision) → done | failed"
65
67
  # Task entity lifecycle (owned by task-lifecycle.yaml):
66
68
  # normal: backlog → todo → wip → testing → done
67
69
  # blocked is bidirectional with todo/wip/testing; done may reopen to wip.
68
70
  # any non-terminal state may transition to cancelled.
69
71
  # Guards:
70
72
  # wip → testing: spur task check <wbs>
71
- # testing → done: spur task check <wbs> --as done, then PASS-verdict provenance
73
+ # testing → done: spur task check <wbs> --as done
72
74
  # Invalid: todo → testing, todo → done, and wip → done.
73
75
  # Pipeline states below are orchestration states, not task statuses.
74
76
  iterationBound: 20
@@ -76,16 +78,13 @@ initialState: precheck
76
78
  terminalStates:
77
79
  - done
78
80
  - failed
79
- - cancelled
80
81
  failureStates:
81
82
  - failed
82
- - cancelled
83
83
  vars:
84
84
  wbs: "0000"
85
85
  profile: "standard"
86
86
  mode: ""
87
87
  __runId: ""
88
- __definitionDigest: ""
89
88
  # PATH-independent spur invocation for shell guards/actions. The CLI overrides this
90
89
  # at run start (resolveSpurBin); the literal default is a safe fallback so direct/dry
91
90
  # runs and `workflow validate` resolve the template. (See AGENTS.md / ADR-026.)
@@ -106,8 +105,8 @@ vars:
106
105
  # Remediation can mutate code only when BOTH this cap and the task's declared
107
106
  # mutationPolicy permit code. none/tests require scoped manual repair, not fixall.
108
107
  mutationPolicy: "code"
109
- # Step-level timeout for agentic hops (review / verify / test-fix) in ms.
110
- # Soft quality-gate shells are unbounded by this var (host shell only).
108
+ # Step-level timeout for agentic hops (review / remediate) in ms. The quality gate is a
109
+ # typed command.gate (its own budget, unbounded like the former shell).
111
110
  # Raised 600s → 1800s (task 0398 R4 / H6 dogfood). Override:
112
111
  # `--vars '{"stepTimeoutMs":"120000"}'`. 30 min is headroom, not a licence to
113
112
  # run unbounded — if a hop still hits this wall, STOP and record it.
@@ -115,71 +114,36 @@ vars:
115
114
  # Timeout for the `implement` step specifically (R2a). Implementation is the
116
115
  # heaviest agent.run step (full read/write/test-probe loop); the old 600s default
117
116
  # failed five consecutive dogfood runs (bugs 742/744/746/748). Default is 45 min
118
- # (task 1108): it governs subprocess surfaces (`spur workflow run`, parallel,
119
- # headless); inline hosts receive it only when their dispatch tool accepts a
120
- # timeout (inline-pipeline-driver.md § Timeout boundary). 45 min is a reasoned
121
- # default, not a proven bound — in P1 one implement still hit 45 min and resumed
122
- # (docs/reports/i31/1107-runall-p1-pilot.md § Timeout attribution).
117
+ # (task 1108); the host executor forwards it only when the host's dispatch tool
118
+ # accepts a timeout.
123
119
  # Override per run: `--vars '{"implementTimeoutMs":"3600000"}'`.
124
120
  implementTimeoutMs: "2700000"
125
- # Answer captured by the approve gate's hitl.confirm (R1): "yes" | "no" | "cancel".
126
- # Empty by default; only meaningful once the approve state has been entered.
121
+ # Answer captured by the review's approval decision point: "yes" | "no" | "cancel"
122
+ # (the responder vocabulary) or "" when the declared binding deferred (no maker, or
123
+ # `profile=auto` with the decision maker disabled). Only meaningful once `review` ran.
127
124
  __hitlAnswer: ""
125
+ # Set beside it by the same gate when `decision.mode` is `evidence`: "accepted" when the
126
+ # evidence answered, "deferred" when it could not (1236). Read by the review → done guard.
127
+ approvalStatus: ""
128
128
  # Operator answer captured by the escalate hop's hitl.input (0933). Empty by
129
- # default; set by `spur workflow continue --answer-text` after the pause and read
130
- # by the escalate→implement / escalate→failed guards. Non-empty = answered.
129
+ # default; read by the escalate→implement / escalate→failed guards. Non-empty = answered.
131
130
  __hitlInput: ""
132
- # Proof-state bracket (task 0612, ADR-071; restructured by task 0703; review joins by task 1135).
133
- # `proofDigest` is the canonical capture taken at quality-gate ENTRY — immediately before the
134
- # evidence-producing final chain (quality → review → verify) — and re-captured at `test-recheck`
135
- # when bounded remediation mutated the tree, so every evidence stage names one fresh digest
136
- # (R2/R4). `proofDigestNow` is the live re-capture compared against `proofDigest` at review,
137
- # verify and record entry; a mismatch means a proof input changed after evidence was established,
138
- # so the run routes to `failed` instead of spending a review dispatch or crossing the completion
139
- # boundary (R5). Task 1135 adds the review-entry compare and the gate-entry tree snapshot
140
- # (`.spur/run/<run-id>-gate-paths.txt`, same exclusions as the git-tree half) so a mismatch also
141
- # names the drifted paths.
142
- # `taskSpecPath` carries the task file path because `docs/tasks*` is excluded from the digest's
143
- # git-tree half — spec content is folded in explicitly or a task-file edit would go undetected.
144
- # The fingerprint scopes task content to the proof-input sections only (Background, Requirements,
145
- # Acceptance Criteria, Design, Plan), so record-time Solution/Testing/Review evidence writes do
146
- # not retroactively invalidate the certified input set (R6).
147
- proofDigest: ""
148
- proofDigestNow: ""
149
- taskSpecPath: ""
150
- # 0785 R2: linked feature spec path, resolved at `test` entry from the task frontmatter.
151
- # Empty (orphan task) means the feature spec is legitimately omitted from the digest inputs
152
- # (empty-string compatibility); a declared feature whose path fails to resolve fails closed.
153
- featureSpecPath: ""
154
- # 0710 R4: task priority tier (P0..P4) extracted from the task frontmatter at the
155
- # quality-gate stage. P0/P1 make the review/verify distinct-executor policy apply;
156
- # unknown/empty priority means fresh-context-only (executor reuse allowed).
157
- taskPriority: ""
158
- # Project quality gate for the `test` hop (probe + fixall + recheck). Override per project
159
- # with the same package-manager surface (this monorepo is Bun-only):
160
- # `--vars '{"qualityGateCmd":"bun run lint && bun run test"}'`. Soft probe, hard recheck,
161
- # and the fixall slash input all use this same var so the command stays single-sourced.
131
+ # Project quality gate for the `gate` hop, overridable per project:
132
+ # `--vars '{"qualityGateCmd":"bun run lint && bun run test"}'`.
162
133
  # INVARIANT (ADR-124, clarified 2026-10-07 by task 1111 R5): only the FULL tier writes PASS.
163
134
  # Under the opt-in `deferQualityGate` policy the per-task token is DEFERRED (light tier) and
164
135
  # the "review runs only after a green full gate" invariant moves to BATCH scope — nothing is
165
- # reported PASS without the integrated full gate on the base ref. Default "false" = per-task
166
- # full gate, unchanged.
167
- # TRUSTED CONFIG ONLY — this string is executed via `sh -c` (see test/test-recheck). Never
136
+ # reported PASS without the integrated full gate on the base ref.
137
+ # TRUSTED CONFIG ONLY — `spur task gate` passes this to the project's gate command. Never
168
138
  # interpolate untrusted operator/LLM input into qualityGateCmd (task 0436 SECUA residual).
169
- # NO `format` PREFIX: `implement` already ran `$formatCmd` on its way out, and `test`
170
- # captures `proofDigest` (onEnter[3]) BEFORE this command runs (onEnter[4]) — a formatter
171
- # inside the gate rewrites the very tree the digest just fingerprinted (ADR-071 proof
172
- # window). It was a no-op only because the implement-stage format got there first; that is
173
- # an accident, not an invariant. The gate observes, it does not mutate.
174
- qualityGateCmd: "bun run spur-check"
175
- # Cheap red-detector run before the full gate on **recheck only**; empty ⇒ no probe
176
- # (full gate every recheck — the pre-0587 behavior). A project overriding qualityGateCmd
177
- # should override this too. TRUSTED CONFIG ONLY — executed via `sh -c` (same surface as
178
- # qualityGateCmd). Invariant: `review` is only ever entered through a full green
179
- # qualityGateCmd — only the full gate writes PASS to <wbs>-test-gate.status.
180
- gateProbeCmd: "bun run lint"
181
- # Max /sp:dev-fixall attempts after a red quality-gate probe/recheck (bounded; no thrash).
182
- # Attempt counter: .spur/run/<wbs>-test-fix-attempt. Default 2 = two fixall hops before failed.
139
+ # The gate observes, it does not mutate: `implement` already ran `$formatCmd` on its way out,
140
+ # and a formatter inside the gate would rewrite the tree the receipt is keyed by.
141
+ qualityGateCmd: "${spurBin:-spur} self exec check"
142
+ # Cheap red-detector run before the full gate on **recheck visits only**; empty ⇒ no probe.
143
+ # A project overriding qualityGateCmd should override this too. TRUSTED CONFIG ONLY.
144
+ gateProbeCmd: "${spurBin:-spur} self exec lint"
145
+ # Max /sp:dev-fixall attempts for one run (bounded; no thrash). Counter:
146
+ # .spur/run/<wbs>-test-fix-attempt. Default 2 = two fixall hops before failed.
183
147
  qualityGateMaxFixAttempts: "2"
184
148
  # Max operator-question pauses (0933) before the implement chain routes to failed.
185
149
  # Escalation counter: .spur/run/<wbs>-escalation-count. Default 2 = two answered
@@ -192,32 +156,46 @@ vars:
192
156
  # Post-implement auto-format. Overridable like qualityGateCmd so a non-Bun seeded
193
157
  # project can point it at its own formatter; invoked best-effort (a missing or
194
158
  # failing formatter must never abort a run — the quality gate is the real gate).
195
- formatCmd: "bun run format"
196
- # Bounded `file:line` anchors extracted from the failing quality-gate log by the
197
- # `test` / `test-recheck` hops (deduped, max 20, space-separated on one line).
198
- # `test-fix` reads `.spur/run/<wbs>-test-gate.findings` into this var and names the
159
+ formatCmd: "${spurBin:-spur} self exec format"
160
+ # Bounded `file:line` anchors extracted from the failing quality-gate log by
161
+ # `spur task gate` (deduped, max 20, space-separated on one line).
162
+ # `remediate` reads .spur/run/<wbs>-test-gate.findings into this var and names the
199
163
  # anchors directly in the /sp:dev-fixall input, so the fix agent starts AT the failing
200
164
  # file:line instead of re-deriving it from a fresh gate run (0482 R3).
201
165
  gateFindings: ""
166
+ # The quality-gate mode this visit runs — `run` on the first gate visit of the run,
167
+ # `recheck` after a remediation hop, `deferred` under the opt-in batch policy. Written by
168
+ # the gate's own shell and projected into this var (args cannot branch) — D65 1239 R2.
169
+ gateMode: ""
170
+ # Review depth resolved from the lane decision (0943 R1/R2): `functional` for the fast
171
+ # lane (requirement/AC traceability only — the reviewer's R → file:line table is what
172
+ # certifies, so the fast lane never drops the evidence), `all` for every other lane.
173
+ reviewFocus: ""
174
+ # Task spec path resolved at `gate` entry (also the decide evidence for the review-lane
175
+ # routing). `docs/tasks*` is a versioned input, not a run artifact.
176
+ taskSpecPath: ""
177
+ # 0710 R4: task priority tier (P0..P4) extracted from the task frontmatter at `gate`
178
+ # entry. P0/P1 make the review distinct-executor policy apply; unknown/empty priority
179
+ # means fresh-context-only (executor reuse allowed).
180
+ taskPriority: ""
202
181
  # Diff-scope guard on the implement hop (R1, task 0487). When the target task
203
182
  # body backticks at least one path, non-corpus changes outside those paths
204
183
  # fail the step by name. New files beside a declared file are allowed. Empty
205
184
  # (default) = on; set to "off" to bypass:
206
185
  # `--vars '{"implementScopeGuard":"off"}'`.
207
186
  implementScopeGuard: ""
208
- # 1111 R1: opt-in parallel-batch gate policy. When "true", `test` / `test-recheck` run the
209
- # LIGHT tier for per-task early feedback and write `DEFERRED` (light PASS) or `FAIL` to
187
+ # 1111 R1: opt-in parallel-batch gate policy. When "true", `gate` runs the LIGHT tier for
188
+ # per-task early feedback and writes `DEFERRED` (light PASS) or `FAIL` to
210
189
  # `.spur/run/<wbs>-test-gate.status` — never `PASS`, because that token means the full
211
190
  # project gate ran green on this tree. The batch orchestrator owes ONE integrated full gate
212
191
  # on the base ref over the merged slices (1111 R3). Default "false" = unchanged behavior,
213
- # and a stray `DEFERRED` file fails the guards. Sequential/inline runs never set it:
214
- # `--defer-gate` requires `--mode parallel`.
192
+ # and a stray `DEFERRED` file fails the guards.
215
193
  deferQualityGate: "false"
216
194
  # 0931 R5: parallel batches defer the per-task feature sync so a task branch never
217
- # touches feature files or docs/features/INDEX.md. When "true", the record step's
218
- # post-record shell appends a deferral note and skips the sync; the parallel batch
219
- # orchestrator runs the sync + `spur feature refresh` once per touched feature on the
220
- # base ref after integration. Sequential/inline keep the default "false" (unchanged).
195
+ # touches feature files or docs/features/INDEX.md. When "true", the close's post-record
196
+ # shell appends a deferral note and skips the sync; the parallel batch orchestrator runs
197
+ # the sync + `spur feature refresh` once per touched feature on the base ref after
198
+ # integration. Sequential/inline keep the default "false" (unchanged).
221
199
  deferFeatureSync: "false"
222
200
 
223
201
  states:
@@ -275,7 +253,7 @@ states:
275
253
  fails or times out, so a halted implement never leaves the task at `wip` with an
276
254
  empty Solution (the F3 half-state). The `--no-lifecycle` flag is correct here: the
277
255
  pipeline owns this transition; the FSM guard would otherwise re-run `spur task
278
- check`, which is precheck's job.
256
+ check`, which precheck's job.
279
257
  NOTE: this step must NOT call /sp:dev-run in full mode (the default) —
280
258
  that command DRIVES this pipeline, so calling it here recurses.
281
259
  --mode implement is the single-step implement entry.
@@ -298,16 +276,14 @@ states:
298
276
  options:
299
277
  agent: ${vars.implementAgent}
300
278
  # Pure slash command only (ADR-043). Anti-recursion / implement discipline
301
- # The worker's brief is OWNED BY the skill, not restated here (task 1110 R3):
302
- # see `plugins/sp/skills/code-implementation/SKILL.md` (targeted-probe scope
303
- # § Implement scope, dependency-aware § Changed-path targeted checks, Solution
304
- # anchor form in § Red Flags). The YAML only names the entry command.
279
+ # The worker's brief is OWNED BY THE SKILL, not restated here (task 1110 R3):
280
+ # see `plugins/sp/skills/code-implementation/SKILL.md`. The YAML only names the
281
+ # entry command.
305
282
  # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
306
283
  role: coder
307
- # B7 R3/R6 (0894): coder stages reuse the role session — the test-fix hop
284
+ # B7 R3/R6 (0894): coder stages reuse the role session — the remediation hop
308
285
  # resumes the implement session instead of re-reading the task cold.
309
286
  session: reuse
310
- # lives in /sp:dev-run --mode implement → sp:code-implementation, not YAML prose.
311
287
  # 0933 R28: --escalation-file names the Q/A transcript the implement
312
288
  # agent appends to when pausing and reads back on re-dispatch; absent
313
289
  # on a first attempt means "no prior escalations".
@@ -316,7 +292,7 @@ states:
316
292
  # R3 (task 0424): empty-implement no-op guard — the agent.run action
317
293
  # fails the step when exit 0 produced zero non-corpus file changes, so
318
294
  # a silent no-op routes the run to `failed` here instead of drifting
319
- # into test/review and being caught a full pass later.
295
+ # into gate/review and being caught a full pass later.
320
296
  requireDiff: true
321
297
  # Verification-only re-run admission (task 0976): when requireDiff
322
298
  # sees zero non-corpus changes because the implementation already
@@ -328,15 +304,12 @@ states:
328
304
  # 0933 R26: escalation contract — the agent.run option names the QUESTION
329
305
  # file (the pause signal, deleted pre-dispatch for freshness); a paused
330
306
  # attempt (agent wrote its question there and exited 0) succeeds with
331
- # data.escalated=true and skips requireDiff for THAT attempt only. The
332
- # Q/A transcript (.spur/run/<wbs>-escalation.md) is separate: it reaches
333
- # the agent via --escalation-file in the input below.
307
+ # data.escalated=true and skips requireDiff for THAT attempt only.
334
308
  escalationFile: .spur/run/${vars.wbs}-question.md
335
309
  # 0706 R6: this stage mutates the working tree unattended under the
336
310
  # auto profile, so it declares minimum execution-capability
337
311
  # requirements. Dispatch fails closed (before spawn) when the
338
- # resolved executor's attestation cannot satisfy them — attest the
339
- # executor in agent config.
312
+ # resolved executor's attestation cannot satisfy them.
340
313
  requiresCapabilities:
341
314
  fsWrite: available
342
315
  processSpawn: available
@@ -355,39 +328,35 @@ states:
355
328
  timeoutMs: 120000
356
329
  # Post-implement cleanup: auto-format any unformatted output the agent produced.
357
330
  # The agent may leave files that don't pass `biome check`, which then fails
358
- # `bun run lint` downstream. Running format here is cheap (< 1 s) and prevents
359
- # the test stage from tripping on a purely mechanical formatting defect (dogfood
360
- # bug-733 — omp left agent-run.test.ts unformatted, causing a spurious lint gate
361
- # failure that the ## Testing section mis-attributed to "pre-existing gaps").
331
+ # `bun run lint` downstream (dogfood bug-733).
362
332
  # Best-effort: `; exit 0` (not `&&`) so a missing/failing formatter in a seeded
363
- # non-Bun project cannot abort a run mid-implement. Formatting is a convenience;
364
- # `${vars.qualityGateCmd}` at `test` is the gate that actually decides.
333
+ # non-Bun project cannot abort a run mid-implement. `${vars.qualityGateCmd}` at
334
+ # `gate` is the gate that actually decides.
365
335
  - kind: shell
366
336
  options:
367
- command: "$formatCmd ; exit 0"
337
+ command: 'sh -c "$formatCmd" ; exit 0'
368
338
  # 1109 R3: qualify this task's unique basename anchors to repo-relative paths.
369
339
  # Placement is load-bearing: it runs AFTER implement's mutations (and the format
370
- # above) but BEFORE the `test` stage's `proof.fingerprint` capture, so a rewrite in
371
- # a digest section cannot invalidate the proof. `--wbs` scopes the pass to this
372
- # task's resolved file — the unscoped form rewrites ~119 unrelated task files.
373
- # Non-fatal (`; exit 0`): the done gate still reports anything left, and this step
374
- # deliberately does NOT address line drift or `L4.anchor-subject-mismatch`
375
- # (those need the implementer to re-anchor). Scope contract: task 1109 R5.
340
+ # above) and BEFORE the gate reads the tree, so the receipt is keyed to the final
341
+ # tree. `--wbs` scopes the pass to this task's resolved file.
342
+ # Non-fatal (`; exit 0`): the done gate still reports anything left. Scope
343
+ # contract: task 1109 R5.
376
344
  - kind: shell
377
345
  options:
378
346
  command: "$spurBin task migrate-anchors --wbs $wbs --json ; exit 0"
379
347
 
380
348
  # ── escalate hop (operator-question pause, 0933) ──────────────────────────
381
349
  # The implement agent paused on an operator question: it wrote
382
- # .spur/run/<wbs>-question.md and exited 0 (agent.run reported
383
- # data.escalated=true, skipping requireDiff for that attempt). This hop bounds
384
- # the asks (counter), surfaces the question verbatim through the HITL responder
385
- # (the run pauses here), and — after `spur workflow continue --answer-text <a>`
386
- # delivers __hitlInput — resumes implement with the Q/A transcript
387
- # (.spur/run/<wbs>-escalation.md) named in the implement input via
388
- # --escalation-file. The transcript append + question-file consumption happen in
389
- # the escalate→implement guard so a stale question can never re-trigger the hop
390
- # after the answered attempt.
350
+ # .spur/run/<wbs>-question.md and exited 0 (agent.run reported data.escalated=true,
351
+ # skipping requireDiff for that attempt). This hop bounds the asks (counter), surfaces
352
+ # the question verbatim through the HITL responder, and — once the answer lands in
353
+ # __hitlInput — resumes implement with the Q/A transcript
354
+ # (.spur/run/<wbs>-escalation.md) named in the implement input via --escalation-file.
355
+ # The transcript append + question-file consumption happen in implement's onEnter so a
356
+ # stale question can never re-trigger the hop after the answered attempt.
357
+ # D65 1239: the ask is answered by the responder in-session (no `pause: true` remains in
358
+ # this file); a headless run without an answer routes to failed(cancelled) with the
359
+ # question preserved, exactly as an empty answer always did.
391
360
  - id: escalate
392
361
  display:
393
362
  phase: implement
@@ -398,135 +367,201 @@ states:
398
367
  Operator-question pause (0933): the implement agent asked a question
399
368
  headlessly; surface it to the operator and resume implement with the answer.
400
369
  onEnter:
401
- # Bounded asks: increment the per-task escalation counter before pausing.
370
+ # Bounded asks: increment the per-task escalation counter before asking.
402
371
  - kind: shell
403
372
  options:
404
373
  command: >-
405
374
  mkdir -p .spur/run; n="$(cat .spur/run/$wbs-escalation-count 2>/dev/null || echo 0)"; echo $((n + 1)) > .spur/run/$wbs-escalation-count; exit 0
406
- # The agent's question, verbatim, becomes the pause prompt.
375
+ # The agent's question, verbatim, becomes the prompt.
407
376
  - kind: file.read.into-var
408
377
  options:
409
378
  file: .spur/run/${vars.wbs}-question.md
410
379
  var: escalationQuestion
411
- # Pause for the operator (responder pause:true stops the run after this
412
- # onEnter; `spur workflow continue --answer-text <answer>` writes
413
- # __hitlInput and re-evaluates the escalate guards).
380
+ # Ask the operator (no pause declaration: the responder answers, and the answer is
381
+ # written to __hitlInput for the escalate guards).
414
382
  - kind: hitl.input
415
383
  options:
416
384
  prompt: "${vars.escalationQuestion}"
417
385
 
418
- # ── test hop (quality gate + bounded auto-fix) ─────────────────────────────
386
+ # ── gate hop (typed quality gate, one state for run/recheck visits) ───────────
419
387
  # NOT /sp:dev-unit. That command (sp:code-testing) *extends/generates* tests toward
420
388
  # a coverage target; it is not the project quality gate. Coverage gap-fill remains
421
389
  # available via router C3/C5 (`/sp:dev-unit`) or inside implement — not this hop.
422
390
  #
423
- # Shape (engine has no per-action "run next only if previous failed" without
424
- # `onError`, and Spur's action schema does not expose `onError`):
425
- # test — soft probe: qualityGateCmd → PASS|FAIL status; always exit 0
426
- # (green path: one gate run → review; no fixall/recheck tax)
427
- # test-fix — increment attempt + pure slash /sp:dev-fixall (≤ max attempts)
428
- # test-recheck — soft recheck → review | test-fix | failed (never silent lifecycle abort)
429
- - id: test
391
+ # D65 1239 R2: `test` and `test-recheck` are one state. `spur task gate` owns the tier,
392
+ # the probe, the receipt and the host-wide lock; the typed `command.gate` owns the exit
393
+ # code (PASS/FAIL/BLOCKED) so a blocked gate routes to `failed` instead of reading as
394
+ # red. The mode is chosen per visit: `run` on the first visit of this run (it resets the
395
+ # attempt counter and consults the tree-hash receipt), `recheck` after a remediation hop
396
+ # (probe-then-full), `deferred` under the opt-in batch policy.
397
+ #
398
+ # The three former routing states are decision CALLS here and in `review` (D65 1239 R1):
399
+ # the lane decision (0943 R1/R2, catalog `task-triage`) picks the review depth, and the
400
+ # gate's own failure class (catalog `failure-class`) drives the red-gate transitions.
401
+ # Both run unconditionally — the engine has no onEnter conditionals — and both degrade to
402
+ # their catalog fallback with no model call while `workflows.decideDecisionMaker` is off.
403
+ - id: gate
430
404
  display:
431
405
  phase: test
432
406
  phaseTitle: Test
433
407
  title: Run quality gate
434
408
  description: >
435
- Proof-chain entry + soft quality-gate probe (single logical gate on the green path; bounded retries only
436
- for SQLite lock contention). Capture of the canonical proof-input digest happens HERE — before any
437
- evidence-producing final check (task 0703, ADR-071) — so the gate, review, and verification evidence
438
- all name one digest. Then runs
439
- `${vars.qualityGateCmd}`, records PASS|FAIL under
440
- `.spur/run/<wbs>-test-gate.status`, resets the fix attempt counter, always exit 0.
409
+ Typed quality gate: resolve the task path/priority, produce the diffstat lane
410
+ evidence, consult `task-triage` for the review depth (deterministic safety floor
411
+ first), run `${vars.qualityGateCmd}` through `spur task gate <wbs> <mode>` under the
412
+ host-wide lock (tree-hash receipt reuse on the same tree), then classify a red gate
413
+ through `failure-class`. Writes the typed outcome to
414
+ `.spur/run/<runId>-gate.status` and the gate's own token to
415
+ `.spur/run/<wbs>-test-gate.status`; always a recorded outcome, never a state failure.
441
416
  onEnter:
442
- # R6/R2 (0703): resolve the task-spec path BEFORE the digest capture. `docs/tasks*` is excluded
443
- # from the digest's git-tree half, so the spec is folded in explicitly via `taskSpecPath`.
444
- # (e) task path + priority resolution; fail-closed on an unresolvable spec (0751 R2, 0710 R4).
417
+ # (e) task path + priority resolution; fail-closed on an unresolvable spec (0751 R2,
418
+ # 0710 R4). The path is the decide evidence for the review lane and the priority is
419
+ # the review/executor distinctness input (0710 R4) — both are consumed downstream, so
420
+ # an unresolved path fails here instead of degrading the run silently.
445
421
  - kind: shell
446
422
  options:
447
- # 0710 R4: resolve the spec path, then extract `priority:` from the TASK FILE itself (not the
448
- # path listing); normalize to upper so requiresDistinctExecutor's exact 'P0'/'P1' match hits.
449
- # 0751 R2: the task path is NOT optional - an unresolved lookup fails
450
- # the step (no `|| true`, no forced `exit 0`, no stderr suppression)
451
- # instead of degrading the proof to whole-tree-only. The priority read
452
- # stays tolerant: a missing line is genuinely optional.
453
- command: 'mkdir -p .spur/run; $spurBin task path $wbs --json | jq -r ".path // .filePath // empty" > ".spur/run/$wbs-taskpath.txt"; task_path="$(cat ".spur/run/$wbs-taskpath.txt")"; if [ -z "$task_path" ]; then echo "fail-closed proof chain (0751 R2): task path for $wbs did not resolve - the task spec cannot be folded into the proof digest" >&2; exit 1; fi; sed -n "s/^priority:[[:space:]]*//p" "$task_path" | head -1 | tr -d "[:space:]" | tr "[:lower:]" "[:upper:]" > ".spur/run/$wbs-priority.txt"'
423
+ command: 'mkdir -p .spur/run; $spurBin task path $wbs --json | jq -r ".path // .filePath // empty" > ".spur/run/$wbs-taskpath.txt"; task_path="$(cat ".spur/run/$wbs-taskpath.txt")"; if [ -z "$task_path" ]; then echo "gate: task path for $wbs did not resolve - the review evidence has no task spec" >&2; exit 1; fi; sed -n "s/^priority:[[:space:]]*//p" "$task_path" | head -1 | tr -d "[:space:]" | tr "[:lower:]" "[:upper:]" > ".spur/run/$wbs-priority.txt"'
454
424
  - kind: file.read.into-var
455
425
  options:
456
426
  path: .spur/run/${vars.wbs}-taskpath.txt
457
427
  var: taskSpecPath
458
- # 0710 R4: carry the task's priority tier into the review/verify risk policy.
428
+ # 0710 R4: carry the task's priority tier into the review's distinctness policy.
459
429
  - kind: file.read.into-var
460
430
  options:
461
431
  path: .spur/run/${vars.wbs}-priority.txt
462
432
  var: taskPriority
463
- # 0785 R2: resolve the task's linked feature spec path so the digest covers it too. An
464
- # orphan task (no feature_id) resolves to empty — legitimately omitted via empty-string
465
- # compatibility; a declared feature whose spec path does not resolve fails closed instead
466
- # of silently folding a stale spec into the proof.
467
- # (e) linked feature-spec path resolution; fail-closed when a declared feature does not resolve (0785 R2).
433
+ # (a) diffstat producer: `spur task diffstat` owns git reading and sensitive-path
434
+ # classification; shell only fails closed on a non-zero exit (writes sensitive:true,
435
+ # which the pre-guard below maps to the safety lane).
468
436
  - kind: shell
469
437
  options:
470
- command: 'mkdir -p .spur/run; FID=$($spurBin task show $wbs --json 2>/dev/null | jq -r ".feature_id // .frontmatter.feature_id // empty" 2>/dev/null); FPATH_OUT=""; if [ -n "$FID" ]; then FPATH_OUT=$($spurBin feature show "$FID" --json | jq -r ".filePath // .path // empty"); if [ -z "$FPATH_OUT" ]; then echo "fail-closed proof chain (0785 R2): feature $FID linked to task $wbs but its spec path did not resolve" >&2; exit 1; fi; fi; printf "%s" "$FPATH_OUT" > ".spur/run/$wbs-featurepath.txt"'
471
- - kind: file.read.into-var
438
+ command: >-
439
+ $spurBin task diffstat "$wbs" --no-logo || { echo "task-diffstat failed closed — spur task diffstat exited $?" >&2; printf '{"files":0,"insertions":0,"deletions":0,"paths":[],"sensitive":true}\n' > ".spur/run/$wbs-diffstat.json"; }; exit 0
440
+ # (b)+(c) lane pre-guard BEFORE the decide: a caller-set mode is projected verbatim
441
+ # (R2b); a sensitive path or >400 changed lines pins safety (R2c) and the decide
442
+ # result is never consulted. A missing/malformed diffstat row pins safety HERE: an
443
+ # empty mode file would let the low decide row project the fast lane downstream, so
444
+ # the "never to fast" fail-safe is enforced at the guard, not by the projection.
445
+ - kind: shell
446
+ options:
447
+ command: >-
448
+ mkdir -p .spur/run; if [ -n "$mode" ]; then printf '%s\n' "$mode" > ".spur/run/$wbs-mode.txt"; exit 0; fi; DF=".spur/run/$wbs-diffstat.json"; if ! jq -e 'type == "object"' "$DF" >/dev/null 2>&1; then printf 'safety\n' > ".spur/run/$wbs-mode.txt"; exit 0; fi; jq -r 'if .sensitive == true or ((.insertions // 0) + (.deletions // 0)) > 400 then "safety" else empty end' "$DF" 2>/dev/null > ".spur/run/$wbs-mode.txt"; exit 0
449
+ # (d) lane decision — degrades to the standard default whenever
450
+ # workflows.decideDecisionMaker is off, so with the flag off the pipeline makes no
451
+ # model call (1114/1116: the bundled catalog owns question, choices and fallback).
452
+ - kind: decide
472
453
  options:
473
- path: .spur/run/${vars.wbs}-featurepath.txt
474
- var: featureSpecPath
475
- # R2 (0703, ADR-071): THE canonical proof capture. Placement is load-bearing: immediately before
476
- # the final evidence chain, after every implement mutation (including the post-implement format).
477
- # Capture-only here; `record` compares. A remediation pass re-captures at `test-recheck` (R4).
478
- # 1135 R5: snapshot the non-corpus tree state FIRST, so a later proof mismatch names the
479
- # drifted paths (not only the D1/D2 pair). Same exclusions as the digest's git-tree half.
454
+ id: task-triage
455
+ decision: task-triage
456
+ params:
457
+ wbs: ${vars.wbs}
458
+ evidence:
459
+ - .spur/run/${vars.wbs}-diffstat.json
460
+ - ${vars.taskSpecPath}
461
+ resultFile: .spur/run/${vars.wbs}-triage.decision
462
+ # (d) project the decision and append the triage route reason (1007 R3: the
463
+ # non-empty mode file is never overridden; the append keeps the failure signal of the
464
+ # original pair). low → fast lane; anything else (standard/high, degraded default,
465
+ # missing row) stays empty → the standard review path.
480
466
  - kind: shell
481
467
  options:
482
468
  command: >-
483
- mkdir -p .spur/run; { git status --porcelain=v1 -uall -- . ':(exclude)docs/tasks*' ':(exclude)docs/features*'; git diff --name-only HEAD -- . ':(exclude)docs/tasks*' ':(exclude)docs/features*'; } > ".spur/run/$__runId-gate-paths.txt" 2>/dev/null; exit 0
484
- - kind: proof.fingerprint
469
+ MF=".spur/run/$wbs-mode.txt"; [ -s "$MF" ] || jq -r 'if .value == "low" then "fast" else empty end' ".spur/run/$wbs-triage.decision" 2>/dev/null > "$MF"; m="$(cat "$MF")"; RUN_ID="$__runId"; [ -n "$RUN_ID" ] || RUN_ID="pipeline-$wbs"; mkdir -p .spur/memory; printf '%s %s %s\n' "$RUN_ID" "$wbs" "$(jq -rn --arg m "$m" --arg c "$mode" 'if $c != "" then ($c + ":caller-set") else {"fast":"fast:triage low lane","safety":"safety:triage deterministic-high","":"safety:triage standard lane"}[$m] // "safety:triage unrecognized mode" end')" >> .spur/memory/task-pipeline-routes.log
470
+ # (d) review depth from the resolved lane (D65 1239 R2): the fast lane reviews the
471
+ # requirement/AC traceability only — that table is the certification input, so the
472
+ # cheap lane never drops the evidence, it drops the SECUA/architecture passes.
473
+ - kind: shell
474
+ options:
475
+ command: >-
476
+ m="$(cat ".spur/run/$wbs-mode.txt" 2>/dev/null)"; if [ "$m" = fast ]; then printf 'functional\n' > ".spur/run/$wbs-review-focus.txt"; else printf 'all\n' > ".spur/run/$wbs-review-focus.txt"; fi; exit 0
477
+ # (d) the resolved lane becomes the `mode` var for the review-depth projection.
478
+ - kind: file.read.into-var
479
+ options:
480
+ path: .spur/run/${vars.wbs}-mode.txt
481
+ var: mode
482
+ - kind: file.read.into-var
483
+ options:
484
+ path: .spur/run/${vars.wbs}-review-focus.txt
485
+ var: reviewFocus
486
+ # D65 1239 R2 (mode by visit): the first gate visit of this run runs `run` — which
487
+ # resets the fix-attempt counter and consults the tree-hash receipt, so a tree that
488
+ # already passed is not re-gated (R11) — and every later visit after a remediation
489
+ # hop runs `recheck` (probe-then-full, same host-wide lock). The visit counter is
490
+ # run-scoped, so a stale attempt counter from an earlier run cannot start in recheck.
491
+ # Two shells: bump the visit counter, then select the mode from the bumped value
492
+ # (one concern each, ADR-115).
493
+ - kind: shell
485
494
  options:
486
- var: proofDigest
487
- taskFile: ${vars.taskSpecPath}
488
- featureFile: ${vars.featureSpecPath}
489
- # (d) `spur task gate` owns the gate; shell only picks the mode and fails closed
490
- # (a non-0/1 exit — spur missing, usage error, crash — writes FAIL).
495
+ command: >-
496
+ mkdir -p .spur/run; V=".spur/run/$__runId-gate-visits"; n="$(cat "$V" 2>/dev/null || echo 0)"; printf '%s\n' "$n" > ".spur/run/$wbs-gate-visit.txt"; echo $((n + 1)) > "$V"; exit 0
491
497
  - kind: shell
492
498
  options:
493
499
  command: >-
494
- M=run; [ "$deferQualityGate" = "true" ] && M=deferred; $spurBin task gate "$wbs" "$M" --no-logo; rc=$?; if [ "$rc" -gt 1 ]; then echo "quality gate failed closed — spur task gate exited $rc" >&2; printf 'FAIL\n' > ".spur/run/$wbs-test-gate.status"; fi; exit 0
500
+ n="$(cat ".spur/run/$wbs-gate-visit.txt" 2>/dev/null)"; M=run; [ "$n" = 0 ] || M=recheck; [ "$deferQualityGate" = "true" ] && M=deferred; printf '%s\n' "$M" > ".spur/run/$wbs-gate-mode.txt"; exit 0
501
+ - kind: file.read.into-var
502
+ options:
503
+ path: .spur/run/${vars.wbs}-gate-mode.txt
504
+ var: gateMode
505
+ # (d) `spur task gate` owns the gate (tier, probe, host-wide lock, tree-hash
506
+ # receipt); the typed outcome owns the routing. `softFail` keeps a red or blocked
507
+ # gate a RECORDED outcome — the run never dies mid-gate, and guards read
508
+ # `.spur/run/<runId>-gate.status` (PASS | FAIL | BLOCKED). Exit 2 (usage, lock
509
+ # timeout) is BLOCKED, never a red gate. The receipt reuse short-circuit inside
510
+ # `run` is what makes a second visit on an unchanged tree free (R11).
511
+ - kind: command.gate
512
+ options:
513
+ id: quality-gate
514
+ executable: "${vars.spurBin}"
515
+ args: ["task", "gate", "${vars.wbs}", "${vars.gateMode}", "--no-logo"]
516
+ resultFile: .spur/run/${vars.__runId}-gate.status
517
+ softFail: true
518
+ # (d) failure class of the gate this visit produced (0943 R3): stop → failed
519
+ # before the cap can mislabel it, fix → the bounded remediation hop. A missing or
520
+ # corrupt row fails closed to failed-check. Runs unconditionally (no onEnter
521
+ # conditionals) and degrades to the catalog `fix` fallback with no model call while
522
+ # the decision switch is off — the green path ignores it.
523
+ - kind: decide
524
+ options:
525
+ id: failure-class
526
+ decision: failure-class
527
+ params:
528
+ wbs: ${vars.wbs}
529
+ evidence:
530
+ - .spur/run/${vars.wbs}-test-gate.findings
531
+ resultFile: .spur/run/${vars.wbs}-failure-class.decision
495
532
 
496
- - id: test-fix
533
+ # ── remediate hop (bounded repair; former `test-fix`) ─────────────────────
534
+ - id: remediate
497
535
  display:
498
536
  phase: test
499
537
  phaseTitle: Test
500
538
  title: Fix failures
501
539
  show: on-entry
502
540
  description: >
503
- Bounded auto-fix hop when the quality gate is red OR final verification returned a
504
- repairable non-PASS (task 0703 R4: remediation never happens inside verify — it loops
505
- through here, then re-enters quality → review → verify on a fresh digest). Increments
506
- `.spur/run/<wbs>-test-fix-attempt` (the shared bound with the quality path), projects the
507
- verify verdict into the gate log when one exists so the repair hop sees it, then pure
508
- slash (ADR-043) `/sp:dev-fixall` against `${vars.qualityGateCmd}`.
541
+ Bounded auto-fix hop when the quality gate is red OR the review returned a repairable
542
+ non-PASS: increments `.spur/run/<wbs>-test-fix-attempt` (the shared bound, so a review
543
+ FAIL cannot buy extra attempts), projects the review verdict into the gate log when one
544
+ exists so the repair hop sees it, then pure slash (ADR-043) `/sp:dev-fixall` against
545
+ `${vars.qualityGateCmd}`. The next state is `gate`, whose visit-counter mode is
546
+ `recheck`, so the chain reruns gate → review on the remediated tree.
509
547
  onEnter:
510
548
  - kind: shell
511
549
  options:
512
550
  command: >-
513
- TASK_POLICY=$($spurBin task show "$wbs" --json | jq -er '.frontmatter.mutationPolicy // ([.content | scan("(?m)^mutationPolicy:[ \t]*([^ \r\n]+)[ \t]*$") | .[0]] | if length == 0 then "code" elif length == 1 then .[0] else "ambiguous" end)') || exit 1; [ "$mutationPolicy:$TASK_POLICY" = "code:code" ] || { echo "test-fix: mutation policy forbids automatic code repair (run=$mutationPolicy, task=$TASK_POLICY) — gate failure preserved, no agent dispatched" >&2; exit 1; }
514
- # (e) fixall attempt counter, verdict hand-off and residual artifact in one log
515
- # append shell (1007 R3: the two appends merged — original commands in order, exit 0).
551
+ TASK_POLICY=$($spurBin task show "$wbs" --json | jq -er '.frontmatter.mutationPolicy // ([.content | scan("(?m)^mutationPolicy:[ \t]*([^ \r\n]+)[ \t]*$") | .[0]] | if length == 0 then "code" elif length == 1 then .[0] else "ambiguous" end)') || exit 1; [ "$mutationPolicy:$TASK_POLICY" = "code:code" ] || { echo "remediate: mutation policy forbids automatic code repair (run=$mutationPolicy, task=$TASK_POLICY) — gate failure preserved, no agent dispatched" >&2; exit 1; }
552
+ # (e) attempt counter + residual artifact projection in one log append
553
+ # (1007 R3: the two appends merged — original commands in order, exit 0).
516
554
  # (d) F96 R3 (0950): dev-fixall treats residual items as fix targets and may defer
517
- # P3/markers to residual-deferrals.json (see dev-fixall.md); fold already merged the
518
- # anchors into <wbs>-test-gate.findings, this carries the structured artifact too.
555
+ # P3/markers to residual-deferrals.json; this carries the structured artifact too.
519
556
  - kind: shell
520
557
  options:
521
558
  command: >-
522
- printf '%s\n' "$(( $(cat ".spur/run/$wbs-test-fix-attempt" 2>/dev/null || echo 0) + 1 ))" > ".spur/run/$wbs-test-fix-attempt"; [ -f ".spur/run/$wbs-verdict.json" ] && echo '--- verify verdict (remediation input, task 0703 R4) ---' >> ".spur/run/$wbs-test-gate.log" && cat ".spur/run/$wbs-verdict.json" >> ".spur/run/$wbs-test-gate.log"; [ -f ".spur/run/$wbs-residuals.json" ] && echo '--- residual artifact (F96 fix targets; deferrals go to residual-deferrals.json, P3/markers only) ---' >> ".spur/run/$wbs-test-gate.log" && cat ".spur/run/$wbs-residuals.json" >> ".spur/run/$wbs-test-gate.log"; exit 0
523
- # Review-lane input (v5, task 1077 run): `review-fail-triage` routes a non-PASS review
524
- # into this same bounded repair hop, but it stages no review evidence — the fixer then
525
- # received a GREEN gate log and an empty findings list, so the review finding it exists
526
- # to repair was invisible to it. Project the reviewer's answer into the same log the
527
- # other lanes use, and only on the review lane (the decision file exists only after
528
- # `review-fail-triage`). Own action, not an append to the one above: ADR-115's 800-char
529
- # shell cap is already 602 chars there.
559
+ printf '%s\n' "$(( $(cat ".spur/run/$wbs-test-fix-attempt" 2>/dev/null || echo 0) + 1 ))" > ".spur/run/$wbs-test-fix-attempt"; [ -f ".spur/run/$wbs-residuals.json" ] && echo '--- residual artifact (F96 fix targets; deferrals go to residual-deferrals.json, P3/markers only) ---' >> ".spur/run/$wbs-test-gate.log" && cat ".spur/run/$wbs-residuals.json" >> ".spur/run/$wbs-test-gate.log"; exit 0
560
+ # Review-lane input: the review failure class routes a non-PASS review into this same
561
+ # bounded repair hop, but it stages no gate evidence — the fixer would receive a GREEN
562
+ # gate log and an empty findings list. Project the reviewer's answer into the same log
563
+ # the other lane uses, and only on the review lane (the decision file exists only
564
+ # after a review FAIL). Own action, not an append to the one above (ADR-115 cap).
530
565
  - kind: shell
531
566
  options:
532
567
  command: >-
@@ -542,17 +577,16 @@ states:
542
577
  options:
543
578
  agent: ${vars.agent}
544
579
  # R3 (0482): `--findings` names the failing anchors inline; `--gate-log` remains
580
+ # the full-context escape hatch.
545
581
  # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
546
582
  role: coder
547
583
  # B7 R3/R6 (0894): coder stages reuse the role session — the fix hop
548
584
  # continues the implement session it is repairing.
549
585
  session: reuse
550
- # the full-context escape hatch when the digest is not enough.
551
586
  input: /sp:dev-fixall "${vars.qualityGateCmd}" --gate-log .spur/run/${vars.wbs}-test-gate.log --findings "${vars.gateFindings}"
552
587
  timeoutMs: ${vars.stepTimeoutMs}
553
588
  # The captured remediation answer is the run's dispatch evidence (0823 R1):
554
- # expectFile fails the step when no answer was captured. Quality stays with
555
- # the review proof digest and the verify verdict.
589
+ # expectFile fails the step when no answer was captured.
556
590
  answerFile: .spur/run/${vars.__runId}-test-fix-answer.txt
557
591
  expectFile: .spur/run/${vars.__runId}-test-fix-answer.txt
558
592
  # 0706 R6: bounded remediation hop — unattended and tree-mutating,
@@ -561,162 +595,30 @@ states:
561
595
  fsWrite: available
562
596
  processSpawn: available
563
597
 
564
- - id: test-recheck
565
- display:
566
- phase: test
567
- phaseTitle: Test
568
- title: Re-run quality gate
569
- show: on-entry
570
- description: >
571
- Soft recheck after fixall with bounded SQLite-lock retry. Writes PASS|FAIL (always exit 0). Transitions
572
- branch to `triage` (PASS — the 0943 lane router), `test-fail-triage` (FAIL — the 0943
573
- failure-class router), or the pipeline `failed` state (corrupt status) — never a
574
- raw lifecycle abort that skips the terminal `failed` state.
575
- onEnter:
576
- # R4 (0703, ADR-071): bounded remediation mutated the tree by design, so the fresh evidence
577
- # chain (recheck gate → review → verify) must start from a NEWLY captured digest. Capture-only;
578
- # the guards and `record` compare against this value.
579
- # 1135 R5: re-snapshot the tree state after the remediation, so the refresh does not compare
580
- # against a pre-remediation snapshot (a legitimate test-fix edit must not read as drift).
581
- - kind: shell
582
- options:
583
- command: >-
584
- mkdir -p .spur/run; { git status --porcelain=v1 -uall -- . ':(exclude)docs/tasks*' ':(exclude)docs/features*'; git diff --name-only HEAD -- . ':(exclude)docs/tasks*' ':(exclude)docs/features*'; } > ".spur/run/$__runId-gate-paths.txt" 2>/dev/null; exit 0
585
- - kind: proof.fingerprint
586
- options:
587
- var: proofDigest
588
- taskFile: ${vars.taskSpecPath}
589
- featureFile: ${vars.featureSpecPath}
590
- # 0587 R3: probe-then-full recheck. A red gateProbeCmd records FAIL and skips the full
591
- # gate (the measured waste is re-running a 110–140s gate to learn the tree is still red);
592
- # a green probe (or empty gateProbeCmd) falls through to the full-gate loop unchanged.
593
- # Only the full gate writes PASS, so the `test-recheck → review` guard (reads PASS)
594
- # still means a full green qualityGateCmd ran — invariant preserved by construction.
595
- # (d) `spur task gate` owns the recheck; shell only picks the mode and fails closed
596
- # (0587 R3 probe-then-full: a red gateProbeCmd is the recheck verdict, the full gate is skipped).
597
- - kind: shell
598
- options:
599
- command: >-
600
- M=recheck; [ "$deferQualityGate" = "true" ] && M=deferred; $spurBin task gate "$wbs" "$M" --no-logo; rc=$?; if [ "$rc" -gt 1 ]; then echo "quality gate failed closed — spur task gate exited $rc" >&2; printf 'FAIL\n' > ".spur/run/$wbs-test-gate.status"; fi; exit 0
601
-
602
- - id: triage
603
- display:
604
- phase: review
605
- phaseTitle: Review
606
- title: Choose review depth
607
- description: >-
608
- Deterministic lane router between a green gate and the review/verify fork (0943 R1).
609
- Writes the diffstat evidence file, honors a caller-set `mode` (never overridden), pins
610
- the safety lane on sensitive paths or >400 changed lines without a model call (R2c),
611
- otherwise consults `decide task-triage` (low → fast lane; anything else stays empty →
612
- standard review path), and appends the triage route reason to
613
- .spur/memory/task-pipeline-routes.log (R5). The `decide` action itself runs
614
- unconditionally (the engine has no onEnter conditionals); its result is consulted only
615
- when neither a caller mode nor the deterministic guard resolved the lane. The resolved
616
- lane lands in the `mode` var for the triage → verify/review guards.
617
- onEnter:
618
- # (a) diffstat producer: `spur task diffstat` owns git reading and sensitive-path
619
- # classification; shell only fails closed on a non-zero exit (writes sensitive:true,
620
- # which the pre-guard below maps to the safety lane).
621
- - kind: shell
622
- options:
623
- command: >-
624
- $spurBin task diffstat "$wbs" --no-logo || { echo "task-diffstat failed closed — spur task diffstat exited $?" >&2; printf '{"files":0,"insertions":0,"deletions":0,"paths":[],"sensitive":true}\n' > ".spur/run/$wbs-diffstat.json"; }; exit 0
625
- # (b)+(c) lane pre-guard BEFORE decide: a caller-set mode is projected verbatim (R2b);
626
- # a sensitive path or >400 changed lines pins safety (R2c) and the decide result below
627
- # is never consulted. A missing/malformed diffstat row (producer crashed or never ran,
628
- # disk error) pins safety HERE: an empty mode file would let the low decide row project
629
- # the fast lane downstream, so the "never to fast" fail-safe is enforced at the guard,
630
- # not by the projection (the full-suite gate run exposed this fail-open path).
631
- - kind: shell
632
- options:
633
- command: >-
634
- mkdir -p .spur/run; if [ -n "$mode" ]; then printf '%s\n' "$mode" > ".spur/run/$wbs-mode.txt"; exit 0; fi; DF=".spur/run/$wbs-diffstat.json"; if ! jq -e 'type == "object"' "$DF" >/dev/null 2>&1; then printf 'safety\n' > ".spur/run/$wbs-mode.txt"; exit 0; fi; jq -r 'if .sensitive == true or ((.insertions // 0) + (.deletions // 0)) > 400 then "safety" else empty end' "$DF" 2>/dev/null > ".spur/run/$wbs-mode.txt"; exit 0
635
- # (d) lane decision — degrades to the standard default whenever workflows.decideDecisionMaker
636
- # is off (R4), so with the flag off this state is pure shell: no model call is made.
637
- # 1114: the inline decide moved to the bundled catalog (`config/decisions/task-pipeline.yaml`
638
- # → `task-triage`), whose R0 samples measured this exact prompt before the migration.
639
- # Routing contract is unchanged: same evidence, same resultFile, same guards, same
640
- # `standard` fallback when the switch is off (field-identical to this form's default).
641
- - kind: decide
642
- options:
643
- id: task-triage
644
- decision: task-triage
645
- params:
646
- wbs: ${vars.wbs}
647
- evidence:
648
- - .spur/run/${vars.wbs}-diffstat.json
649
- - ${vars.taskSpecPath}
650
- resultFile: .spur/run/${vars.wbs}-triage.decision
651
- # (d) project the decision, then append the triage route reason (1007 R3: the
652
- # mode-fallback shell merged into the memory-append shell — original commands in
653
- # order, the non-empty mode file is still never overridden; the append keeps the
654
- # failure signal of the original pair). low → fast lane; anything else (standard/high,
655
- # degraded default, missing row) leaves the mode empty → standard review path.
656
- - kind: shell
657
- options:
658
- command: >-
659
- MF=".spur/run/$wbs-mode.txt"; [ -s "$MF" ] || jq -r 'if .value == "low" then "fast" else empty end' ".spur/run/$wbs-triage.decision" 2>/dev/null > "$MF"; m="$(cat "$MF")"; RUN_ID="$__runId"; [ -n "$RUN_ID" ] || RUN_ID="pipeline-$wbs"; mkdir -p .spur/memory; printf '%s %s %s\n' "$RUN_ID" "$wbs" "$(jq -rn --arg m "$m" --arg c "$mode" 'if $c != "" then ($c + ":caller-set") else {"fast":"fast:triage low lane","safety":"safety:triage deterministic-high","":"safety:triage standard lane"}[$m] // "safety:triage unrecognized mode" end')" >> .spur/memory/task-pipeline-routes.log
660
- # (d) the resolved lane becomes the `mode` var for the triage → verify/review guards.
661
- - kind: file.read.into-var
662
- options:
663
- path: .spur/run/${vars.wbs}-mode.txt
664
- var: mode
665
-
666
- - id: test-fail-triage
667
- display:
668
- phase: test
669
- phaseTitle: Test
670
- title: Triage test failure
671
- show: on-entry
672
- description: >-
673
- Deterministic failure-class router on a red gate (0943 R3). The engine has no onEnter
674
- conditionals, so the FAIL branch from `test`/`test-recheck` lands here as its own
675
- deterministic state (the sanctioned alternative), while the green branch goes to
676
- `triage`. `decide failure-class` reads the bounded gate findings and routes: fix →
677
- test-fix (the bounded repair hop; its default, recorded as `source: default` when no
678
- backend decides — 0976 R2), stop → failed with terminalReason failed-check. A
679
- missing/corrupt decision fails closed to failed-check. (0976 R2 collapsed the former
680
- `retryable` lane: the declared default made it unreachable on the inline path.)
681
- onEnter:
682
- # 1115: catalog-reference decide (task 1094 form) — question/choices/fallback live in
683
- # the bundled catalog (`config/decisions/task-pipeline.yaml` → `failure-class`), whose
684
- # R0 samples measured this exact prompt before the migration. Routing contract is
685
- # unchanged: same resultFile, same guards, same `fix` fallback when the switch is off.
686
- - kind: decide
687
- options:
688
- id: failure-class
689
- decision: failure-class
690
- params:
691
- wbs: ${vars.wbs}
692
- evidence:
693
- - .spur/run/${vars.wbs}-test-gate.findings
694
- resultFile: .spur/run/${vars.wbs}-failure-class.decision
695
-
598
+ # ── review hop (certification: verdict + R/AC traceability + approval decision) ──
599
+ # D65 1239 R2: `verify` merged into `review`. The reviewer's answer must carry the
600
+ # requirement/AC traceability table (`| Req | Status | Evidence |`, evidence = `file:line`
601
+ # or a named test/command) — sp:functional-review produces it and the answer contract owns
602
+ # the shape. `spur task verdict --from-answer` lints that answer, derives PASS/PARTIAL/FAIL
603
+ # from the rows and writes the canonical `.spur/run/<wbs>-verdict.json` that `spur task
604
+ # record` consumes for box flips and `## Testing`; a review whose rows do not resolve (an
605
+ # UNMET requirement, a missing row, a bad AC identity) cannot derive PASS. The review-gate
606
+ # residual check (1122) still blocks an open P1-P3 finding. Everything is folded into one
607
+ # status token the transitions read, so a non-PASS review routes into the bounded repair
608
+ # hop BEFORE `done` spends the close.
696
609
  - id: review
697
610
  display:
698
611
  phase: review
699
612
  phaseTitle: Review
700
613
  title: Review changes
701
614
  description: >-
702
- Three-dimensional code review via /sp:dev-review (functional requirements traceability +
703
- SECUA framework (Security, Efficiency, Correctness, Usability, Architecture) +
704
- architecture depth), findings written to `## Review`. Cost at this decision point (1122):
705
- open P1-P3 review findings block `done` — the PASS edges below also run the review-gate
706
- residual check, and a PASS carrying an open P1-P3 row routes to review-fail-triage
707
- before verify. Fixing a finding invalidates the certified digest and re-runs
708
- quality → review → verify on a fresh digest; only P4 rows and DEFER-ed P3 rows are wrap
709
- residuals.
615
+ Three-dimensional code review via /sp:dev-review (functional requirements traceability
616
+ + SECUA framework + architecture depth), findings written to `## Review`, then
617
+ deterministic certification: `spur task verdict --from-answer` over the reviewer's
618
+ own traceability table, the review-gate residual check, and the operator/maker
619
+ approval decision point. Cost at this decision point (1122): open P1-P3 review
620
+ findings block `done`; only P4 rows and DEFER-ed P3 rows are wrap residuals.
710
621
  onEnter:
711
- # 1135 R4: review joins the proof bracket. A pre-review compare refuses to spend a review
712
- # dispatch on a state that has drifted from the certified digest; the mismatch routes
713
- # through the existing tripwire to `failed` (no new re-certify path).
714
- - kind: proof.fingerprint
715
- options:
716
- var: proofDigestNow
717
- taskFile: ${vars.taskSpecPath}
718
- featureFile: ${vars.featureSpecPath}
719
- expect: ${vars.proofDigest}
720
622
  - kind: agent.run
721
623
  options:
722
624
  # 0710 R2: review always runs on a fresh session — no implementation-session
@@ -726,10 +628,25 @@ states:
726
628
  # through the executor registry, and the runner enforces (pre-dispatch,
727
629
  # fail-closed) that a P0/P1 task's review resolves a DIFFERENT executor
728
630
  # spec than the implement stage recorded in __agentRouting_implement.
729
- input: /sp:dev-review --tasks ${vars.wbs} --auto
730
- # The captured review answer is the run's dispatch evidence (0823 R1):
731
- # expectFile fails the step when no answer was captured. Quality stays with
732
- # the review proof digest and the verify verdict.
631
+ # 0943 R1/R2: `--focus` carries the resolved lane depth — `functional` for the
632
+ # fast lane (traceability, which is the certification input), `all` otherwise.
633
+ # D65 1240: this answer IS the certification input — `spur task verdict --from-answer`
634
+ # derives the run's verdict from its requirement rows, so the answer contract is part
635
+ # of the prompt. A report body ending with a `| WBS | Verdict | P1 | … |` summary and
636
+ # no per-requirement rows derives UNKNOWN and can never certify, which looped every
637
+ # run through review -> remediate.
638
+ input: >-
639
+ /sp:dev-review --tasks ${vars.wbs} --auto --focus ${vars.reviewFocus}
640
+ Answer contract (machine-read): end with exactly one plain `Verdict: PASS|FAIL`
641
+ line, preceded by a `| Req | Status | Evidence |` table with one row per
642
+ requirement (R1, R2, ...) and a `| AC | Status | Evidence Type | Evidence |` table
643
+ with one row per acceptance criterion, and one `Confidence: HIGH|MEDIUM|LOW` line
644
+ whose level YOUR finding dispositions derive (ADR-141: no open P1-P3 and no
645
+ carried residual is HIGH; a carried residual is MEDIUM; an open P1-P3 or a
646
+ non-PASS verdict is LOW) — the gate recomputes it and rejects a disagreement.
647
+ Every row cites a repo-relative `path:line`; no row may be omitted or merged.
648
+ # The captured review answer is the run's certification input (and the
649
+ # `task verdict` input below): expectFile fails the step when no answer landed.
733
650
  answerFile: .spur/run/${vars.__runId}-review-answer.txt
734
651
  expectFile: .spur/run/${vars.__runId}-review-answer.txt
735
652
  # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
@@ -742,45 +659,33 @@ states:
742
659
  priority: ${vars.taskPriority}
743
660
  compareExecutorWith: implement
744
661
  timeoutMs: ${vars.stepTimeoutMs}
745
- # 0785 R4: the reviewer's completion evidence for THIS run — the current proof digest is
746
- # written to a run-scoped marker by the review stage itself, not caller-stamped. The
747
- # verify stamp marks review `completed` only when this marker equals the current digest;
748
- # otherwise `skipped` (an honest downgrade, never a fake completed), and the bound
749
- # `record` registration refuses a skipped review.
750
- # Session finding after 1088: the review VERDICT is read straight out of the reviewer's
751
- # own answer file by the review → * guards below, so this stage needs no extra artifact
752
- # and no extra action (ADR-115 legibility cap).
662
+ # (d) `spur task verdict` owns the answer lint and the verdict derivation (task 1003
663
+ # R2): one complete requirement row per declared requirement, AC identities resolved,
664
+ # status/evidence vocabularies accepted, hedged or uncited evidence rejected. It
665
+ # writes `.spur/run/<wbs>-verdict.json` — the review verdict artifact `spur task
666
+ # record` consumes — and exits 1 on a non-PASS verdict BY DESIGN, which `softFail`
667
+ # records as the typed outcome the guards route. A lint failure (exit 1) is a failed
668
+ # certification, never a warning, and there is no grep fallback.
669
+ - kind: command.gate
670
+ options:
671
+ id: review-verdict
672
+ executable: "${vars.spurBin}"
673
+ args: ["task", "verdict", "${vars.wbs}", "--from-answer", ".spur/run/${vars.__runId}-review-answer.txt"]
674
+ resultFile: .spur/run/${vars.__runId}-review-verdict.status
675
+ softFail: true
676
+ # (e) one fold, one token: PASS requires the typed verdict PASS, the artifact's own
677
+ # PASS, and a clean review gate (open P1-P3 finding or an uncited claim blocks it).
678
+ # Fail-closed: any missing/corrupt input leaves FAIL, so the transitions can route a
679
+ # non-PASS review into remediation instead of dead-ending the run.
753
680
  - kind: shell
754
681
  options:
755
- command: 'mkdir -p .spur/run; printf "%s" "$proofDigest" > ".spur/run/$__runId-review-proof.digest"; exit 0'
756
-
757
- # ── review-fail-triage (session finding after 1088) ────────────────────────
758
- # A review that reports FAIL used to have no outgoing edge: the graph could not see the
759
- # verdict and nothing consumed it, so remediating a failing review meant driving the
760
- # states by hand. This state is the review-side twin of `test-fail-triage` (0943 R3):
761
- # the deterministic classifier routes the failure into the SAME bounded fix budget
762
- # (`.spur/run/<wbs>-test-fix-attempt` against `qualityGateMaxFixAttempts`), and the fix
763
- # re-enters quality → review → verify on a freshly captured digest. The `decide` action
764
- # itself always runs (the engine has no onEnter conditionals); with
765
- # `workflows.decideDecisionMaker` off it degrades to its declared default.
766
- - id: review-fail-triage
767
- display:
768
- phase: review
769
- phaseTitle: Review
770
- title: Triage review findings
771
- show: on-entry
772
- description: >-
773
- Deterministic router for a review that did NOT certify PASS (session finding after 1088).
774
- `decide review-failure-class` reads the reviewer's answer and the task spec and routes:
775
- fix → the shared test-fix hop, stop → failed with terminalReason failed-check. A
776
- missing/corrupt decision fails closed to failed-check. Cost either way (1122): open
777
- P1-P3 review findings block `done`; the fix lane re-enters quality → review → verify on a
778
- fresh digest; only P4 rows and DEFER-ed P3 rows are wrap residuals.
779
- onEnter:
780
- # 1116: the inline decide moved to the bundled catalog (`config/decisions/task-pipeline.yaml`
781
- # → `review-failure-class`), whose R0 samples measured this exact prompt before the
782
- # migration. Routing contract is unchanged: same evidence, same resultFile, same guards,
783
- # same `fix` fallback when the switch is off.
682
+ command: >-
683
+ mkdir -p .spur/run; S=FAIL; if [ "$(cat ".spur/run/$__runId-review-verdict.status" 2>/dev/null)" = PASS ] && [ "$(jq -r '.verdict // empty' ".spur/run/$wbs-verdict.json" 2>/dev/null)" = PASS ] && $spurBin task residual "$wbs" review-gate --no-logo >&2; then S=PASS; fi; printf '%s\n' "$S" > ".spur/run/$wbs-review.status"; exit 0
684
+ # (d) review failure class (session finding after 1088): stop → failed before the cap
685
+ # can mislabel it, fix → the SAME bounded repair hop the red gate uses, so the
686
+ # iteration bound stays finite across both lanes. Degrades to the catalog `fix`
687
+ # fallback with no model call while the decision switch is off; a missing/corrupt row
688
+ # fails closed to failed-check.
784
689
  - kind: decide
785
690
  options:
786
691
  id: review-failure-class
@@ -791,170 +696,68 @@ states:
791
696
  - .spur/run/${vars.__runId}-review-answer.txt
792
697
  - ${vars.taskSpecPath}
793
698
  resultFile: .spur/run/${vars.wbs}-review-failure-class.decision
794
-
795
- - id: approve
796
- display:
797
- phase: review
798
- phaseTitle: Review
799
- title: Operator approval
800
- show: on-entry
801
- description: >
802
- Human-in-the-loop approval gate. Under `profile=auto` this state is never entered
803
- (review routes around it straight to verify). In interactive mode this state pauses
804
- the run for `spur workflow continue` (E3), making approval an explicit operator action.
805
- The operator's answer is routed (R1, bug-750): yes -> verify; no -> failed (rejection
806
- recorded, not silently approved); cancel -> cancelled (a distinct terminal state).
807
- pause: true
808
- onEnter:
699
+ # (d) Operator approval as a DECISION POINT, not a state (D65 1239 R1/R5, ADR-140
700
+ # item 5): interactively the operator responder answers; under `profile=auto` the
701
+ # declared binding answers (evidence status var, catalog fallback when the maker
702
+ # degrades), so `--auto` never prompts a human and can still deny. A `no`/`cancel`
703
+ # (or an unanswered interactive gate) routes to failed with terminalReason
704
+ # `cancelled` — a recorded denial, distinct from a gate or review failure. The trace
705
+ # records the answering source (operator / maker / catalog-fallback, 1236).
809
706
  - kind: hitl.confirm
810
707
  options:
811
- prompt: "Approve task ${vars.wbs} to proceed to verification?"
708
+ prompt: "Approve task ${vars.wbs} to proceed to completion?"
812
709
  decision:
813
- mode: never
814
-
815
- - id: verify
816
- display:
817
- phase: verify
818
- phaseTitle: "Verify & record"
819
- title: Verify requirements and AC
820
- description: >
821
- Observe-only functional verification (BDD + traceability) via /sp:dev-verify --fix none
822
- (task 0703 R1, ADR-071): the verifier certifies the state, it never repairs its own subject.
823
- The answer-shape contract this stage enforces is OWNED BY the skill, not restated here
824
- (task 1110 R3): see `plugins/sp/skills/code-verification/SKILL.md` § Answer-File Schema
825
- Contract (`:340-375`), with the row-id/evidence vocabularies in
826
- `plugins/sp/skills/code-verification/references/verdict-schema.md`. The YAML only names
827
- the lint that consumes it (`task verdict --from-answer`).
828
- A live digest compare BEFORE the agent refuses to certify a state that drifted after the
829
- quality/review evidence was produced (R2). The agent's
830
- captured answer is written to `.spur/run/<wbs>-verify-answer.txt` and MUST follow
831
- the answer-file schema contract (explicit `Verdict: PASS|PARTIAL|FAIL` line plus
832
- `| Req | Status | Evidence |` and `| AC | Status | Evidence Type | Evidence |` tables);
833
- a deterministic shell step then derives the verdict and writes the gate artifact
834
- `.spur/run/<wbs>-verdict.json` (so the completion gate reads a real verdict, not
835
- agent discretion — R9) with a proof block naming the digest and the per-stage results (R3).
836
- Repairable non-PASS routes once through the bounded remediation hop (verify → test-fix,
837
- R4); the chain reruns on a fresh digest.
838
- onEnter:
839
- # R2 (0703): midpoint bracket compare — refuse to spend verification on a state that no
840
- # longer matches the digest the quality/review evidence names. Reuses `proofDigestNow`:
841
- # set here and re-set by the bound `run.artifact` registration at `record` entry (0785 R3).
842
- - kind: proof.fingerprint
843
- options:
844
- var: proofDigestNow
845
- taskFile: ${vars.taskSpecPath}
846
- featureFile: ${vars.featureSpecPath}
847
- expect: ${vars.proofDigest}
848
- - kind: agent.run
849
- options:
850
- # 0710 R2: verify is a separate fresh-session execution — never the review
851
- # session, never the implement session (R6).
852
- # 0710 R4/R5: role-routed like review; P0/P1 demands a distinct executor.
853
- # R1 (0703, ADR-071): `--fix none` — final verification is observe-only. Remediation
854
- # belongs to the bounded test-fix hop, never to the certifying pass.
855
- input: /sp:dev-verify ${vars.wbs} --auto --fix none --focus all
856
- # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
857
- role: reviewer
858
- # B7 R3/R6 (0894): declared fresh policy (reviewer default).
859
- # `freshSession: true` kept as the action-level hard guarantee (0710 R2).
860
- freshSession: true
861
- session: fresh
862
- priority: ${vars.taskPriority}
863
- compareExecutorWith: implement
864
- timeoutMs: ${vars.stepTimeoutMs}
865
- expectFile: .spur/run/${vars.wbs}-verify-answer.txt
866
- # (d) `spur task verdict` owns the answer lint (task 1003 R2) — the shell below
867
- # fails closed when the verifier's answer is malformed, before any verdict artifact.
868
- - kind: shell
869
- # `task verdict` exits 1 on a non-PASS verdict by design (1003 R2). The `continue`
870
- # policy keeps that exit code as recorded evidence (the action row is still `failed`)
871
- # while letting the state finish its remaining actions and reach the guards below;
872
- # without it the default 'fail' policy halts the run here and `verify → test-fix`
873
- # (the declared bounded remediation lane) can never fire (v5, task 1077 run).
874
- onError: continue
875
- options:
876
- command: "$spurBin task verdict $wbs --from-answer .spur/run/$wbs-verify-answer.txt"
877
- # (e) one jq mutation binds the verdict to the proof digest (0703 R3; runId 0730 §B.2 /
878
- # 0757 R4; definitionDigest 0759 R5; honest review stamp 0785 R4; soft action + hard guard).
710
+ mode: evidence
711
+ statusVar: approvalStatus
712
+ evidenceNodes:
713
+ - review
714
+ # (e) The denial is EVIDENCE, not just a variable: fold the answer into one recorded
715
+ # approval token the transitions read. `profile=auto` proceeds unless the maker said
716
+ # `no` (a deferred answer is not a denial — no maker means no objection under auto);
717
+ # interactively only an explicit `yes` approves. Fail-closed to DENIED.
879
718
  - kind: shell
880
719
  options:
881
720
  command: >-
882
- V=".spur/run/$wbs-verdict.json"; [ -f "$V" ] && [ -n "$proofDigest" ] || exit 0; jq --arg d "$proofDigest" --arg r "$__runId" --arg dd "$__definitionDigest" --arg rp "$(cat .spur/run/$__runId-review-proof.digest 2>/dev/null)" --arg g "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null || echo UNKNOWN)" '. + {proof: {digest: $d, runId: $r, definitionDigest: $dd, capturePoint: "quality-gate-entry", stages: {qualityGate: {status: $g, digest: $d}, review: {status: (if $rp == $d then "completed" else "skipped" end), digest: $d}, verification: {status: .verdict, digest: $d}}}} | .checks += [{name: "proof-input-digest", status: "pass", evidence: $d}]' "$V" > "$V.tmp" && mv "$V.tmp" "$V"; exit 0
721
+ mkdir -p .spur/run; A=APPROVED; if [ "$profile" = auto ]; then [ "$__hitlAnswer" = no ] && A=DENIED; else [ "$__hitlAnswer" = yes ] || A=DENIED; fi; printf '%s\n' "$A" > ".spur/run/$wbs-approval.status"; exit 0
883
722
 
884
- - id: record
885
- display:
886
- phase: verify
887
- phaseTitle: "Verify & record"
888
- title: Record evidence
723
+ - id: done
889
724
  description: >
890
- Record pipeline results into the task file via `spur task record` —
891
- Testing/Review from the verdict, Solution backfilled from git diff as a
892
- safety net, optional transition to testing. Entry is gated by the bound
893
- `run.artifact` registration (0785 R3): the verify verdict is ledger-registered
894
- against a freshly captured proof digest — including review-completion marker
895
- evidence (0785 R4) — before any task record or status mutation. Post-record step
896
- conditionally syncs feature status via `spur feature sync`
897
- (task 0411 retry-suppression) if `feature_id` is present, or appends an orphan
898
- link proposal to the run report if absent (task 0328 / ADR-0322). The post-record
899
- residual sweep (0983) re-scans the task file after the verdict-proven box flips and
900
- folds blocking leftovers into the verdict before `record → done`; a downgrade
901
- re-records Testing from the final artifact so a failed task never keeps a stale
902
- PASS narrative.
725
+ Terminal — the task is closed deterministically. onEnter performs the close the
726
+ former `record` state owned: `spur task record` writes Testing/Review from the review
727
+ verdict and flips the requirement/AC boxes it proves, the residual sweep folds any
728
+ leftover, and `spur task update <wbs> done --no-lifecycle` runs the verb's own
729
+ target-aware `task check` as the completion gate — a task whose boxes or evidence do
730
+ not certify cannot reach the stored `done` status.
903
731
  onEnter:
904
- # 0785 R3: BOUND verify-verdict registration — FIRST action, before any task record or
905
- # status mutation, replacing the redundant proof.fingerprint compare (the action itself
906
- # freshly re-captures the proof inputs via its processExecutor seam). The action refuses
907
- # — before the ledger write AND before `task record` — unless: the run's declared digest
908
- # equals a fresh capture over the canonical task spec (+ linked feature when linked);
909
- # the verdict artifact is canonical-valid PASS for this wbs; its raw proof block names
910
- # the fresh digest, the certifying run id, and the run row's resume/definition digest;
911
- # qualityGate and verification stages are PASS with that digest; review is completed and
912
- # independently evidenced by the run-scoped marker `<runId>-review-proof.digest`
913
- # (0785 R4). Task-spec evidence writes (Testing/Review/Solution, R6) happen only in the
914
- # actions AFTER this registration, and the fingerprint scopes task content to the
915
- # proof-input sections, so they cannot invalidate the certified input set.
916
- - kind: run.artifact
917
- options:
918
- path: .spur/run/${vars.wbs}-verdict.json
919
- artifactKind: verify-verdict
920
- proofBinding: current
921
- taskFile: ${vars.taskSpecPath}
922
- featureFile: ${vars.featureSpecPath}
923
- # (c) command.gate owns the record write with classified transient retry.
732
+ # (c) command.gate owns the record write with classified transient retry. 0980:
733
+ # --no-lifecycle mirrors the wip/done hops — the pipeline run is already the lifecycle
734
+ # record, so a record-stage transition must not spawn a nested task-lifecycle run.
924
735
  - kind: command.gate
925
736
  options:
926
- id: task-record-transition
737
+ id: task-record-close
927
738
  executable: "${vars.spurBin}"
928
- # 0980: --no-lifecycle mirrors the wip/done hops — the pipeline run is
929
- # already the lifecycle record, so a record-stage transition must not
930
- # spawn a nested task-lifecycle run that outlives the pipeline as a
931
- # `running` orphan. The target-aware check gate still runs (inline).
932
739
  args: ["task", "record", "${vars.wbs}", "--solution-from-diff", "--transition", "testing", "--no-lifecycle"]
933
740
  retry:
934
741
  maxAttempts: 2
935
742
  delayMs: 2000
936
743
  on: ["sqlite-busy", "ENOENT", "EBUSY", "ENOTEMPTY"]
937
- resultFile: .spur/run/${vars.__runId}-record-transition.status
744
+ resultFile: .spur/run/${vars.__runId}-record.status
938
745
  softFail: false
939
746
  timeoutMs: 120000
940
- # (d) Best-effort `exit 0` — feature status sync is a follow-up, not a completion gate;
941
- # `record → done` runs `spur task check`. 0931 R5: with deferFeatureSync "true" (parallel
942
- # mode) the deferral note lands in the task report and the sync is skipped entirely, so
943
- # task branches never touch feature corpus files; the batch orchestrator performs the
944
- # deferred sync on the base ref. 1004 R4: the sync is a bare `feature sync` — repeated-
945
- # BLOCKED suppression (0411) now lives in the feature sync service itself, so the shell
946
- # resolves feature_id via jq and delegates without a helper script.
747
+ # (d) Best-effort `exit 0` — feature status sync is a follow-up, not a completion gate.
748
+ # 0931 R5: with deferFeatureSync "true" (parallel mode) the deferral note lands in the
749
+ # task report and the sync is skipped entirely, so task branches never touch feature
750
+ # corpus files. 1004 R4: the sync is a bare `feature sync` — repeated-BLOCKED
751
+ # suppression (0411) lives in the feature sync service itself.
947
752
  - kind: shell
948
753
  options:
949
754
  command: >-
950
755
  if [ "$deferFeatureSync" = "true" ]; then echo "feature sync deferred to batch integration" >> ".spur/run/$wbs-report.txt"; else FID="$($spurBin task show $wbs --json 2>/dev/null | jq -r '(.feature_id // .frontmatter.feature_id) // empty' 2>/dev/null)"; if [ -n "$FID" ]; then $spurBin feature sync "$FID" --json || echo "feature sync failed — re-run: $spurBin feature sync $FID" >> ".spur/run/$wbs-report.txt"; else echo "Orphan task $wbs — no feature_id linked — proposal: consider linking to a parent feature." >> ".spur/run/$wbs-report.txt"; fi; fi; exit 0
951
756
  # (d) F96 (0983): `spur task residual` owns the sweep; shell only sequences it. The
952
- # scan moved here from verify: it must read the task file AFTER `task record`
953
- # flipped the verdict-proven Requirement/AC boxes (0692), or a proven-but-unticked
954
- # box folds every good run to PARTIAL and the done guard is unreachable (0967).
955
- # Classification unchanged: any box still unchecked post-record (unproven R/AC,
956
- # unfinished Plan) stays blocking and fails the record → done guard closed.
957
- # ONE hard action (scan+fold): a scanner crash fails record closed.
757
+ # scan runs AFTER `task record` flipped the verdict-proven boxes, or a proven-but-
758
+ # unticked box folds every good run to PARTIAL and the done gate is unreachable
759
+ # (0967). Any box still unchecked post-record stays blocking.
760
+ # ONE hard action (scan+fold): a scanner crash fails the close closed.
958
761
  - kind: shell
959
762
  options:
960
763
  command: >-
@@ -962,22 +765,19 @@ states:
962
765
  # (e) F96 R5 (0983): a sweep downgrade must reach the task record — re-record from
963
766
  # the FINAL (post-fold) artifact so Testing carries the downgraded verdict; a failed
964
767
  # task must not retain a stale PASS Testing narrative. Soft (exit 0): the downgrade
965
- # itself already fails the record → done guard; a re-record failure prints its
966
- # re-run command and the failed state renders the recovery report.
967
- # 0980: --no-lifecycle for the same orphan reason as the main record command —
968
- # inert today (the task is already at testing, so this is a same-status no-op),
969
- # but it keeps the no-nested-lifecycle invariant if the flow shifts.
768
+ # itself fails the re-assert below.
769
+ # 0980: --no-lifecycle for the same orphan reason as the main record command.
970
770
  - kind: shell
971
771
  options:
972
772
  command: >-
973
773
  verdict="$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)"; if [ -n "$verdict" ] && [ "$verdict" != PASS ]; then $spurBin task record $wbs --solution-from-diff --transition testing --no-lifecycle || echo "post-downgrade re-record failed — re-run: $spurBin task record $wbs" >&2; fi; exit 0
974
-
975
- - id: done
976
- description: >
977
- Terminal — task executed, verified, and recorded. The `record → done`
978
- guard runs `spur task check` before certifying; a genuinely non-compliant
979
- task routes to `failed` instead of a silent bad `done`.
980
- onEnter:
774
+ # (e) The completion contract, re-asserted after the sweep: a post-record residual
775
+ # downgrade must block the close (hard action, no `; exit 0`). The verdict artifact is
776
+ # the review's own evidence, not a re-captured digest — the old proof re-assertion is
777
+ # gone, the fail-closed check on the verdict it certifies is not.
778
+ - kind: shell
779
+ options:
780
+ command: 'jq -e ".verdict == \"PASS\"" .spur/run/$wbs-verdict.json >/dev/null'
981
781
  # (d) F96 R4 (0950): `spur task residual` owns the settle.
982
782
  # Soft best-effort (exit 0) — runs after certification and must never change
983
783
  # the outcome; failure prints the re-run command.
@@ -985,7 +785,9 @@ states:
985
785
  options:
986
786
  command: >-
987
787
  $spurBin task residual "$wbs" settle --no-logo || echo "residual-settle failed — re-run: spur task residual $wbs settle" >&2; exit 0
988
- # (c) command.gate owns the done transition with classified transient retry.
788
+ # (c) command.gate owns the done transition with classified transient retry. The verb
789
+ # runs its own target-aware `task check --as done` (the completion gate) before the
790
+ # status write, so a task that does not certify a `done` target never stores `done`.
989
791
  - kind: command.gate
990
792
  options:
991
793
  id: task-done-transition
@@ -999,18 +801,19 @@ states:
999
801
  softFail: false
1000
802
  timeoutMs: 120000
1001
803
  # (e) advisory terminal checkpoint, one printf (0711 R1–R3; binding corrected 0784 R4).
1002
- # 1007 R3: the former done note ("Pipeline complete ...") is dropped — the checkpoint
1003
- # below is the terminal record.
804
+ # The `digest` field now carries the gate receipt's tree hash — the run's evidence
805
+ # identity since D65 1239 — instead of the retired proof digest (empty when no receipt
806
+ # bound a tree, which the contract treats as "not reported").
1004
807
  - kind: shell
1005
808
  options:
1006
809
  command: >-
1007
- mkdir -p .spur/memory/sessions; CP_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"; CP_COMMIT="$(git rev-parse HEAD 2>/dev/null || echo unknown)"; printf '%s\n' '---' 'schema_version: 1' "session_id: $(date -u +%Y-%m-%d)-$wbs" 'workflow: task-pipeline' "run_id: $__runId" "task_wbs: $wbs" 'feature_id: ""' 'phase: done' 'status: done' 'last_gate: record' "source_commit: $CP_COMMIT" "digest: $proofDigest" "generated_at: $CP_TS" "updated_at: $CP_TS" "next_action: none - task $wbs complete (terminal; advisory only)" 'artifacts:' " - .spur/run/$wbs-verdict.json" " - .spur/run/$wbs-test-gate.log" '---' '' '## Session Notes' '' "Terminal checkpoint for task $wbs (task-pipeline done)." 'Advisory only; the task file is authoritative.' > .spur/memory/sessions/$wbs-checkpoint.md; exit 0
810
+ mkdir -p .spur/memory/sessions; CP_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"; CP_COMMIT="$(git rev-parse HEAD 2>/dev/null || echo unknown)"; CP_TREE="$(jq -r .inputDigest ".spur/run/$wbs-check-receipt.json" 2>/dev/null)"; printf '%s\n' '---' 'schema_version: 1' "session_id: $(date -u +%Y-%m-%d)-$wbs" 'workflow: task-pipeline' "run_id: $__runId" "task_wbs: $wbs" 'feature_id: ""' 'phase: done' 'status: done' 'last_gate: gate' "source_commit: $CP_COMMIT" "digest: $CP_TREE" "generated_at: $CP_TS" "updated_at: $CP_TS" "next_action: none - task $wbs complete" 'artifacts:' " - .spur/run/$wbs-verdict.json" " - .spur/run/$wbs-check-receipt.json" '---' '' '## Session Notes' '' "Terminal checkpoint for task $wbs (task-pipeline done)." 'Advisory only.' > .spur/memory/sessions/$wbs-checkpoint.md; exit 0
1008
811
 
1009
812
  - id: failed
1010
813
  description: >
1011
- Terminal — precheck, quality-gate exhaustion, verify non-PASS, record check
1012
- failure, or operator rejection; reported, not advanced. Also receives an
1013
- escalation-bound pause or an operator give-up (0933).
814
+ Terminal — precheck, quality-gate exhaustion, a review that did not certify, a
815
+ completion check failure, operator rejection/cancel at the approval decision point,
816
+ or an escalation give-up; reported, not advanced.
1014
817
  onEnter:
1015
818
  # 0933 R30 (guard-lines compression): the escalation-bound report note lives
1016
819
  # here as a conditional — no-op unless the run reached failed with a pending
@@ -1032,9 +835,6 @@ states:
1032
835
  command: >-
1033
836
  $spurBin task residual "$wbs" report --no-logo || echo "residual-report failed — re-run: spur task residual $wbs report" >&2; exit 0
1034
837
 
1035
- - id: cancelled
1036
- description: Terminal — pipeline cancelled by operator at the approval gate (R1).
1037
-
1038
838
  transitions:
1039
839
  # ── precheck: task check --precheck (size + evidence + four-layer, 1002 R4) → implement; ──
1040
840
  # else → failed. The flag folds the two deleted precheck scripts into one fail-closed
@@ -1062,7 +862,7 @@ transitions:
1062
862
  # attempt that already exhausted its maxEscalations asks routes to failed (with a
1063
863
  # report note appended in the same guard) instead of asking forever; then the
1064
864
  # question-presence hop; then the normal always edge. The bound guard requires a
1065
- # fresh question file so a COMPLETED implement after max asks still routes to test.
865
+ # fresh question file so a COMPLETED implement after max asks still routes to gate.
1066
866
  - from: implement
1067
867
  to: failed
1068
868
  terminalReason: retry-exhausted
@@ -1077,15 +877,15 @@ transitions:
1077
877
  test "$n" -ge "$maxEscalations" -a -s .spur/run/$wbs-question.md
1078
878
  - from: implement
1079
879
  to: escalate
1080
- description: Implement agent paused on an operator question (non-empty question file) — pause for an answer.
880
+ description: Implement agent paused on an operator question (non-empty question file) — ask for an answer.
1081
881
  guard:
1082
882
  kind: shell
1083
883
  options:
1084
884
  command: 'test -s .spur/run/$wbs-question.md'
1085
885
  # ── linear body ──
1086
886
  - from: implement
1087
- to: test
1088
- description: Implementation done — quality-gate probe.
887
+ to: gate
888
+ description: Implementation done — run the typed quality gate.
1089
889
  guard:
1090
890
  kind: always
1091
891
  # escalate branching (0933 R27): an answered pause resumes implement — the transcript
@@ -1107,91 +907,46 @@ transitions:
1107
907
  kind: shell
1108
908
  options:
1109
909
  command: 'test -z "$__hitlInput"'
1110
- # Soft probe branching (declaration order: PASS → triage, FAIL → test-fail-triage, defense).
1111
- # 0943 R1/R3: the four former gate-PASS edges (test/test-recheck → verify/review, split on
1112
- # `$mode`) are replaced by the deterministic `triage` lane producer; a red gate lands in
1113
- # `test-fail-triage` where `decide failure-class` routes the failure instead of a static fixall.
1114
- - from: test
1115
- to: triage
1116
- description: Quality gate green — route the lane deterministically (triage, 0943 R1).
1117
- guard:
1118
- kind: shell
1119
- options:
1120
- # 1111 R2: `DEFERRED` is accepted ONLY when the opt-in var is "true" — with the
1121
- # default "false" a stray DEFERRED file fails the guard exactly like a corrupt one.
1122
- command: >-
1123
- gate_status="$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)";
1124
- test "$gate_status" = PASS || { test "$gate_status" = DEFERRED && test "$deferQualityGate" = "true"; }
1125
- - from: test
1126
- to: test-fail-triage
1127
- description: Quality gate red — classify the failure before the repair hop (0943 R3).
1128
- guard:
1129
- kind: shell
1130
- options:
1131
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL'
1132
- # 0943 review P3#2: the former `test → test-fix` FAIL edge is removed — it was dead (the
1133
- # identical guard on test → test-fail-triage fires first) and a silent spec-bypass trap:
1134
- # restoring it would skip failure-class on every red gate. The always-defense below keeps
1135
- # pre-0943 corrupt-status behavior (fixall then recheck).
1136
- - from: test
1137
- to: test-fix
1138
- description: Probe status missing/corrupt — attempt fixall then recheck.
1139
- guard:
1140
- kind: always
1141
- - from: test-fix
1142
- to: test-recheck
1143
- description: Fixall finished — soft recheck the same quality gate.
1144
- guard:
1145
- kind: always
1146
- # Recheck branching (PASS → triage; red → test-fail-triage; defense). The two former
1147
- # static FAIL edges (under-max → test-fix, exhausted → failed retry-exhausted) moved into
1148
- # test-fail-triage's decision routing (0943 R3).
1149
- - from: test-recheck
1150
- to: triage
1151
- description: Quality gate green after fixall — route the lane deterministically (triage, 0943 R1).
910
+
911
+ # ── gate routing (0943 R1/R3 collapsed into one state, D65 1239 R2) ──
912
+ # Declaration order is the routing contract. PASS first (the green path), then a BLOCKED
913
+ # typed outcome (the gate could not decide — never read as red), then the failure class:
914
+ # stop before the cap (never mislabeled), the shared attempt cap, the fix lane, and the
915
+ # fail-closed defense so a missing/corrupt status or decision always terminates.
916
+ - from: gate
917
+ to: review
918
+ description: Quality gate green — the typed outcome is PASS and the gate wrote its own full-tier PASS token.
1152
919
  guard:
1153
920
  kind: shell
1154
921
  options:
1155
- # 1111 R2: same var-gated acceptance of the deferred token as `test → triage`.
922
+ # Two predicates over two result files (ADR-115): the typed `command.gate` outcome
923
+ # says the step ran and succeeded, and the gate's own token says the FULL tier wrote
924
+ # PASS (only the full tier writes PASS, ADR-124). A stale status file cannot ride a
925
+ # blocked or crashed visit into review because the typed outcome must be PASS too.
1156
926
  command: >-
1157
- gate_status="$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)";
1158
- test "$gate_status" = PASS || { test "$gate_status" = DEFERRED && test "$deferQualityGate" = "true"; }
1159
- - from: test-recheck
1160
- to: test-fail-triage
1161
- description: Still red after fixall — classify the failure (0943 R3).
927
+ test "$(cat ".spur/run/$__runId-gate.status" 2>/dev/null)" = PASS &&
928
+ test "$(cat ".spur/run/$wbs-test-gate.status" 2>/dev/null)" = PASS
929
+ - from: gate
930
+ to: review
931
+ description: Opt-in batch policy — the typed outcome is PASS and the gate wrote the DEFERRED (light-tier) token.
1162
932
  guard:
1163
933
  kind: shell
1164
934
  options:
935
+ # 1111 R2: `DEFERRED` is accepted ONLY when the opt-in var is "true" — with the
936
+ # default "false" a stray DEFERRED file fails the guard exactly like a corrupt one.
1165
937
  command: >-
1166
- gate_status="$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)";
1167
- test "$gate_status" = FAIL
1168
- # Defense: corrupt recheck status — failed, not review.
1169
- - from: test-recheck
938
+ test "$deferQualityGate" = "true" &&
939
+ test "$(cat ".spur/run/$__runId-gate.status" 2>/dev/null)" = PASS &&
940
+ test "$(cat ".spur/run/$wbs-test-gate.status" 2>/dev/null)" = DEFERRED
941
+ - from: gate
1170
942
  to: failed
1171
943
  terminalReason: failed-check
1172
- description: Recheck status missing/corrupt — stop at failed.
1173
- guard:
1174
- kind: always
1175
- # ── triage routing (0943 R1/R2): the lane producer replaced the four gate-PASS edges.
1176
- # mode=fast → verify; anything else (standard/high, deterministic-high, degraded default,
1177
- # caller-set non-fast) → review. Together the two guards are exhaustive, so triage never hangs.
1178
- - from: triage
1179
- to: verify
1180
- description: Triage resolved the fast lane (caller-set fast or task-triage low) — skip review.
944
+ description: The gate could not decide (BLOCKED — usage error, lock timeout, killed process) — stop instead of reading it as red.
1181
945
  guard:
1182
946
  kind: shell
1183
947
  options:
1184
- command: 'test "$mode" = fast'
1185
- - from: triage
1186
- to: review
1187
- description: Triage resolved a review lane (standard/high, deterministic-high, or caller-set) — review.
1188
- guard:
1189
- kind: shell
1190
- options:
1191
- command: 'test "$mode" != fast'
1192
- # ── failure-class routing (0943 R3): stop first (never mislabeled by the cap), then the
1193
- # attempt cap, then the fix lane, then the fail-closed defense. Declaration order matters.
1194
- - from: test-fail-triage
948
+ command: 'test "$(cat ".spur/run/$__runId-gate.status" 2>/dev/null)" = BLOCKED'
949
+ - from: gate
1195
950
  to: failed
1196
951
  terminalReason: failed-check
1197
952
  description: Failure class stop — the run terminates at failed, never bypassed.
@@ -1200,222 +955,93 @@ transitions:
1200
955
  options:
1201
956
  command: >-
1202
957
  jq -e '.value == "stop"' ".spur/run/$wbs-failure-class.decision" >/dev/null 2>&1
1203
- - from: test-fail-triage
958
+ - from: gate
1204
959
  to: failed
1205
960
  terminalReason: retry-exhausted
1206
- description: Attempt cap reached — qualityGateMaxFixAttempts bounds the fix lane.
1207
- # (warn) 5 commands: named fix attempts (legibility, 0874).
961
+ description: Attempt cap reached — qualityGateMaxFixAttempts bounds the remediation lane.
1208
962
  guard:
1209
963
  kind: shell
1210
964
  options:
1211
965
  command: >-
1212
966
  fix_attempts="$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)";
1213
967
  test "$fix_attempts" -ge "$qualityGateMaxFixAttempts"
1214
- - from: test-fail-triage
1215
- to: test-fix
1216
- description: Failure class fix and under the cap — the bounded repair hop, as before.
968
+ - from: gate
969
+ to: remediate
970
+ description: Failure class fix and under the cap — the bounded repair hop, then a recheck visit of this same gate.
1217
971
  guard:
1218
972
  kind: shell
1219
973
  options:
1220
974
  command: >-
1221
975
  jq -e '.value == "fix"' ".spur/run/$wbs-failure-class.decision" >/dev/null 2>&1
1222
- # Defense: missing/corrupt failure-class decision — fail closed instead of silently repairing.
1223
- - from: test-fail-triage
976
+ # Defense: missing/corrupt gate status or failure-class decision — fail closed.
977
+ - from: gate
1224
978
  to: failed
1225
979
  terminalReason: failed-check
1226
- description: Failure-class decision missing/corrupt — stop at failed.
980
+ description: Gate status or failure-class decision missing/corrupt — stop at failed.
1227
981
  guard:
1228
982
  kind: always
1229
- # ── review-failure routing (session finding after 1088): the review-side twin of the
1230
- # failure-class router above — stop first, then the shared attempt cap, then the fix lane,
1231
- # then the fail-closed defense. The counter is the SAME one the quality-gate loop uses, so a
1232
- # review FAIL cannot buy extra attempts and the iteration bound stays finite.
1233
- - from: review-fail-triage
1234
- to: failed
1235
- terminalReason: failed-check
1236
- description: Review failure class stop — the run terminates at failed, never bypassed.
1237
- guard:
1238
- kind: shell
1239
- options:
1240
- command: >-
1241
- jq -e '.value == "stop"' ".spur/run/$wbs-review-failure-class.decision" >/dev/null 2>&1
1242
- - from: review-fail-triage
1243
- to: failed
1244
- terminalReason: retry-exhausted
1245
- description: Attempt cap reached — qualityGateMaxFixAttempts bounds the review repair lane.
1246
- guard:
1247
- kind: shell
1248
- options:
1249
- command: >-
1250
- fix_attempts="$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)";
1251
- test "$fix_attempts" -ge "$qualityGateMaxFixAttempts"
1252
- - from: review-fail-triage
1253
- to: test-fix
1254
- description: Review failure class fix and under the cap — the shared bounded repair hop.
1255
- guard:
1256
- kind: shell
1257
- options:
1258
- command: >-
1259
- jq -e '.value == "fix"' ".spur/run/$wbs-review-failure-class.decision" >/dev/null 2>&1
1260
- # Defense: missing/corrupt review-failure decision — fail closed instead of silently repairing.
1261
- - from: review-fail-triage
1262
- to: failed
1263
- terminalReason: failed-check
1264
- description: Review-failure decision missing/corrupt — stop at failed.
983
+ - from: remediate
984
+ to: gate
985
+ description: Repair finished — re-enter the gate, whose visit counter selects the recheck mode.
1265
986
  guard:
1266
987
  kind: always
1267
- # ── review routing: FAIL → bounded remediation; PASS → approve or the auto-skip (R4) ──
1268
- # Declaration order matters and is FAIL-CLOSED: the two PASS edges each demand the reviewer's
1269
- # own `Verdict: PASS` line AND a passing review-gate residual check (1122 R3) — the SAME
1270
- # parse/classify/deferrals the record sweep enforces, resolved through the same source-repo /
1271
- # installed scanner pair as the record step; a missing scanner fails closed into the catch-all.
1272
- # EVERYTHING ELSE — FAIL, a missing or unparseable answer, or a PASS carrying an open P1-P3
1273
- # review finding (record would terminally fail it after a full verify + record) — enters
1274
- # `review-fail-triage` and its bounded repair hop BEFORE verify spends the cycle, then
1275
- # re-enters quality → review → verify on a fresh digest. Before 1088 a FAIL review had no
1276
- # outgoing edge at all; before 1122 a PASS-with-findings cost a full re-certification cycle per
1277
- # attempt (knowledge-kit 0210/0213). The reviewer writes `Verdict: PASS|FAIL` in its answer;
1278
- # both plain and bold forms are accepted. (F511: the v4-era PASS confidence grading — the
1279
- # reviewer's self-stated HIGH/MEDIUM/LOW level and its `ackLowConfidence` escape hatch — was
1280
- # retired; a PASS with no open P1-P3 finding is what the gate reads.) Review stays advisory:
1281
- # verify certifies.
1282
- # Under `profile=auto` the approve gate is skipped, so the PASS+auto edge is tried first and the
1283
- # interactive PASS edge second.
1284
- - from: review
1285
- to: verify
1286
- description: profile=auto, the review certified a PASS with no open P1-P3 finding — skip the human approval gate.
1287
- guard:
1288
- kind: shell
1289
- options:
1290
- command: >-
1291
- test "$profile" = auto &&
1292
- grep -qiE '^[[:space:]]*\**Verdict:\** *PASS' ".spur/run/$__runId-review-answer.txt" &&
1293
- $spurBin task residual "$wbs" review-gate --no-logo >&2
988
+
989
+ # ── review routing (D65 1239 R2/R3): certification, approval, bounded repair ──
990
+ # Declaration order is the contract. `done` demands the certified review status AND the
991
+ # approval answer: under `profile=auto` a recorded maker denial (`__hitlAnswer = no`) blocks
992
+ # it, while a deferred answer (no maker configured) is not a denial — `--auto` runs do not
993
+ # gain a human gate they never had. Interactively only an explicit yes certifies.
1294
994
  - from: review
1295
- to: approve
1296
- description: Review certified a PASS with no open P1-P3 finding — gate on human approval before verification.
995
+ to: done
996
+ description: Review certified PASS (verdict + traceability + clean review gate) and the approval decision point approved — close the task.
1297
997
  guard:
1298
998
  kind: shell
1299
999
  options:
1000
+ # Two recorded tokens, two predicates (ADR-115): the certification status and the
1001
+ # approval status the decision point folded. Nothing is inferred from variables.
1300
1002
  command: >-
1301
- grep -qiE '^[[:space:]]*\**Verdict:\** *PASS' ".spur/run/$__runId-review-answer.txt" &&
1302
- $spurBin task residual "$wbs" review-gate --no-logo >&2
1003
+ test "$(cat ".spur/run/$wbs-review.status" 2>/dev/null)" = PASS &&
1004
+ test "$(cat ".spur/run/$wbs-approval.status" 2>/dev/null)" = APPROVED
1303
1005
  - from: review
1304
- to: review-fail-triage
1305
- description: >-
1306
- No certified PASS without an open P1-P3 finding (FAIL, PARTIAL, missing, unparseable, or
1307
- a PASS whose review-gate check found an open P1-P3 row; fail closed) —
1308
- classify the failure and route it into the bounded repair hop instead of dead-ending the run.
1309
- guard:
1310
- kind: always
1311
-
1312
- # ── approve: HITL gate (reached only when profile != auto). Routed by the
1313
- # operator's captured answer (R1, bug-750) — three ordered guards, declaration
1314
- # order matters (yes first, then no, then cancel), mirroring idea-pipeline's
1315
- # design-approval gate (.spur/workflows/idea-pipeline.yaml:352-366). No
1316
- # `always` edge remains out of `approve`. ──
1317
- - from: approve
1318
- to: verify
1319
- description: Approved — proceed to verification.
1320
- guard:
1321
- kind: shell
1322
- options:
1323
- command: 'test "$__hitlAnswer" = yes'
1324
- - from: approve
1325
1006
  to: failed
1326
1007
  terminalReason: cancelled
1327
- description: Operator rejected at the approval gate — report and stop.
1008
+ description: The approval decision point denied (no/cancel interactively, a maker denial under auto, or no operator answer) — a recorded denial, not a check failure.
1328
1009
  guard:
1329
1010
  kind: shell
1330
1011
  options:
1331
- command: 'test "$__hitlAnswer" = no'
1332
- - from: approve
1333
- to: cancelled
1334
- terminalReason: cancelled
1335
- description: Operator cancelled at the approval gate.
1336
- guard:
1337
- kind: shell
1338
- options:
1339
- command: 'test "$__hitlAnswer" = cancel'
1340
-
1341
- # ── completion gate (the YAML-native replacement for rd3's default-on --postflight-verify) ──
1342
- # The verify step emits .spur/run/<wbs>-verdict.json with a required proof block (task 0703 R3/R5).
1343
- # Only `verdict: PASS` PLUS a proof block whose digest — top level and every named stage — equals
1344
- # the captured `proofDigest` clears the gate to `record`; any non-PASS, a missing file, malformed
1345
- # JSON, or missing/mismatched proof evidence does not. (F511: the v4-era LOW/absent-confidence
1346
- # clause and its `ackLowConfidence` escape hatch were retired.) Declaration order: PASS+proof
1347
- # guard FIRST, then the bounded remediation route (R4), then the always catch-all so a PASS
1348
- # verdict with a missing/malformed proof block still terminates at `failed` instead of hanging
1349
- # the state.
1350
- - from: verify
1351
- to: record
1352
- description: Verification verdict is PASS and its proof block names the captured digest on every stage — record results and proceed to done.
1012
+ command: 'test "$(cat ".spur/run/$wbs-approval.status" 2>/dev/null)" = DENIED'
1013
+ - from: review
1014
+ to: failed
1015
+ terminalReason: failed-check
1016
+ description: Review failure class stop — the run terminates at failed, never bypassed.
1353
1017
  guard:
1354
- # (one jq -e predicate) PASS verdict; proof, quality-gate, review and verification digests
1355
- # equal to proofDigest; review completed; matching run id and definition digest (0703 R5, 0785 R4).
1356
1018
  kind: shell
1357
1019
  options:
1358
1020
  command: >-
1359
- jq -e --arg d "$proofDigest" --arg r "$__runId" --arg dd "$__definitionDigest" '
1360
- .verdict == "PASS"
1361
- and (.proof.digest // "") == $d
1362
- and (.proof.stages.qualityGate.digest // "") == $d
1363
- and (.proof.stages.review.digest // "") == $d
1364
- and (.proof.stages.review.status // "") == "completed"
1365
- and (.proof.stages.verification.digest // "") == $d
1366
- and (.proof.runId // "") == $r
1367
- and (.proof.definitionDigest // "") == $dd
1368
- ' ".spur/run/$wbs-verdict.json" >/dev/null 2>&1
1369
- - from: verify
1370
- to: test-fix
1371
- description: >-
1372
- Verification returned a repairable non-PASS and the shared fix budget is not exhausted —
1373
- bounded remediation hop (task 0703 R4); the chain re-enters quality → review → verify on a
1374
- freshly captured digest. Never reached on PASS: remediation cannot follow certification.
1021
+ jq -e '.value == "stop"' ".spur/run/$wbs-review-failure-class.decision" >/dev/null 2>&1
1022
+ - from: review
1023
+ to: failed
1024
+ terminalReason: retry-exhausted
1025
+ description: Attempt cap reached — the shared counter bounds the review repair lane too.
1375
1026
  guard:
1376
- # (e) remediation route: verdict not PASS and fix attempts below qualityGateMaxFixAttempts (0703 R4).
1377
1027
  kind: shell
1378
1028
  options:
1379
1029
  command: >-
1380
- verdict="$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)";
1381
- test -n "$verdict" && test "$verdict" != PASS &&
1382
- test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -lt "$qualityGateMaxFixAttempts"
1383
- - from: verify
1384
- to: failed
1385
- terminalReason: retry-exhausted
1386
- description: >-
1387
- Non-PASS with the fix budget exhausted, or a PASS/missing/malformed verdict whose proof
1388
- block is absent or mismatched (task 0703 R5) — block before done; defense catch-all so the
1389
- state always has a viable outgoing edge.
1390
- guard:
1391
- kind: always
1392
- # ── done gate: record → done/failed gated on `spur task check` (ADR-026 amendment 2026-06-23)
1393
- # PLUS the proof-block re-assertion (task 0703 R5): the verdict artifact must still be PASS and
1394
- # still name the captured digest — a forged or mutated completion artifact fails closed here.
1395
- # Declaration order: pass guard first.
1396
- - from: record
1397
- to: done
1398
- description: Task check passed and the verdict proof block still names the captured digest — certify done.
1030
+ fix_attempts="$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)";
1031
+ test "$fix_attempts" -ge "$qualityGateMaxFixAttempts"
1032
+ - from: review
1033
+ to: remediate
1034
+ description: Review failure class fix and under the cap — the shared bounded repair hop, then gate → review on the remediated tree.
1399
1035
  guard:
1400
1036
  kind: shell
1401
1037
  options:
1402
- # 0804 R6: `--as done` projects the check onto the done target so open
1403
- # Plan/AC checkboxes fail HERE — before the done action runs — mirroring
1404
- # the CLI's own target-aware `--no-lifecycle` backstop (F92 R3). No `#`
1405
- # comments inside the folded scalar: `>-` joins same-indent lines into
1406
- # one shell line.
1407
- # Two logical commands, deliberately (ADR-115): the `task check` projection plus
1408
- # ONE jq predicate re-asserting the whole completion contract — PASS verdict and the
1409
- # captured proof digest.
1410
1038
  command: >-
1411
- $spurBin task check $wbs --as done &&
1412
- jq -e --arg p "$proofDigest" '
1413
- .verdict == "PASS"
1414
- and ((.proof.digest // "") == $p)
1415
- ' ".spur/run/$wbs-verdict.json" >/dev/null 2>&1
1416
- - from: record
1039
+ jq -e '.value == "fix"' ".spur/run/$wbs-review-failure-class.decision" >/dev/null 2>&1
1040
+ # Defense: a review that did not certify and no clearer route — fail closed instead of
1041
+ # dead-ending the run (a FAIL, PARTIAL, missing or unparseable certification lands here).
1042
+ - from: review
1417
1043
  to: failed
1418
1044
  terminalReason: failed-check
1419
- description: Task check failed, a post-record residual downgrade, or proof evidence missing/malformed/mismatched — block before done.
1045
+ description: Review did not certify and no route classified it — stop at failed.
1420
1046
  guard:
1421
1047
  kind: always