@gobing-ai/spur 0.2.11 → 0.3.0

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.
@@ -7,7 +7,8 @@
7
7
  # (0055) apply identically. Run linkage is written to `task_run_links` (kind=pipeline).
8
8
  #
9
9
  # Shape: precheck → implement → test → review → approve(HITL) → verify → record → done
10
- # (precheck failure short-circuits to `failed`).
10
+ # (precheck failure short-circuits to `failed`; approve routes to `failed` on
11
+ # operator rejection or `cancelled` on operator cancel — R1, bug-750).
11
12
  #
12
13
  # Vars (passed as a JSON object via `--vars`):
13
14
  # wbs — the task WBS (required), e.g. `--vars '{"wbs":"0042"}'`
@@ -24,6 +25,7 @@ initialState: precheck
24
25
  terminalStates:
25
26
  - done
26
27
  - failed
28
+ - cancelled
27
29
  vars:
28
30
  wbs: "0000"
29
31
  profile: "standard"
@@ -39,6 +41,15 @@ vars:
39
41
  # subprocess is killed (not abandoned) and the step fails → pipeline routes to
40
42
  # `failed`. Override per run with `--vars '{"stepTimeoutMs":120000}'`.
41
43
  stepTimeoutMs: "600000"
44
+ # Timeout for the `implement` step specifically (R2a). Implementation is the
45
+ # heaviest agent.run step (full read/write/test-probe loop) and has timed out
46
+ # at the 600s default in five consecutive dogfood runs (bugs 742/744/746/748),
47
+ # a 100% failure rate at that budget. 30 min gives real headroom; STOP and
48
+ # record honestly rather than raising it further without operator sign-off.
49
+ implementTimeoutMs: "1800000"
50
+ # Answer captured by the approve gate's hitl.confirm (R1): "yes" | "no" | "cancel".
51
+ # Empty by default; only meaningful once the approve state has been entered.
52
+ __hitlAnswer: ""
42
53
 
43
54
  states:
44
55
  - id: precheck
@@ -46,6 +57,9 @@ states:
46
57
  Pre-flight structural validation. The precheck→implement guard runs
47
58
  `spur task check <wbs>`; a failure routes to `failed` (blocked report).
48
59
  onEnter:
60
+ - kind: shell
61
+ options:
62
+ command: "${vars.spurBin} agent doctor ${vars.agent}"
49
63
  - kind: note
50
64
  options:
51
65
  # R4 task_run_links linkage (kind=pipeline) is written by a WorkflowService
@@ -70,11 +84,24 @@ states:
70
84
  - kind: agent.run
71
85
  options:
72
86
  agent: ${vars.agent}
73
- input: /sp:dev-run --mode implement ${vars.wbs} --auto
74
- timeoutMs: ${vars.stepTimeoutMs}
87
+ input: >-
88
+ /sp:dev-run --mode implement ${vars.wbs} --auto.
89
+ Work only in this working tree on task ${vars.wbs}; NEVER invoke
90
+ `spur workflow run` or `/sp:dev-run` without `--mode implement` —
91
+ this step IS the pipeline (bug-742).
92
+ timeoutMs: ${vars.implementTimeoutMs}
75
93
  - kind: shell
76
94
  options:
77
95
  command: "${vars.spurBin} task update ${vars.wbs} wip --no-lifecycle"
96
+ # Post-implement cleanup: auto-format any unformatted output the agent produced.
97
+ # The agent may leave files that don't pass `biome check`, which then fails
98
+ # `bun run lint` downstream. Running format here is cheap (< 1 s) and prevents
99
+ # the test stage from tripping on a purely mechanical formatting defect (dogfood
100
+ # bug-733 — omp left agent-run.test.ts unformatted, causing a spurious lint gate
101
+ # failure that the ## Testing section mis-attributed to "pre-existing gaps").
102
+ - kind: shell
103
+ options:
104
+ command: "bun run format"
78
105
 
79
106
  - id: test
80
107
  description: Test execution + coverage via /sp:dev-unit.
@@ -84,6 +111,13 @@ states:
84
111
  agent: ${vars.agent}
85
112
  input: /sp:dev-unit ${vars.wbs} --auto
86
113
  timeoutMs: ${vars.stepTimeoutMs}
