@gobing-ai/spur 0.3.73 → 0.3.74

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 (88) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +15 -4
  3. package/config/pipeline-budgets.json +4 -4
  4. package/config/rules/strict/runtime-boundaries.yaml +1 -1
  5. package/config/templates/AGENTS.md +5 -0
  6. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +2 -0
  7. package/config/workflows/basic.yaml +5 -2
  8. package/config/workflows/docs-pipeline.yaml +92 -55
  9. package/config/workflows/feature-dev.yaml +43 -20
  10. package/config/workflows/feature-lifecycle.yaml +1 -0
  11. package/config/workflows/history-anatomy.yaml +8 -4
  12. package/config/workflows/idea-pipeline.yaml +66 -33
  13. package/config/workflows/pr-review.yaml +31 -8
  14. package/config/workflows/task-lifecycle.yaml +1 -0
  15. package/config/workflows/task-pipeline.yaml +929 -905
  16. package/config/workflows/wayfinder-resolution.yaml +124 -90
  17. package/config/workflows/wrapup-pipeline.yaml +208 -83
  18. package/package.json +1 -1
  19. package/plugins/sp/plugin.json +1 -1
  20. package/plugins/sp/scripts/history-anatomy-cache.mjs +7 -1
  21. package/plugins/sp/scripts/history-anatomy-cache.ts +11 -2
  22. package/plugins/sp/scripts/pr-reviewing.mjs +5 -1
  23. package/plugins/sp/scripts/pr-reviewing.ts +7 -1
  24. package/plugins/sp/scripts/transition-shim-check.ts +6 -6
  25. package/plugins/sp/skills/pr-reviewing/SKILL.md +4 -1
  26. package/plugins/sp/skills/spur-cli/references/self.md +17 -3
  27. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
  28. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +5 -5
  29. package/plugins/sp/skills/spur-cli/references/workflows.md +4 -4
  30. package/plugins/sp/skills/spur-dev/SKILL.md +17 -0
  31. package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
  32. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -2
  33. package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
  34. package/spur.js +1125 -784
  35. package/web/_astro/{BoardApp.BYCNkMOn.js → BoardApp.C60RJZRj.js} +78 -77
  36. package/web/_astro/BoardApp.ymfj1EHA.js +1 -0
  37. package/web/_astro/{TaskDetail.CgUreSP2.js → TaskDetail.Guk8VuNu.js} +1 -1
  38. package/web/_astro/{arc.BySSh34M.js → arc.CAZtlWJM.js} +1 -1
  39. package/web/_astro/{architectureDiagram-3BPJPVTR.DM46TS_h.js → architectureDiagram-3BPJPVTR.9-XbF_Tz.js} +1 -1
  40. package/web/_astro/{blockDiagram-GPEHLZMM.tZhvNUHA.js → blockDiagram-GPEHLZMM.DC4WLx3L.js} +1 -1
  41. package/web/_astro/{c4Diagram-AAUBKEIU.PT4Or4Nf.js → c4Diagram-AAUBKEIU.JgjUQMgG.js} +1 -1
  42. package/web/_astro/channel.Bxxe2Byc.js +1 -0
  43. package/web/_astro/{chunk-2J33WTMH.J9r0_Bbe.js → chunk-2J33WTMH.D_JFiXa-.js} +1 -1
  44. package/web/_astro/{chunk-4BX2VUAB.hzyeIvhR.js → chunk-4BX2VUAB.Bk_RxoeT.js} +1 -1
  45. package/web/_astro/{chunk-55IACEB6.B0rO7qVh.js → chunk-55IACEB6.a_Rj2VxL.js} +1 -1
  46. package/web/_astro/{chunk-727SXJPM.wE_Uk5D4.js → chunk-727SXJPM.C2NoS1U6.js} +1 -1
  47. package/web/_astro/{chunk-AQP2D5EJ.DqEEjQw7.js → chunk-AQP2D5EJ.D1Xn6CNa.js} +1 -1
  48. package/web/_astro/{chunk-FMBD7UC4.CDoD9sBX.js → chunk-FMBD7UC4.BhJrSBsX.js} +1 -1
  49. package/web/_astro/{chunk-ND2GUHAM.CtX5nF9P.js → chunk-ND2GUHAM.ByVGduYQ.js} +1 -1
  50. package/web/_astro/{chunk-QZHKN3VN.CK_EwfaT.js → chunk-QZHKN3VN.B47Paunq.js} +1 -1
  51. package/web/_astro/{classDiagram-4FO5ZUOK.DLt5a8Lh.js → classDiagram-4FO5ZUOK.BERMSD5C.js} +1 -1
  52. package/web/_astro/{classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js → classDiagram-v2-Q7XG4LA2.BERMSD5C.js} +1 -1
  53. package/web/_astro/{cose-bilkent-S5V4N54A.CMCWP49h.js → cose-bilkent-S5V4N54A.O_rHGlhU.js} +1 -1
  54. package/web/_astro/{cynefin-OW5HDTMX.HyXw_vdS.js → cynefin-OW5HDTMX.qCW2GdNp.js} +1 -1
  55. package/web/_astro/{dagre-BM42HDAG.BTuAzh01.js → dagre-BM42HDAG.C5Y4lH_u.js} +1 -1
  56. package/web/_astro/{diagram-2AECGRRQ.D9dr9wfT.js → diagram-2AECGRRQ.DxZmRXxF.js} +1 -1
  57. package/web/_astro/{diagram-5GNKFQAL.C4Rot0hj.js → diagram-5GNKFQAL.qSxZeYSK.js} +1 -1
  58. package/web/_astro/{diagram-KO2AKTUF.B_TK5uWC.js → diagram-KO2AKTUF.-7vl3bXs.js} +1 -1
  59. package/web/_astro/{diagram-LMA3HP47.JkXKK7CO.js → diagram-LMA3HP47.BHVV-UH3.js} +1 -1
  60. package/web/_astro/{diagram-OG6HWLK6.BzMN8Bd6.js → diagram-OG6HWLK6.DHAuW9sK.js} +1 -1
  61. package/web/_astro/{erDiagram-TEJ5UH35.DVZaWGUd.js → erDiagram-TEJ5UH35.DMRLQwPN.js} +1 -1
  62. package/web/_astro/{flowDiagram-I6XJVG4X.rjEiWUfR.js → flowDiagram-I6XJVG4X.B3EpPp_8.js} +1 -1
  63. package/web/_astro/{ganttDiagram-6RSMTGT7.C_EgAarK.js → ganttDiagram-6RSMTGT7.BgUrExXM.js} +1 -1
  64. package/web/_astro/{gitGraphDiagram-PVQCEYII.B-QQSDsK.js → gitGraphDiagram-PVQCEYII.CU-9yVN4.js} +1 -1
  65. package/web/_astro/{index.B5MTfe7k.css → index.BhvM3djt.css} +1 -1
  66. package/web/_astro/{infoDiagram-5YYISTIA.DlWesz7T.js → infoDiagram-5YYISTIA.CvVTCRLe.js} +1 -1
  67. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUMZOawi.js → ishikawaDiagram-YF4QCWOH.BUzOfKPf.js} +1 -1
  68. package/web/_astro/{journeyDiagram-JHISSGLW.CWfkxfjY.js → journeyDiagram-JHISSGLW.BBTb7ziz.js} +1 -1
  69. package/web/_astro/{kanban-definition-UN3LZRKU.B-YpMwXf.js → kanban-definition-UN3LZRKU.CTQQr70i.js} +1 -1
  70. package/web/_astro/{linear.D7uqzENp.js → linear.CLKlJPiS.js} +1 -1
  71. package/web/_astro/{mermaid.core.CxrNppBD.js → mermaid.core.BA4wFhLP.js} +4 -4
  72. package/web/_astro/{mindmap-definition-RKZ34NQL.B4Qe7cM2.js → mindmap-definition-RKZ34NQL.t1vG2l5e.js} +1 -1
  73. package/web/_astro/{pieDiagram-4H26LBE5.Ds-5j2ro.js → pieDiagram-4H26LBE5.C8zJhyTu.js} +1 -1
  74. package/web/_astro/{quadrantDiagram-W4KKPZXB.tBd38uNC.js → quadrantDiagram-W4KKPZXB.DqYYvb7T.js} +1 -1
  75. package/web/_astro/{requirementDiagram-4Y6WPE33.sFENkWl3.js → requirementDiagram-4Y6WPE33.lzfDZ9nI.js} +1 -1
  76. package/web/_astro/{sankeyDiagram-5OEKKPKP.BeB-Hk7C.js → sankeyDiagram-5OEKKPKP.lm6N5ORM.js} +1 -1
  77. package/web/_astro/{sequenceDiagram-3UESZ5HK.DnTeaSpx.js → sequenceDiagram-3UESZ5HK.E6_IV4Dj.js} +1 -1
  78. package/web/_astro/{stateDiagram-AJRCARHV.B-8Jt5EJ.js → stateDiagram-AJRCARHV.Bnv6Ok2p.js} +1 -1
  79. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Br7xoqMW.js → stateDiagram-v2-BHNVJYJU.mJHH_Ng_.js} +1 -1
  80. package/web/_astro/{timeline-definition-PNZ67QCA.C-3WdOyi.js → timeline-definition-PNZ67QCA.BhRgqSLa.js} +1 -1
  81. package/web/_astro/{vennDiagram-CIIHVFJN.DCIs7Lc6.js → vennDiagram-CIIHVFJN.pjb5DMC9.js} +1 -1
  82. package/web/_astro/{wardleyDiagram-YWT4CUSO.rGAL-bbz.js → wardleyDiagram-YWT4CUSO.CW20KUng.js} +1 -1
  83. package/web/_astro/{xychartDiagram-2RQKCTM6.hkfQKiRl.js → xychartDiagram-2RQKCTM6.vX9_yuMl.js} +1 -1
  84. package/web/index.html +2 -2
  85. package/config/corpus-baseline.json +0 -1805
  86. package/config/workflow-composition-baseline.json +0 -615
  87. package/web/_astro/BoardApp.E12MFjOS.js +0 -1
  88. package/web/_astro/channel.5cYKr5cs.js +0 -1
@@ -27,6 +27,9 @@
27
27
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
28
28
  kind: state-machine
29
29
  name: task-pipeline
30
+ # 0772 R2: explicit identity tag. Set only after the proof failure-path tests
31
+ # passed; the engine treats unversioned external definitions as supported.
32
+ version: "1"
30
33
  description: "Standard task pipeline: precheck → implement → quality-gate (test/fix/recheck) → review → approve → verify → record → done"
31
34
  # Task entity lifecycle (owned by task-lifecycle.yaml):
32
35
  # normal: backlog → todo → wip → testing → done
@@ -40,928 +43,949 @@ description: "Standard task pipeline: precheck → implement → quality-gate (t
40
43
  iterationBound: 20
41
44
  initialState: precheck
42
45
  terminalStates:
43
- - done
44
- - failed
45
- - cancelled
46
+ - done
47
+ - failed
48
+ - cancelled
46
49
  failureStates:
47
- - failed
48
- - cancelled
50
+ - failed
51
+ - cancelled
49
52
  vars:
