@gobing-ai/spur 0.3.30 → 0.3.32

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 (68) hide show
  1. package/config/rules/strict/runtime-boundaries.yaml +2 -1
  2. package/config/workflows/basic.yaml +92 -14
  3. package/config/workflows/docs-pipeline.yaml +71 -25
  4. package/config/workflows/feature-dev.yaml +104 -35
  5. package/config/workflows/feature-lifecycle.yaml +10 -2
  6. package/config/workflows/idea-pipeline.yaml +118 -54
  7. package/config/workflows/planning-pipeline.yaml +78 -30
  8. package/config/workflows/task-lifecycle.yaml +10 -2
  9. package/config/workflows/task-pipeline.yaml +219 -56
  10. package/config/workflows/wayfinder-resolution.yaml +64 -27
  11. package/config/workflows/wrapup-pipeline.yaml +79 -39
  12. package/package.json +9 -9
  13. package/schemas/state-machine-workflow.schema.json +5 -0
  14. package/spur.js +1302 -707
  15. package/web/_astro/BoardApp.CBVEjvSW.js +1 -0
  16. package/web/_astro/{BoardApp.BwAdAgK9.js → BoardApp.CyyIiK76.js} +74 -74
  17. package/web/_astro/TaskDetail.BfUuRsfM.js +1 -0
  18. package/web/_astro/{arc.CO-I_PZe.js → arc.C7fql4LL.js} +1 -1
  19. package/web/_astro/{architectureDiagram-3BPJPVTR.Si-A1Ncl.js → architectureDiagram-3BPJPVTR.IJPsRX-V.js} +1 -1
  20. package/web/_astro/{blockDiagram-GPEHLZMM.CeNy_ta6.js → blockDiagram-GPEHLZMM.B1AUwBxq.js} +1 -1
  21. package/web/_astro/{c4Diagram-AAUBKEIU.BgN_scEU.js → c4Diagram-AAUBKEIU.BuSJPR3z.js} +1 -1
  22. package/web/_astro/channel.D3uZSASb.js +1 -0
  23. package/web/_astro/{chunk-2J33WTMH.C3oDtmdi.js → chunk-2J33WTMH.CaOPTr9l.js} +1 -1
  24. package/web/_astro/{chunk-4BX2VUAB.Bv8Fxh4H.js → chunk-4BX2VUAB.DQzxExn8.js} +1 -1
  25. package/web/_astro/{chunk-55IACEB6.BT-TqtV6.js → chunk-55IACEB6.IIb103c7.js} +1 -1
  26. package/web/_astro/{chunk-727SXJPM.DkGMsI0B.js → chunk-727SXJPM.DOYz1K1b.js} +1 -1
  27. package/web/_astro/{chunk-AQP2D5EJ.B_-SqGnF.js → chunk-AQP2D5EJ.CFcfW-wt.js} +1 -1
  28. package/web/_astro/{chunk-FMBD7UC4.DwR3skMv.js → chunk-FMBD7UC4.C7Hn0jo3.js} +1 -1
  29. package/web/_astro/{chunk-ND2GUHAM.BaTQutFl.js → chunk-ND2GUHAM.Ba83VQ1m.js} +1 -1
  30. package/web/_astro/{chunk-QZHKN3VN.BJMSqqJp.js → chunk-QZHKN3VN.CizNbIIP.js} +1 -1
  31. package/web/_astro/{classDiagram-4FO5ZUOK.SaqCNUPF.js → classDiagram-4FO5ZUOK.2NYYPXHq.js} +1 -1
  32. package/web/_astro/{classDiagram-v2-Q7XG4LA2.SaqCNUPF.js → classDiagram-v2-Q7XG4LA2.2NYYPXHq.js} +1 -1
  33. package/web/_astro/{cose-bilkent-S5V4N54A.BkmjA_E2.js → cose-bilkent-S5V4N54A.DwBqVzml.js} +1 -1
  34. package/web/_astro/{dagre-BM42HDAG.CNUvWhrX.js → dagre-BM42HDAG.DqnYsHWG.js} +1 -1
  35. package/web/_astro/{diagram-2AECGRRQ.C68Cg9uE.js → diagram-2AECGRRQ.D8TKhgy-.js} +1 -1
  36. package/web/_astro/{diagram-5GNKFQAL.DbHkPLEn.js → diagram-5GNKFQAL.BUD6Prj5.js} +1 -1
  37. package/web/_astro/{diagram-KO2AKTUF.Dt0EdErs.js → diagram-KO2AKTUF.Cvg7hmND.js} +1 -1
  38. package/web/_astro/{diagram-LMA3HP47.Bixs6mnm.js → diagram-LMA3HP47.FUG_B9eS.js} +1 -1
  39. package/web/_astro/{diagram-OG6HWLK6.B4KBI6KT.js → diagram-OG6HWLK6.XsBZQ0hQ.js} +1 -1
  40. package/web/_astro/{erDiagram-TEJ5UH35.C_4i9Xvx.js → erDiagram-TEJ5UH35.M_LSGgLQ.js} +1 -1
  41. package/web/_astro/{flowDiagram-I6XJVG4X.CfGFczAY.js → flowDiagram-I6XJVG4X.DZikZBRa.js} +1 -1
  42. package/web/_astro/{ganttDiagram-6RSMTGT7.C2AIvtmg.js → ganttDiagram-6RSMTGT7.DTKuEmXY.js} +1 -1
  43. package/web/_astro/{gitGraphDiagram-PVQCEYII.HlXuGqQg.js → gitGraphDiagram-PVQCEYII.Dt-E9XmY.js} +1 -1
  44. package/web/_astro/index.yAse9IaO.css +1 -0
  45. package/web/_astro/{infoDiagram-5YYISTIA.BWQJsJ4Z.js → infoDiagram-5YYISTIA.D5snevU1.js} +1 -1
  46. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CCHgIHik.js → ishikawaDiagram-YF4QCWOH.DmXdBf9W.js} +1 -1
  47. package/web/_astro/{journeyDiagram-JHISSGLW.D9M6YwOJ.js → journeyDiagram-JHISSGLW.HPJSRkkU.js} +1 -1
  48. package/web/_astro/{kanban-definition-UN3LZRKU.K20D-a1_.js → kanban-definition-UN3LZRKU.BxCqCnL1.js} +1 -1
  49. package/web/_astro/{linear.DeP5Qsol.js → linear.BnXQUpS-.js} +1 -1
  50. package/web/_astro/{mermaid.core.ClzxNMPU.js → mermaid.core.D-ZeOmpg.js} +4 -4
  51. package/web/_astro/{mindmap-definition-RKZ34NQL.CGn9RUNj.js → mindmap-definition-RKZ34NQL.DxrftiFR.js} +1 -1
  52. package/web/_astro/{pieDiagram-4H26LBE5.CoA92Csb.js → pieDiagram-4H26LBE5.D-ZcWxwg.js} +1 -1
  53. package/web/_astro/{quadrantDiagram-W4KKPZXB.CJ_UPjM1.js → quadrantDiagram-W4KKPZXB.C-dVJ7GW.js} +1 -1
  54. package/web/_astro/{requirementDiagram-4Y6WPE33.KkEPNNUU.js → requirementDiagram-4Y6WPE33.lalE_R8X.js} +1 -1
  55. package/web/_astro/{sankeyDiagram-5OEKKPKP.CoCWbQgh.js → sankeyDiagram-5OEKKPKP.C4uPynAF.js} +1 -1
  56. package/web/_astro/{sequenceDiagram-3UESZ5HK.DUFpV3EP.js → sequenceDiagram-3UESZ5HK.uBOenkqh.js} +1 -1
  57. package/web/_astro/{stateDiagram-AJRCARHV.CgWX0_9m.js → stateDiagram-AJRCARHV.CQOusSP2.js} +1 -1
  58. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BKUXeGSf.js → stateDiagram-v2-BHNVJYJU.Bw0ZdjYb.js} +1 -1
  59. package/web/_astro/{timeline-definition-PNZ67QCA.CXayAguY.js → timeline-definition-PNZ67QCA.DbkPA5ce.js} +1 -1
  60. package/web/_astro/{vennDiagram-CIIHVFJN.DPak_Elh.js → vennDiagram-CIIHVFJN.CIKhzwoA.js} +1 -1
  61. package/web/_astro/{wardley-L42UT6IY.t3ZlDKd0.js → wardley-L42UT6IY.0P-tqaFs.js} +1 -1
  62. package/web/_astro/{wardleyDiagram-YWT4CUSO.CwPSVASQ.js → wardleyDiagram-YWT4CUSO.DrwTvM_M.js} +1 -1
  63. package/web/_astro/{xychartDiagram-2RQKCTM6.CJfrQgpF.js → xychartDiagram-2RQKCTM6.BgJsQVms.js} +1 -1
  64. package/web/index.html +2 -2
  65. package/web/_astro/BoardApp.Bk3T0o6s.js +0 -1
  66. package/web/_astro/TaskDetail.L36slEqE.js +0 -1
  67. package/web/_astro/channel.CYdGnFHM.js +0 -1
  68. package/web/_astro/index.D5Wqk2Tw.css +0 -1