114
+ # Post-test gate: run the project's full lint gate (biome check + tsc). If the
115
+ # implement or test step left unformatted files or type errors, the run routes to
116
+ # `failed` rather than letting an agent-authored `## Testing` claim "all pass"
117
+ # against a red gate (dogfood bug-733).
118
+ - kind: shell
119
+ options:
120
+ command: "bun run lint"
87
121
 
88
122
  - id: review
89
123
  description: SECUA-framework code review via /sp:dev-review (Security, Efficiency, Correctness, Usability, Architecture).
@@ -99,6 +133,8 @@ states:
99
133
  Human-in-the-loop approval gate. Under `profile=auto` this state is never entered
100
134
  (review routes around it straight to verify). In interactive mode this state pauses
101
135
  the run for `spur workflow continue` (E3), making approval an explicit operator action.
136
+ The operator's answer is routed (R1, bug-750): yes -> verify; no -> failed (rejection
137
+ recorded, not silently approved); cancel -> cancelled (a distinct terminal state).
102
138
  pause: true
103
139
  onEnter:
104
140
  - kind: hitl.confirm
@@ -147,10 +183,17 @@ states:
147
183
  - kind: note
148
184
  options:
149
185
  message: "Pipeline complete for task ${vars.wbs} (done gate cleared)."
186
+ # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
187
+ - kind: shell
188
+ options:
189
+ command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: task-pipeline done wbs=${vars.wbs} ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/${vars.wbs}-checkpoint.md'
150
190
 
151
191
  - id: failed
152
192
  description: Terminal — precheck (or a gated step) failed; reported, not advanced.
153
193
 
194
+ - id: cancelled
195
+ description: Terminal — pipeline cancelled by operator at the approval gate (R1).
196
+
154
197
  transitions:
155
198
  # ── precheck: shell guard runs `spur task check`; pass → implement, fail → failed ──
156
199
  - from: precheck
@@ -197,12 +240,33 @@ transitions:
197
240
  guard:
198
241
  kind: always
199
242
 
200
- # ── approve: HITL gate (reached only when profile != auto) ──
243
+ # ── approve: HITL gate (reached only when profile != auto). Routed by the
244
+ # operator's captured answer (R1, bug-750) — three ordered guards, declaration
245
+ # order matters (yes first, then no, then cancel), mirroring idea-pipeline's
246
+ # design-approval gate (config/workflows/idea-pipeline.yaml:352-366). No
247
+ # `always` edge remains out of `approve`. ──
201
248
  - from: approve
202
249
  to: verify
203
250
  description: Approved — proceed to verification.
204
251
  guard:
205
- kind: always
252
+ kind: shell
253
+ options:
254
+ command: 'test "${vars.__hitlAnswer}" = yes'
255
+ - from: approve
256
+ to: failed
257
+ description: Operator rejected at the approval gate — report and stop.
258
+ guard:
259
+ kind: shell
260
+ options:
261
+ command: 'test "${vars.__hitlAnswer}" = no'
262
+ - from: approve
263
+ to: cancelled
264
+ description: Operator cancelled at the approval gate.
265
+ guard:
266
+ kind: shell
267
+ options:
268
+ command: 'test "${vars.__hitlAnswer}" = cancel'
269
+
206
270
  # ── completion gate (the YAML-native replacement for rd3's default-on --postflight-verify) ──
207
271
  # The verify step emits .spur/run/<wbs>-verdict.json. Only `verdict: PASS` clears
208
272
  # the gate to `record`; any non-PASS (PARTIAL/FAIL), a missing file, or malformed