50
- wbs: "0000"
51
- profile: "standard"
52
- mode: ""
53
- __runId: ""
54
- __definitionDigest: ""
55
- # PATH-independent spur invocation for shell guards/actions. The CLI overrides this
56
- # at run start (resolveSpurBin); the literal default is a safe fallback so direct/dry
57
- # runs and `workflow validate` resolve the template. (See AGENTS.md / ADR-026.)
58
- spurBin: "spur"
59
- # Agent the pipeline's agent.run steps invoke. Override per run with
60
- # `--vars '{"agent":"claude"}'`. `auto` is the reserved config-resolving selector:
61
- # `agent.default` role -> tier -> cheapest USABLE executor. A named literal here would
62
- # pin a box-specific binary into tracked SSOT and escape that usability ladder.
63
- agent: "auto"
64
- # Implement-only executor override (R1, task 0454). Resolves like `agent` unless
65
- # overridden. The `--agent` flag (execution-batch.md §3.2) forwards into BOTH
66
- # `agent` and `implementAgent`, so a pinned `--agent X` reaches implement too. To
67
- # pin ONLY implement while other hops keep the default, pass
68
- # `--vars '{"implementAgent":"omp-zai"}'`.
69
- implementAgent: "auto"
70
- # Step-level timeout for agentic hops (review / verify / test-fix) in ms.
71
- # Soft quality-gate shells are unbounded by this var (host shell only).
72
- # Raised 600s 1800s (task 0398 R4 / H6 dogfood). Override:
73
- # `--vars '{"stepTimeoutMs":"120000"}'`. 30 min is headroom, not a licence to
74
- # run unbounded — if a hop still hits this wall, STOP and record it.
75
- stepTimeoutMs: "1800000"
76
- # Timeout for the `implement` step specifically (R2a). Implementation is the
77
- # heaviest agent.run step (full read/write/test-probe loop) and has timed out
78
- # at the 600s default in five consecutive dogfood runs (bugs 742/744/746/748),
79
- # a 100% failure rate at that budget. 30 min gives real headroom; STOP and
80
- # record honestly rather than raising it further without operator sign-off.
81
- implementTimeoutMs: "1800000"
82
- # Answer captured by the approve gate's hitl.confirm (R1): "yes" | "no" | "cancel".
83
- # Empty by default; only meaningful once the approve state has been entered.
84
- __hitlAnswer: ""
85
- # Proof-state bracket (task 0612, ADR-071; restructured by task 0703). `proofDigest` is the
86
- # canonical capture taken at quality-gate ENTRY — immediately before the evidence-producing
87
- # final chain (quality review verify) and re-captured at `test-recheck` when bounded
88
- # remediation mutated the tree, so every evidence stage names one fresh digest (R2/R4).
89
- # `proofDigestNow` is the live re-capture compared against `proofDigest` at verify entry and
90
- # immediately before `record`; a mismatch means a proof input changed after evidence was
91
- # established, so the run routes to `failed` instead of crossing the completion boundary (R5).
92
- # `taskSpecPath` carries the task file path because `docs/tasks*` is excluded from the digest's
93
- # git-tree half spec content is folded in explicitly or a task-file edit would go undetected.
94
- # The fingerprint scopes task content to the proof-input sections only (Background, Requirements,
95
- # Acceptance Criteria, Design, Plan), so record-time Solution/Testing/Review evidence writes do
96
- # not retroactively invalidate the certified input set (R6).
97
- proofDigest: ""
98
- proofDigestNow: ""
99
- taskSpecPath: ""
100
- # 0710 R4: task priority tier (P0..P4) extracted from the task frontmatter at the
101
- # quality-gate stage. P0/P1 make the review/verify distinct-executor policy apply;
102
- # unknown/empty priority means fresh-context-only (executor reuse allowed).
103
- taskPriority: ""
104
- # Project quality gate for the `test` hop (probe + fixall + recheck). Override per project
105
- # with the same package-manager surface (this monorepo is Bun-only):
106
- # `--vars '{"qualityGateCmd":"bun run lint && bun run test"}'`. Soft probe, hard recheck,
107
- # and the fixall slash input all use this same var so the command stays single-sourced.
108
- # TRUSTED CONFIG ONLY — this string is executed via `sh -c` (see test/test-recheck). Never
109
- # interpolate untrusted operator/LLM input into qualityGateCmd (task 0436 SECUA residual).
110
- # NO `format` PREFIX: `implement` already ran `$formatCmd` on its way out, and `test`
111
- # captures `proofDigest` (onEnter[3]) BEFORE this command runs (onEnter[4]) a formatter
112
- # inside the gate rewrites the very tree the digest just fingerprinted (ADR-071 proof
113
- # window). It was a no-op only because the implement-stage format got there first; that is
114
- # an accident, not an invariant. The gate observes, it does not mutate.
115
- qualityGateCmd: "bun run spur-check"
116
- # Cheap red-detector run before the full gate on **recheck only**; empty ⇒ no probe
117
- # (full gate every recheck the pre-0587 behavior). A project overriding qualityGateCmd
118
- # should override this too. TRUSTED CONFIG ONLY executed via `sh -c` (same surface as
119
- # qualityGateCmd). Invariant: `review` is only ever entered through a full green
120
- # qualityGateCmd only the full gate writes PASS to <wbs>-test-gate.status.
121
- gateProbeCmd: "bun run lint"
122
- # Max /sp:dev-fixall attempts after a red quality-gate probe/recheck (bounded; no thrash).
123
- # Attempt counter: .spur/run/<wbs>-test-fix-attempt. Default 2 = two fixall hops before failed.
124
- qualityGateMaxFixAttempts: "2"
125
- # Post-implement auto-format. Overridable like qualityGateCmd so a non-Bun seeded
126
- # project can point it at its own formatter; invoked best-effort (a missing or
127
- # failing formatter must never abort a run — the quality gate is the real gate).
128
- formatCmd: "bun run format"
129
- # Bounded `file:line` anchors extracted from the failing quality-gate log by the
130
- # `test` / `test-recheck` hops (deduped, max 20, space-separated on one line).
131
- # `test-fix` reads `.spur/run/<wbs>-test-gate.findings` into this var and names the
132
- # anchors directly in the /sp:dev-fixall input, so the fix agent starts AT the failing
133
- # file:line instead of re-deriving it from a fresh gate run (0482 R3).
134
- gateFindings: ""
135
- # Max R-items in ## Requirements before size precheck fails (R2, task 0454).
136
- # Override with `--vars '{"maxImplementReqs":"20"}'`.
137
- maxImplementReqs: "10"
138
- # Max checklist items under ## Plan before size precheck fails (R2, task 0454).
139
- # Override with `--vars '{"maxImplementPlanItems":"32"}'`.
140
- maxImplementPlanItems: "16"
141
- # Diff-scope guard on the implement hop (R1, task 0487). When the target task
142
- # body backticks at least one path, non-corpus changes outside those paths
143
- # fail the step by name. New files beside a declared file are allowed. Empty
144
- # (default) = on; set to "off" to bypass:
145
- # `--vars '{"implementScopeGuard":"off"}'`.
146
- implementScopeGuard: ""
53
+ wbs: "0000"
54
+ profile: "standard"
55
+ mode: ""
56
+ __runId: ""
57
+ __definitionDigest: ""
58
+ # PATH-independent spur invocation for shell guards/actions. The CLI overrides this
59
+ # at run start (resolveSpurBin); the literal default is a safe fallback so direct/dry
60
+ # runs and `workflow validate` resolve the template. (See AGENTS.md / ADR-026.)
61
+ spurBin: "spur"
62
+ # Agent the pipeline's agent.run steps invoke. Override per run with
63
+ # `--vars '{"agent":"claude"}'`. `auto` is the reserved config-resolving selector:
64
+ # `agent.default` role -> tier -> cheapest USABLE executor. A named literal here would
65
+ # pin a box-specific binary into tracked SSOT and escape that usability ladder.
66
+ # It does not inherit the host session's interactive model; required capability
67
+ # attestation still gates the chosen executor before dispatch (0706).
68
+ agent: "auto"
69
+ # Implement-only executor override (R1, task 0454). Resolves like `agent` unless
70
+ # overridden. The `--agent` flag (execution-batch.md §3.2) forwards into BOTH
71
+ # `agent` and `implementAgent`, so a pinned `--agent X` reaches implement too. To
72
+ # pin ONLY implement while other hops keep the default, pass
73
+ # `--vars '{"implementAgent":"pi-zai"}'`.
74
+ implementAgent: "auto"
75
+ # Remediation can mutate code only when BOTH this cap and the task's declared
76
+ # mutationPolicy permit code. none/tests require scoped manual repair, not fixall.
77
+ mutationPolicy: "code"
78
+ # Step-level timeout for agentic hops (review / verify / test-fix) in ms.
79
+ # Soft quality-gate shells are unbounded by this var (host shell only).
80
+ # Raised 600s 1800s (task 0398 R4 / H6 dogfood). Override:
81
+ # `--vars '{"stepTimeoutMs":"120000"}'`. 30 min is headroom, not a licence to
82
+ # run unbounded if a hop still hits this wall, STOP and record it.
83
+ stepTimeoutMs: "1800000"
84
+ # Timeout for the `implement` step specifically (R2a). Implementation is the
85
+ # heaviest agent.run step (full read/write/test-probe loop) and has timed out
86
+ # at the 600s default in five consecutive dogfood runs (bugs 742/744/746/748),
87
+ # a 100% failure rate at that budget. 30 min gives real headroom; STOP and
88
+ # record honestly rather than raising it further without operator sign-off.
89
+ implementTimeoutMs: "1800000"
90
+ # Answer captured by the approve gate's hitl.confirm (R1): "yes" | "no" | "cancel".
91
+ # Empty by default; only meaningful once the approve state has been entered.
92
+ __hitlAnswer: ""
93
+ # Proof-state bracket (task 0612, ADR-071; restructured by task 0703). `proofDigest` is the
94
+ # canonical capture taken at quality-gate ENTRY immediately before the evidence-producing
95
+ # final chain (quality review verify) and re-captured at `test-recheck` when bounded
96
+ # remediation mutated the tree, so every evidence stage names one fresh digest (R2/R4).
97
+ # `proofDigestNow` is the live re-capture compared against `proofDigest` at verify entry and
98
+ # immediately before `record`; a mismatch means a proof input changed after evidence was
99
+ # established, so the run routes to `failed` instead of crossing the completion boundary (R5).
100
+ # `taskSpecPath` carries the task file path because `docs/tasks*` is excluded from the digest's
101
+ # git-tree half — spec content is folded in explicitly or a task-file edit would go undetected.
102
+ # The fingerprint scopes task content to the proof-input sections only (Background, Requirements,
103
+ # Acceptance Criteria, Design, Plan), so record-time Solution/Testing/Review evidence writes do
104
+ # not retroactively invalidate the certified input set (R6).
105
+ proofDigest: ""
106
+ proofDigestNow: ""
107
+ taskSpecPath: ""
108
+ # 0710 R4: task priority tier (P0..P4) extracted from the task frontmatter at the
109
+ # quality-gate stage. P0/P1 make the review/verify distinct-executor policy apply;
110
+ # unknown/empty priority means fresh-context-only (executor reuse allowed).
111
+ taskPriority: ""
112
+ # Project quality gate for the `test` hop (probe + fixall + recheck). Override per project
113
+ # with the same package-manager surface (this monorepo is Bun-only):
114
+ # `--vars '{"qualityGateCmd":"bun run lint && bun run test"}'`. Soft probe, hard recheck,
115
+ # and the fixall slash input all use this same var so the command stays single-sourced.
116
+ # TRUSTED CONFIG ONLY this string is executed via `sh -c` (see test/test-recheck). Never
117
+ # interpolate untrusted operator/LLM input into qualityGateCmd (task 0436 SECUA residual).
118
+ # NO `format` PREFIX: `implement` already ran `$formatCmd` on its way out, and `test`
119
+ # captures `proofDigest` (onEnter[3]) BEFORE this command runs (onEnter[4]) a formatter
120
+ # inside the gate rewrites the very tree the digest just fingerprinted (ADR-071 proof
121
+ # window). It was a no-op only because the implement-stage format got there first; that is
122
+ # an accident, not an invariant. The gate observes, it does not mutate.
123
+ qualityGateCmd: "bun run spur-check"
124
+ # Cheap red-detector run before the full gate on **recheck only**; empty ⇒ no probe
125
+ # (full gate every recheck the pre-0587 behavior). A project overriding qualityGateCmd
126
+ # should override this too. TRUSTED CONFIG ONLY executed via `sh -c` (same surface as
127
+ # qualityGateCmd). Invariant: `review` is only ever entered through a full green
128
+ # qualityGateCmd only the full gate writes PASS to <wbs>-test-gate.status.
129
+ gateProbeCmd: "bun run lint"
130
+ # Max /sp:dev-fixall attempts after a red quality-gate probe/recheck (bounded; no thrash).
131
+ # Attempt counter: .spur/run/<wbs>-test-fix-attempt. Default 2 = two fixall hops before failed.
132
+ qualityGateMaxFixAttempts: "2"
133
+ # Post-implement auto-format. Overridable like qualityGateCmd so a non-Bun seeded
134
+ # project can point it at its own formatter; invoked best-effort (a missing or
135
+ # failing formatter must never abort a run the quality gate is the real gate).
136
+ formatCmd: "bun run format"
137
+ # Bounded `file:line` anchors extracted from the failing quality-gate log by the
138
+ # `test` / `test-recheck` hops (deduped, max 20, space-separated on one line).
139
+ # `test-fix` reads `.spur/run/<wbs>-test-gate.findings` into this var and names the
140
+ # anchors directly in the /sp:dev-fixall input, so the fix agent starts AT the failing
141
+ # file:line instead of re-deriving it from a fresh gate run (0482 R3).
142
+ gateFindings: ""
143
+ # Max R-items in ## Requirements before size precheck fails (R2, task 0454).
144
+ # Override with `--vars '{"maxImplementReqs":"20"}'`.
145
+ maxImplementReqs: "10"
146
+ # Max checklist items under ## Plan before size precheck fails (R2, task 0454).
147
+ # Override with `--vars '{"maxImplementPlanItems":"32"}'`.
148
+ maxImplementPlanItems: "16"
149
+ # Diff-scope guard on the implement hop (R1, task 0487). When the target task
150
+ # body backticks at least one path, non-corpus changes outside those paths
151
+ # fail the step by name. New files beside a declared file are allowed. Empty
152
+ # (default) = on; set to "off" to bypass:
153
+ # `--vars '{"implementScopeGuard":"off"}'`.
154
+ implementScopeGuard: ""
147
155
 
148
156
  states:
149
- - id: precheck
150
- description: >
151
- Fast deterministic task readiness and size checks. Failures route to the
152
- `failed` terminal state (not a raw lifecycle abort mid-enter).
153
- onEnter:
154
- # R6 (0487): pre-launch hygiene WARNING (never a block) — starting a task on
155
- # a tree already dirty with another task's implementation is how 0485's diff
156
- # got swept into 0486's run. Corpus dirs are excluded: the pipeline writes
157
- # those itself.
158
- # R1 (0511): task-corpus dirt gets its own advisory NOTE — docs/tasks* is
159
- # excluded from the non-corpus warning, so pre-existing task-corpus edits are
160
- # invisible at launch yet can be accidentally staged with the current task
161
- # later. Advisory only (never a block): the pipeline legitimately updates
162
- # task files, so corpus dirt is launch-time visibility, not a lifecycle guard.
163
- - kind: shell
164
- options:
165
- command: >-
166
- DIRTY=$(git status --porcelain -- . ':(exclude)docs/tasks*' ':(exclude)docs/features' 2>/dev/null);
167
- if [ -n "$DIRTY" ]; then
168
- echo "precheck: WARNING - working tree has uncommitted non-corpus changes; commit or stash before starting a new task:";
169
- echo "$DIRTY";
170
- fi;
171
- CORPUS_DIRTY=$(git status --porcelain -- ':(glob)docs/tasks*/**' 2>/dev/null);
172
- if [ -n "$CORPUS_DIRTY" ]; then
173
- echo "precheck: NOTE - task corpus has uncommitted changes; review before staging with this task:";
174
- echo "$CORPUS_DIRTY";
175
- fi;
176
- exit 0
177
- - kind: note
178
- options:
179
- # R4 task_run_links linkage (kind=pipeline) is written by a WorkflowService
180
- # hook at run start — see the 0062 follow-up note; there is no link-writing
181
- # CLI verb to call from a shell step, so it cannot live in pure YAML.
182
- message: "Pipeline start for task ${vars.wbs}."
183
- # R1 (0453): auto-profile precheck reopens a done feature before task check.
184
- # Under profile=auto, resolve feature_id, sync (preferred) or update to active.
185
- # Under non-auto, leave R4 message to guide the operator.
186
- # R3 (0723): a real reactivation failure is surfaced, not swallowed —
187
- # the default 'fail' onEnter policy halts the sequence and routes the
188
- # run to `failed` before implementation. Verbs stay single-shot:
189
- # one sync, then one update fallback, never retried in a loop.
190
- - kind: shell
191
- options:
192
- command: >-
193
- if [ "$profile" = auto ]; then
194
- FID=$($spurBin task show $wbs --json 2>/dev/null |
195
- jq -r '.feature_id // .frontmatter.feature_id // empty' 2>/dev/null);
196
- if [ -n "$FID" ]; then
197
- if ! $spurBin feature sync "$FID" --force 2>/dev/null; then
198
- if ! $spurBin feature update "$FID" active 2>/dev/null; then
199
- echo "precheck: FAIL - feature reactivation $FID failed;" >&2;
200
- echo "precheck: feature sync + feature update both errored" >&2;
201
- exit 1;
202
- fi;
203
- fi;
204
- fi;
205
- fi;
206
- exit 0
207
- # R2 (0454, 0723): task size precheck — deterministic count-only
208
- # evaluation of R-item and Plan-item counts. No executor-tier policy:
209
- # dispatch-time requiresCapabilities at `agent.run` is the
210
- # authoritative capability check. Writes PASS/FAIL to
211
- # .spur/run/<wbs>-precheck-size.status. Always exit 0 (soft action);
212
- # the precheck→implement guard reads the file, so a missing checker
213
- # fails closed (writes FAIL, never PASS).
214
- - kind: shell
215
- options:
216
- command: >-
217
- SIZE_FILE=".spur/run/$wbs-precheck-size.status" &&
218
- mkdir -p .spur/run &&
219
- SIZE_SCRIPT="plugins/sp/scripts/task-size-precheck.ts";
220
- [ -f "$SIZE_SCRIPT" ] ||
221
- SIZE_SCRIPT="$(superskill script path sp task-size-precheck.ts 2>/dev/null)";
222
- if [ -n "$SIZE_SCRIPT" ] && [ -f "$SIZE_SCRIPT" ]; then
223
- bun "$SIZE_SCRIPT" "$wbs"
224
- --spur-bin "$spurBin" --max-reqs "$maxImplementReqs"
225
- --max-plan-items "$maxImplementPlanItems";
226
- else
227
- echo "task-size-precheck failed closed — checker not found in" >&2 &&
228
- echo "plugins/sp/scripts/ nor staged; run 'superskill install sp'." >&2 &&
229
- echo "FAIL" > "$SIZE_FILE";
230
- fi &&
231
- exit 0
232
- # 0726 R2: task evidence precheck — deterministic live-data
233
- # evidence-channel proof before implement dispatch. Writes PASS/FAIL to
234
- # .spur/run/<wbs>-precheck-evidence.status. Always exit 0 (soft action);
235
- # the precheck→implement guard reads the file, so a missing checker
236
- # fails closed (writes FAIL, never PASS).
237
- - kind: shell
238
- options:
239
- command: >-
240
- EVID_FILE=".spur/run/$wbs-precheck-evidence.status" &&
241
- mkdir -p .spur/run &&
242
- EVID_SCRIPT="plugins/sp/scripts/task-evidence-precheck.ts";
243
- [ -f "$EVID_SCRIPT" ] ||
244
- EVID_SCRIPT="$(superskill script path sp task-evidence-precheck.ts 2>/dev/null)";
245
- if [ -n "$EVID_SCRIPT" ] && [ -f "$EVID_SCRIPT" ]; then
246
- bun "$EVID_SCRIPT" "$wbs"
247
- --spur-bin "$spurBin";
248
- else
249
- echo "task-evidence-precheck failed closed — checker not found" >&2 &&
250
- echo "in plugins/sp/scripts/ nor staged; run 'superskill install sp'." >&2 &&
251
- echo "FAIL" > "$EVID_FILE";
252
- fi &&
253
- exit 0
254
- # Proportional route table evaluation (0759 R1/R4). The reason artifact is RUN-scoped, not
255
- # wbs-scoped: ADR-107 names `.spur/run/<runId>-route-reason.txt`, and a wbs-scoped path lets
256
- # a re-run of the same task overwrite the earlier run's route claim, so the artifact could
257
- # not attribute a route to the run that took it (0759 R5). `__runId` is injected by
258
- # WorkflowAppService.run(); the wbs fallback keeps a driver-less invocation from writing to
259
- # a bare "-route-reason.txt". The log line carries the run id for the same reason — an
260
- # unattributed append is log scraping, which R5 explicitly rejects as evidence.
261
- - kind: shell
262
- options:
263
- command: >-
264
- mkdir -p .spur/run .spur/memory &&
265
- RUN_ID="$__runId" &&
266
- if [ -z "$RUN_ID" ]; then RUN_ID="pipeline-$wbs"; fi &&
267
- REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
268
- if [ "$mode" = "fast" ]; then
269
- echo "fast:evidence complete+consistent" > "$REASON_FILE";
270
- elif [ -z "$mode" ]; then
271
- echo "safety:standard verification" > "$REASON_FILE";
272
- elif [ "$mode" = "unknown" ]; then
273
- echo "safety:unknown evidence quality" > "$REASON_FILE";
274
- elif [ "$mode" = "conflict" ]; then
275
- echo "safety:conflicting evidence" > "$REASON_FILE";
276
- else
277
- echo "safety:unrecognized evidence (mode=$mode)" > "$REASON_FILE";
278
- fi &&
279
- printf '%s %s %s\n' "$RUN_ID" "$wbs" "$(cat "$REASON_FILE")"
280
- >> .spur/memory/task-pipeline-routes.log &&
281
- exit 0
157
+ - id: precheck
158
+ description: >
159
+ Fast deterministic task readiness and size checks. Failures route to the
160
+ `failed` terminal state (not a raw lifecycle abort mid-enter).
161
+ onEnter:
162
+ # R6 (0487): pre-launch hygiene WARNING (never a block) — starting a task on
163
+ # a tree already dirty with another task's implementation is how 0485's diff
164
+ # got swept into 0486's run. Corpus dirs are excluded: the pipeline writes
165
+ # those itself.
166
+ # R1 (0511): task-corpus dirt gets its own advisory NOTE — docs/tasks* is
167
+ # excluded from the non-corpus warning, so pre-existing task-corpus edits are
168
+ # invisible at launch yet can be accidentally staged with the current task
169
+ # later. Advisory only (never a block): the pipeline legitimately updates
170
+ # task files, so corpus dirt is launch-time visibility, not a lifecycle guard.
171
+ - kind: shell
172
+ options:
173
+ command: >-
174
+ DIRTY=$(git status --porcelain -- . ':(exclude)docs/tasks*' ':(exclude)docs/features' 2>/dev/null);
175
+ if [ -n "$DIRTY" ]; then
176
+ echo "precheck: WARNING - working tree has uncommitted non-corpus changes; commit or stash before starting a new task:";
177
+ echo "$DIRTY";
178
+ fi;
179
+ CORPUS_DIRTY=$(git status --porcelain -- ':(glob)docs/tasks*/**' 2>/dev/null);
180
+ if [ -n "$CORPUS_DIRTY" ]; then
181
+ echo "precheck: NOTE - task corpus has uncommitted changes; review before staging with this task:";
182
+ echo "$CORPUS_DIRTY";
183
+ fi;
184
+ exit 0
185
+ - kind: note
186
+ options:
187
+ # R4 task_run_links linkage (kind=pipeline) is written by a WorkflowService
188
+ # hook at run start — see the 0062 follow-up note; there is no link-writing
189
+ # CLI verb to call from a shell step, so it cannot live in pure YAML.
190
+ message: "Pipeline start for task ${vars.wbs}."
191
+ # R1 (0453): auto-profile precheck reopens a done feature before task check.
192
+ # Under profile=auto, resolve feature_id, sync (preferred) or update to active.
193
+ # Under non-auto, leave R4 message to guide the operator.
194
+ # R3 (0723): a real reactivation failure is surfaced, not swallowed —
195
+ # the default 'fail' onEnter policy halts the sequence and routes the
196
+ # run to `failed` before implementation. Verbs stay single-shot:
197
+ # one sync, then one update fallback, never retried in a loop.
198
+ - kind: shell
199
+ options:
200
+ command: >-
201
+ if [ "$profile" = auto ]; then
202
+ FID=$($spurBin task show $wbs --json 2>/dev/null |
203
+ jq -r '.feature_id // .frontmatter.feature_id // empty' 2>/dev/null);
204
+ if [ -n "$FID" ]; then
205
+ if ! $spurBin feature sync "$FID" --force 2>/dev/null; then
206
+ if ! $spurBin feature update "$FID" active 2>/dev/null; then
207
+ echo "precheck: FAIL - feature reactivation $FID failed;" >&2;
208
+ echo "precheck: feature sync + feature update both errored" >&2;
209
+ exit 1;
210
+ fi;
211
+ fi;
212
+ fi;
213
+ fi;
214
+ exit 0
215
+ # R2 (0454, 0723): task size precheck — deterministic count-only
216
+ # evaluation of R-item and Plan-item counts. No executor-tier policy:
217
+ # dispatch-time requiresCapabilities at `agent.run` is the
218
+ # authoritative capability check. Writes PASS/FAIL to
219
+ # .spur/run/<wbs>-precheck-size.status. Always exit 0 (soft action);
220
+ # the precheck→implement guard reads the file, so a missing checker
221
+ # fails closed (writes FAIL, never PASS).
222
+ - kind: shell
223
+ options:
224
+ command: >-
225
+ SIZE_FILE=".spur/run/$wbs-precheck-size.status" &&
226
+ mkdir -p .spur/run &&
227
+ SIZE_SCRIPT="plugins/sp/scripts/task-size-precheck.ts";
228
+ [ -f "$SIZE_SCRIPT" ] ||
229
+ SIZE_SCRIPT="$(superskill script path sp task-size-precheck.ts 2>/dev/null)";
230
+ if [ -n "$SIZE_SCRIPT" ] && [ -f "$SIZE_SCRIPT" ]; then
231
+ bun "$SIZE_SCRIPT" "$wbs"
232
+ --spur-bin "$spurBin" --max-reqs "$maxImplementReqs"
233
+ --max-plan-items "$maxImplementPlanItems";
234
+ else
235
+ echo "task-size-precheck failed closed — checker not found in" >&2 &&
236
+ echo "plugins/sp/scripts/ nor staged; run 'superskill install sp'." >&2 &&
237
+ echo "FAIL" > "$SIZE_FILE";
238
+ fi &&
239
+ exit 0
240
+ # 0726 R2: task evidence precheck — deterministic live-data
241
+ # evidence-channel proof before implement dispatch. Writes PASS/FAIL to
242
+ # .spur/run/<wbs>-precheck-evidence.status. Always exit 0 (soft action);
243
+ # the precheck→implement guard reads the file, so a missing checker
244
+ # fails closed (writes FAIL, never PASS).
245
+ - kind: shell
246
+ options:
247
+ command: >-
248
+ EVID_FILE=".spur/run/$wbs-precheck-evidence.status" &&
249
+ mkdir -p .spur/run &&
250
+ EVID_SCRIPT="plugins/sp/scripts/task-evidence-precheck.ts";
251
+ [ -f "$EVID_SCRIPT" ] ||
252
+ EVID_SCRIPT="$(superskill script path sp task-evidence-precheck.ts 2>/dev/null)";
253
+ if [ -n "$EVID_SCRIPT" ] && [ -f "$EVID_SCRIPT" ]; then
254
+ bun "$EVID_SCRIPT" "$wbs"
255
+ --spur-bin "$spurBin";
256
+ else
257
+ echo "task-evidence-precheck failed closed — checker not found" >&2 &&
258
+ echo "in plugins/sp/scripts/ nor staged; run 'superskill install sp'." >&2 &&
259
+ echo "FAIL" > "$EVID_FILE";
260
+ fi &&
261
+ exit 0
262
+ # Proportional route table evaluation (0759 R1/R4). The reason artifact is RUN-scoped, not
263
+ # wbs-scoped: ADR-107 names `.spur/run/<runId>-route-reason.txt`, and a wbs-scoped path lets
264
+ # a re-run of the same task overwrite the earlier run's route claim, so the artifact could
265
+ # not attribute a route to the run that took it (0759 R5). `__runId` is injected by
266
+ # WorkflowAppService.run(); the wbs fallback keeps a driver-less invocation from writing to
267
+ # a bare "-route-reason.txt". The log line carries the run id for the same reason — an
268
+ # unattributed append is log scraping, which R5 explicitly rejects as evidence.
269
+ - kind: shell
270
+ options:
271
+ command: >-
272
+ mkdir -p .spur/run .spur/memory &&
273
+ RUN_ID="$__runId" &&
274
+ if [ -z "$RUN_ID" ]; then RUN_ID="pipeline-$wbs"; fi &&
275
+ REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
276
+ if [ "$mode" = "fast" ]; then
277
+ echo "fast:evidence complete+consistent" > "$REASON_FILE";
278
+ elif [ -z "$mode" ]; then
279
+ echo "safety:standard verification" > "$REASON_FILE";
280
+ elif [ "$mode" = "unknown" ]; then
281
+ echo "safety:unknown evidence quality" > "$REASON_FILE";
282
+ elif [ "$mode" = "conflict" ]; then
283
+ echo "safety:conflicting evidence" > "$REASON_FILE";
284
+ else
285
+ echo "safety:unrecognized evidence (mode=$mode)" > "$REASON_FILE";
286
+ fi &&
287
+ printf '%s %s %s\n' "$RUN_ID" "$wbs" "$(cat "$REASON_FILE")"
288
+ >> .spur/memory/task-pipeline-routes.log &&
289
+ exit 0
282
290
 