@@ -1,3 +1,15 @@
1
+ # Wayfinder resolution pipeline — research/specification tasks without the code
2
+ # implementation pipeline (no task-pipeline / /sp:dev-run recursion).
3
+ #
4
+ # Reliability (aligned with task-pipeline / ADR-043):
5
+ # - Soft precheck (task check + show) → failed via transitions (status file, always exit 0)
6
+ # - collect fail-closed on non-empty input bundle
7
+ # - investigate→verify enforces Testing content floor (0300 R2) before independent verify
8
+ # - verify writes machine-readable PASS|PARTIAL|FAIL with expectFile
9
+ # - HITL approve routing is exhaustive (yes/no/cancel)
10
+ # - record → done is fail-closed on check + actual status=done
11
+ # - Free-form agent inputs are intentional: no pure-slash research surface yet
12
+
1
13
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
2
14
  kind: state-machine
3
15
  name: wayfinder-resolution
@@ -8,12 +20,15 @@ terminalStates:
8
20
  - done
9
21
  - failed
10
22
  - cancelled
23
+ failureStates:
24
+ - failed
25
+ - cancelled
11
26
  vars:
12
27
  wbs: "0000"
13
28
  profile: "standard"
14
29
  spurBin: "spur"
15
30
  agent: "omp"