@@ -0,0 +1,240 @@
1
+ # Wrap-up pipeline — post-execution wrap-up for one task or a batch (design §wrapup-pipeline).
2
+ #
3
+ # Orchestration is configuration (ADR-022 / §3.2): this is YAML over the existing
4
+ # dual-workflow engine — zero new engine code. The pipeline NEVER mutates task status —
5
+ # wrap-up consumes completed tasks and produces learning/metrics/doc artifacts only.
6
+ # Feature transitions go through `spur feature update` so the feature-lifecycle guards
7
+ # apply identically. Branch cleanup is an irreversible HITL gate — it always pauses,
8
+ # even under --auto (Iron Law #6: irreversible action -> surface to human).
9
+ #
10
+ # Shape: start -> task-resolve -> doc-sync -> learning-capture -> metrics-record
11
+ # -> feature-transition (conditional: if vars.feature set)
12
+ # -> branch-cleanup (conditional: if vars.merge=true)
13
+ # -> done
14
+ # (task-resolve with empty list short-circuits to `skipped`).
15
+ #
16
+ # Vars (passed as a JSON object via `--vars`):
17
+ # tasks — JSON array of WBS strings, passed as a JSON-encoded STRING value (the CLI
18
+ # rejects non-string --vars values), e.g. --vars '{"tasks":"[\"0167\"]"}'
19
+ # feature — feature id to advance through legal lifecycle edges (optional)
20
+ # profile — set --vars '{"profile":"auto"}' to skip objective confirmations
21
+ # merge — set --vars '{"merge":"true"}' to run branch cleanup (irreversible HITL)
22
+ # spurBin — PATH-independent spur invocation (overridden by CLI at run start)
23
+ # agent — agent for agent.run steps (default: omp)
24
+ #
25
+ # Seeded by `spur init`; adapt the agent.run inputs to your project's command set.
26
+
27
+ "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
28
+ kind: state-machine
29
+ name: wrapup-pipeline
30
+ description: "Post-execution wrap-up: doc-sync, learning-capture, metrics, feature-transition, branch-cleanup"
31
+ iterationBound: 10
32
+ initialState: start
33
+ terminalStates:
34
+ - done
35
+ - skipped
36
+ vars:
37
+ tasks: "[]"
38
+ feature: ""
39
+ profile: "standard"
40
+ merge: "false"
41
+ spurBin: "spur"
42
+ agent: "omp"
43
+ stepTimeoutMs: "600000"
44
+
45
+ states:
46
+ - id: start
47
+ description: >
48
+ Pipeline start. Task statuses are NOT mutated by wrap-up — wrap-up consumes
49
+ completed tasks and produces learning/metrics/doc artifacts only.
50
+ onEnter:
51
+ - kind: note
52
+ options:
53
+ message: "Wrap-up pipeline start for tasks: ${vars.tasks}. Task statuses are NOT mutated."
54
+
55
+ - id: task-resolve
56
+ description: >
57
+ Validate the task list passed by the wrapper is non-empty. The command wrapper
58
+ (dev-wrap/dev-wrapall) resolves the task list and passes it as vars.tasks;
59
+ this state validates it before proceeding. Empty list routes to `skipped`.
60
+ onEnter:
61
+ - kind: note
62
+ options:
63
+ message: "Resolving task list: ${vars.tasks}"
64
+
65
+ - id: doc-sync
66
+ 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.
71
+ onEnter:
72
+ - kind: agent.run
73
+ options:
74
+ 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."
76
+ timeoutMs: ${vars.stepTimeoutMs}
77
+
78
+ - id: learning-capture
79
+ 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.
85
+ onEnter:
86
+ - kind: agent.run
87
+ options:
88
+ 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."
90
+ answerFile: .spur/run/wrapup-learnings.md
91
+ expectFile: .spur/run/wrapup-learnings.md
92
+ timeoutMs: ${vars.stepTimeoutMs}
93
+ - kind: shell
94
+ 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'
96
+
97
+ - id: metrics-record
98
+ 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).
106
+ onEnter:
107
+ - kind: agent.run
108
+ options:
109
+ 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."
111
+ answerFile: .spur/run/wrapup-metrics.jsonl
112
+ expectFile: .spur/run/wrapup-metrics.jsonl
113
+ timeoutMs: ${vars.stepTimeoutMs}
114
+ - kind: shell
115
+ 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'
117
+
118
+ - id: feature-transition
119
+ description: >
120
+ If vars.feature is set, advance the feature through the legal forward path of
121
+ the feature-lifecycle FSM via `spur feature advance` (task 0180 F9c / ADR-029).
122
+ The verb walks backlog → active → verifying → done idempotently, verifying the
123
+ observed status after every hop, and returns a per-hop trail. Replaces the prior
124
+ ~20-line inline shell ladder; the verb now owns the multi-hop walk + legal-edge
125
+ enforcement. Task statuses are NOT mutated.
126
+ onEnter:
127
+ - kind: shell
128
+ options:
129
+ command: '${vars.spurBin} feature advance ${vars.feature} --json'
130
+
131
+ - id: branch-cleanup
132
+ description: >
133
+ Irreversible HITL gate. If vars.merge=true, dispatch branch cleanup (merge or
134
+ delete). This gate ALWAYS pauses — even under --auto — because branch operations
135
+ are irreversible (Auto-Decision Principle #6). The operator must explicitly confirm.
136
+ pause: true
137
+ onEnter:
138
+ - kind: hitl.confirm
139
+ options:
140
+ prompt: "Branch cleanup for tasks ${vars.tasks}. This is IRREVERSIBLE (merge or delete). Confirm to proceed?"
141
+
142
+ - id: done
143
+ description: >
144
+ Terminal — wrap-up complete. Output summary: tasks wrapped, learnings captured,
145
+ metrics recorded, feature advanced (if applicable), branch cleaned (if applicable).
146
+ onEnter:
147
+ - kind: note
148
+ options:
149
+ message: "Wrap-up pipeline complete for tasks: ${vars.tasks}. Learnings at .spur/memory/learnings.md, metrics at .spur/memory/wrapup-metrics.jsonl."
150
+ # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
151
+ - kind: shell
152
+ options:
153
+ 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'
154
+
155
+ - id: skipped
156
+ description: Terminal — wrap-up skipped (empty task list or operator abort).
157
+
158
+ transitions:
159
+ # ── start -> task-resolve ──
160
+ - from: start
161
+ to: task-resolve
162
+ description: Begin wrap-up.
163
+ guard:
164
+ kind: always
165
+
166
+ # ── task-resolve: non-empty -> doc-sync, empty -> skipped ──
167
+ - from: task-resolve
168
+ to: doc-sync
169
+ description: Task list resolved and non-empty — begin doc-sync.
170
+ guard:
171
+ kind: shell
172
+ options:
173
+ command: 'test "$(echo "${vars.tasks}" | jq length)" -gt 0'
174
+ - from: task-resolve
175
+ to: skipped
176
+ description: Task list is empty — skip wrap-up.
177
+ guard:
178
+ kind: shell
179
+ options:
180
+ command: 'test "$(echo "${vars.tasks}" | jq length)" -eq 0'
181
+
182
+ # ── linear body: doc-sync -> learning-capture -> metrics-record ──
183
+ - from: doc-sync
184
+ to: learning-capture
185
+ description: Doc sync complete — capture learnings.
186
+ guard:
187
+ kind: always
188
+ - from: learning-capture
189
+ to: metrics-record
190
+ description: Learnings captured — record metrics.
191
+ guard:
192
+ kind: always
193
+
194
+ # ── metrics-record: conditional routing based on feature and merge ──
195
+ # Declaration order matters: feature-transition is tried first (if feature is set),
196
+ # then branch-cleanup (if merge=true but no feature), then done (neither).
197
+ - from: metrics-record
198
+ to: feature-transition
199
+ description: Feature id is set — advance feature through legal lifecycle edges.
200
+ guard:
201
+ kind: shell
202
+ options:
203
+ command: 'test -n "${vars.feature}"'
204
+ - from: metrics-record
205
+ to: branch-cleanup
206
+ description: No feature id, but merge is requested — go to branch cleanup.
207
+ guard:
208
+ kind: shell
209
+ options:
210
+ command: 'test -z "${vars.feature}" && test "${vars.merge}" = true'
211
+ - from: metrics-record
212
+ to: done
213
+ description: No feature id and no merge — wrap-up is done.
214
+ guard:
215
+ kind: shell
216
+ options:
217
+ command: 'test -z "${vars.feature}" && test "${vars.merge}" != true'
218
+
219
+ # ── feature-transition: -> branch-cleanup (if merge) or done ──
220
+ - from: feature-transition
221
+ to: branch-cleanup
222
+ description: Feature advanced — merge is requested.
223
+ guard:
224
+ kind: shell
225
+ options:
226
+ command: 'test "${vars.merge}" = true'
227
+ - from: feature-transition
228
+ to: done
229
+ description: Feature advanced — no merge requested.
230
+ guard:
231
+ kind: shell
232
+ options:
233
+ command: 'test "${vars.merge}" != true'
234
+
235
+ # ── branch-cleanup: HITL confirmed -> done ──
236
+ - from: branch-cleanup
237
+ to: done
238
+ description: Branch cleanup confirmed — wrap-up complete.
239
+ guard:
240
+ kind: always