283
- - id: implement
284
- description: >
285
- Run agent-driven implementation via /sp:dev-run --mode implement, THEN move the
286
- task to `wip` via the normal verb. The lifecycle transition fires AFTER the
287
- implement agent.run succeeds — onEnter runs in declaration order and the default
288
- 'fail' policy halts the sequence (routing the run to `failed`) if the agent.run
289
- fails or times out, so a halted implement never leaves the task at `wip` with an
290
- empty Solution (the F3 half-state). The `--no-lifecycle` flag is correct here: the
291
- pipeline owns this transition; the FSM guard would otherwise re-run `spur task
292
- check`, which is precheck's job.
293
- NOTE: this step must NOT call /sp:dev-run in full mode (the default) —
294
- that command DRIVES this pipeline, so calling it here recurses.
295
- --mode implement is the single-step implement entry.
296
- onEnter:
297
- - kind: agent.run
298
- options:
299
- agent: ${vars.implementAgent}
300
- # Pure slash command only (ADR-043). Anti-recursion / implement discipline
301
- # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
302
- role: coder
303
- # lives in /sp:dev-run --mode implement → sp:code-implementation, not YAML prose.
304
- input: /sp:dev-run --mode implement ${vars.wbs} --auto
305
- timeoutMs: ${vars.implementTimeoutMs}
306
- # R3 (task 0424): empty-implement no-op guard — the agent.run action
307
- # fails the step when exit 0 produced zero non-corpus file changes, so
308
- # a silent no-op routes the run to `failed` here instead of drifting
309
- # into test/review and being caught a full pass later.
310
- requireDiff: true
311
- # 0706 R6: this stage mutates the working tree unattended under the
312
- # auto profile, so it declares minimum execution-capability
313
- # requirements. Dispatch fails closed (before spawn) when the
314
- # resolved executor's attestation cannot satisfy them — attest the
315
- # executor in agent config.
316
- requiresCapabilities:
317
- fsWrite: available
318
- processSpawn: available
319
- - kind: shell
320
- options:
321
- command: >-
322
- retry_transient() {
323
- RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
324
- printf '%s\n' "$RETRY_OUTPUT";
325
- if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
326
- if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
327
- sleep 2;
328
- RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
329
- printf '%s\n' "$RETRY_SECOND_OUTPUT";
330
- if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
331
- echo "node_modules link/dependency broken — run bun install and retry";
332
- fi;
333
- return "$RETRY_RC";
334
- };
335
- retry_transient $spurBin task update "$wbs" wip --no-lifecycle
336
- # Post-implement cleanup: auto-format any unformatted output the agent produced.
337
- # The agent may leave files that don't pass `biome check`, which then fails
338
- # `bun run lint` downstream. Running format here is cheap (< 1 s) and prevents
339
- # the test stage from tripping on a purely mechanical formatting defect (dogfood
340
- # bug-733 — omp left agent-run.test.ts unformatted, causing a spurious lint gate
341
- # failure that the ## Testing section mis-attributed to "pre-existing gaps").
342
- # Best-effort: `; exit 0` (not `&&`) so a missing/failing formatter in a seeded
343
- # non-Bun project cannot abort a run mid-implement. Formatting is a convenience;
344
- # `${vars.qualityGateCmd}` at `test` is the gate that actually decides.
345
- - kind: shell
346
- options:
347
- command: "$formatCmd ; exit 0"
291
+ - id: implement
292
+ description: >
293
+ Run agent-driven implementation via /sp:dev-run --mode implement, THEN move the
294
+ task to `wip` via the normal verb. The lifecycle transition fires AFTER the
295
+ implement agent.run succeeds — onEnter runs in declaration order and the default
296
+ 'fail' policy halts the sequence (routing the run to `failed`) if the agent.run
297
+ fails or times out, so a halted implement never leaves the task at `wip` with an
298
+ empty Solution (the F3 half-state). The `--no-lifecycle` flag is correct here: the
299
+ pipeline owns this transition; the FSM guard would otherwise re-run `spur task
300
+ check`, which is precheck's job.
301
+ NOTE: this step must NOT call /sp:dev-run in full mode (the default) —
302
+ that command DRIVES this pipeline, so calling it here recurses.
303
+ --mode implement is the single-step implement entry.
304
+ onEnter:
305
+ - kind: agent.run
306
+ options:
307
+ agent: ${vars.implementAgent}
308
+ # Pure slash command only (ADR-043). Anti-recursion / implement discipline
309
+ # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
310
+ role: coder
311
+ # lives in /sp:dev-run --mode implement → sp:code-implementation, not YAML prose.
312
+ input: /sp:dev-run --mode implement ${vars.wbs} --auto
313
+ timeoutMs: ${vars.implementTimeoutMs}
314
+ # R3 (task 0424): empty-implement no-op guard — the agent.run action
315
+ # fails the step when exit 0 produced zero non-corpus file changes, so
316
+ # a silent no-op routes the run to `failed` here instead of drifting
317
+ # into test/review and being caught a full pass later.
318
+ requireDiff: true
319
+ # 0706 R6: this stage mutates the working tree unattended under the
320
+ # auto profile, so it declares minimum execution-capability
321
+ # requirements. Dispatch fails closed (before spawn) when the
322
+ # resolved executor's attestation cannot satisfy them — attest the
323
+ # executor in agent config.
324
+ requiresCapabilities:
325
+ fsWrite: available
326
+ processSpawn: available
327
+ - kind: shell
328
+ options:
329
+ command: >-
330
+ retry_transient() {
331
+ RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
332
+ printf '%s\n' "$RETRY_OUTPUT";
333
+ if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
334
+ if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
335
+ sleep 2;
336
+ RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
337
+ printf '%s\n' "$RETRY_SECOND_OUTPUT";
338
+ if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
339
+ echo "node_modules link/dependency broken — run bun install and retry";
340
+ fi;
341
+ return "$RETRY_RC";
342
+ };
343
+ retry_transient $spurBin task update "$wbs" wip --no-lifecycle
344
+ # Post-implement cleanup: auto-format any unformatted output the agent produced.
345
+ # The agent may leave files that don't pass `biome check`, which then fails
346
+ # `bun run lint` downstream. Running format here is cheap (< 1 s) and prevents
347
+ # the test stage from tripping on a purely mechanical formatting defect (dogfood
348
+ # bug-733 — omp left agent-run.test.ts unformatted, causing a spurious lint gate
349
+ # failure that the ## Testing section mis-attributed to "pre-existing gaps").
350
+ # Best-effort: `; exit 0` (not `&&`) so a missing/failing formatter in a seeded
351
+ # non-Bun project cannot abort a run mid-implement. Formatting is a convenience;
352
+ # `${vars.qualityGateCmd}` at `test` is the gate that actually decides.
353
+ - kind: shell
354
+ options:
355
+ command: "$formatCmd ; exit 0"
348
356
 
349
- # ── test hop (quality gate + bounded auto-fix) ─────────────────────────────
350
- # NOT /sp:dev-unit. That command (sp:code-testing) *extends/generates* tests toward
351
- # a coverage target; it is not the project quality gate. Coverage gap-fill remains
352
- # available via router C3/C5 (`/sp:dev-unit`) or inside implement — not this hop.
353
- #
354
- # Shape (engine has no per-action "run next only if previous failed" without
355
- # `onError`, and Spur's action schema does not expose `onError`):
356
- # test — soft probe: qualityGateCmd → PASS|FAIL status; always exit 0
357
- # (green path: one gate run → review; no fixall/recheck tax)
358
- # test-fix — increment attempt + pure slash /sp:dev-fixall (≤ max attempts)
359
- # test-recheck — soft recheck → review | test-fix | failed (never silent lifecycle abort)
360
- - id: test
361
- description: >
362
- Proof-chain entry + soft quality-gate probe (single logical gate on the green path; bounded retries only
363
- for SQLite lock contention). Capture of the canonical proof-input digest happens HERE — before any
364
- evidence-producing final check (task 0703, ADR-071) — so the gate, review, and verification evidence
365
- all name one digest. Then runs
366
- `${vars.qualityGateCmd}`, records PASS|FAIL under
367
- `.spur/run/<wbs>-test-gate.status`, resets the fix attempt counter, always exit 0.
368
- onEnter:
369
- # R6/R2 (0703): resolve the task-spec path BEFORE the digest capture. `docs/tasks*` is excluded
370
- # from the digest's git-tree half, so the spec is folded in explicitly via `taskSpecPath`.
371
- - kind: shell
372
- options:
373
- # 0710 R4: resolve the spec path, then extract `priority:` from the TASK FILE itself (not the
374
- # path listing); normalize to upper so requiresDistinctExecutor's exact 'P0'/'P1' match hits.
375
- # 0751 R2: the task path is NOT optional - an unresolved lookup fails
376
- # the step (no `|| true`, no forced `exit 0`, no stderr suppression)
377
- # instead of degrading the proof to whole-tree-only. The priority read
378
- # stays tolerant: a missing line is genuinely optional.
379
- 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"'
380
- - kind: file.read.into-var
381
- options:
382
- path: .spur/run/${vars.wbs}-taskpath.txt
383
- var: taskSpecPath
384
- # 0710 R4: carry the task's priority tier into the review/verify risk policy.
385
- - kind: file.read.into-var
386
- options:
387
- path: .spur/run/${vars.wbs}-priority.txt
388
- var: taskPriority
389
- # R2 (0703, ADR-071): THE canonical proof capture. Placement is load-bearing: immediately before
390
- # the final evidence chain, after every implement mutation (including the post-implement format).
391
- # Capture-only here; `record` compares. A remediation pass re-captures at `test-recheck` (R4).
392
- - kind: proof.fingerprint
393
- options:
394
- var: proofDigest
395
- taskFile: ${vars.taskSpecPath}
396
- - kind: shell
397
- options:
398
- command: >-
399
- mkdir -p .spur/run &&
400
- STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
401
- LOG_FILE=".spur/run/$wbs-test-gate.log" &&
402
- ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
403
- echo 0 > "$ATTEMPT_FILE" &&
404
- : > "$LOG_FILE" &&
405
- gate_attempt=1;
406
- while [ "$gate_attempt" -le 5 ]; do
407
- ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
408
- sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
409
- gate_locked=0;
410
- grep -q 'SQLiteError: database is locked' "$ATTEMPT_LOG" && gate_locked=1;
411
- cat "$ATTEMPT_LOG" >> "$LOG_FILE";
412
- rm -f "$ATTEMPT_LOG";
413
- if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
414
- printf 'quality gate: database is locked; retrying (%s/5) in 10s\n' "$gate_attempt" | tee -a "$LOG_FILE";
415
- sleep 10;
416
- gate_attempt=$((gate_attempt + 1));
417
- done &&
418
- cat "$LOG_FILE" &&
419
- FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
420
- set +e; grep -oE '[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+' "$LOG_FILE" | sort -u | head -20 | tr '\n' ' ' > "$FINDINGS_FILE"; set -e &&
421
- if [ "$gate_rc" -eq 0 ]; then
422
- printf 'PASS\n' > "$STATUS_FILE";
423
- else
424
- printf 'FAIL\n' > "$STATUS_FILE";
425
- fi &&
426
- printf 'proof-digest: %s\n' "$proofDigest" >> "$LOG_FILE" &&
427
- exit 0
357
+ # ── test hop (quality gate + bounded auto-fix) ─────────────────────────────
358
+ # NOT /sp:dev-unit. That command (sp:code-testing) *extends/generates* tests toward
359
+ # a coverage target; it is not the project quality gate. Coverage gap-fill remains
360
+ # available via router C3/C5 (`/sp:dev-unit`) or inside implement — not this hop.
361
+ #
362
+ # Shape (engine has no per-action "run next only if previous failed" without
363
+ # `onError`, and Spur's action schema does not expose `onError`):
364
+ # test — soft probe: qualityGateCmd → PASS|FAIL status; always exit 0
365
+ # (green path: one gate run → review; no fixall/recheck tax)
366
+ # test-fix — increment attempt + pure slash /sp:dev-fixall (≤ max attempts)
367
+ # test-recheck — soft recheck → review | test-fix | failed (never silent lifecycle abort)
368
+ - id: test
369
+ description: >
370
+ Proof-chain entry + soft quality-gate probe (single logical gate on the green path; bounded retries only
371
+ for SQLite lock contention). Capture of the canonical proof-input digest happens HERE — before any
372
+ evidence-producing final check (task 0703, ADR-071) — so the gate, review, and verification evidence
373
+ all name one digest. Then runs
374
+ `${vars.qualityGateCmd}`, records PASS|FAIL under
375
+ `.spur/run/<wbs>-test-gate.status`, resets the fix attempt counter, always exit 0.
376
+ onEnter:
377
+ # R6/R2 (0703): resolve the task-spec path BEFORE the digest capture. `docs/tasks*` is excluded
378
+ # from the digest's git-tree half, so the spec is folded in explicitly via `taskSpecPath`.
379
+ - kind: shell
380
+ options:
381
+ # 0710 R4: resolve the spec path, then extract `priority:` from the TASK FILE itself (not the
382
+ # path listing); normalize to upper so requiresDistinctExecutor's exact 'P0'/'P1' match hits.
383
+ # 0751 R2: the task path is NOT optional - an unresolved lookup fails
384
+ # the step (no `|| true`, no forced `exit 0`, no stderr suppression)
385
+ # instead of degrading the proof to whole-tree-only. The priority read
386
+ # stays tolerant: a missing line is genuinely optional.
387
+ 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"'
388
+ - kind: file.read.into-var
389
+ options:
390
+ path: .spur/run/${vars.wbs}-taskpath.txt
391
+ var: taskSpecPath
392
+ # 0710 R4: carry the task's priority tier into the review/verify risk policy.
393
+ - kind: file.read.into-var
394
+ options:
395
+ path: .spur/run/${vars.wbs}-priority.txt
396
+ var: taskPriority
397
+ # R2 (0703, ADR-071): THE canonical proof capture. Placement is load-bearing: immediately before
398
+ # the final evidence chain, after every implement mutation (including the post-implement format).
399
+ # Capture-only here; `record` compares. A remediation pass re-captures at `test-recheck` (R4).
400
+ - kind: proof.fingerprint
401
+ options:
402
+ var: proofDigest
403
+ taskFile: ${vars.taskSpecPath}
404
+ - kind: shell
405
+ options:
406
+ command: >-
407
+ mkdir -p .spur/run &&
408
+ STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
409
+ LOG_FILE=".spur/run/$wbs-test-gate.log" &&
410
+ ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
411
+ echo 0 > "$ATTEMPT_FILE" &&
412
+ : > "$LOG_FILE" &&
413
+ gate_attempt=1;
414
+ while [ "$gate_attempt" -le 5 ]; do
415
+ ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
416
+ sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
417
+ gate_locked=0;
418
+ grep -q 'SQLiteError: database is locked' "$ATTEMPT_LOG" && gate_locked=1;
419
+ cat "$ATTEMPT_LOG" >> "$LOG_FILE";
420
+ rm -f "$ATTEMPT_LOG";
421
+ if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
422
+ printf 'quality gate: database is locked; retrying (%s/5) in 10s\n' "$gate_attempt" | tee -a "$LOG_FILE";
423
+ sleep 10;
424
+ gate_attempt=$((gate_attempt + 1));
425
+ done &&
426
+ if [ "$gate_rc" -eq 0 ]; then
427
+ printf 'quality gate PASS (attempts: %s; log: %s; bytes: %s)\n' "$gate_attempt" "$LOG_FILE" "$(wc -c < "$LOG_FILE" | tr -d "[:space:]")";
428
+ else
429
+ printf 'quality gate FAIL — last 40 lines follow (full log: %s)\n' "$LOG_FILE";
430
+ tail -n 40 "$LOG_FILE";
431
+ fi &&
432
+ FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
433
+ set +e; grep -oE '[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+' "$LOG_FILE" | sort -u | head -20 | tr '\n' ' ' > "$FINDINGS_FILE"; set -e &&
434
+ if [ "$gate_rc" -eq 0 ]; then
435
+ printf 'PASS\n' > "$STATUS_FILE";
436
+ else
437
+ printf 'FAIL\n' > "$STATUS_FILE";
438
+ fi &&
439
+ printf 'proof-digest: %s\n' "$proofDigest" >> "$LOG_FILE" &&
440
+ exit 0
428
441
 