16
- stepTimeoutMs: "900000"
31
+ stepTimeoutMs: "1800000"
17
32
  approval: "required"
18
33
  resolutionMode: "research"
19
34
  evidencePolicy: "spur-cli-first"
@@ -21,14 +36,24 @@ vars:
21
36
 
22
37
  states:
23
38
  - id: precheck
24
- description: "Validate the task and confirm the requested resolution mode before delegation."
39
+ description: >
40
+ Soft precheck: task check + show, status files, always exit 0 so failures
41
+ route to `failed` via transitions (not a raw lifecycle abort).
25
42
  onEnter:
26
43
  - kind: shell
27
44
  options:
28
- command: "${vars.spurBin} task check ${vars.wbs}"
29
- - kind: shell
30
- options:
31
- command: "${vars.spurBin} task show ${vars.wbs} --json"
45
+ command: >-
46
+ mkdir -p .spur/run/wayfinder &&
47
+ set +e &&
48
+ $spurBin task check $wbs; check_rc=$?;
49
+ $spurBin task show $wbs --json > .spur/run/wayfinder/$wbs-show.json; show_rc=$?;
50
+ set -e &&
51
+ if [ "$check_rc" -eq 0 ] && [ "$show_rc" -eq 0 ]; then
52
+ printf 'PASS\n' > .spur/run/wayfinder/$wbs-precheck.status;
53
+ else
54
+ printf 'FAIL\n' > .spur/run/wayfinder/$wbs-precheck.status;
55
+ fi &&
56
+ exit 0
32
57
  - kind: note
33
58
  options:
34
59
  message: "Wayfinder resolution started for task ${vars.wbs} (mode=${vars.resolutionMode}, evidence=${vars.evidencePolicy}); no code implementation pipeline is permitted."
@@ -38,13 +63,17 @@ states:
38
63
  onEnter:
39
64
  - kind: shell
40
65
  options:
41
- command: "mkdir -p .spur/run/wayfinder && ${vars.spurBin} task show ${vars.wbs} --json > .spur/run/wayfinder/${vars.wbs}-input.json"
66
+ command: "mkdir -p .spur/run/wayfinder && $spurBin task show $wbs --json > .spur/run/wayfinder/$wbs-input.json"
67
+ # `.spur/workflows/` is the runtime read path in every layout — a symlink to
68
+ # config/workflows/ inside the Spur monorepo, a real directory in a seeded
69
+ # project. The previous `config/workflows/…` literal only resolved in the
70
+ # monorepo and hard-failed `collect` anywhere else.
42
71
  - kind: shell
43
72
  options:
44
- command: "${vars.spurBin} workflow validate config/workflows/wayfinder-resolution.yaml > .spur/run/wayfinder/${vars.wbs}-workflow-check.txt"
73
+ command: "$spurBin workflow validate .spur/workflows/wayfinder-resolution.yaml > .spur/run/wayfinder/$wbs-workflow-check.txt"
45
74
  - kind: shell
46
75
  options:
47
- command: "${vars.spurBin} task check ${vars.wbs} > .spur/run/wayfinder/${vars.wbs}-precheck.txt"
76
+ command: "$spurBin task check $wbs > .spur/run/wayfinder/$wbs-precheck.txt"
48
77
 
49
78
  - id: investigate
50
79
  description: "Resolve the task's research/specification requirements and author evidence-backed sections through Spur CLI."
@@ -79,6 +108,9 @@ states:
79
108
  - id: verify
80
109
  description: "Perform an independent evidence and traceability check for the resolved research task."
81
110
  onEnter:
111
+ - kind: shell
112
+ options:
113
+ command: 'rm -f .spur/run/wayfinder/$wbs-resolution-verdict.txt'
82
114
  - kind: agent.run
83
115
  options:
84
116
  agent: ${vars.agent}
