@gobing-ai/spur 0.3.91 → 0.3.93

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 (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -9
  3. package/config/plugin-scripts.json +27 -1
  4. package/config/templates/AGENTS.md +4 -0
  5. package/config/templates/docs/02_ROADMAP.md +2 -0
  6. package/config/templates/docs/03_ARCHITECTURE.md +3 -1
  7. package/config/templates/docs/04_DESIGN.md +2 -0
  8. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +10 -2
  9. package/config/workflow-candidates.json +72 -1
  10. package/config/workflows/feature-lifecycle.yaml +14 -5
  11. package/config/workflows/feature-verification.yaml +36 -27
  12. package/config/workflows/history-anatomy.yaml +16 -25
  13. package/config/workflows/idea-pipeline.yaml +76 -23
  14. package/config/workflows/pr-review.yaml +8 -0
  15. package/config/workflows/task-pipeline.yaml +362 -40
  16. package/config/workflows/wayfinder-resolution.yaml +5 -0
  17. package/config/workflows/wrapup-pipeline.yaml +102 -19
  18. package/package.json +9 -9
  19. package/plugins/sp/README.md +7 -2
  20. package/plugins/sp/agents/super-planner.md +14 -5
  21. package/plugins/sp/commands/dev-dogfood.md +4 -4
  22. package/plugins/sp/commands/dev-fixall.md +8 -5
  23. package/plugins/sp/commands/dev-run.md +8 -2
  24. package/plugins/sp/commands/dev-runall.md +14 -8
  25. package/plugins/sp/commands/dev-verify.md +9 -0
  26. package/plugins/sp/commands/dev-verifyall.md +5 -0
  27. package/plugins/sp/lib/idea-handoff.generated.mjs +306 -301
  28. package/plugins/sp/lib/inline-run.generated.d.mts +18 -0
  29. package/plugins/sp/lib/inline-run.generated.mjs +1468 -0
  30. package/plugins/sp/plugin.json +1 -1
  31. package/plugins/sp/references/environment-lens.md +1 -1
  32. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
  33. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
  34. package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
  35. package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
  36. package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
  37. package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
  38. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +2 -1
  39. package/plugins/sp/scripts/inline-run-setup.mjs +421 -0
  40. package/plugins/sp/scripts/inline-run-setup.ts +325 -78
  41. package/plugins/sp/scripts/quality-gate.mjs +248 -6
  42. package/plugins/sp/scripts/quality-gate.ts +410 -9
  43. package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
  44. package/plugins/sp/scripts/record-feature-sync.ts +84 -0
  45. package/plugins/sp/scripts/residual-scan.mjs +484 -0
  46. package/plugins/sp/scripts/residual-scan.ts +640 -0
  47. package/plugins/sp/scripts/task-diffstat.mjs +156 -0
  48. package/plugins/sp/scripts/task-diffstat.ts +229 -0
  49. package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
  50. package/plugins/sp/scripts/task-size-precheck.ts +8 -3
  51. package/plugins/sp/scripts/wrapup-drift-probe.mjs +181 -0
  52. package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
  53. package/plugins/sp/scripts/wrapup-steps.mjs +60 -1
  54. package/plugins/sp/scripts/wrapup-steps.ts +89 -4
  55. package/plugins/sp/skills/brainstorm/SKILL.md +2 -0
  56. package/plugins/sp/skills/brainstorm/references/workflows.md +17 -2
  57. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  58. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
  59. package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
  60. package/plugins/sp/skills/code-verification/SKILL.md +21 -0
  61. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -2
  62. package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
  63. package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
  64. package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
  65. package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
  66. package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
  67. package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
  68. package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
  69. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  70. package/plugins/sp/skills/session-review/SKILL.md +12 -2
  71. package/plugins/sp/skills/spur-check/SKILL.md +112 -0
  72. package/plugins/sp/skills/spur-cli/references/features.md +1 -1
  73. package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
  74. package/plugins/sp/skills/spur-cli/references/workflows.md +39 -19
  75. package/plugins/sp/skills/spur-dev/SKILL.md +13 -5
  76. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +18 -3
  77. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
  78. package/plugins/sp/skills/spur-dev/references/document-authoring.md +85 -0
  79. package/plugins/sp/skills/spur-dev/references/execution-batch.md +286 -58
  80. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +1 -1
  81. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +27 -4
  82. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
  83. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +61 -16
  84. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +10 -5
  85. package/plugins/sp/skills/spur-dev/templates/design.md +31 -0
  86. package/plugins/sp/skills/spur-dev/templates/plan.md +32 -0
  87. package/plugins/sp/skills/spur-doctor/SKILL.md +60 -14
  88. package/schemas/state-machine-workflow.schema.json +4 -0
  89. package/spur.js +22501 -19644
  90. package/web/_astro/BoardApp.CerSBgis.js +192 -0
  91. package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
  92. package/web/_astro/{TaskDetail.CXGltuT_.js → TaskDetail.DCqiC-OZ.js} +1 -1
  93. package/web/_astro/arc.CPwg6Rw0.js +1 -0
  94. package/web/_astro/{architectureDiagram-3BPJPVTR.DJ8DHkWE.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
  95. package/web/_astro/{blockDiagram-GPEHLZMM.D8DHK3Jl.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
  96. package/web/_astro/{c4Diagram-AAUBKEIU.BugQbX9u.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
  97. package/web/_astro/channel.MYZLKNwy.js +1 -0
  98. package/web/_astro/{chunk-2J33WTMH.Mv26KlVn.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
  99. package/web/_astro/{chunk-4BX2VUAB.CD51JoT_.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
  100. package/web/_astro/{chunk-55IACEB6.D_PFaIEe.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
  101. package/web/_astro/{chunk-727SXJPM.DemMW1ao.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
  102. package/web/_astro/{chunk-AQP2D5EJ.Iu2V5-ex.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
  103. package/web/_astro/{chunk-FMBD7UC4.MsNgSP-E.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
  104. package/web/_astro/{chunk-ND2GUHAM.DfbRaAlm.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
  105. package/web/_astro/{chunk-QZHKN3VN.BbJAQ4h-.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
  106. package/web/_astro/{classDiagram-4FO5ZUOK.CPurtiC2.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
  107. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CPurtiC2.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
  108. package/web/_astro/{cose-bilkent-S5V4N54A.CKKdx1bM.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
  109. package/web/_astro/{cynefin-OW5HDTMX.CoKMTg-R.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
  110. package/web/_astro/{dagre-BM42HDAG.D4h4_k56.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
  111. package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
  112. package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
  113. package/web/_astro/{diagram-KO2AKTUF.BFoCkiCr.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
  114. package/web/_astro/{diagram-LMA3HP47.exHn9OVx.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
  115. package/web/_astro/{diagram-OG6HWLK6.CeqO34nN.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
  116. package/web/_astro/{erDiagram-TEJ5UH35.D_v7HqxR.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
  117. package/web/_astro/{flowDiagram-I6XJVG4X.EUmrpbwh.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
  118. package/web/_astro/{ganttDiagram-6RSMTGT7.BOCF5lII.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
  119. package/web/_astro/{gitGraphDiagram-PVQCEYII.Di7otYZD.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
  120. package/web/_astro/index.Hjbr15fG.css +1 -0
  121. package/web/_astro/{infoDiagram-5YYISTIA.TcBkCAJk.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
  122. package/web/_astro/{ishikawaDiagram-YF4QCWOH.D-2y4M0c.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
  123. package/web/_astro/{journeyDiagram-JHISSGLW.DPbJI_n2.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
  124. package/web/_astro/{kanban-definition-UN3LZRKU.EFxhQ9Fj.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
  125. package/web/_astro/{linear.DSAsQLzs.js → linear.C2_IkbZT.js} +1 -1
  126. package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
  127. package/web/_astro/{mindmap-definition-RKZ34NQL.CJY1N_7V.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
  128. package/web/_astro/{pieDiagram-4H26LBE5.567ZNoL2.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
  129. package/web/_astro/{quadrantDiagram-W4KKPZXB.mqfz9-MY.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
  130. package/web/_astro/{requirementDiagram-4Y6WPE33.Bv1Gv9In.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
  131. package/web/_astro/{sankeyDiagram-5OEKKPKP.B6Gs4X4r.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
  132. package/web/_astro/{sequenceDiagram-3UESZ5HK.BhYj4v-m.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
  133. package/web/_astro/{stateDiagram-AJRCARHV.BPbBnkpw.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
  134. package/web/_astro/{stateDiagram-v2-BHNVJYJU.C4squMNK.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
  135. package/web/_astro/{timeline-definition-PNZ67QCA.C_SwIHgl.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
  136. package/web/_astro/{vennDiagram-CIIHVFJN.Bz4NZGpQ.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
  137. package/web/_astro/{wardleyDiagram-YWT4CUSO.CozMVZ3i.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
  138. package/web/_astro/{xychartDiagram-2RQKCTM6.BwMGBwjB.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
  139. package/web/index.html +2 -2
  140. package/config/workflows/decision-routing-example.yaml +0 -134
  141. package/web/_astro/BoardApp.BEDWpzsr.js +0 -188
  142. package/web/_astro/BoardApp.DQG2xfEz.js +0 -1
  143. package/web/_astro/arc.C0rrflm_.js +0 -1
  144. package/web/_astro/channel.SRrg1P-w.js +0 -1
  145. package/web/_astro/diagram-2AECGRRQ.DJ0h9zgw.js +0 -43
  146. package/web/_astro/diagram-5GNKFQAL.DvPk1jYd.js +0 -10
  147. package/web/_astro/index.Bx6GY4RH.css +0 -1
  148. package/web/_astro/mermaid.core.kAZjgJHG.js +0 -301
@@ -6,13 +6,18 @@
6
6
  # through the normal `spur task update <wbs> <status>` verb so the lifecycle guards
7
7
  # (0055) apply identically. Run linkage is written to `task_run_links` (kind=pipeline).
8
8
  #
9
- # Shape: precheck → implement → test[→test-fix↔test-recheck] → review → approve(HITL)
9
+ # Shape: precheck → implement[→escalate] → test[→test-fix↔test-recheck] → review → approve(HITL)
10
10
  # → verify → record → done
11
11
  # (precheck failure short-circuits to `failed`; approve routes to `failed` on
12
12
  # operator rejection or `cancelled` on operator cancel — R1, bug-750).
13
13
  # `test` is the project quality gate (shell + bounded /sp:dev-fixall), not
14
14
  # /sp:dev-unit (coverage gap-fill; router C3/C5).
15
15
  #
16
+ # F96 residual sweep (0950): precheck captures the resume-safe base commit; verify
17
+ # scans and folds residuals between the verdict and the proof bind (blocking leftovers
18
+ # downgrade PASS → PARTIAL and take the existing remediation edge); done settles
19
+ # deferrals; failed renders the recovery report. No new state, edge, or model query.
20
+ #
16
21
  # Vars (passed as a JSON object via `--vars`):
17
22
  # wbs — task WBS (required)
18
23
  # profile — "auto" skips HITL approve (R4)
@@ -21,6 +26,7 @@
21
26
  # implementTimeoutMs — implement agent.run budget (ms)
22
27
  # qualityGateCmd — project gate (default: bun run spur-check)
23
28
  # qualityGateMaxFixAttempts — max /sp:dev-fixall hops after a red gate (default: 2)
29
+ # maxEscalations — max operator-question pauses per implement chain (default: 2)
24
30
  #
25
31
  # Seeded by `spur init`. agent.run inputs are pure slash commands (ADR-043).
26
32
 
@@ -90,6 +96,10 @@ vars:
90
96
  # Answer captured by the approve gate's hitl.confirm (R1): "yes" | "no" | "cancel".
91
97
  # Empty by default; only meaningful once the approve state has been entered.
92
98
  __hitlAnswer: ""
99
+ # Operator answer captured by the escalate hop's hitl.input (0933). Empty by
100
+ # default; set by `spur workflow continue --answer-text` after the pause and read
101
+ # by the escalate→implement / escalate→failed guards. Non-empty = answered.
102
+ __hitlInput: ""
93
103
  # Proof-state bracket (task 0612, ADR-071; restructured by task 0703). `proofDigest` is the
94
104
  # canonical capture taken at quality-gate ENTRY — immediately before the evidence-producing
95
105
  # final chain (quality → review → verify) — and re-captured at `test-recheck` when bounded
@@ -134,6 +144,14 @@ vars:
134
144
  # Max /sp:dev-fixall attempts after a red quality-gate probe/recheck (bounded; no thrash).
135
145
  # Attempt counter: .spur/run/<wbs>-test-fix-attempt. Default 2 = two fixall hops before failed.
136
146
  qualityGateMaxFixAttempts: "2"
147
+ # Max operator-question pauses (0933) before the implement chain routes to failed.
148
+ # Escalation counter: .spur/run/<wbs>-escalation-count. Default 2 = two answered
149
+ # questions; a third pause routes to failed with a report note.
150
+ maxEscalations: "2"
151
+ # Runtime-written, 0933: the escalate hop's file.read.into-var fills this from
152
+ # .spur/run/<wbs>-question.md; the hitl.input prompt below references it. Declared
153
+ # here (empty) so the static var-reference validator sees the template source.
154
+ escalationQuestion: ""
137
155
  # Post-implement auto-format. Overridable like qualityGateCmd so a non-Bun seeded
138
156
  # project can point it at its own formatter; invoked best-effort (a missing or
139
157
  # failing formatter must never abort a run — the quality gate is the real gate).
@@ -156,6 +174,12 @@ vars:
156
174
  # (default) = on; set to "off" to bypass:
157
175
  # `--vars '{"implementScopeGuard":"off"}'`.
158
176
  implementScopeGuard: ""
177
+ # 0931 R5: parallel batches defer the per-task feature sync so a task branch never
178
+ # touches feature files or docs/features/INDEX.md. When "true", the record step's
179
+ # post-record shell appends a deferral note and skips the sync; the parallel batch
180
+ # orchestrator runs the sync + `spur feature refresh` once per touched feature on the
181
+ # base ref after integration. Sequential/inline keep the default "false" (unchanged).
182
+ deferFeatureSync: "false"
159
183
 
160
184
  states:
161
185
  - id: precheck
@@ -189,6 +213,12 @@ states:
189
213
  options:
190
214
  command: >-
191
215
  mkdir -p .spur/run; S=plugins/sp/scripts/task-evidence-precheck.ts; [ -f "$S" ] || S="$(superskill script path sp task-evidence-precheck.ts 2>/dev/null)"; if [ -f "$S" ]; then bun "$S" "$wbs" --spur-bin "$spurBin"; else echo "task-evidence-precheck failed closed — checker not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'." >&2; echo "FAIL" > ".spur/run/$wbs-precheck-evidence.status"; fi; exit 0
216
+ # (e) F96 R1 (0950): capture the run's base commit once — a resumed or re-entered run
217
+ # keeps its original base so residual scans stay anchored to the same diff.
218
+ - kind: shell
219
+ options:
220
+ command: >-
221
+ mkdir -p .spur/run; [ -f ".spur/run/$wbs-base.sha" ] || git rev-parse HEAD > ".spur/run/$wbs-base.sha"; exit 0
192
222
  # (e) route-reason lookup and routes log; proportional-routing tests locate it
193
223
  # (0759 R1/R5 run-scoped artifact; 0804 R8 run-id safety — one line, no in-scalar `#`).
194
224
  - kind: shell
@@ -222,6 +252,20 @@ states:
222
252
  that command DRIVES this pipeline, so calling it here recurses.
223
253
  --mode implement is the single-step implement entry.
224
254
  onEnter:
255
+ # 0933 R27 (guard-lines compression): the transcript append lives HERE, on
256
+ # (re-)entry, not in the escalate→implement guard. On first entry there is no
257
+ # pending question (no-op); on a resume entry the guard already confirmed a
258
+ # non-empty $__hitlInput and a fresh question file, so this shell records the
259
+ # Q/A pair (## Q<n>/## A<n>, R3) and consumes the question file before the
260
+ # agent.run re-dispatches — which also disarms the implement→escalate edge.
261
+ - kind: shell
262
+ options:
263
+ command: >-
264
+ test -n "$__hitlInput" -a -s .spur/run/$wbs-question.md || exit 0;
265
+ qn="$(cat .spur/run/$wbs-escalation-count 2>/dev/null || echo 0)";
266
+ printf '## Q%s\n%s\n\n## A%s\n%s\n' "$qn" "$(cat .spur/run/$wbs-question.md 2>/dev/null)" "$qn" "$__hitlInput" >> .spur/run/$wbs-escalation.md;
267
+ rm -f .spur/run/$wbs-question.md;
268
+ exit 0
225
269
  - kind: agent.run
226
270
  options:
227
271
  agent: ${vars.implementAgent}
@@ -232,13 +276,23 @@ states:
232
276
  # resumes the implement session instead of re-reading the task cold.
233
277
  session: reuse
234
278
  # lives in /sp:dev-run --mode implement → sp:code-implementation, not YAML prose.
235
- input: /sp:dev-run --mode implement ${vars.wbs} --auto
279
+ # 0933 R28: --escalation-file names the Q/A transcript the implement
280
+ # agent appends to when pausing and reads back on re-dispatch; absent
281
+ # on a first attempt means "no prior escalations".
282
+ input: /sp:dev-run --mode implement ${vars.wbs} --auto --escalation-file .spur/run/${vars.wbs}-escalation.md
236
283
  timeoutMs: ${vars.implementTimeoutMs}
237
284
  # R3 (task 0424): empty-implement no-op guard — the agent.run action
238
285
  # fails the step when exit 0 produced zero non-corpus file changes, so
239
286
  # a silent no-op routes the run to `failed` here instead of drifting
240
287
  # into test/review and being caught a full pass later.
241
288
  requireDiff: true
289
+ # 0933 R26: escalation contract — the agent.run option names the QUESTION
290
+ # file (the pause signal, deleted pre-dispatch for freshness); a paused
291
+ # attempt (agent wrote its question there and exited 0) succeeds with
292
+ # data.escalated=true and skips requireDiff for THAT attempt only. The
293
+ # Q/A transcript (.spur/run/<wbs>-escalation.md) is separate: it reaches
294
+ # the agent via --escalation-file in the input below.
295
+ escalationFile: .spur/run/${vars.wbs}-question.md
242
296
  # 0706 R6: this stage mutates the working tree unattended under the
243
297
  # auto profile, so it declares minimum execution-capability
244
298
  # requirements. Dispatch fails closed (before spawn) when the
@@ -273,6 +327,39 @@ states:
273
327
  options:
274
328
  command: "$formatCmd ; exit 0"
275
329
 
330
+ # ── escalate hop (operator-question pause, 0933) ──────────────────────────
331
+ # The implement agent paused on an operator question: it wrote
332
+ # .spur/run/<wbs>-question.md and exited 0 (agent.run reported
333
+ # data.escalated=true, skipping requireDiff for that attempt). This hop bounds
334
+ # the asks (counter), surfaces the question verbatim through the HITL responder
335
+ # (the run pauses here), and — after `spur workflow continue --answer-text <a>`
336
+ # delivers __hitlInput — resumes implement with the Q/A transcript
337
+ # (.spur/run/<wbs>-escalation.md) named in the implement input via
338
+ # --escalation-file. The transcript append + question-file consumption happen in
339
+ # the escalate→implement guard so a stale question can never re-trigger the hop
340
+ # after the answered attempt.
341
+ - id: escalate
342
+ description: >-
343
+ Operator-question pause (0933): the implement agent asked a question
344
+ headlessly; surface it to the operator and resume implement with the answer.
345
+ onEnter:
346
+ # Bounded asks: increment the per-task escalation counter before pausing.
347
+ - kind: shell
348
+ options:
349
+ command: >-
350
+ mkdir -p .spur/run; n="$(cat .spur/run/$wbs-escalation-count 2>/dev/null || echo 0)"; echo $((n + 1)) > .spur/run/$wbs-escalation-count; exit 0
351
+ # The agent's question, verbatim, becomes the pause prompt.
352
+ - kind: file.read.into-var
353
+ options:
354
+ file: .spur/run/${vars.wbs}-question.md
355
+ var: escalationQuestion
356
+ # Pause for the operator (responder pause:true stops the run after this
357
+ # onEnter; `spur workflow continue --answer-text <answer>` writes
358
+ # __hitlInput and re-evaluates the escalate guards).
359
+ - kind: hitl.input
360
+ options:
361
+ prompt: "${vars.escalationQuestion}"
362
+
276
363
  # ── test hop (quality gate + bounded auto-fix) ─────────────────────────────
277
364
  # NOT /sp:dev-unit. That command (sp:code-testing) *extends/generates* tests toward
278
365
  # a coverage target; it is not the project quality gate. Coverage gap-fill remains
@@ -358,6 +445,14 @@ states:
358
445
  options:
359
446
  command: >-
360
447
  mkdir -p .spur/run; A=".spur/run/$wbs-test-fix-attempt"; n=$(cat "$A" 2>/dev/null || echo 0); printf '%s\n' "$((n + 1))" > "$A"; [ ! -f ".spur/run/$wbs-verdict.json" ] || { echo '--- verify verdict (remediation input, task 0703 R4) ---'; cat ".spur/run/$wbs-verdict.json"; } >> ".spur/run/$wbs-test-gate.log"; exit 0
448
+ # (d) F96 R3 (0950): hand the residual list to the remediation loop — dev-fixall
449
+ # treats residual items as fix targets and may defer P3/markers to
450
+ # residual-deferrals.json (see dev-fixall.md). Fold already merged the anchors
451
+ # into <wbs>-test-gate.findings; this carries the structured artifact too.
452
+ - kind: shell
453
+ options:
454
+ command: >-
455
+ [ ! -f ".spur/run/$wbs-residuals.json" ] || { echo '--- residual artifact (F96 fix targets; deferrals go to residual-deferrals.json, P3/markers only) ---'; cat ".spur/run/$wbs-residuals.json"; } >> ".spur/run/$wbs-test-gate.log"; exit 0
361
456
  # R3 (0482): project the extracted gate anchors into a var so the dispatch input
362
457
  # can NAME the failing file:line, not merely point at a log. A vars template cannot
363
458
  # run a shell, so the read is an action, not an inline `$(cat ...)` substitution.
@@ -391,8 +486,8 @@ states:
391
486
  - id: test-recheck
392
487
  description: >
393
488
  Soft recheck after fixall with bounded SQLite-lock retry. Writes PASS|FAIL (always exit 0). Transitions
394
- branch to review (PASS), another test-fix (FAIL and under max attempts),
395
- or the pipeline `failed` state (FAIL and attempts exhausted) — never a
489
+ branch to `triage` (PASS — the 0943 lane router), `test-fail-triage` (FAIL — the 0943
490
+ failure-class router), or the pipeline `failed` state (corrupt status) — never a
396
491
  raw lifecycle abort that skips the terminal `failed` state.
397
492
  onEnter:
398
493
  # R4 (0703, ADR-071): bounded remediation mutated the tree by design, so the fresh evidence
@@ -415,6 +510,99 @@ states:
415
510
  command: >-
416
511
  mkdir -p .spur/run; if [ -f plugins/sp/scripts/quality-gate.ts ]; then bun plugins/sp/scripts/quality-gate.ts recheck; elif Q="$(superskill script path sp quality-gate.mjs 2>/dev/null)" && [ -f "$Q" ]; then node "$Q" recheck; else echo "quality gate failed closed — quality-gate script not found — run 'superskill install sp'" >&2; printf 'FAIL\n' > ".spur/run/$wbs-test-gate.status"; fi; exit 0
417
512
 
513
+ - id: triage
514
+ description: >-
515
+ Deterministic lane router between a green gate and the review/verify fork (0943 R1).
516
+ Writes the diffstat evidence file, honors a caller-set `mode` (never overridden), pins
517
+ the safety lane on sensitive paths or >400 changed lines without a model call (R2c),
518
+ otherwise consults `decide task-triage` (low → fast lane; anything else stays empty →
519
+ standard review path), and appends the triage route reason to
520
+ .spur/memory/task-pipeline-routes.log (R5). The `decide` action itself runs
521
+ unconditionally (the engine has no onEnter conditionals); its result is consulted only
522
+ when neither a caller mode nor the deterministic guard resolved the lane. The resolved
523
+ lane lands in the `mode` var for the triage → verify/review guards.
524
+ onEnter:
525
+ # (a) diffstat producer: task-diffstat.ts owns git reading and sensitive-path
526
+ # classification; shell only resolves it and fails closed (writes sensitive:true,
527
+ # which the pre-guard below maps to the safety lane).
528
+ - kind: shell
529
+ options:
530
+ command: >-
531
+ mkdir -p .spur/run; if [ -f plugins/sp/scripts/task-diffstat.ts ]; then bun plugins/sp/scripts/task-diffstat.ts --spur-bin "$spurBin"; elif D="$(superskill script path sp task-diffstat.mjs 2>/dev/null)" && [ -f "$D" ]; then node "$D"; else echo "task-diffstat failed closed — task-diffstat script not found — run 'superskill install sp'" >&2; printf '{"files":0,"insertions":0,"deletions":0,"paths":[],"sensitive":true}\n' > ".spur/run/$wbs-diffstat.json"; fi; exit 0
532
+ # (b)+(c) lane pre-guard BEFORE decide: a caller-set mode is projected verbatim (R2b);
533
+ # a sensitive path or >400 changed lines pins safety (R2c) and the decide result below
534
+ # is never consulted. A missing/malformed diffstat row (producer crashed or never ran,
535
+ # disk error) pins safety HERE: an empty mode file would let the low decide row project
536
+ # the fast lane downstream, so the "never to fast" fail-safe is enforced at the guard,
537
+ # not by the projection (the full-suite gate run exposed this fail-open path).
538
+ - kind: shell
539
+ options:
540
+ command: >-
541
+ mkdir -p .spur/run; if [ -n "$mode" ]; then printf '%s\n' "$mode" > ".spur/run/$wbs-mode.txt"; exit 0; fi; DF=".spur/run/$wbs-diffstat.json"; if ! jq -e 'type == "object"' "$DF" >/dev/null 2>&1; then printf 'safety\n' > ".spur/run/$wbs-mode.txt"; exit 0; fi; jq -r 'if .sensitive == true or ((.insertions // 0) + (.deletions // 0)) > 400 then "safety" else empty end' "$DF" 2>/dev/null > ".spur/run/$wbs-mode.txt"; exit 0
542
+ # (d) lane decision — degrades to the standard default whenever workflow.decideDecisionMaker
543
+ # is off (R4), so with the flag off this state is pure shell: no model call is made.
544
+ - kind: decide
545
+ options:
546
+ id: task-triage
547
+ method: choice
548
+ question: >-
549
+ The quality gate is green. Which verification lane does this diff deserve —
550
+ low (small, contained diff — fast lane), standard, or high caution?
551
+ choices: [low, standard, high]
552
+ default: standard
553
+ evidence:
554
+ - .spur/run/${vars.wbs}-diffstat.json
555
+ - ${vars.taskSpecPath}
556
+ resultFile: .spur/run/${vars.wbs}-triage.decision
557
+ # (d) project the decision: low → fast lane; anything else (standard/high, degraded
558
+ # default, missing row) leaves the mode empty → standard review path. A non-empty mode
559
+ # file (caller-set or deterministic-high) is never overridden.
560
+ - kind: shell
561
+ options:
562
+ command: >-
563
+ MF=".spur/run/$wbs-mode.txt"; [ -s "$MF" ] && exit 0; jq -r 'if .value == "low" then "fast" else empty end' ".spur/run/$wbs-triage.decision" 2>/dev/null > "$MF"; exit 0
564
+ # R5: append the triage route reason in the shared routes-log format.
565
+ - kind: shell
566
+ options:
567
+ command: >-
568
+ m="$(cat ".spur/run/$wbs-mode.txt")"; RUN_ID="$__runId"; [ -n "$RUN_ID" ] || RUN_ID="pipeline-$wbs"; mkdir -p .spur/memory; printf '%s %s %s\n' "$RUN_ID" "$wbs" "$(jq -rn --arg m "$m" --arg c "$mode" 'if $c != "" then ($c + ":caller-set") else {"fast":"fast:triage low lane","safety":"safety:triage deterministic-high","":"safety:triage standard lane"}[$m] // "safety:triage unrecognized mode" end')" >> .spur/memory/task-pipeline-routes.log
569
+ # (d) the resolved lane becomes the `mode` var for the triage → verify/review guards.
570
+ - kind: file.read.into-var
571
+ options:
572
+ path: .spur/run/${vars.wbs}-mode.txt
573
+ var: mode
574
+
575
+ - id: test-fail-triage
576
+ description: >-
577
+ Deterministic failure-class router on a red gate (0943 R3). The engine has no onEnter
578
+ conditionals, so the FAIL branch from `test`/`test-recheck` lands here as its own
579
+ deterministic state (the sanctioned alternative), while the green branch goes to
580
+ `triage`. `decide failure-class` reads the bounded gate findings and routes: retryable
581
+ → test-recheck (the attempt is counted on entry, so qualityGateMaxFixAttempts still
582
+ bounds the loop), fix → test-fix (the repair hop, as before), stop → failed with
583
+ terminalReason failed-check. A missing/corrupt decision fails closed to failed-check.
584
+ onEnter:
585
+ - kind: decide
586
+ options:
587
+ id: failure-class
588
+ method: choice
589
+ question: >-
590
+ The quality gate failed. Is the failure retryable (transient or environmental
591
+ — just re-run the gate), fixable by the repair agent, or should the run stop
592
+ here?
593
+ choices: [retryable, fix, stop]
594
+ default: fix
595
+ evidence:
596
+ - .spur/run/${vars.wbs}-test-gate.findings
597
+ resultFile: .spur/run/${vars.wbs}-failure-class.decision
598
+ # A retryable classification still counts an attempt so the existing
599
+ # qualityGateMaxFixAttempts cap bounds the recheck loop (0943 R3; the cap edge reads
600
+ # the same counter the fixall path uses).
601
+ - kind: shell
602
+ options:
603
+ command: >-
604
+ A=".spur/run/$wbs-test-fix-attempt"; [ "$(jq -r '.value // ""' ".spur/run/$wbs-failure-class.decision" 2>/dev/null)" = retryable ] && { n="$(cat "$A" 2>/dev/null || echo 0)"; printf '%s\n' "$((n + 1))" > "$A"; }; exit 0
605
+
418
606
  - id: review
419
607
  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`.
420
608
  onEnter:
@@ -530,6 +718,16 @@ states:
530
718
  - kind: shell
531
719
  options:
532
720
  command: "$spurBin task verdict $wbs --from-answer .spur/run/$wbs-verify-answer.txt"
721
+ # (d) F96 R2 (0950): residual-scan.ts owns the sweep; shell only resolves it.
722
+ # ONE hard action (scan+fold): a scanner crash fails verify closed — the
723
+ # verify→failed catch-all routes a missing/malformed verdict. Runs AFTER
724
+ # `task verdict` and BEFORE the jq bind so a blocking residual turns PASS
725
+ # into PARTIAL (fold rewrites the verdict artifact) and takes the existing
726
+ # verify→test-fix edge while attempts remain.
727
+ - kind: shell
728
+ options:
729
+ command: >-
730
+ S=plugins/sp/scripts/residual-scan.ts; [ -f "$S" ] || S="$(superskill script path sp residual-scan.mjs 2>/dev/null)"; if [ ! -f "$S" ]; then echo "residual-scan failed closed — scanner not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'" >&2; exit 1; fi; case "$S" in *.mjs) RUNNER=node ;; *) RUNNER=bun ;; esac; "$RUNNER" "$S" scan "$wbs" --spur-bin "$spurBin" && "$RUNNER" "$S" fold "$wbs" --spur-bin "$spurBin"
533
731
  # (e) one jq mutation binds the verdict to the proof digest (0703 R3; runId 0730 §B.2 /
534
732
  # 0757 R4; definitionDigest 0759 R5; honest review stamp 0785 R4; soft action + hard guard).
535
733
  - kind: shell
@@ -584,10 +782,16 @@ states:
584
782
  # (d) feature-sync-bounded.ts owns the sync; shell adds the orphan note and fallbacks
585
783
  # (0411 retry-suppression; 0328 / ADR-0322). Best-effort `exit 0` — feature status sync is a
586
784
  # follow-up, not a completion gate; `record → done` runs `spur task check`.
785
+ # 0931 R5: with deferFeatureSync "true" (parallel mode) the deferral note lands in the
786
+ # task report and the sync is skipped entirely, so task branches never touch feature
787
+ # corpus files; the batch orchestrator performs the deferred sync on the base ref.
788
+ # The sync itself is owned by record-feature-sync.ts (ADR-115: the guard plus the old
789
+ # inline chain cannot share one shell); resolution follows the standard in-repo-first,
790
+ # superskill-staged fallback used by every pipeline checker.
587
791
  - kind: shell
588
792
  options:
589
793
  command: >-
590
- FID=$($spurBin task show $wbs --json 2>/dev/null | jq -r '.feature_id // .frontmatter.feature_id // empty' 2>/dev/null); if [ -z "$FID" ]; then echo "Orphan task $wbs — no feature_id linked — proposal: consider linking to a parent feature." >> ".spur/run/$wbs-report.txt"; elif [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then bun plugins/sp/scripts/feature-sync-bounded.ts "$FID" --spur-bin "$spurBin" --json; elif M="$(superskill script path sp feature-sync-bounded.mjs 2>/dev/null)" && [ -f "$M" ]; then node "$M" "$FID" --spur-bin "$spurBin" --json; else $spurBin feature sync "$FID" --json; fi; exit 0
794
+ S=plugins/sp/scripts/record-feature-sync.ts; [ -f "$S" ] || S="$(superskill script path sp record-feature-sync.mjs 2>/dev/null)"; [ "$deferFeatureSync" = "true" ] && echo "feature sync deferred to batch integration" >> ".spur/run/$wbs-report.txt" || if [ -f "$S" ]; then bun "$S" --spur-bin "$spurBin"; else echo "feature sync skipped — record-feature-sync not found in plugins/sp/scripts/ nor staged" >> ".spur/run/$wbs-report.txt"; fi; exit 0
591
795
 
592
796
  - id: done
593
797
  description: >
@@ -595,6 +799,13 @@ states:
595
799
  guard runs `spur task check` before certifying; a genuinely non-compliant
596
800
  task routes to `failed` instead of a silent bad `done`.
597
801
  onEnter:
802
+ # (d) F96 R4 (0950): residual-scan.ts owns the settle; shell only resolves it.
803
+ # Soft best-effort (exit 0) — runs after certification and must never change
804
+ # the outcome; failure prints the re-run command.
805
+ - kind: shell
806
+ options:
807
+ command: >-
808
+ S=plugins/sp/scripts/residual-scan.ts; [ -f "$S" ] || S="$(superskill script path sp residual-scan.mjs 2>/dev/null)"; if [ -f "$S" ]; then case "$S" in *.mjs) RUNNER=node ;; *) RUNNER=bun ;; esac; "$RUNNER" "$S" settle "$wbs" --spur-bin "$spurBin" || echo "residual-settle failed — re-run: residual-scan settle $wbs" >&2; else echo "residual-scan not staged — settle skipped — re-run: residual-scan settle $wbs" >&2; fi; exit 0
598
809
  # (c) command.gate owns the done transition with classified transient retry.
599
810
  - kind: command.gate
600
811
  options:
@@ -620,7 +831,28 @@ states:
620
831
  - id: failed
621
832
  description: >
622
833
  Terminal — precheck, quality-gate exhaustion, verify non-PASS, record check
623
- failure, or operator rejection; reported, not advanced.
834
+ failure, or operator rejection; reported, not advanced. Also receives an
835
+ escalation-bound pause or an operator give-up (0933).
836
+ onEnter:
837
+ # 0933 R30 (guard-lines compression): the escalation-bound report note lives
838
+ # here as a conditional — no-op unless the run reached failed with a pending
839
+ # question at (or above) the ask bound (bound pause or final give-up). The
840
+ # unanswered question itself is appended to the task report (report.txt, R2).
841
+ - kind: shell
842
+ options:
843
+ command: >-
844
+ n="$(cat .spur/run/$wbs-escalation-count 2>/dev/null || echo 0)";
845
+ test "$n" -ge "$maxEscalations" -a -s .spur/run/$wbs-question.md || exit 0;
846
+ printf '\n## Escalation bound reached\n\nThe implement step asked an operator question %s times (bound %s) and ended without a resumable answer. Unanswered question:\n\n' "$n" "$maxEscalations" >> .spur/run/$wbs-report.txt;
847
+ cat .spur/run/$wbs-question.md >> .spur/run/$wbs-report.txt;
848
+ exit 0
849
+ # (d) F96 R5 (0950): residual-scan.ts owns the report; shell only resolves it.
850
+ # Soft (exit 0): a run that exhausts its fix budget on residuals leaves the
851
+ # report + recovery line while the task stays `wip`.
852
+ - kind: shell
853
+ options:
854
+ command: >-
855
+ S=plugins/sp/scripts/residual-scan.ts; [ -f "$S" ] || S="$(superskill script path sp residual-scan.mjs 2>/dev/null)"; if [ -f "$S" ]; then case "$S" in *.mjs) RUNNER=node ;; *) RUNNER=bun ;; esac; "$RUNNER" "$S" report "$wbs" --spur-bin "$spurBin" || echo "residual-report failed — re-run: residual-scan report $wbs" >&2; else echo "residual-scan not staged — report skipped — re-run: residual-scan report $wbs" >&2; fi; exit 0
624
856
 
625
857
  - id: cancelled
626
858
  description: Terminal — pipeline cancelled by operator at the approval gate (R1).
@@ -640,43 +872,85 @@ transitions:
640
872
  test "$size_status" = PASS && test "$evidence_status" = PASS && $spurBin task check $wbs
641
873
  - from: precheck
642
874
  to: failed
875
+ terminalReason: failed-check
643
876
  description: Size and/or task check failed — stop before implement.
644
877
  guard:
645
878
  kind: always
646
879
 
880
+ # ── implement routing: escalation first (0933 R26/R30), then the linear body ──
881
+ # Declaration order matters: the escalation bound is checked FIRST — a paused
882
+ # attempt that already exhausted its maxEscalations asks routes to failed (with a
883
+ # report note appended in the same guard) instead of asking forever; then the
884
+ # question-presence hop; then the normal always edge. The bound guard requires a
885
+ # fresh question file so a COMPLETED implement after max asks still routes to test.
886
+ - from: implement
887
+ to: failed
888
+ terminalReason: retry-exhausted
889
+ description: Escalation bound exhausted — the agent paused again after maxEscalations operator answers.
890
+ # 0933 R30 (guard-lines compression): thin predicate; the report note is written
891
+ # by the failed state's onEnter when it observes a bound-exhausted pause.
892
+ guard:
893
+ kind: shell
894
+ options:
895
+ command: >-
896
+ n="$(cat .spur/run/$wbs-escalation-count 2>/dev/null || echo 0)";
897
+ test "$n" -ge "$maxEscalations" -a -s .spur/run/$wbs-question.md
898
+ - from: implement
899
+ to: escalate
900
+ description: Implement agent paused on an operator question (non-empty question file) — pause for an answer.
901
+ guard:
902
+ kind: shell
903
+ options:
904
+ command: 'test -s .spur/run/$wbs-question.md'
647
905
  # ── linear body ──
648
906
  - from: implement
649
907
  to: test
650
908
  description: Implementation done — quality-gate probe.
651
909
  guard:
652
910
  kind: always
653
- # Soft probe branching (declaration order: PASS first, then FAIL, then defense).
654
- - from: test
655
- to: verify
656
- description: Quality gate already green and mode is fast — proportional fast path bypasses review.
911
+ # escalate branching (0933 R27): an answered pause resumes implement — the transcript
912
+ # append and question consumption happen in implement's onEnter (guards stay thin
913
+ # under ADR-115 guard-lines caps); an empty answer is an operator give-up → failed
914
+ # (question + transcript preserved as evidence).
915
+ - from: escalate
916
+ to: implement
917
+ description: Operator answered — re-enter implement, which records the Q/A transcript and consumes the question.
657
918
  guard:
658
919
  kind: shell
659
920
  options:
660
- command: >-
661
- gate_status="$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)";
662
- test "$gate_status" = PASS && test "$mode" = fast
921
+ command: 'test -n "$__hitlInput"'
922
+ - from: escalate
923
+ to: failed
924
+ terminalReason: cancelled
925
+ description: Empty operator answer — give-up routes to failed (question + transcript preserved as evidence).
926
+ guard:
927
+ kind: shell
928
+ options:
929
+ command: 'test -z "$__hitlInput"'
930
+ # Soft probe branching (declaration order: PASS → triage, FAIL → test-fail-triage, defense).
931
+ # 0943 R1/R3: the four former gate-PASS edges (test/test-recheck → verify/review, split on
932
+ # `$mode`) are replaced by the deterministic `triage` lane producer; a red gate lands in
933
+ # `test-fail-triage` where `decide failure-class` routes the failure instead of a static fixall.
663
934
  - from: test
664
- to: review
665
- description: Quality gate already green and safety mode — proceed to review.
935
+ to: triage
936
+ description: Quality gate green — route the lane deterministically (triage, 0943 R1).
666
937
  guard:
667
938
  kind: shell
668
939
  options:
669
940
  command: >-
670
941
  gate_status="$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)";
671
- test "$gate_status" = PASS && test "$mode" != fast
942
+ test "$gate_status" = PASS
672
943
  - from: test
673
- to: test-fix
674
- description: Quality gate red — start bounded fixall loop.
944
+ to: test-fail-triage
945
+ description: Quality gate red — classify the failure before the repair hop (0943 R3).
675
946
  guard:
676
947
  kind: shell
677
948
  options:
678
949
  command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL'
679
- # Defense: missing/corrupt status — treat as FAIL path, not silent PASS.
950
+ # 0943 review P3#2: the former `test → test-fix` FAIL edge is removed — it was dead (the
951
+ # identical guard on test → test-fail-triage fires first) and a silent spec-bypass trap:
952
+ # restoring it would skip failure-class on every red gate. The always-defense below keeps
953
+ # pre-0943 corrupt-status behavior (fixall then recheck).
680
954
  - from: test
681
955
  to: test-fix
682
956
  description: Probe status missing/corrupt — attempt fixall then recheck.
@@ -687,51 +961,95 @@ transitions:
687
961
  description: Fixall finished — soft recheck the same quality gate.
688
962
  guard:
689
963
  kind: always
690
- # Recheck branching (PASS first; under-max FAIL → fixall again; exhausted → failed).
964
+ # Recheck branching (PASS → triage; red → test-fail-triage; defense). The two former
965
+ # static FAIL edges (under-max → test-fix, exhausted → failed retry-exhausted) moved into
966
+ # test-fail-triage's decision routing (0943 R3).
691
967
  - from: test-recheck
692
- to: verify
693
- description: Quality gate green after fixall and mode is fast — proportional fast path bypasses review.
968
+ to: triage
969
+ description: Quality gate green after fixall — route the lane deterministically (triage, 0943 R1).
694
970
  guard:
695
971
  kind: shell
696
972
  options:
697
973
  command: >-
698
974
  gate_status="$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)";
699
- test "$gate_status" = PASS && test "$mode" = fast
975
+ test "$gate_status" = PASS
700
976
  - from: test-recheck
701
- to: review
702
- description: Quality gate green after fixall and safety mode — proceed to review.
977
+ to: test-fail-triage
978
+ description: Still red after fixall — classify the failure (0943 R3).
703
979
  guard:
704
980
  kind: shell
705
981
  options:
706
982
  command: >-
707
983
  gate_status="$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)";
708
- test "$gate_status" = PASS && test "$mode" != fast
984
+ test "$gate_status" = FAIL
985
+ # Defense: corrupt recheck status — failed, not review.
709
986
  - from: test-recheck
710
- to: test-fix
711
- description: Still red and under qualityGateMaxFixAttempts — another fixall hop.
712
- # (warn) 5 commands: named gate status + fix attempts (legibility, 0874).
987
+ to: failed
988
+ terminalReason: failed-check
989
+ description: Recheck status missing/corrupt — stop at failed.
990
+ guard:
991
+ kind: always
992
+ # ── triage routing (0943 R1/R2): the lane producer replaced the four gate-PASS edges.
993
+ # mode=fast → verify; anything else (standard/high, deterministic-high, degraded default,
994
+ # caller-set non-fast) → review. Together the two guards are exhaustive, so triage never hangs.
995
+ - from: triage
996
+ to: verify
997
+ description: Triage resolved the fast lane (caller-set fast or task-triage low) — skip review.
998
+ guard:
999
+ kind: shell
1000
+ options:
1001
+ command: 'test "$mode" = fast'
1002
+ - from: triage
1003
+ to: review
1004
+ description: Triage resolved a review lane (standard/high, deterministic-high, or caller-set) — review.
1005
+ guard:
1006
+ kind: shell
1007
+ options:
1008
+ command: 'test "$mode" != fast'
1009
+ # ── failure-class routing (0943 R3): stop first (never mislabeled by the cap), then the
1010
+ # attempt cap (bounds BOTH lanes — retryable counted on entry, fix as before), then the
1011
+ # decision lanes, then the fail-closed defense. Declaration order matters.
1012
+ - from: test-fail-triage
1013
+ to: failed
1014
+ terminalReason: failed-check
1015
+ description: Failure class stop — the run terminates at failed, never bypassed.
713
1016
  guard:
714
1017
  kind: shell
715
1018
  options:
716
1019
  command: >-
717
- gate_status="$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)";
718
- fix_attempts="$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)";
719
- test "$gate_status" = FAIL && test "$fix_attempts" -lt "$qualityGateMaxFixAttempts"
720
- - from: test-recheck
1020
+ jq -e '.value == "stop"' ".spur/run/$wbs-failure-class.decision" >/dev/null 2>&1
1021
+ - from: test-fail-triage
721
1022
  to: failed
722
- description: Still red after max fixall attempts — stop at failed (not silent abort).
723
- # (warn) 5 commands: named gate status + fix attempts (legibility, 0874).
1023
+ terminalReason: retry-exhausted
1024
+ description: Attempt cap reached — qualityGateMaxFixAttempts bounds the retryable recheck and fix lanes alike.
1025
+ # (warn) 5 commands: named fix attempts (legibility, 0874).
724
1026
  guard:
725
1027
  kind: shell
726
1028
  options:
727
1029
  command: >-
728
- gate_status="$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)";
729
1030
  fix_attempts="$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)";
730
- test "$gate_status" = FAIL && test "$fix_attempts" -ge "$qualityGateMaxFixAttempts"
731
- # Defense: corrupt recheck status — failed, not review.
732
- - from: test-recheck
1031
+ test "$fix_attempts" -ge "$qualityGateMaxFixAttempts"
1032
+ - from: test-fail-triage
1033
+ to: test-recheck
1034
+ description: Failure class retryable and under the cap — re-run the gate without fixall; the attempt was counted on entry.
1035
+ guard:
1036
+ kind: shell
1037
+ options:
1038
+ command: >-
1039
+ jq -e '.value == "retryable"' ".spur/run/$wbs-failure-class.decision" >/dev/null 2>&1
1040
+ - from: test-fail-triage
1041
+ to: test-fix
1042
+ description: Failure class fix and under the cap — the bounded repair hop, as before.
1043
+ guard:
1044
+ kind: shell
1045
+ options:
1046
+ command: >-
1047
+ jq -e '.value == "fix"' ".spur/run/$wbs-failure-class.decision" >/dev/null 2>&1
1048
+ # Defense: missing/corrupt failure-class decision — fail closed instead of silently repairing.
1049
+ - from: test-fail-triage
733
1050
  to: failed
734
- description: Recheck status missing/corrupt — stop at failed.
1051
+ terminalReason: failed-check
1052
+ description: Failure-class decision missing/corrupt — stop at failed.
735
1053
  guard:
736
1054
  kind: always
737
1055
  # ── review → approve, OR skip the HITL gate entirely when profile=auto (R4) ──
@@ -766,6 +1084,7 @@ transitions:
766
1084
  command: 'test "$__hitlAnswer" = yes'
767
1085
  - from: approve
768
1086
  to: failed
1087
+ terminalReason: cancelled
769
1088
  description: Operator rejected at the approval gate — report and stop.
770
1089
  guard:
771
1090
  kind: shell
@@ -773,6 +1092,7 @@ transitions:
773
1092
  command: 'test "$__hitlAnswer" = no'
774
1093
  - from: approve
775
1094
  to: cancelled
1095
+ terminalReason: cancelled
776
1096
  description: Operator cancelled at the approval gate.
777
1097
  guard:
778
1098
  kind: shell
@@ -821,6 +1141,7 @@ transitions:
821
1141
  test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -lt "$qualityGateMaxFixAttempts"
822
1142
  - from: verify
823
1143
  to: failed
1144
+ terminalReason: retry-exhausted
824
1145
  description: >-
825
1146
  Non-PASS with the fix budget exhausted, or a PASS/missing/malformed verdict whose proof
826
1147
  block is absent or mismatched (task 0703 R5) — block before done; defense catch-all so the
@@ -850,6 +1171,7 @@ transitions:
850
1171
  test "$verdict" = PASS && test "$proof_digest" = "$proofDigest"
851
1172
  - from: record
852
1173
  to: failed
1174
+ terminalReason: failed-check
853
1175
  description: Task check failed or proof evidence missing/malformed/mismatched — block before done.
854
1176
  guard:
855
1177
  kind: always