429
- - id: test-fix
430
- description: >
431
- Bounded auto-fix hop when the quality gate is red OR final verification returned a
432
- repairable non-PASS (task 0703 R4: remediation never happens inside verify — it loops
433
- through here, then re-enters quality → review → verify on a fresh digest). Increments
434
- `.spur/run/<wbs>-test-fix-attempt` (the shared bound with the quality path), projects the
435
- verify verdict into the gate log when one exists so the repair hop sees it, then pure
436
- slash (ADR-043) `/sp:dev-fixall` against `${vars.qualityGateCmd}`.
437
- onEnter:
438
- - kind: shell
439
- options:
440
- command: >-
441
- mkdir -p .spur/run &&
442
- ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
443
- n=$(cat "$ATTEMPT_FILE" 2>/dev/null || echo 0) &&
444
- printf '%s\n' "$((n + 1))" > "$ATTEMPT_FILE" &&
445
- if [ -f ".spur/run/$wbs-verdict.json" ]; then
446
- { echo '--- verify verdict (remediation input, task 0703 R4) ---';
447
- cat ".spur/run/$wbs-verdict.json";
448
- } >> ".spur/run/$wbs-test-gate.log";
449
- fi;
450
- exit 0
451
- # R3 (0482): project the extracted gate anchors into a var so the dispatch input
452
- # can NAME the failing file:line, not merely point at a log. A vars template cannot
453
- # run a shell, so the read is an action, not an inline `$(cat ...)` substitution.
454
- - kind: file.read.into-var
455
- options:
456
- path: .spur/run/${vars.wbs}-test-gate.findings
457
- var: gateFindings
458
- - kind: agent.run
459
- options:
460
- agent: ${vars.agent}
461
- # R3 (0482): `--findings` names the failing anchors inline; `--gate-log` remains
462
- # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
463
- role: coder
464
- # the full-context escape hatch when the digest is not enough.
465
- input: /sp:dev-fixall "${vars.qualityGateCmd}" --gate-log .spur/run/${vars.wbs}-test-gate.log --findings "${vars.gateFindings}"
466
- timeoutMs: ${vars.stepTimeoutMs}
467
- # 0706 R6: bounded remediation hop unattended and tree-mutating,
468
- # so it declares the same minimum requirements as `implement`.
469
- requiresCapabilities:
470
- fsWrite: available
471
- processSpawn: available
442
+ - id: test-fix
443
+ description: >
444
+ Bounded auto-fix hop when the quality gate is red OR final verification returned a
445
+ repairable non-PASS (task 0703 R4: remediation never happens inside verify — it loops
446
+ through here, then re-enters quality → review → verify on a fresh digest). Increments
447
+ `.spur/run/<wbs>-test-fix-attempt` (the shared bound with the quality path), projects the
448
+ verify verdict into the gate log when one exists so the repair hop sees it, then pure
449
+ slash (ADR-043) `/sp:dev-fixall` against `${vars.qualityGateCmd}`.
450
+ onEnter:
451
+ - kind: shell
452
+ options:
453
+ command: >-
454
+ TASK_POLICY=$($spurBin task show "$wbs" --json |
455
+ 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;
456
+ if [ "$mutationPolicy:$TASK_POLICY" != "code:code" ]; then
457
+ echo "test-fix: mutation policy forbids automatic code repair (run=$mutationPolicy, task=$TASK_POLICY); gate failure preserved, no agent dispatched" >&2;
458
+ exit 1;
459
+ fi;
460
+ mkdir -p .spur/run &&
461
+ ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
462
+ n=$(cat "$ATTEMPT_FILE" 2>/dev/null || echo 0) &&
463
+ printf '%s\n' "$((n + 1))" > "$ATTEMPT_FILE" &&
464
+ if [ -f ".spur/run/$wbs-verdict.json" ]; then
465
+ { echo '--- verify verdict (remediation input, task 0703 R4) ---';
466
+ cat ".spur/run/$wbs-verdict.json";
467
+ } >> ".spur/run/$wbs-test-gate.log";
468
+ fi;
469
+ exit 0
470
+ # R3 (0482): project the extracted gate anchors into a var so the dispatch input
471
+ # can NAME the failing file:line, not merely point at a log. A vars template cannot
472
+ # run a shell, so the read is an action, not an inline `$(cat ...)` substitution.
473
+ - kind: file.read.into-var
474
+ options:
475
+ path: .spur/run/${vars.wbs}-test-gate.findings
476
+ var: gateFindings
477
+ - kind: agent.run
478
+ options:
479
+ agent: ${vars.agent}
480
+ # R3 (0482): `--findings` names the failing anchors inline; `--gate-log` remains
481
+ # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
482
+ role: coder
483
+ # the full-context escape hatch when the digest is not enough.
484
+ input: /sp:dev-fixall "${vars.qualityGateCmd}" --gate-log .spur/run/${vars.wbs}-test-gate.log --findings "${vars.gateFindings}"
485
+ timeoutMs: ${vars.stepTimeoutMs}
486
+ # 0706 R6: bounded remediation hop — unattended and tree-mutating,
487
+ # so it declares the same minimum requirements as `implement`.
488
+ requiresCapabilities:
489
+ fsWrite: available
490
+ processSpawn: available
472
491
 
473
- - id: test-recheck
474
- description: >
475
- Soft recheck after fixall with bounded SQLite-lock retry. Writes PASS|FAIL (always exit 0). Transitions
476
- branch to review (PASS), another test-fix (FAIL and under max attempts),
477
- or the pipeline `failed` state (FAIL and attempts exhausted) — never a
478
- raw lifecycle abort that skips the terminal `failed` state.
479
- onEnter:
480
- # R4 (0703, ADR-071): bounded remediation mutated the tree by design, so the fresh evidence
481
- # chain (recheck gate → review → verify) must start from a NEWLY captured digest. Capture-only;
482
- # the guards and `record` compare against this value.
483
- - kind: proof.fingerprint
484
- options:
485
- var: proofDigest
486
- taskFile: ${vars.taskSpecPath}
487
- # 0587 R3: probe-then-full recheck. A red gateProbeCmd records FAIL and skips the full
488
- # gate (the measured waste is re-running a 110–140s gate to learn the tree is still red);
489
- # a green probe (or empty gateProbeCmd) falls through to the full-gate loop unchanged.
490
- # Only the full gate writes PASS, so the `test-recheck → review` guard (reads PASS)
491
- # still means a full green qualityGateCmd ran — invariant preserved by construction.
492
- - kind: shell
493
- options:
494
- command: >-
495
- mkdir -p .spur/run &&
496
- STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
497
- LOG_FILE=".spur/run/$wbs-test-gate.log" &&
498
- FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
499
- : > "$LOG_FILE" &&
500
- probe_rc=0;
501
- if [ -n "$gateProbeCmd" ]; then
502
- sh -c "$gateProbeCmd" > "$LOG_FILE.probe" 2>&1; probe_rc=$?;
503
- cat "$LOG_FILE.probe" >> "$LOG_FILE";
504
- rm -f "$LOG_FILE.probe";
505
- fi;
506
- if [ "$probe_rc" -ne 0 ]; then
507
- gate_rc=$probe_rc;
508
- else
509
- gate_attempt=1;
510
- while [ "$gate_attempt" -le 5 ]; do
511
- ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
512
- sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
513
- gate_locked=0;
514
- grep -q 'SQLiteError: database is locked' "$ATTEMPT_LOG" && gate_locked=1;
515
- cat "$ATTEMPT_LOG" >> "$LOG_FILE";
516
- rm -f "$ATTEMPT_LOG";
517
- if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
518
- printf 'quality gate: database is locked; retrying (%s/5) in 10s\n' "$gate_attempt" | tee -a "$LOG_FILE";
519
- sleep 10;
520
- gate_attempt=$((gate_attempt + 1));
521
- done;
522
- fi &&
523
- cat "$LOG_FILE" &&
524
- set +e; grep -oE '[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+' "$LOG_FILE" | sort -u | head -20 | tr '\n' ' ' > "$FINDINGS_FILE"; set -e &&
525
- if [ "$gate_rc" -eq 0 ]; then
526
- printf 'PASS\n' > "$STATUS_FILE";
527
- else
528
- printf 'FAIL\n' > "$STATUS_FILE";
529
- fi &&
530
- printf 'proof-digest: %s\n' "$proofDigest" >> "$LOG_FILE" &&
531
- exit 0
492
+ - id: test-recheck
493
+ description: >
494
+ Soft recheck after fixall with bounded SQLite-lock retry. Writes PASS|FAIL (always exit 0). Transitions
495
+ branch to review (PASS), another test-fix (FAIL and under max attempts),
496
+ or the pipeline `failed` state (FAIL and attempts exhausted) — never a
497
+ raw lifecycle abort that skips the terminal `failed` state.
498
+ onEnter:
499
+ # R4 (0703, ADR-071): bounded remediation mutated the tree by design, so the fresh evidence
500
+ # chain (recheck gate → review → verify) must start from a NEWLY captured digest. Capture-only;
501
+ # the guards and `record` compare against this value.
502
+ - kind: proof.fingerprint
503
+ options:
504
+ var: proofDigest
505
+ taskFile: ${vars.taskSpecPath}
506
+ # 0587 R3: probe-then-full recheck. A red gateProbeCmd records FAIL and skips the full
507
+ # gate (the measured waste is re-running a 110–140s gate to learn the tree is still red);
508
+ # a green probe (or empty gateProbeCmd) falls through to the full-gate loop unchanged.
509
+ # Only the full gate writes PASS, so the `test-recheck → review` guard (reads PASS)
510
+ # still means a full green qualityGateCmd ran — invariant preserved by construction.
511
+ - kind: shell
512
+ options:
513
+ command: >-
514
+ mkdir -p .spur/run &&
515
+ STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
516
+ LOG_FILE=".spur/run/$wbs-test-gate.log" &&
517
+ FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
518
+ : > "$LOG_FILE" &&
519
+ probe_rc=0;
520
+ if [ -n "$gateProbeCmd" ]; then
521
+ sh -c "$gateProbeCmd" > "$LOG_FILE.probe" 2>&1; probe_rc=$?;
522
+ cat "$LOG_FILE.probe" >> "$LOG_FILE";
523
+ rm -f "$LOG_FILE.probe";
524
+ fi;
525
+ if [ "$probe_rc" -ne 0 ]; then
526
+ gate_rc=$probe_rc;
527
+ else
528
+ gate_attempt=1;
529
+ while [ "$gate_attempt" -le 5 ]; do
530
+ ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
531
+ sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
532
+ gate_locked=0;
533
+ grep -q 'SQLiteError: database is locked' "$ATTEMPT_LOG" && gate_locked=1;
534
+ cat "$ATTEMPT_LOG" >> "$LOG_FILE";
535
+ rm -f "$ATTEMPT_LOG";
536
+ if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
537
+ printf 'quality gate: database is locked; retrying (%s/5) in 10s\n' "$gate_attempt" | tee -a "$LOG_FILE";
538
+ sleep 10;
539
+ gate_attempt=$((gate_attempt + 1));
540
+ done;
541
+ fi &&
542
+ if [ "$gate_rc" -eq 0 ]; then
543
+ printf 'quality gate PASS (attempts: %s; log: %s; bytes: %s)\n' "$gate_attempt" "$LOG_FILE" "$(wc -c < "$LOG_FILE" | tr -d "[:space:]")";
544
+ else
545
+ printf 'quality gate FAIL — last 40 lines follow (full log: %s)\n' "$LOG_FILE";
546
+ tail -n 40 "$LOG_FILE";
547
+ fi &&
548
+ set +e; grep -oE '[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+' "$LOG_FILE" | sort -u | head -20 | tr '\n' ' ' > "$FINDINGS_FILE"; set -e &&
549
+ if [ "$gate_rc" -eq 0 ]; then
550
+ printf 'PASS\n' > "$STATUS_FILE";
551
+ else
552
+ printf 'FAIL\n' > "$STATUS_FILE";
553
+ fi &&
554
+ printf 'proof-digest: %s\n' "$proofDigest" >> "$LOG_FILE" &&
555
+ exit 0
532
556
 
533
- - id: review
534
- description: Three-dimensional code review via /sp:dev-review (functional requirements traceability + SECUA framework (Security, Efficiency, Correctness, Usability, Architecture) + architecture depth), findings written to `## Review`.
535
- onEnter:
536
- - kind: agent.run
537
- options:
538
- # 0710 R2: review always runs on a fresh session — no implementation-session
539
- # inheritance, no latch resume; implementation context reaches the reviewer
540
- # only via the persisted task spec, the recorded diff, and run artifacts.
541
- # 0710 R4/R5: the agent pin is deliberately gone — role: reviewer routes
542
- # through the executor registry, and the runner enforces (pre-dispatch,
543
- # fail-closed) that a P0/P1 task's review resolves a DIFFERENT executor
544
- # spec than the implement stage recorded in __agentRouting_implement.
545
- input: /sp:dev-review ${vars.wbs} --auto
546
- # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
547
- role: reviewer
548
- freshSession: true
549
- priority: ${vars.taskPriority}
550
- compareExecutorWith: implement
551
- timeoutMs: ${vars.stepTimeoutMs}
557
+ - id: review
558
+ description: Three-dimensional code review via /sp:dev-review (functional requirements traceability + SECUA framework (Security, Efficiency, Correctness, Usability, Architecture) + architecture depth), findings written to `## Review`.
559
+ onEnter:
560
+ - kind: agent.run
561
+ options:
562
+ # 0710 R2: review always runs on a fresh session — no implementation-session
563
+ # inheritance, no latch resume; implementation context reaches the reviewer
564
+ # only via the persisted task spec, the recorded diff, and run artifacts.
565
+ # 0710 R4/R5: the agent pin is deliberately gone — role: reviewer routes
566
+ # through the executor registry, and the runner enforces (pre-dispatch,
567
+ # fail-closed) that a P0/P1 task's review resolves a DIFFERENT executor
568
+ # spec than the implement stage recorded in __agentRouting_implement.
569
+ input: /sp:dev-review ${vars.wbs} --auto
570
+ # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
571
+ role: reviewer
572
+ freshSession: true
573
+ priority: ${vars.taskPriority}
574
+ compareExecutorWith: implement
575
+ timeoutMs: ${vars.stepTimeoutMs}
552
576
 
553
- - id: approve
554
- description: >
555
- Human-in-the-loop approval gate. Under `profile=auto` this state is never entered
556
- (review routes around it straight to verify). In interactive mode this state pauses
557
- the run for `spur workflow continue` (E3), making approval an explicit operator action.
558
- The operator's answer is routed (R1, bug-750): yes -> verify; no -> failed (rejection
559
- recorded, not silently approved); cancel -> cancelled (a distinct terminal state).
560
- pause: true
561
- onEnter:
562
- - kind: hitl.confirm
563
- options:
564
- prompt: "Approve task ${vars.wbs} to proceed to verification?"
577
+ - id: approve
578
+ description: >
579
+ Human-in-the-loop approval gate. Under `profile=auto` this state is never entered
580
+ (review routes around it straight to verify). In interactive mode this state pauses
581
+ the run for `spur workflow continue` (E3), making approval an explicit operator action.
582
+ The operator's answer is routed (R1, bug-750): yes -> verify; no -> failed (rejection
583
+ recorded, not silently approved); cancel -> cancelled (a distinct terminal state).
584
+ pause: true
585
+ onEnter:
586
+ - kind: hitl.confirm
587
+ options:
588
+ prompt: "Approve task ${vars.wbs} to proceed to verification?"
565
589
 