@@ -92,6 +124,7 @@ states:
92
124
  Do not implement plugin code. Write the final verdict word (exactly PASS, PARTIAL,
93
125
  or FAIL, nothing else) to `.spur/run/wayfinder/${vars.wbs}-resolution-verdict.txt`,
94
126
  then finish with the same verdict and stop.
127
+ expectFile: .spur/run/wayfinder/${vars.wbs}-resolution-verdict.txt
95
128
  timeoutMs: ${vars.stepTimeoutMs}
96
129
 
97
130
  - id: approve
@@ -105,14 +138,20 @@ states:
105
138
  - id: record
106
139
  description: "Record the resolution through the canonical task CLI and transition only after checks pass."
107
140
  onEnter:
141
+ # Both steps are soft (`; exit 0`) BY DESIGN, not laxity: the real gate is the
142
+ # `record → done` guard below, which re-runs `spur task check` AND asserts the
143
+ # status actually reached done. Hard-failing here aborted the run inside the
144
+ # state, so the declared `record → failed` edge was unreachable and a blocked
145
+ # lifecycle transition surfaced as a bare "Command exited with 1" instead of
146
+ # the terminal `failed` this workflow documents.
108
147
  - kind: shell
109
148
  options:
110
- command: "${vars.spurBin} task check ${vars.wbs}"
149
+ command: "$spurBin task check $wbs ; exit 0"
111
150
  - kind: shell
112
151
  options:
113
152
  # PASS-only completion (preserve-list P4): lifecycle guards stay on; a forced
114
153
  # done without verdict evidence is the 0280 regression this replaces (see 0292).
115
- command: "${vars.spurBin} task update ${vars.wbs} done"
154
+ command: "$spurBin task update $wbs done ; exit 0"
116
155
 
117
156
  - id: done
118
157
  description: "Research task resolved and ready to feed the implementation backlog."
@@ -130,53 +169,51 @@ states:
130
169
  transitions:
131
170
  - from: precheck
132
171
  to: collect
133
- description: "Precheck passed — collect deterministic local inputs."
172
+ description: "Precheck PASS — collect deterministic local inputs."
134
173
  guard:
135
174
  kind: shell
136
175
  options:
137
- command: "${vars.spurBin} task check ${vars.wbs}"
176
+ command: 'test "$(cat .spur/run/wayfinder/$wbs-precheck.status 2>/dev/null)" = PASS'
138
177
  - from: precheck
139
178
  to: failed
140
- description: "Precheck failed — stop before delegation."
179
+ description: "Precheck FAIL — stop before delegation."
141
180
  guard:
142
- kind: shell
143
- options:
144
- command: "! ${vars.spurBin} task check ${vars.wbs}"
181
+ kind: always
145
182
  - from: collect
146
183
  to: investigate
147
184
  description: "Input bundle collected — resolve the research task."
148
185
  guard:
149
186
  kind: shell
150
187
  options:
151
- command: "test -s .spur/run/wayfinder/${vars.wbs}-input.json && test -s .spur/run/wayfinder/${vars.wbs}-precheck.txt"
188
+ command: "test -s .spur/run/wayfinder/$wbs-input.json && test -s .spur/run/wayfinder/$wbs-precheck.txt"
152
189
  - from: collect
153
190
  to: failed
154
191
  description: "Input collection failed — stop before delegation."
155
192
  guard:
156
193
  kind: shell
157
194
  options:
158
- command: "! test -s .spur/run/wayfinder/${vars.wbs}-input.json || ! test -s .spur/run/wayfinder/${vars.wbs}-precheck.txt"
195
+ command: "! test -s .spur/run/wayfinder/$wbs-input.json || ! test -s .spur/run/wayfinder/$wbs-precheck.txt"
159
196
  - from: investigate
160
197
  to: verify
161
198
  description: "Research resolution authored — verify evidence and traceability. Guard enforces the evidence content floor (0300 R2): the task's Testing section must be non-placeholder (>5 lines, >60 words) before independent verification may begin."
162
199
  guard:
163
200
  kind: shell
164
201
  options:
165
- command: 'test "$(${vars.spurBin} task show ${vars.wbs} --json | jq -r ".content" | awk "/^### Testing/{f=1;next} /^### /{f=0} f" | wc -l)" -gt 5 && test "$(${vars.spurBin} task show ${vars.wbs} --json | jq -r ".content" | awk "/^### Testing/{f=1;next} /^### /{f=0} f" | wc -w)" -gt 60'
202
+ command: 'test "$($spurBin task show $wbs --json | jq -r ".content" | awk "/^### Testing/{f=1;next} /^### /{f=0} f" | wc -l)" -gt 5 && test "$($spurBin task show $wbs --json | jq -r ".content" | awk "/^### Testing/{f=1;next} /^### /{f=0} f" | wc -w)" -gt 60'
166
203
  - from: investigate
