@gobing-ai/spur 0.3.73 → 0.3.75

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