566
- - id: verify
567
- description: >
568
- Observe-only functional verification (BDD + traceability) via /sp:dev-verify --fix none
569
- (task 0703 R1, ADR-071): the verifier certifies the state, it never repairs its own subject.
570
- A live digest compare BEFORE the agent refuses to certify a state that drifted after the
571
- quality/review evidence was produced (R2). The agent's
572
- captured answer is written to `.spur/run/<wbs>-verify-answer.txt` and MUST follow
573
- the answer-file schema contract (explicit `Verdict: PASS|PARTIAL|FAIL` line plus
574
- `| Req | Status | Evidence |` and `| AC | Status | Evidence Type | Evidence |` tables);
575
- a deterministic shell step then derives the verdict and writes the gate artifact
576
- `.spur/run/<wbs>-verdict.json` (so the completion gate reads a real verdict, not
577
- agent discretion — R9) with a proof block naming the digest and the per-stage results (R3).
578
- Repairable non-PASS routes once through the bounded remediation hop (verify → test-fix,
579
- R4); the chain reruns on a fresh digest.
580
- onEnter:
581
- # R2 (0703): midpoint bracket compare — refuse to spend verification on a state that no
582
- # longer matches the digest the quality/review evidence names. Reuses `proofDigestNow`:
583
- # set here and re-set by the final compare at `record` entry.
584
- - kind: proof.fingerprint
585
- options:
586
- var: proofDigestNow
587
- taskFile: ${vars.taskSpecPath}
588
- expect: ${vars.proofDigest}
589
- - kind: agent.run
590
- options:
591
- # 0710 R2: verify is a separate fresh-session execution — never the review
592
- # session, never the implement session (R6).
593
- # 0710 R4/R5: role-routed like review; P0/P1 demands a distinct executor.
594
- # R1 (0703, ADR-071): `--fix none` — final verification is observe-only. Remediation
595
- # belongs to the bounded test-fix hop, never to the certifying pass.
596
- input: /sp:dev-verify ${vars.wbs} --auto --fix none --focus all
597
- # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
598
- role: reviewer
599
- freshSession: true
600
- priority: ${vars.taskPriority}
601
- compareExecutorWith: implement
602
- timeoutMs: ${vars.stepTimeoutMs}
603
- expectFile: .spur/run/${vars.wbs}-verify-answer.txt
604
- # 0726 R3: hard lint gate over the verifier-owned answer — shape and
605
- # evidence-row identity, before the verdict derivation reads it.
606
- # Hard action: a malformed answer halts the sequence here instead of
607
- # poisoning the verdict parse downstream.
608
- - kind: shell
609
- options:
610
- command: >-
611
- LINT_SCRIPT="plugins/sp/scripts/verify-answer-lint.ts";
612
- [ -f "$LINT_SCRIPT" ] ||
613
- LINT_SCRIPT="$(superskill script path sp verify-answer-lint.ts 2>/dev/null)";
614
- if [ -z "$LINT_SCRIPT" ] || [ ! -f "$LINT_SCRIPT" ]; then
615
- echo "verify-answer-lint: checker not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'" >&2;
616
- exit 1;
617
- fi;
618
- bun "$LINT_SCRIPT" "$wbs"
619
- --answer ".spur/run/$wbs-verify-answer.txt"
620
- --spur-bin "$spurBin"
621
- - kind: shell
622
- options:
623
- command: "$spurBin task verdict $wbs --from-answer .spur/run/$wbs-verify-answer.txt"
624
- # R3 (0703): write the required proof block into the verdict artifact — the digest, the
625
- # certifying run id, the capture point, and the named per-stage results, each stage carrying
626
- # the SAME digest value (prose asserting proof validity is insufficient). `runId` closes
627
- # 0730 §B.2 (task 0757 R4): without it the verified-outcome fold has to accept ANY linked
628
- # run as certifying, so a dry-run probe linked to the same wbs reads as proof of completion.
629
- # `definitionDigest` closes 0759 R5: the record binds to the certifying run AND the exact
630
- # workflow definition it executed — a stale-definition resume or a definition edited
631
- # between run and record cannot certify silently. `__definitionDigest` is injected at run
632
- # start (workflow-service.ts) and equals the digest stamped on the run row (task 0603).
633
- # Also keeps the flat
634
- # `proof-input-digest` check row for consumers that read `checks[]`. Soft action + hard
635
- # guard: a missing/malformed stamp fails the `verify → record` guard below, not this step.
636
- - kind: shell
637
- options:
638
- command: >-
639
- V=".spur/run/$wbs-verdict.json";
640
- if [ -f "$V" ] && [ -n "$proofDigest" ]; then
641
- jq --arg d "$proofDigest" --arg r "$__runId" --arg dd "$__definitionDigest" --arg g "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null || echo UNKNOWN)"
642
- '. + {proof: {digest: $d, runId: $r, definitionDigest: $dd, capturePoint: "quality-gate-entry", stages: {
643
- qualityGate: {status: $g, digest: $d},
644
- review: {status: "completed", digest: $d},
645
- verification: {status: .verdict, digest: $d}}}}
646
- | .checks += [{name: "proof-input-digest", status: "pass", evidence: $d}]'
647
- "$V" > "$V.tmp" && mv "$V.tmp" "$V";
648
- fi;
649
- exit 0
590
+ - id: verify
591
+ description: >
592
+ Observe-only functional verification (BDD + traceability) via /sp:dev-verify --fix none
593
+ (task 0703 R1, ADR-071): the verifier certifies the state, it never repairs its own subject.
594
+ A live digest compare BEFORE the agent refuses to certify a state that drifted after the
595
+ quality/review evidence was produced (R2). The agent's
596
+ captured answer is written to `.spur/run/<wbs>-verify-answer.txt` and MUST follow
597
+ the answer-file schema contract (explicit `Verdict: PASS|PARTIAL|FAIL` line plus
598
+ `| Req | Status | Evidence |` and `| AC | Status | Evidence Type | Evidence |` tables);
599
+ a deterministic shell step then derives the verdict and writes the gate artifact
600
+ `.spur/run/<wbs>-verdict.json` (so the completion gate reads a real verdict, not
601
+ agent discretion — R9) with a proof block naming the digest and the per-stage results (R3).
602
+ Repairable non-PASS routes once through the bounded remediation hop (verify → test-fix,
603
+ R4); the chain reruns on a fresh digest.
604
+ onEnter:
605
+ # R2 (0703): midpoint bracket compare — refuse to spend verification on a state that no
606
+ # longer matches the digest the quality/review evidence names. Reuses `proofDigestNow`:
607
+ # set here and re-set by the final compare at `record` entry.
608
+ - kind: proof.fingerprint
609
+ options:
610
+ var: proofDigestNow
611
+ taskFile: ${vars.taskSpecPath}
612
+ expect: ${vars.proofDigest}
613
+ - kind: agent.run
614
+ options:
615
+ # 0710 R2: verify is a separate fresh-session execution — never the review
616
+ # session, never the implement session (R6).
617
+ # 0710 R4/R5: role-routed like review; P0/P1 demands a distinct executor.
618
+ # R1 (0703, ADR-071): `--fix none` — final verification is observe-only. Remediation
619
+ # belongs to the bounded test-fix hop, never to the certifying pass.
620
+ input: /sp:dev-verify ${vars.wbs} --auto --fix none --focus all
621
+ # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
622
+ role: reviewer
623
+ freshSession: true
624
+ priority: ${vars.taskPriority}
625
+ compareExecutorWith: implement
626
+ timeoutMs: ${vars.stepTimeoutMs}
627
+ expectFile: .spur/run/${vars.wbs}-verify-answer.txt
628
+ # 0726 R3: hard lint gate over the verifier-owned answer — shape and
629
+ # evidence-row identity, before the verdict derivation reads it.
630
+ # Hard action: a malformed answer halts the sequence here instead of
631
+ # poisoning the verdict parse downstream.
632
+ - kind: shell
633
+ options:
634
+ command: >-
635
+ LINT_SCRIPT="plugins/sp/scripts/verify-answer-lint.ts";
636
+ [ -f "$LINT_SCRIPT" ] ||
637
+ LINT_SCRIPT="$(superskill script path sp verify-answer-lint.ts 2>/dev/null)";
638
+ if [ -z "$LINT_SCRIPT" ] || [ ! -f "$LINT_SCRIPT" ]; then
639
+ echo "verify-answer-lint: checker not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'" >&2;
640
+ exit 1;
641
+ fi;
642
+ bun "$LINT_SCRIPT" "$wbs"
643
+ --answer ".spur/run/$wbs-verify-answer.txt"
644
+ --spur-bin "$spurBin"
645
+ - kind: shell
646
+ options:
647
+ command: "$spurBin task verdict $wbs --from-answer .spur/run/$wbs-verify-answer.txt"
648
+ # R3 (0703): write the required proof block into the verdict artifact — the digest, the
649
+ # certifying run id, the capture point, and the named per-stage results, each stage carrying
650
+ # the SAME digest value (prose asserting proof validity is insufficient). `runId` closes
651
+ # 0730 §B.2 (task 0757 R4): without it the verified-outcome fold has to accept ANY linked
652
+ # run as certifying, so a dry-run probe linked to the same wbs reads as proof of completion.
653
+ # `definitionDigest` closes 0759 R5: the record binds to the certifying run AND the exact
654
+ # workflow definition it executed — a stale-definition resume or a definition edited
655
+ # between run and record cannot certify silently. `__definitionDigest` is injected at run
656
+ # start (workflow-service.ts) and equals the digest stamped on the run row (task 0603).
657
+ # Also keeps the flat
658
+ # `proof-input-digest` check row for consumers that read `checks[]`. Soft action + hard
659
+ # guard: a missing/malformed stamp fails the `verify → record` guard below, not this step.
660
+ - kind: shell
661
+ options:
662
+ command: >-
663
+ V=".spur/run/$wbs-verdict.json";
664
+ if [ -f "$V" ] && [ -n "$proofDigest" ]; then
665
+ jq --arg d "$proofDigest" --arg r "$__runId" --arg dd "$__definitionDigest" --arg g "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null || echo UNKNOWN)"
666
+ '. + {proof: {digest: $d, runId: $r, definitionDigest: $dd, capturePoint: "quality-gate-entry", stages: {
667
+ qualityGate: {status: $g, digest: $d},
668
+ review: {status: "completed", digest: $d},
669
+ verification: {status: .verdict, digest: $d}}}}
670
+ | .checks += [{name: "proof-input-digest", status: "pass", evidence: $d}]'
671
+ "$V" > "$V.tmp" && mv "$V.tmp" "$V";
672
+ fi;
673
+ exit 0
650
674
 