167
204
  to: failed
168
205
  description: "Evidence content floor not met (0300 R2) — investigate authored a placeholder Testing section; stop before verification rather than certify hollow evidence."
169
206
  guard:
170
207
  kind: shell
171
208
  options:
172
- command: 'test "$(${vars.spurBin} task show ${vars.wbs} --json | jq -r ".content" | awk "/^### Testing/{f=1;next} /^### /{f=0} f" | wc -l)" -le 5 || test "$(${vars.spurBin} task show ${vars.wbs} --json | jq -r ".content" | awk "/^### Testing/{f=1;next} /^### /{f=0} f" | wc -w)" -le 60'
209
+ command: 'test "$($spurBin task show $wbs --json | jq -r ".content" | awk "/^### Testing/{f=1;next} /^### /{f=0} f" | wc -l)" -le 5 || test "$($spurBin task show $wbs --json | jq -r ".content" | awk "/^### Testing/{f=1;next} /^### /{f=0} f" | wc -w)" -le 60'
173
210
  - from: verify
174
211
  to: record
175
212
  description: "Auto profile — record only on a machine-readable PASS verdict (fail-closed to the approve gate otherwise)."
176
213
  guard:
177
214
  kind: shell
178
215
  options:
179
- command: 'test "${vars.approval}" = auto && grep -qx PASS .spur/run/wayfinder/${vars.wbs}-resolution-verdict.txt'
216
+ command: 'test "$approval" = auto && grep -qx PASS .spur/run/wayfinder/$wbs-resolution-verdict.txt'
180
217
  - from: verify
181
218
  to: approve
182
219
  description: "Verification completed — require explicit review approval."
@@ -188,32 +225,32 @@ transitions:
188
225
  guard:
189
226
  kind: shell
190
227
  options:
191
- command: 'test "${vars.__hitlAnswer}" = yes'
228
+ command: 'test "$__hitlAnswer" = yes'
192
229
  - from: approve
193
230
  to: cancelled
194
231
  description: "Operator cancelled the research resolution."
195
232
  guard:
196
233
  kind: shell
197
234
  options:
198
- command: 'test "${vars.__hitlAnswer}" = cancel'
235
+ command: 'test "$__hitlAnswer" = cancel'
199
236
  - from: approve
200
237
  to: failed
201
238
  description: "Operator rejected the research resolution."
202
239
  guard:
203
240
  kind: shell
204
241
  options:
205
- command: 'test "${vars.__hitlAnswer}" = no'
242
+ command: 'test "$__hitlAnswer" = no'
206
243
  - from: record
207
244
  to: done
208
245
  description: "Task check passed and the task status is actually done (the lifecycle transition succeeded)."
209
246
  guard:
210
247
  kind: shell
211
248
  options:
212
- command: '${vars.spurBin} task check ${vars.wbs} && test "$(${vars.spurBin} task show ${vars.wbs} --json | jq -r .status)" = done'
249
+ command: '$spurBin task check $wbs && test "$($spurBin task show $wbs --json | jq -r .status)" = done'
213
250
  - from: record
214
251
  to: failed
215
252
  description: "Recording/check failed or the task never reached done — stop without claiming completion."
216
253
  guard:
217
254
  kind: shell
218
255
  options:
219
- command: '! ${vars.spurBin} task check ${vars.wbs} || test "$(${vars.spurBin} task show ${vars.wbs} --json | jq -r .status)" != done'
256
+ command: '! $spurBin task check $wbs || test "$($spurBin task show $wbs --json | jq -r .status)" != done'
@@ -22,7 +22,15 @@
22
22
  # spurBin — PATH-independent spur invocation (overridden by CLI at run start)
23
23
  # agent — agent for agent.run steps (default: omp)
24
24
  #
25
- # Seeded by `spur init`; adapt the agent.run inputs to your project's command set.
25
+ # Reliability (aligned with task-pipeline / ADR-043):
26
+ # - Prefer pure slash commands when a command exists; free-form inputs remain only
27
+ # where capture artifacts (answerFile) have no dedicated slash surface yet.
28
+ # - learning-capture / metrics-record use expectFile + soft append (empty capture
29
+ # does not abort wrap-up after prior steps already landed).
30
+ # - feature-transition is a soft shell (always exit 0) so a blocked feature sync
31
+ # does not abort wrap-up after learnings/metrics already landed.
32
+ # - branch-cleanup HITL is exhaustive (yes/no/cancel → done; missing answer defense).
33
+ # - Wrap-up never mutates task status (consumes completed work only).
26
34
 
27
35
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
28
36
  kind: state-machine
@@ -40,7 +48,8 @@ vars:
40
48
  merge: "false"
41
49
  spurBin: "spur"
42
50
  agent: "omp"
