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