651
- - id: record
652
- description: >
653
- Record pipeline results into the task file via `spur task record` —
654
- Testing/Review from the verdict, Solution backfilled from git diff as a
655
- safety net, optional transition to testing. Post-record step conditionally syncs
656
- feature status via the bounded `feature-sync-bounded` wrapper (task 0411
657
- retry-suppression) if `feature_id` is present, or appends an orphan link proposal
658
- to the run report if absent (task 0328 / ADR-0322).
659
- onEnter:
660
- # Proof-state compare (task 0612, ADR-071; bracket closed against the pre-chain
661
- # capture per task 0703) — FIRST action in the state, before any record write.
662
- # Re-captures the digest and asserts it equals the value the evidence chain started
663
- # from. A mismatch means a proof input changed after evidence was established; the
664
- # default `fail` policy halts the sequence and routes the run to `failed` rather
665
- # than crossing into `record`. Task-spec evidence writes (Testing/Review/Solution,
666
- # R6) happen only in the actions AFTER this comparison, and the fingerprint scopes
667
- # task content to the proof-input sections, so they cannot invalidate the certified
668
- # input set.
669
- - kind: proof.fingerprint
670
- options:
671
- var: proofDigestNow
672
- taskFile: ${vars.taskSpecPath}
673
- expect: ${vars.proofDigest}
674
- - kind: shell
675
- options:
676
- command: >-
677
- retry_transient() {
678
- RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
679
- printf '%s\n' "$RETRY_OUTPUT";
680
- if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
681
- if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
682
- sleep 2;
683
- RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
684
- printf '%s\n' "$RETRY_SECOND_OUTPUT";
685
- if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
686
- echo "node_modules link/dependency broken — run bun install and retry";
687
- fi;
688
- return "$RETRY_RC";
689
- };
690
- retry_transient $spurBin task record "$wbs" --solution-from-diff --transition testing
691
- # Best-effort, and portable across layouts. `feature-sync-bounded.ts` is a
692
- # Spur-monorepo path — `spur init` never scaffolds `plugins/sp/`, so a seeded
693
- # project must fall back to the plain `spur feature sync` verb rather than
694
- # abort a run that already produced a PASS verdict. Trailing `exit 0` keeps the
695
- # whole step advisory for the same reason (feature status sync is a follow-up,
696
- # not a completion gate — `record → done` runs `spur task check`).
697
- - kind: shell
698
- options:
699
- command: >-
700
- FID=$($spurBin task show $wbs --json 2>/dev/null |
701
- jq -r ".feature_id // .frontmatter.feature_id // empty" 2>/dev/null);
702
- if [ -n "$FID" ]; then
703
- if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then
704
- bun plugins/sp/scripts/feature-sync-bounded.ts "$FID" --spur-bin "$spurBin" --json;
705
- elif SYNC_MJS="$(superskill script path sp feature-sync-bounded.mjs 2>/dev/null)" && [ -f "$SYNC_MJS" ]; then
706
- node "$SYNC_MJS" "$FID" --spur-bin "$spurBin" --json;
707
- else
708
- $spurBin feature sync "$FID" --json;
709
- fi;
710
- else
711
- echo "Orphan task $wbs — no feature_id linked; proposal: consider linking to a parent feature." >> ".spur/run/$wbs-report.txt";
712
- fi;
713
- exit 0
675
+ - id: record
676
+ description: >
677
+ Record pipeline results into the task file via `spur task record` —
678
+ Testing/Review from the verdict, Solution backfilled from git diff as a
679
+ safety net, optional transition to testing. Post-record step conditionally syncs
680
+ feature status via the bounded `feature-sync-bounded` wrapper (task 0411
681
+ retry-suppression) if `feature_id` is present, or appends an orphan link proposal
682
+ to the run report if absent (task 0328 / ADR-0322).
683
+ onEnter:
684
+ # Proof-state compare (task 0612, ADR-071; bracket closed against the pre-chain
685
+ # capture per task 0703) — FIRST action in the state, before any record write.
686
+ # Re-captures the digest and asserts it equals the value the evidence chain started
687
+ # from. A mismatch means a proof input changed after evidence was established; the
688
+ # default `fail` policy halts the sequence and routes the run to `failed` rather
689
+ # than crossing into `record`. Task-spec evidence writes (Testing/Review/Solution,
690
+ # R6) happen only in the actions AFTER this comparison, and the fingerprint scopes
691
+ # task content to the proof-input sections, so they cannot invalidate the certified
692
+ # input set.
693
+ - kind: proof.fingerprint
694
+ options:
695
+ var: proofDigestNow
696
+ taskFile: ${vars.taskSpecPath}
697
+ expect: ${vars.proofDigest}
698
+ - kind: shell
699
+ options:
700
+ command: >-
701
+ retry_transient() {
702
+ RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
703
+ printf '%s\n' "$RETRY_OUTPUT";
704
+ if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
705
+ if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
706
+ sleep 2;
707
+ RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
708
+ printf '%s\n' "$RETRY_SECOND_OUTPUT";
709
+ if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
710
+ echo "node_modules link/dependency broken — run bun install and retry";
711
+ fi;
712
+ return "$RETRY_RC";
713
+ };
714
+ retry_transient $spurBin task record "$wbs" --solution-from-diff --transition testing
715
+ # Best-effort, and portable across layouts. `feature-sync-bounded.ts` is a
716
+ # Spur-monorepo path — `spur init` never scaffolds `plugins/sp/`, so a seeded
717
+ # project must fall back to the plain `spur feature sync` verb rather than
718
+ # abort a run that already produced a PASS verdict. Trailing `exit 0` keeps the
719
+ # whole step advisory for the same reason (feature status sync is a follow-up,
720
+ # not a completion gate — `record → done` runs `spur task check`).
721
+ - kind: shell
722
+ options:
723
+ command: >-
724
+ FID=$($spurBin task show $wbs --json 2>/dev/null |
725
+ jq -r ".feature_id // .frontmatter.feature_id // empty" 2>/dev/null);
726
+ if [ -n "$FID" ]; then
727
+ if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then
728
+ bun plugins/sp/scripts/feature-sync-bounded.ts "$FID" --spur-bin "$spurBin" --json;
729
+ elif SYNC_MJS="$(superskill script path sp feature-sync-bounded.mjs 2>/dev/null)" && [ -f "$SYNC_MJS" ]; then
730
+ node "$SYNC_MJS" "$FID" --spur-bin "$spurBin" --json;
731
+ else
732
+ $spurBin feature sync "$FID" --json;
733
+ fi;
734
+ else
735
+ echo "Orphan task $wbs — no feature_id linked; proposal: consider linking to a parent feature." >> ".spur/run/$wbs-report.txt";
736
+ fi;
737
+ exit 0
714
738
 
715
- - id: done
716
- description: >
717
- Terminal — task executed, verified, and recorded. The `record → done`
718
- guard runs `spur task check` before certifying; a genuinely non-compliant
719
- task routes to `failed` instead of a silent bad `done`.
720
- onEnter:
721
- - kind: shell
722
- options:
723
- command: >-
724
- retry_transient() {
725
- RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
726
- printf '%s\n' "$RETRY_OUTPUT";
727
- if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
728
- if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
729
- sleep 2;
730
- RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
731
- printf '%s\n' "$RETRY_SECOND_OUTPUT";
732
- if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
733
- echo "node_modules link/dependency broken — run bun install and retry";
734
- fi;
735
- return "$RETRY_RC";
736
- };
737
- retry_transient $spurBin task update "$wbs" done --no-lifecycle
738
- # D5-L: the verify verdict is this pipeline's proof of completion, so its ownership
739
- # belongs to a deterministic primitive rather than an untracked file on disk.
740
- # `run.artifact` records path + kind + runId only — never the body (ADR-069).
741
- - kind: run.artifact
742
- options:
743
- path: .spur/run/${vars.wbs}-verdict.json
744
- artifactKind: verify-verdict
745
- # 0751 R4: bind the verdict to the run's captured proof digest. `record`
746
- # re-captures `proofDigestNow` with expect=proofDigest, so the binding
747
- # holds by construction here — making the option non-decorative.
748
- proofBinding: current
749
- - kind: note
750
- options:
751
- message: "Pipeline complete for task ${vars.wbs} (done gate cleared)."
752
- # Checkpoint write: record session state for resume (0711 R1–R3) —
753
- # canonical frontmatter contract; mirrors the Session Checkpoint
754
- # Convention in plugins/sp/skills/spur-dev/references/cross-cutting.md.
755
- # Advisory only: the task file and the persisted run row stay authoritative.
756
- - kind: shell
757
- options:
758
- # Single logical line: the composition-baseline argument-split lint flags any
759
- # command whose continuation lines look like argument lists (heredocs with
760
- # `- item` entries trip it), so the checkpoint body is one printf with \n escapes.
761
- command: >-
762
- 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_DIGEST="$(cut -d= -f2 .spur/run/$wbs-proofdigest.txt 2>/dev/null || echo '')"; CP_RUN="$SPUR_RUN_ID"; [ -z "$CP_RUN" ] && CP_RUN="$RUN_ID"; printf '%s\n' '---' 'schema_version: 1' "session_id: $(date -u +%Y-%m-%d)-$wbs" 'workflow: task-pipeline' "run_id: $CP_RUN" "task_wbs: $wbs" 'feature_id: ""' 'phase: done' 'status: done' 'last_gate: record' "source_commit: $CP_COMMIT" "digest: $CP_DIGEST" "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
739
+ - id: done
740
+ description: >
741
+ Terminal — task executed, verified, and recorded. The `record → done`
742
+ guard runs `spur task check` before certifying; a genuinely non-compliant
743
+ task routes to `failed` instead of a silent bad `done`.
744
+ onEnter:
745
+ - kind: shell
746
+ options:
747
+ command: >-
748
+ retry_transient() {
749
+ RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
750
+ printf '%s\n' "$RETRY_OUTPUT";
751
+ if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
752
+ if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
753
+ sleep 2;
754
+ RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
755
+ printf '%s\n' "$RETRY_SECOND_OUTPUT";
756
+ if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
757
+ echo "node_modules link/dependency broken — run bun install and retry";
758
+ fi;
759
+ return "$RETRY_RC";
760
+ };
761
+ retry_transient $spurBin task update "$wbs" done --no-lifecycle
762
+ # D5-L: the verify verdict is this pipeline's proof of completion, so its ownership
763
+ # belongs to a deterministic primitive rather than an untracked file on disk.
764
+ # `run.artifact` records path + kind + runId only — never the body (ADR-069).
765
+ - kind: run.artifact
766
+ options:
767
+ path: .spur/run/${vars.wbs}-verdict.json
768
+ artifactKind: verify-verdict
769
+ # 0751 R4: bind the verdict to the run's captured proof digest. `record`
770
+ # re-captures `proofDigestNow` with expect=proofDigest, so the binding
771
+ # holds by construction here — making the option non-decorative.
772
+ proofBinding: current
773
+ - kind: note
774
+ options:
775
+ message: "Pipeline complete for task ${vars.wbs} (done gate cleared)."
776
+ # Checkpoint write: record session state for resume (0711 R1–R3) —
777
+ # canonical frontmatter contract; mirrors the Session Checkpoint
778
+ # Convention in plugins/sp/skills/spur-dev/references/cross-cutting.md.
779
+ # Advisory only: the task file and the persisted run row stay authoritative.
780
+ - kind: shell
781
+ options:
782
+ # Single logical line: the composition-baseline argument-split lint flags any
783
+ # command whose continuation lines look like argument lists (heredocs with
784
+ # `- item` entries trip it), so the checkpoint body is one printf with \n escapes.
785
+ command: >-
786
+ 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_DIGEST="$(cut -d= -f2 .spur/run/$wbs-proofdigest.txt 2>/dev/null || echo '')"; CP_RUN="$SPUR_RUN_ID"; [ -z "$CP_RUN" ] && CP_RUN="$RUN_ID"; printf '%s\n' '---' 'schema_version: 1' "session_id: $(date -u +%Y-%m-%d)-$wbs" 'workflow: task-pipeline' "run_id: $CP_RUN" "task_wbs: $wbs" 'feature_id: ""' 'phase: done' 'status: done' 'last_gate: record' "source_commit: $CP_COMMIT" "digest: $CP_DIGEST" "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
763
787
 
764
- - id: failed
765
- description: >
766
- Terminal — precheck, quality-gate exhaustion, verify non-PASS, record check
767
- failure, or operator rejection; reported, not advanced.
788
+ - id: failed
789
+ description: >
790
+ Terminal — precheck, quality-gate exhaustion, verify non-PASS, record check
791
+ failure, or operator rejection; reported, not advanced.
768
792
 
769
- - id: cancelled
770
- description: Terminal — pipeline cancelled by operator at the approval gate (R1).
793
+ - id: cancelled
794
+ description: Terminal — pipeline cancelled by operator at the approval gate (R1).
771
795
 
772
796
  transitions:
773
- # ── precheck: size PASS + task check → implement; else → failed ──
774
- - from: precheck
775
- to: implement
776
- description: Deterministic size, evidence, and task checks are green — begin implementation.
777
- guard:
778
- kind: shell
779
- options:
780
- command: 'test "$(cat .spur/run/$wbs-precheck-size.status 2>/dev/null)" = PASS && test "$(cat .spur/run/$wbs-precheck-evidence.status 2>/dev/null)" = PASS && $spurBin task check $wbs'
781
- - from: precheck
782
- to: failed
783
- description: Size and/or task check failed — stop before implement.
784
- guard:
785
- kind: always
797
+ # ── precheck: size PASS + task check → implement; else → failed ──
798
+ - from: precheck
799
+ to: implement
800
+ description: Deterministic size, evidence, and task checks are green — begin implementation.
801
+ guard:
802
+ kind: shell
803
+ options:
804
+ command: 'test "$(cat .spur/run/$wbs-precheck-size.status 2>/dev/null)" = PASS && test "$(cat .spur/run/$wbs-precheck-evidence.status 2>/dev/null)" = PASS && $spurBin task check $wbs'
805
+ - from: precheck
806
+ to: failed
807
+ description: Size and/or task check failed — stop before implement.
808
+ guard:
809
+ kind: always
786
810
 