43
- stepTimeoutMs: "600000"
51
+ stepTimeoutMs: "1800000"
52
+ __hitlAnswer: ""
44
53
 
45
54
  states:
46
55
  - id: start
@@ -64,67 +73,81 @@ states:
64
73
 
65
74
  - id: doc-sync
66
75
  description: >
67
- Dispatch sp:doc-evolve once for the entire batch. Project-level doc drift repair
68
- runs once, not per-task. The agent reads the batch's completed tasks, identifies
69
- doc drift (04_DESIGN, 03_ARCHITECTURE, 00_ADR, docs/design/*), and repairs it
70
- following the constitution's edit rules. Does NOT write task or feature corpus.
76
+ Project-level doc drift repair for the batch (04_DESIGN, 03_ARCHITECTURE,
77
+ 00_ADR, docs/design/*). No dedicated slash command yet free-form skill
78
+ pointer until a /sp:doc-evolve command exists (ADR-043 residual).
71
79
  onEnter:
72
80
  - kind: agent.run
73
81
  options:
74
82
  agent: ${vars.agent}
75
- input: "Run sp:doc-evolve for completed tasks ${vars.tasks}. Repair doc drift in 04_DESIGN, 03_ARCHITECTURE, 00_ADR, docs/design/* following the constitution edit rules. Do not write task or feature corpus files."
83
+ input: "Skill(skill=\"sp:doc-evolve\", args=\"wrapup tasks=${vars.tasks}\"). Repair drift in docs/00_ADR.md, docs/03_ARCHITECTURE.md, docs/04_DESIGN.md, docs/design/* per constitution edit rules. Do not write task/feature corpus."
76
84
  timeoutMs: ${vars.stepTimeoutMs}
77
85
 
78
86
  - id: learning-capture
79
87
  description: >
80
- Append working learnings to .spur/memory/learnings.md. The agent extracts
81
- insights from the batch and writes them to a capture file; a downstream shell
82
- step appends that file to the learnings scratchpad. This decouples content
83
- generation from file persistence: the shell step's exit code is the real
84
- success signal, and the capture file is verified before the append.
88
+ Append working learnings to .spur/memory/learnings.md. Capture+shell decoupling:
89
+ expectFile gates non-empty capture before append; append is best-effort soft
90
+ (empty capture skips without aborting wrap-up).
85
91
  onEnter:
86
92
  - kind: agent.run
87
93
  options:
88
94
  agent: ${vars.agent}
89
- input: "Extract working learnings from tasks ${vars.tasks}. Output as markdown grouped by date and task WBS. Include: conventions discovered, errors hit and resolved, patterns that worked, gotchas. Output raw markdown (no code fences) — your answer will be captured to a file and appended to .spur/memory/learnings.md."
95
+ input: "Extract working learnings from tasks ${vars.tasks}. Output raw markdown (no fences) grouped by date and task WBS: conventions, errors fixed, patterns, gotchas. Captured to .spur/run/wrapup-learnings.md."
90
96
  answerFile: .spur/run/wrapup-learnings.md
91
97
  expectFile: .spur/run/wrapup-learnings.md
92
98
  timeoutMs: ${vars.stepTimeoutMs}
93
99
  - kind: shell
94
100
  options:
95
- command: 'test -s .spur/run/wrapup-learnings.md && mkdir -p .spur/memory && cat .spur/run/wrapup-learnings.md >> .spur/memory/learnings.md && printf "\n" >> .spur/memory/learnings.md'
101
+ command: >-
102
+ mkdir -p .spur/memory &&
103
+ if test -s .spur/run/wrapup-learnings.md; then
104
+ cat .spur/run/wrapup-learnings.md >> .spur/memory/learnings.md &&
105
+ printf '\n' >> .spur/memory/learnings.md;
106
+ fi &&
107
+ exit 0
96
108
 
97
109
  - id: metrics-record
98
110
  description: >
99
- Append one JSONL row per task to .spur/memory/wrapup-metrics.jsonl. The agent
100
- produces the JSONL content to a capture file; a downstream shell step appends
101
- it. Each row records: task WBS, feature id, status, verdict, timestamp.
102
- Append-only and machine-readable. The capture+shell decoupling means a
103
- non-empty capture file is verified before the append (the agent.run exit-0
104
- is no longer the sole success signal — the shell step's exit code gates the
105
- write).
111
+ Append one JSONL row per task to .spur/memory/wrapup-metrics.jsonl. Soft append
112
+ (empty capture does not abort wrap-up after learnings already landed).
106
113
  onEnter:
107
114
  - kind: agent.run
108
115
  options:
109
116
  agent: ${vars.agent}
110
- input: "For each task in ${vars.tasks}, read the task file and output one JSONL line per task with fields: wbs, feature_id, status, verdict (from .spur/run/<wbs>-verdict.json if available), timestamp. Output raw JSONL (no code fences) — your answer will be captured to a file and appended to .spur/memory/wrapup-metrics.jsonl."
117
+ input: "For each task in ${vars.tasks}, output one JSONL line (no fences) with fields: wbs, feature_id, status, verdict, timestamp. Captured to .spur/run/wrapup-metrics.jsonl."
111
118
  answerFile: .spur/run/wrapup-metrics.jsonl
112
119
  expectFile: .spur/run/wrapup-metrics.jsonl
113
120
  timeoutMs: ${vars.stepTimeoutMs}
114
121
  - kind: shell
115
122
  options:
116
- command: 'test -s .spur/run/wrapup-metrics.jsonl && mkdir -p .spur/memory && cat .spur/run/wrapup-metrics.jsonl >> .spur/memory/wrapup-metrics.jsonl'
123
+ command: >-
124
+ mkdir -p .spur/memory &&
125
+ if test -s .spur/run/wrapup-metrics.jsonl; then
126
+ cat .spur/run/wrapup-metrics.jsonl >> .spur/memory/wrapup-metrics.jsonl;
127
+ fi &&
128
+ exit 0
117
129
 
118
130
  - id: feature-transition
119
131
  description: >
120
- If vars.feature is set, derive and sync feature status with linked task states via the
121
- bounded `feature-sync-bounded` wrapper (task 0328 / ADR-0322, task 0411 retry-suppression).
122
- Replaces prior unconditional `spur feature advance` with conservative status sync that
123
- suppresses identical blocked retries until relevant inputs change.
132
+ If vars.feature is set, sync feature status via bounded feature-sync-bounded
133
+ (0411). Soft shell blocked/failed sync must not abort wrap-up after metrics.
124
134
  onEnter:
135
+ # Genuinely soft: the terminating `exit 0` must be reached on BOTH branches, so
136
+ # the sync is chained with `;` — with `&&` a non-zero sync skipped `exit 0` and
137
+ # aborted the run at this state, discarding the learnings/metrics that already
138
+ # landed and never reaching branch-cleanup/done (the exact failure the state
139
+ # description promises against). `feature-sync-bounded.ts` is a Spur-monorepo
140
+ # path (`spur init` does not scaffold `plugins/sp/`), so seeded projects fall
141
+ # back to the plain `spur feature sync` verb.
125
142
  - kind: shell
126
143
  options:
127
- command: 'bun plugins/sp/scripts/feature-sync-bounded.ts ${vars.feature} --spur-bin "${vars.spurBin}" --json'
144
+ command: >-
145
+ if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then
146
+ bun plugins/sp/scripts/feature-sync-bounded.ts "$feature" --spur-bin "$spurBin" --json;
147
+ else
148
+ $spurBin feature sync "$feature" --json;
149
+ fi;
150
+ exit 0
128
151
 
129
152
  - id: branch-cleanup
130
153
  description: >
@@ -148,7 +171,7 @@ states:
148
171
  # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
149
172
  - kind: shell
150
173
  options:
151
- command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: wrapup-pipeline done tasks=${vars.tasks} ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/wrapup-checkpoint.md'
174
+ command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: wrapup-pipeline done tasks=$tasks ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/wrapup-checkpoint.md'
152
175
 
153
176
  - id: skipped
154
177
  description: Terminal — wrap-up skipped (empty task list or operator abort).
@@ -161,21 +184,30 @@ transitions:
161
184
  guard:
162
185
  kind: always
163
186
 
164
- # ── task-resolve: non-empty -> doc-sync, empty -> skipped ──
187
+ # ── task-resolve: non-empty -> doc-sync, empty/unparseable -> skipped ──
188
+ # `|| echo 0` is load-bearing: a malformed vars.tasks (or a box without `jq`) makes
189
+ # `jq length` emit nothing, and bare `test "" -gt 0` exits 2 — so BOTH edges failed
190
+ # and the run died with an opaque `no-passing-transition` instead of skipping. The
191
+ # trailing `always` edge is the belt-and-braces defense for any other guard fault.
165
192
  - from: task-resolve
166
193
  to: doc-sync
167
194
  description: Task list resolved and non-empty — begin doc-sync.
168
195
  guard:
169
196
  kind: shell
170
197
  options:
171
- command: 'test "$(echo "${vars.tasks}" | jq length)" -gt 0'
198
+ command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -gt 0'
172
199
  - from: task-resolve
173
200
  to: skipped
174
201
  description: Task list is empty — skip wrap-up.
175
202
  guard:
176
203
  kind: shell
177
204
  options:
178
- command: 'test "$(echo "${vars.tasks}" | jq length)" -eq 0'
205
+ command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -eq 0'
206
+ - from: task-resolve
207
+ to: skipped
208
+ description: Defense — task list unparseable; skip rather than fail with no-passing-transition.
209
+ guard:
210
+ kind: always
179
211
 
180
212
  # ── linear body: doc-sync -> learning-capture -> metrics-record ──
181
213
  - from: doc-sync
@@ -198,21 +230,21 @@ transitions:
198
230
  guard:
199
231
  kind: shell
200
232
  options:
201
- command: 'test -n "${vars.feature}"'
233
+ command: 'test -n "$feature"'
202
234
  - from: metrics-record
203
235
  to: branch-cleanup
204
236
  description: No feature id, but merge is requested — go to branch cleanup.
205
237
  guard:
206
238
  kind: shell
207
239
  options:
208
- command: 'test -z "${vars.feature}" && test "${vars.merge}" = true'
240
+ command: 'test -z "$feature" && test "$merge" = true'
209
241
  - from: metrics-record
210
242
  to: done
211
243
  description: No feature id and no merge — wrap-up is done.
212
244
  guard:
213
245
  kind: shell
214
246
  options:
215
- command: 'test -z "${vars.feature}" && test "${vars.merge}" != true'
247
+ command: 'test -z "$feature" && test "$merge" != true'
216
248
 
217
249
  # ── feature-transition: -> branch-cleanup (if merge) or done ──
218
250
  - from: feature-transition
@@ -221,18 +253,26 @@ transitions:
221
253
  guard:
222
254
  kind: shell
223
255
  options:
224
- command: 'test "${vars.merge}" = true'
256
+ command: 'test "$merge" = true'
225
257
  - from: feature-transition
226
258
  to: done
227
259
  description: Feature advanced — no merge requested.
228
260
  guard:
229
261
  kind: shell
230
262
  options:
231
- command: 'test "${vars.merge}" != true'
263
+ command: 'test "$merge" != true'
232
264
 
233
- # ── branch-cleanup: HITL confirmed -> done ──
265
+ # ── branch-cleanup: HITL exhaustive routing (yes / no / cancel → done) ──
266
+ # No irreversible git op is wired yet; confirmation is recorded, then wrap completes.
267
+ - from: branch-cleanup
268
+ to: done
269
+ description: Branch cleanup HITL answered — wrap-up complete.
270
+ guard:
271
+ kind: shell
272
+ options:
273
+ command: 'test "$__hitlAnswer" = yes || test "$__hitlAnswer" = no || test "$__hitlAnswer" = cancel'
234
274
  - from: branch-cleanup
235
275
  to: done
236
- description: Branch cleanup confirmed wrap-up complete.
276
+ description: Defense missing HITL answer still completes wrap (cleanup is optional).
237
277
  guard:
238
278
  kind: always
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.30",
3
+ "version": "0.3.32",
4
4
  "description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
5
5
  "keywords": [
6
6
  "spur",
@@ -50,14 +50,14 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@commander-js/extra-typings": "^14.0.0",
53
- "@gobing-ai/ts-db": "^0.4.16",
54
- "@gobing-ai/ts-ai-runner": "^0.4.16",
55
- "@gobing-ai/ts-dual-workflow-engine": "^0.4.16",
56
- "@gobing-ai/ts-infra": "^0.4.16",
57
- "@gobing-ai/ts-llm-jsonl-importer": "^0.4.16",
58
- "@gobing-ai/ts-rule-engine": "^0.4.16",
59
- "@gobing-ai/ts-runtime": "^0.4.16",
60
- "@gobing-ai/ts-utils": "^0.4.16",
53
+ "@gobing-ai/ts-db": "^0.4.18",
54
+ "@gobing-ai/ts-ai-runner": "^0.4.18",
55
+ "@gobing-ai/ts-dual-workflow-engine": "^0.4.18",
56
+ "@gobing-ai/ts-infra": "^0.4.18",
57
+ "@gobing-ai/ts-llm-jsonl-importer": "^0.4.18",
58
+ "@gobing-ai/ts-rule-engine": "^0.4.18",
59
+ "@gobing-ai/ts-runtime": "^0.4.18",
60
+ "@gobing-ai/ts-utils": "^0.4.18",
61
61
  "@types/bun": "1.3.14",
62
62
  "@types/figlet": "^1.7.0",
63
63
  "@types/node-notifier": "8.0.5",
@@ -13,6 +13,11 @@
13
13
  "description": { "type": "string" },
14
14
  "initialState": { "type": "string", "minLength": 1 },
15
15
  "terminalStates": { "type": "array", "items": { "type": "string", "minLength": 1 } },
16
+ "failureStates": {
17
+ "type": "array",
18
+ "items": { "type": "string", "minLength": 1 },
19
+ "description": "Subset of terminalStates whose reach finalizes the run as failed. Absent ⇒ every terminal is a success."
20
+ },
16
21
  "iterationBound": { "type": "integer", "exclusiveMinimum": 0 },
17
22
  "vars": {
18
23
  "type": "object",