787
- # ── linear body ──
788
- - from: implement
789
- to: test
790
- description: Implementation done — quality-gate probe.
791
- guard:
792
- kind: always
793
- # Soft probe branching (declaration order: PASS first, then FAIL, then defense).
794
- - from: test
795
- to: verify
796
- description: Quality gate already green and mode is fast — proportional fast path bypasses review.
797
- guard:
798
- kind: shell
799
- options:
800
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" = fast'
801
- - from: test
802
- to: review
803
- description: Quality gate already green and safety mode — proceed to review.
804
- guard:
805
- kind: shell
806
- options:
807
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" != fast'
808
- - from: test
809
- to: test-fix
810
- description: Quality gate red — start bounded fixall loop.
811
- guard:
812
- kind: shell
813
- options:
814
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL'
815
- # Defense: missing/corrupt status — treat as FAIL path, not silent PASS.
816
- - from: test
817
- to: test-fix
818
- description: Probe status missing/corrupt — attempt fixall then recheck.
819
- guard:
820
- kind: always
821
- - from: test-fix
822
- to: test-recheck
823
- description: Fixall finished — soft recheck the same quality gate.
824
- guard:
825
- kind: always
826
- # Recheck branching (PASS first; under-max FAIL → fixall again; exhausted → failed).
827
- - from: test-recheck
828
- to: verify
829
- description: Quality gate green after fixall and mode is fast — proportional fast path bypasses review.
830
- guard:
831
- kind: shell
832
- options:
833
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" = fast'
834
- - from: test-recheck
835
- to: review
836
- description: Quality gate green after fixall and safety mode — proceed to review.
837
- guard:
838
- kind: shell
839
- options:
840
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" != fast'
841
- - from: test-recheck
842
- to: test-fix
843
- description: Still red and under qualityGateMaxFixAttempts — another fixall hop.
844
- guard:
845
- kind: shell
846
- options:
847
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL && test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -lt "$qualityGateMaxFixAttempts"'
848
- - from: test-recheck
849
- to: failed
850
- description: Still red after max fixall attempts — stop at failed (not silent abort).
851
- guard:
852
- kind: shell
853
- options:
854
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL && test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -ge "$qualityGateMaxFixAttempts"'
855
- # Defense: corrupt recheck status — failed, not review.
856
- - from: test-recheck
857
- to: failed
858
- description: Recheck status missing/corrupt — stop at failed.
859
- guard:
860
- kind: always
861
- # ── review → approve, OR skip the HITL gate entirely when profile=auto (R4) ──
862
- # Declaration order matters: the auto-skip guard is tried FIRST, so under
863
- # profile=auto the run routes straight to verify and never ENTERS approve
864
- # (whose onEnter hitl.confirm would otherwise block). Otherwise fall through
865
- # to approve for the human gate.
866
- - from: review
867
- to: verify
868
- description: profile=auto — skip the human approval gate, go straight to verify.
869
- guard:
870
- kind: shell
871
- options:
872
- command: 'test "$profile" = auto'
873
- - from: review
874
- to: approve
875
- description: Review done — gate on human approval.
876
- guard:
877
- kind: always
811
+ # ── linear body ──
812
+ - from: implement
813
+ to: test
814
+ description: Implementation done — quality-gate probe.
815
+ guard:
816
+ kind: always
817
+ # Soft probe branching (declaration order: PASS first, then FAIL, then defense).
818
+ - from: test
819
+ to: verify
820
+ description: Quality gate already green and mode is fast — proportional fast path bypasses review.
821
+ guard:
822
+ kind: shell
823
+ options:
824
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" = fast'
825
+ - from: test
826
+ to: review
827
+ description: Quality gate already green and safety mode — proceed to review.
828
+ guard:
829
+ kind: shell
830
+ options:
831
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" != fast'
832
+ - from: test
833
+ to: test-fix
834
+ description: Quality gate red — start bounded fixall loop.
835
+ guard:
836
+ kind: shell
837
+ options:
838
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL'
839
+ # Defense: missing/corrupt status — treat as FAIL path, not silent PASS.
840
+ - from: test
841
+ to: test-fix
842
+ description: Probe status missing/corrupt — attempt fixall then recheck.
843
+ guard:
844
+ kind: always
845
+ - from: test-fix
846
+ to: test-recheck
847
+ description: Fixall finished — soft recheck the same quality gate.
848
+ guard:
849
+ kind: always
850
+ # Recheck branching (PASS first; under-max FAIL → fixall again; exhausted → failed).
851
+ - from: test-recheck
852
+ to: verify
853
+ description: Quality gate green after fixall and mode is fast — proportional fast path bypasses review.
854
+ guard:
855
+ kind: shell
856
+ options:
857
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" = fast'
858
+ - from: test-recheck
859
+ to: review
860
+ description: Quality gate green after fixall and safety mode — proceed to review.
861
+ guard:
862
+ kind: shell
863
+ options:
864
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" != fast'
865
+ - from: test-recheck
866
+ to: test-fix
867
+ description: Still red and under qualityGateMaxFixAttempts — another fixall hop.
868
+ guard:
869
+ kind: shell
870
+ options:
871
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL && test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -lt "$qualityGateMaxFixAttempts"'
872
+ - from: test-recheck
873
+ to: failed
874
+ description: Still red after max fixall attempts — stop at failed (not silent abort).
875
+ guard:
876
+ kind: shell
877
+ options:
878
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL && test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -ge "$qualityGateMaxFixAttempts"'
879
+ # Defense: corrupt recheck status — failed, not review.
880
+ - from: test-recheck
881
+ to: failed
882
+ description: Recheck status missing/corrupt — stop at failed.
883
+ guard:
884
+ kind: always
885
+ # ── review → approve, OR skip the HITL gate entirely when profile=auto (R4) ──
886
+ # Declaration order matters: the auto-skip guard is tried FIRST, so under
887
+ # profile=auto the run routes straight to verify and never ENTERS approve
888
+ # (whose onEnter hitl.confirm would otherwise block). Otherwise fall through
889
+ # to approve for the human gate.
890
+ - from: review
891
+ to: verify
892
+ description: profile=auto — skip the human approval gate, go straight to verify.
893
+ guard:
894
+ kind: shell
895
+ options:
896
+ command: 'test "$profile" = auto'
897
+ - from: review
898
+ to: approve
899
+ description: Review done — gate on human approval.
900
+ guard:
901
+ kind: always
878
902
 
879
- # ── approve: HITL gate (reached only when profile != auto). Routed by the
880
- # operator's captured answer (R1, bug-750) — three ordered guards, declaration
881
- # order matters (yes first, then no, then cancel), mirroring idea-pipeline's
882
- # design-approval gate (.spur/workflows/idea-pipeline.yaml:352-366). No
883
- # `always` edge remains out of `approve`. ──
884
- - from: approve
885
- to: verify
886
- description: Approved — proceed to verification.
887
- guard:
888
- kind: shell
889
- options:
890
- command: 'test "$__hitlAnswer" = yes'
891
- - from: approve
892
- to: failed
893
- description: Operator rejected at the approval gate — report and stop.
894
- guard:
895
- kind: shell
896
- options:
897
- command: 'test "$__hitlAnswer" = no'
898
- - from: approve
899
- to: cancelled
900
- description: Operator cancelled at the approval gate.
901
- guard:
902
- kind: shell
903
- options:
904
- command: 'test "$__hitlAnswer" = cancel'
903
+ # ── approve: HITL gate (reached only when profile != auto). Routed by the
904
+ # operator's captured answer (R1, bug-750) — three ordered guards, declaration
905
+ # order matters (yes first, then no, then cancel), mirroring idea-pipeline's
906
+ # design-approval gate (.spur/workflows/idea-pipeline.yaml:352-366). No
907
+ # `always` edge remains out of `approve`. ──
908
+ - from: approve
909
+ to: verify
910
+ description: Approved — proceed to verification.
911
+ guard:
912
+ kind: shell
913
+ options:
914
+ command: 'test "$__hitlAnswer" = yes'
915
+ - from: approve
916
+ to: failed
917
+ description: Operator rejected at the approval gate — report and stop.
918
+ guard:
919
+ kind: shell
920
+ options:
921
+ command: 'test "$__hitlAnswer" = no'
922
+ - from: approve
923
+ to: cancelled
924
+ description: Operator cancelled at the approval gate.
925
+ guard:
926
+ kind: shell
927
+ options:
928
+ command: 'test "$__hitlAnswer" = cancel'
905
929
 
906
- # ── completion gate (the YAML-native replacement for rd3's default-on --postflight-verify) ──
907
- # The verify step emits .spur/run/<wbs>-verdict.json with a required proof block (task 0703 R3/R5).
908
- # Only `verdict: PASS` PLUS a proof block whose digest — top level and every named stage — equals
909
- # the captured `proofDigest` clears the gate to `record`; any non-PASS, a missing file, malformed
910
- # JSON, or missing/mismatched proof evidence does not. Declaration order: PASS+proof guard FIRST,
911
- # then the bounded remediation route (R4), then the always catch-all so a PASS verdict with a
912
- # missing/malformed proof block still terminates at `failed` instead of hanging the state.
913
- - from: verify
914
- to: record
915
- description: Verification verdict is PASS and its proof block names the captured digest on every stage — record results and proceed to done.
916
- guard:
917
- kind: shell
918
- options:
919
- command: >-
920
- V=".spur/run/$wbs-verdict.json";
921
- test "$(jq -r .verdict "$V" 2>/dev/null)" = PASS &&
922
- test "$(jq -r '.proof.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
923
- test "$(jq -r '.proof.stages.qualityGate.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
924
- test "$(jq -r '.proof.stages.review.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
925
- test "$(jq -r '.proof.stages.verification.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
926
- test "$(jq -r '.proof.runId // ""' "$V" 2>/dev/null)" = "$__runId" &&
927
- test "$(jq -r '.proof.definitionDigest // ""' "$V" 2>/dev/null)" = "$__definitionDigest"
928
- - from: verify
929
- to: test-fix
930
- description: >-
931
- Verification returned a repairable non-PASS and the shared fix budget is not exhausted —
932
- bounded remediation hop (task 0703 R4); the chain re-enters quality → review → verify on a
933
- freshly captured digest. Never reached on PASS: remediation cannot follow certification.
934
- guard:
935
- kind: shell
936
- options:
937
- command: >-
938
- V="$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)";
939
- test -n "$V" && test "$V" != PASS &&
940
- test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -lt "$qualityGateMaxFixAttempts"
941
- - from: verify
942
- to: failed
943
- description: >-
944
- Non-PASS with the fix budget exhausted, or a PASS/missing/malformed verdict whose proof
945
- block is absent or mismatched (task 0703 R5) — block before done; defense catch-all so the
946
- state always has a viable outgoing edge.
947
- guard:
948
- kind: always
949
- # ── done gate: record → done/failed gated on `spur task check` (ADR-026 amendment 2026-06-23)
950
- # PLUS the proof-block re-assertion (task 0703 R5): the verdict artifact must still be PASS and
951
- # still name the captured digest — a forged or mutated completion artifact fails closed here.
952
- # Declaration order: pass guard first.
953
- - from: record
954
- to: done
955
- description: Task check passed and the verdict proof block still names the captured digest — certify done.
956
- guard:
957
- kind: shell
958
- options:
959
- command: >-
960
- $spurBin task check $wbs &&
961
- test "$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)" = PASS &&
962
- test "$(jq -r '.proof.digest // ""' .spur/run/$wbs-verdict.json 2>/dev/null)" = "$proofDigest"
963
- - from: record
964
- to: failed
965
- description: Task check failed or proof evidence missing/malformed/mismatched — block before done.
966
- guard:
967
- kind: always
930
+ # ── completion gate (the YAML-native replacement for rd3's default-on --postflight-verify) ──
931
+ # The verify step emits .spur/run/<wbs>-verdict.json with a required proof block (task 0703 R3/R5).
932
+ # Only `verdict: PASS` PLUS a proof block whose digest — top level and every named stage — equals
933
+ # the captured `proofDigest` clears the gate to `record`; any non-PASS, a missing file, malformed
934
+ # JSON, or missing/mismatched proof evidence does not. Declaration order: PASS+proof guard FIRST,
935
+ # then the bounded remediation route (R4), then the always catch-all so a PASS verdict with a
936
+ # missing/malformed proof block still terminates at `failed` instead of hanging the state.
937
+ - from: verify
938
+ to: record
939
+ description: Verification verdict is PASS and its proof block names the captured digest on every stage — record results and proceed to done.
940
+ guard:
941
+ kind: shell
942
+ options:
943
+ command: >-
944
+ V=".spur/run/$wbs-verdict.json";
945
+ test "$(jq -r .verdict "$V" 2>/dev/null)" = PASS &&
946
+ test "$(jq -r '.proof.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
947
+ test "$(jq -r '.proof.stages.qualityGate.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
948
+ test "$(jq -r '.proof.stages.review.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
949
+ test "$(jq -r '.proof.stages.verification.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
950
+ test "$(jq -r '.proof.runId // ""' "$V" 2>/dev/null)" = "$__runId" &&
951
+ test "$(jq -r '.proof.definitionDigest // ""' "$V" 2>/dev/null)" = "$__definitionDigest"
952
+ - from: verify
953
+ to: test-fix
954
+ description: >-
955
+ Verification returned a repairable non-PASS and the shared fix budget is not exhausted —
956
+ bounded remediation hop (task 0703 R4); the chain re-enters quality → review → verify on a
957
+ freshly captured digest. Never reached on PASS: remediation cannot follow certification.
958
+ guard:
959
+ kind: shell
960
+ options:
961
+ command: >-
962
+ V="$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)";
963
+ test -n "$V" && test "$V" != PASS &&
964
+ test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -lt "$qualityGateMaxFixAttempts"
965
+ - from: verify
966
+ to: failed
967
+ description: >-
968
+ Non-PASS with the fix budget exhausted, or a PASS/missing/malformed verdict whose proof
969
+ block is absent or mismatched (task 0703 R5) — block before done; defense catch-all so the
970
+ state always has a viable outgoing edge.
971
+ guard:
972
+ kind: always
973
+ # ── done gate: record → done/failed gated on `spur task check` (ADR-026 amendment 2026-06-23)
974
+ # PLUS the proof-block re-assertion (task 0703 R5): the verdict artifact must still be PASS and
975
+ # still name the captured digest — a forged or mutated completion artifact fails closed here.
976
+ # Declaration order: pass guard first.
977
+ - from: record
978
+ to: done
979
+ description: Task check passed and the verdict proof block still names the captured digest — certify done.
980
+ guard:
981
+ kind: shell
982
+ options:
983
+ command: >-
984
+ $spurBin task check $wbs &&
985
+ test "$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)" = PASS &&
986
+ test "$(jq -r '.proof.digest // ""' .spur/run/$wbs-verdict.json 2>/dev/null)" = "$proofDigest"
987
+ - from: record
988
+ to: failed
989
+ description: Task check failed or proof evidence missing/malformed/mismatched — block before done.
990
+ guard:
991
+ kind: always