@gobing-ai/spur 0.2.11 → 0.2.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
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",
@@ -86,6 +86,11 @@ states:
86
86
 
87
87
  - id: done
88
88
  description: Terminal — feature planned, all tasks executed, and the feature verified.
89
+ # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
90
+ onEnter:
91
+ - kind: shell
92
+ options:
93
+ command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: feature-dev done featureId=${vars.featureId} ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/${vars.featureId}-checkpoint.md'
89
94
 
90
95
  - id: failed
91
96
  description: Terminal — plan rejected, a task failed verification, or the feature check did not pass.
@@ -0,0 +1,285 @@
1
+ # Idea-to-feature pipeline — unified entry from a vague idea to a feature, AC, and task batch
2
+ # (design §idea-pipeline).
3
+ #
4
+ # Orchestration is configuration (ADR-022 / §3.2): this is YAML over the existing
5
+ # dual-workflow engine — zero new engine code. The pipeline STOPS at handoff — tasks are
6
+ # created but NOT executed. Use task-pipeline.yaml or feature-dev.yaml for execution.
7
+ # No state in this pipeline inlines another pipeline's state graph (no-nesting principle,
8
+ # design §System Principles 4).
9
+ #
10
+ # Shape: start -> discovery -> feature-create -> ac-generate -> feature-check
11
+ # -> system-design (conditional: needs_design signal or --design)
12
+ # -> design-approval (taste HITL gate; not auto-clicked by --auto)
13
+ # -> decompose -> batch-create -> handoff
14
+ # (feature-check failure routes back to ac-generate; batch-create failure to decompose).
15
+ #
16
+ # Vars (passed as a JSON object via `--vars`):
17
+ # idea — the idea text (required), e.g. --vars '{"idea":"add a --dry-run flag to dev-wrap"}'
18
+ # profile — set --vars '{"profile":"auto"}' to skip objective HITL gates (feature-check, batch-create)
19
+ # design — "auto" (default, signal-driven), "force" (--design), "skip" (--skip-design)
20
+ # spurBin — PATH-independent spur invocation (overridden by CLI at run start)
21
+ # agent — agent for agent.run steps (default: omp)
22
+ #
23
+ # Seeded by `spur init`; adapt the agent.run inputs to your project's command set.
24
+
25
+ "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
26
+ kind: state-machine
27
+ name: idea-pipeline
28
+ description: "Idea to feature + AC + task batch: discovery, feature-create, ac-generate, feature-check, system-design, decompose, batch-create, handoff"
29
+ iterationBound: 15
30
+ initialState: start
31
+ terminalStates:
32
+ - handoff
33
+ - cancelled
34
+ - failed
35
+ vars:
36
+ idea: ""
37
+ profile: "standard"
38
+ design: "auto"
39
+ spurBin: "spur"
40
+ agent: "omp"
41
+ stepTimeoutMs: "600000"
42
+
43
+ states:
44
+ - id: start
45
+ description: >
46
+ Pipeline start. The idea text arrives as vars.idea. The pipeline stops at
47
+ handoff — no task execution.
48
+ onEnter:
49
+ - kind: note
50
+ options:
51
+ message: "Idea pipeline start for idea: ${vars.idea}"
52
+ - kind: shell
53
+ options:
54
+ command: 'rm -f .spur/run/idea-ac-retry-count .spur/run/idea-decompose-retry-count'
55
+
56
+ - id: discovery
57
+ description: >
58
+ Dispatch sp:brainstorm to explore the idea, generate approaches with trade-offs,
59
+ and record a design summary. The brainstorm ALWAYS records a design summary
60
+ ("nothing is too simple" pattern). The brainstorm also emits a needs_design
61
+ boolean signal written to .spur/run/idea-needs-design.json — this determines
62
+ whether the system-design state runs.
63
+ onEnter:
64
+ - kind: agent.run
65
+ options:
66
+ agent: ${vars.agent}
67
+ input: "Run sp:brainstorm for the idea: ${vars.idea}. Generate 2-3 approaches with trade-offs and a design summary. Perform the scope decomposition check and write the needs_design boolean to .spur/run/idea-needs-design.json as {\"needs_design\": true|false}. Criteria: true if multiple subsystems/schema/transport/dependency changes; false if single-module/bug-fix/pattern-following. Ties lean true. ALWAYS record a design summary — no idea is too simple."
68
+ timeoutMs: ${vars.stepTimeoutMs}
69
+
70
+ - id: feature-create
71
+ description: >
72
+ Create a feature via spur feature create, or select an existing feature id.
73
+ The agent writes the feature id to .spur/run/idea-feature-id.txt for subsequent
74
+ states to read.
75
+ onEnter:
76
+ - kind: agent.run
77
+ options:
78
+ agent: ${vars.agent}
79
+ input: "Create a feature for the idea: ${vars.idea}. Use 'spur feature create \"<name>\" --json' to create it. Write the feature id to .spur/run/idea-feature-id.txt. If an existing feature is appropriate, use its id instead."
80
+ timeoutMs: ${vars.stepTimeoutMs}
81
+
82
+ - id: ac-generate
83
+ description: >
84
+ Generate acceptance criteria per ac-style-guide.md. Write AC scenarios to the
85
+ feature file via spur feature update (direct body edit, not --section). The
86
+ agent authors R-numbered Gherkin scenarios tied to the design summary.
87
+ onEnter:
88
+ - kind: agent.run
89
+ options:
90
+ agent: ${vars.agent}
91
+ input: "Generate acceptance criteria for the feature whose id is in .spur/run/idea-feature-id.txt. Read the feature file, author R-numbered BDD Gherkin scenarios per ac-style-guide.md, and write them to the feature body via direct file edit (spur feature update does not support --section for features). Run 'spur feature check <id>' to validate."
92
+ timeoutMs: ${vars.stepTimeoutMs}
93
+
94
+ - id: feature-check
95
+ description: >
96
+ Objective gate: run spur feature check <id> --strict. Under --auto, routes
97
+ around the HITL gate when the check passes. On failure, routes back to
98
+ ac-generate for AC revision. The feature id is read from
99
+ .spur/run/idea-feature-id.txt.
100
+ onEnter:
101
+ - kind: note
102
+ options:
103
+ message: "Running feature check for feature $(cat .spur/run/idea-feature-id.txt)"
104
+
105
+ - id: system-design
106
+ description: >
107
+ Dispatch sp:sys-architecture to produce the system design. This state runs
108
+ only when needs_design=true (or --design forces it). The agent creates ADR
109
+ entries, architecture updates, and design satellites through constitution rules.
110
+ The design-approval state follows (taste gate).
111
+ onEnter:
112
+ - kind: agent.run
113
+ options:
114
+ agent: ${vars.agent}
115
+ input: "Run sp:sys-architecture for the feature whose id is in .spur/run/idea-feature-id.txt. Read the brainstorm artifact and feature AC. Produce ADR entries, architecture updates, and design satellites (docs/design/<slug>.md) following the constitution edit rules. Do not write task or feature corpus files directly."
116
+ timeoutMs: ${vars.stepTimeoutMs}
117
+
118
+ - id: design-approval
119
+ description: >
120
+ HITL taste gate. The operator reviews the system design and approves or
121
+ rejects it. This gate is NOT auto-clicked by --auto (Auto-Decision Principle
122
+ #5: taste-decision -> surface to human). Only routes around when
123
+ vars.design_approved=true (explicit prior approval).
124
+ pause: true
125
+ onEnter:
126
+ - kind: hitl.confirm
127
+ options:
128
+ prompt: "Review the system design for feature $(cat .spur/run/idea-feature-id.txt). Approve to proceed to decomposition?"
129
+
130
+ - id: decompose
131
+ description: >
132
+ Dispatch sp:spec-decomposition with the brainstorm artifact, feature AC, and
133
+ design doc as input. The agent produces a task-batch JSON file at
134
+ .spur/run/idea-task-batch.json, validated against task-batch.schema.json.
135
+ onEnter:
136
+ - kind: agent.run
137
+ options:
138
+ agent: ${vars.agent}
139
+ input: "Run sp:spec-decomposition for the feature whose id is in .spur/run/idea-feature-id.txt. Read the brainstorm artifact, feature AC, and design doc. Produce a task-batch JSON array at .spur/run/idea-task-batch.json, validated against apps/cli/schemas/task-batch.schema.json. Each entry must have filled Requirements, AC, Design, and Plan sections."
140
+ timeoutMs: ${vars.stepTimeoutMs}
141
+
142
+ - id: batch-create
143
+ description: >
144
+ Objective gate: run spur task batch-create with the batch JSON from the
145
+ decompose state. Under --auto, routes around the HITL gate when the batch
146
+ validates. On failure, routes back to decompose.
147
+ onEnter:
148
+ - kind: note
149
+ options:
150
+ message: "Running batch-create for feature $(cat .spur/run/idea-feature-id.txt)"
151
+
152
+ - id: handoff
153
+ description: >
154
+ Terminal — idea pipeline complete. Output: feature id, task WBS list, and
155
+ the next command to run (/sp:dev-run <first-wbs> or /sp:dev-runall --tasks feature:<id>).
156
+ No task execution — the pipeline stops here.
157
+ onEnter:
158
+ - kind: note
159
+ options:
160
+ message: "Idea pipeline handoff. Feature: $(cat .spur/run/idea-feature-id.txt). Tasks created. Next: /sp:dev-runall --tasks feature:$(cat .spur/run/idea-feature-id.txt)"
161
+ # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
162
+ - kind: shell
163
+ options:
164
+ command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: idea-pipeline handoff feature=$(cat .spur/run/idea-feature-id.txt) ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/idea-checkpoint.md'
165
+
166
+ - id: cancelled
167
+ description: Terminal — pipeline cancelled by operator or error.
168
+
169
+ - id: failed
170
+ description: >
171
+ Terminal — pipeline failed after exhausting retry caps on cyclic edges
172
+ (feature-check → ac-generate or batch-create → decompose). The error
173
+ message in the run trace identifies which edge exceeded the cap.
174
+
175
+ transitions:
176
+ # ── start -> discovery ──
177
+ - from: start
178
+ to: discovery
179
+ description: Begin discovery — brainstorm the idea.
180
+ guard:
181
+ kind: always
182
+
183
+ # ── discovery -> feature-create ──
184
+ - from: discovery
185
+ to: feature-create
186
+ description: Discovery complete — create or select a feature.
187
+ guard:
188
+ kind: always
189
+
190
+ # ── feature-create -> ac-generate ──
191
+ - from: feature-create
192
+ to: ac-generate
193
+ description: Feature created — generate acceptance criteria.
194
+ guard:
195
+ kind: always
196
+
197
+ # ── ac-generate -> feature-check ──
198
+ - from: ac-generate
199
+ to: feature-check
200
+ description: AC generated — run feature check.
201
+ guard:
202
+ kind: always
203
+
204
+ # ── feature-check: pass + design route -> system-design, pass + skip route -> decompose, fail -> ac-generate ──
205
+ # Declaration order matters: system-design route tried first, then skip route, then fail.
206
+ - from: feature-check
207
+ to: system-design
208
+ description: Feature check passed, design route — run system design.
209
+ guard:
210
+ kind: shell
211
+ options:
212
+ command: '${vars.spurBin} feature check $(cat .spur/run/idea-feature-id.txt) --strict && (test "${vars.design}" = force || (test "${vars.design}" = auto && test "$(jq -r .needs_design .spur/run/idea-needs-design.json 2>/dev/null)" != false))'
213
+ - from: feature-check
214
+ to: decompose
215
+ description: Feature check passed, skip-design route — go directly to decompose.
216
+ guard:
217
+ kind: shell
218
+ options:
219
+ command: '${vars.spurBin} feature check $(cat .spur/run/idea-feature-id.txt) --strict && (test "${vars.design}" = skip || (test "${vars.design}" = auto && test "$(jq -r .needs_design .spur/run/idea-needs-design.json 2>/dev/null)" = false))'
220
+ - from: feature-check
221
+ to: ac-generate
222
+ description: "Feature check failed — revise AC (retry cap: 3)."
223
+ guard:
224
+ kind: shell
225
+ options:
226
+ command: '! ${vars.spurBin} feature check $(cat .spur/run/idea-feature-id.txt) --strict && test "$(cat .spur/run/idea-ac-retry-count 2>/dev/null || echo 0)" -lt 3 && echo $(( $(cat .spur/run/idea-ac-retry-count 2>/dev/null || echo 0) + 1 )) > .spur/run/idea-ac-retry-count'
227
+ - from: feature-check
228
+ to: failed
229
+ description: Feature check failed after 3 retries — escalate to failed.
230
+ guard:
231
+ kind: shell
232
+ options:
233
+ command: '! ${vars.spurBin} feature check $(cat .spur/run/idea-feature-id.txt) --strict && test "$(cat .spur/run/idea-ac-retry-count 2>/dev/null || echo 0)" -ge 3'
234
+
235
+ # ── system-design: -> design-approval (normal) OR -> decompose (auto + prior approval) ──
236
+ # Declaration order: auto-skip guard tried FIRST (like task-pipeline's review->verify pattern).
237
+ - from: system-design
238
+ to: decompose
239
+ description: profile=auto AND design_approved=true — skip design approval gate.
240
+ guard:
241
+ kind: shell
242
+ options:
243
+ command: 'test "${vars.profile}" = auto && test "${vars.design_approved}" = true'
244
+ - from: system-design
245
+ to: design-approval
246
+ description: System design done — gate on design approval.
247
+ guard:
248
+ kind: always
249
+
250
+ # ── design-approval -> decompose (after operator approval) ──
251
+ - from: design-approval
252
+ to: decompose
253
+ description: Design approved — proceed to decomposition.
254
+ guard:
255
+ kind: always
256
+
257
+ # ── decompose -> batch-create ──
258
+ - from: decompose
259
+ to: batch-create
260
+ description: Decomposition complete — create task batch.
261
+ guard:
262
+ kind: always
263
+
264
+ # ── batch-create: success -> handoff, failure -> decompose (retry cap: 3), failure+cap -> failed ──
265
+ - from: batch-create
266
+ to: handoff
267
+ description: Batch created — handoff.
268
+ guard:
269
+ kind: shell
270
+ options:
271
+ command: '${vars.spurBin} task batch-create --file .spur/run/idea-task-batch.json'
272
+ - from: batch-create
273
+ to: decompose
274
+ description: "Batch-create failed — revise decomposition (retry cap: 3)."
275
+ guard:
276
+ kind: shell
277
+ options:
278
+ command: '! ${vars.spurBin} task batch-create --file .spur/run/idea-task-batch.json && test "$(cat .spur/run/idea-decompose-retry-count 2>/dev/null || echo 0)" -lt 3 && echo $(( $(cat .spur/run/idea-decompose-retry-count 2>/dev/null || echo 0) + 1 )) > .spur/run/idea-decompose-retry-count'
279
+ - from: batch-create
280
+ to: failed
281
+ description: Batch-create failed after 3 retries — escalate to failed.
282
+ guard:
283
+ kind: shell
284
+ options:
285
+ command: '! ${vars.spurBin} task batch-create --file .spur/run/idea-task-batch.json && test "$(cat .spur/run/idea-decompose-retry-count 2>/dev/null || echo 0)" -ge 3'
@@ -12,7 +12,9 @@
12
12
  # commit (Q3 — hybrid by risk).
13
13
  #
14
14
  # Shape: phasing(HITL) → feature-id → design-gen → design-approval(HITL) → handoff
15
- # (phasing/design-approval skipped when profile=auto).
15
+ # (phasing skipped when profile=auto; design-approval skipped when profile=auto
16
+ # AND design_approved=true — taste gates pause even in auto unless prior approval
17
+ # is encoded, per design doc HITL taxonomy).
16
18
  #
17
19
  # Vars (passed as a JSON object via `--vars`):
18
20
  # slug — the design-doc slug, e.g. `--vars '{"slug":"auth-migration"}'`
@@ -78,8 +80,11 @@ states:
78
80
  - id: design-approval
79
81
  description: >
80
82
  HITL gate — the highest-leverage gate in the pipeline. Loop until the operator
81
- approves the design doc. Under profile=auto this state is never entered (design-gen
82
- routes around it straight to handoff).
83
+ approves the design doc. Under profile=auto this state is skipped ONLY when
84
+ vars.design_approved=true (explicit prior approval); otherwise the taste gate
85
+ pauses even in auto mode (design doc HITL taxonomy). Loop back to design-gen
86
+ for revisions.
87
+ pause: true
83
88
  onEnter:
84
89
  - kind: hitl.confirm
85
90
  options:
@@ -90,6 +95,11 @@ states:
90
95
  Terminal — design doc approved; the drafted feature list is ready. Hand off to
91
96
  sp:spur-dev (steps 7–12), which starts at `spur feature create`. The seam is the
92
97
  drafted-feature-list file at docs/plans/${vars.slug}-drafted.md.
98
+ # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
99
+ onEnter:
100
+ - kind: shell
101
+ options:
102
+ command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: planning-pipeline handoff slug=${vars.slug} ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/${vars.slug}-checkpoint.md'
93
103
 
94
104
  - id: cancelled
95
105
  description: Terminal — planning abandoned by the operator.
@@ -131,16 +141,17 @@ transitions:
131
141
  guard:
132
142
  kind: always
133
143
 
134
- # ── design-gen → design-approval, OR skip the HITL gate when profile=auto ──
135
- # Auto-skip guard tried FIRST: under profile=auto, route straight to handoff
136
- # without entering design-approval (whose onEnter hitl.confirm would block).
144
+ # ── design-gen → design-approval, OR skip the HITL gate when profile=auto AND design_approved=true ──
145
+ # Auto-skip guard tried FIRST: under profile=auto with explicit prior approval,
146
+ # route straight to handoff without entering design-approval. Without prior
147
+ # approval, the taste gate pauses even in auto (design doc HITL taxonomy).
137
148
  - from: design-gen
138
149
  to: handoff
139
- description: profile=auto — skip the design-approval HITL gate, hand off directly.
150
+ description: profile=auto AND design_approved=true — skip the design-approval HITL gate.
140
151
  guard:
141
152
  kind: shell
142
153
  options:
143
- command: 'test "${vars.profile}" = auto'
154
+ command: 'test "${vars.profile}" = auto && test "${vars.design_approved}" = true'
144
155
  - from: design-gen
145
156
  to: design-approval
146
157
  description: Design doc drafted — gate on human approval.
@@ -75,6 +75,15 @@ states:
75
75
  - kind: shell
76
76
  options:
77
77
  command: "${vars.spurBin} task update ${vars.wbs} wip --no-lifecycle"
78
+ # Post-implement cleanup: auto-format any unformatted output the agent produced.
79
+ # The agent may leave files that don't pass `biome check`, which then fails
80
+ # `bun run lint` downstream. Running format here is cheap (< 1 s) and prevents
81
+ # the test stage from tripping on a purely mechanical formatting defect (dogfood
82
+ # bug-733 — omp left agent-run.test.ts unformatted, causing a spurious lint gate
83
+ # failure that the ## Testing section mis-attributed to "pre-existing gaps").
84
+ - kind: shell
85
+ options:
86
+ command: "bun run format"
78
87
 
79
88
  - id: test
80
89
  description: Test execution + coverage via /sp:dev-unit.
@@ -84,6 +93,13 @@ states:
84
93
  agent: ${vars.agent}
85
94
  input: /sp:dev-unit ${vars.wbs} --auto
86
95
  timeoutMs: ${vars.stepTimeoutMs}
96
+ # Post-test gate: run the project's full lint gate (biome check + tsc). If the
97
+ # implement or test step left unformatted files or type errors, the run routes to
98
+ # `failed` rather than letting an agent-authored `## Testing` claim "all pass"
99
+ # against a red gate (dogfood bug-733).
100
+ - kind: shell
101
+ options:
102
+ command: "bun run lint"
87
103
 
88
104
  - id: review
89
105
  description: SECUA-framework code review via /sp:dev-review (Security, Efficiency, Correctness, Usability, Architecture).
@@ -147,6 +163,10 @@ states:
147
163
  - kind: note
148
164
  options:
149
165
  message: "Pipeline complete for task ${vars.wbs} (done gate cleared)."
166
+ # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
167
+ - kind: shell
168
+ options:
169
+ 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
170
 
151
171
  - id: failed
152
172
  description: Terminal — precheck (or a gated step) failed; reported, not advanced.
@@ -0,0 +1,225 @@
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 (required), e.g. --vars '{"tasks":["0167"]}'
18
+ # feature — feature id to advance through legal lifecycle edges (optional)
19
+ # profile — set --vars '{"profile":"auto"}' to skip objective confirmations
20
+ # merge — set --vars '{"merge":"true"}' to run branch cleanup (irreversible HITL)
21
+ # spurBin — PATH-independent spur invocation (overridden by CLI at run start)
22
+ # agent — agent for agent.run steps (default: omp)
23
+ #
24
+ # Seeded by `spur init`; adapt the agent.run inputs to your project's command set.
25
+
26
+ "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
27
+ kind: state-machine
28
+ name: wrapup-pipeline
29
+ description: "Post-execution wrap-up: doc-sync, learning-capture, metrics, feature-transition, branch-cleanup"
30
+ iterationBound: 10
31
+ initialState: start
32
+ terminalStates:
33
+ - done
34
+ - skipped
35
+ vars:
36
+ tasks: "[]"
37
+ feature: ""
38
+ profile: "standard"
39
+ merge: "false"
40
+ spurBin: "spur"
41
+ agent: "omp"
42
+ stepTimeoutMs: "600000"
43
+
44
+ states:
45
+ - id: start
46
+ description: >
47
+ Pipeline start. Task statuses are NOT mutated by wrap-up — wrap-up consumes
48
+ completed tasks and produces learning/metrics/doc artifacts only.
49
+ onEnter:
50
+ - kind: note
51
+ options:
52
+ message: "Wrap-up pipeline start for tasks: ${vars.tasks}. Task statuses are NOT mutated."
53
+
54
+ - id: task-resolve
55
+ description: >
56
+ Validate the task list passed by the wrapper is non-empty. The command wrapper
57
+ (dev-wrap/dev-wrapall) resolves the task list and passes it as vars.tasks;
58
+ this state validates it before proceeding. Empty list routes to `skipped`.
59
+ onEnter:
60
+ - kind: note
61
+ options:
62
+ message: "Resolving task list: ${vars.tasks}"
63
+
64
+ - id: doc-sync
65
+ description: >
66
+ Dispatch sp:doc-evolve once for the entire batch. Project-level doc drift repair
67
+ runs once, not per-task. The agent reads the batch's completed tasks, identifies
68
+ doc drift (04_DESIGN, 03_ARCHITECTURE, 00_ADR, docs/design/*), and repairs it
69
+ following the constitution's edit rules. Does NOT write task or feature corpus.
70
+ onEnter:
71
+ - kind: agent.run
72
+ options:
73
+ agent: ${vars.agent}
74
+ 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."
75
+ timeoutMs: ${vars.stepTimeoutMs}
76
+
77
+ - id: learning-capture
78
+ description: >
79
+ Append working learnings to .spur/memory/learnings.md. The agent extracts insights
80
+ from the batch — conventions discovered, errors hit and resolved, patterns that
81
+ worked, gotchas for future tasks. Grouped by date and task WBS. Working scratchpad,
82
+ not a CLI-gated corpus artifact.
83
+ onEnter:
84
+ - kind: agent.run
85
+ options:
86
+ agent: ${vars.agent}
87
+ input: "Extract working learnings from tasks ${vars.tasks}. Append to .spur/memory/learnings.md grouped by date and task WBS. Include: conventions discovered, errors hit and resolved, patterns that worked, gotchas. This is a working scratchpad, not a validated corpus."
88
+ timeoutMs: ${vars.stepTimeoutMs}
89
+
90
+ - id: metrics-record
91
+ description: >
92
+ Append one JSONL row per task to .spur/memory/wrapup-metrics.jsonl. Each row
93
+ records: task WBS, feature id, status, verdict, timestamp. Append-only and
94
+ machine-readable.
95
+ onEnter:
96
+ - kind: agent.run
97
+ options:
98
+ agent: ${vars.agent}
99
+ input: "For each task in ${vars.tasks}, read the task file and append one JSONL row to .spur/memory/wrapup-metrics.jsonl with fields: wbs, feature_id, status, verdict (from .spur/run/<wbs>-verdict.json if available), timestamp. Append-only; do not overwrite existing rows."
100
+ timeoutMs: ${vars.stepTimeoutMs}
101
+
102
+ - id: feature-transition
103
+ description: >
104
+ If vars.feature is set, advance the feature through legal lifecycle edges only
105
+ via spur feature update. Never attempt backlog|active -> done directly. The
106
+ legal path: backlog -> active (always), active -> verifying (feature check
107
+ guard), verifying -> done (strict feature check guard). Task statuses are NOT
108
+ mutated. The agent checks current status and advances idempotently.
109
+ onEnter:
110
+ - kind: agent.run
111
+ options:
112
+ agent: ${vars.agent}
113
+ input: "Advance feature ${vars.feature} through legal lifecycle edges using spur feature update. Check current status with 'spur feature show ${vars.feature} --json'. If backlog, transition to active. If active, run 'spur feature check ${vars.feature}' then transition to verifying. If verifying, run 'spur feature check ${vars.feature} --strict' then transition to done. Never skip edges (no backlog->done directly). Report transitions made."
114
+ timeoutMs: ${vars.stepTimeoutMs}
115
+
116
+ - id: branch-cleanup
117
+ description: >
118
+ Irreversible HITL gate. If vars.merge=true, dispatch branch cleanup (merge or
119
+ delete). This gate ALWAYS pauses — even under --auto — because branch operations
120
+ are irreversible (Auto-Decision Principle #6). The operator must explicitly confirm.
121
+ pause: true
122
+ onEnter:
123
+ - kind: hitl.confirm
124
+ options:
125
+ prompt: "Branch cleanup for tasks ${vars.tasks}. This is IRREVERSIBLE (merge or delete). Confirm to proceed?"
126
+
127
+ - id: done
128
+ description: >
129
+ Terminal — wrap-up complete. Output summary: tasks wrapped, learnings captured,
130
+ metrics recorded, feature advanced (if applicable), branch cleaned (if applicable).
131
+ onEnter:
132
+ - kind: note
133
+ options:
134
+ message: "Wrap-up pipeline complete for tasks: ${vars.tasks}. Learnings at .spur/memory/learnings.md, metrics at .spur/memory/wrapup-metrics.jsonl."
135
+ # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
136
+ - kind: shell
137
+ options:
138
+ 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'
139
+
140
+ - id: skipped
141
+ description: Terminal — wrap-up skipped (empty task list or operator abort).
142
+
143
+ transitions:
144
+ # ── start -> task-resolve ──
145
+ - from: start
146
+ to: task-resolve
147
+ description: Begin wrap-up.
148
+ guard:
149
+ kind: always
150
+
151
+ # ── task-resolve: non-empty -> doc-sync, empty -> skipped ──
152
+ - from: task-resolve
153
+ to: doc-sync
154
+ description: Task list resolved and non-empty — begin doc-sync.
155
+ guard:
156
+ kind: shell
157
+ options:
158
+ command: 'test "$(echo "${vars.tasks}" | jq length)" -gt 0'
159
+ - from: task-resolve
160
+ to: skipped
161
+ description: Task list is empty — skip wrap-up.
162
+ guard:
163
+ kind: shell
164
+ options:
165
+ command: 'test "$(echo "${vars.tasks}" | jq length)" -eq 0'
166
+
167
+ # ── linear body: doc-sync -> learning-capture -> metrics-record ──
168
+ - from: doc-sync
169
+ to: learning-capture
170
+ description: Doc sync complete — capture learnings.
171
+ guard:
172
+ kind: always
173
+ - from: learning-capture
174
+ to: metrics-record
175
+ description: Learnings captured — record metrics.
176
+ guard:
177
+ kind: always
178
+
179
+ # ── metrics-record: conditional routing based on feature and merge ──
180
+ # Declaration order matters: feature-transition is tried first (if feature is set),
181
+ # then branch-cleanup (if merge=true but no feature), then done (neither).
182
+ - from: metrics-record
183
+ to: feature-transition
184
+ description: Feature id is set — advance feature through legal lifecycle edges.
185
+ guard:
186
+ kind: shell
187
+ options:
188
+ command: 'test -n "${vars.feature}"'
189
+ - from: metrics-record
190
+ to: branch-cleanup
191
+ description: No feature id, but merge is requested — go to branch cleanup.
192
+ guard:
193
+ kind: shell
194
+ options:
195
+ command: 'test -z "${vars.feature}" && test "${vars.merge}" = true'
196
+ - from: metrics-record
197
+ to: done
198
+ description: No feature id and no merge — wrap-up is done.
199
+ guard:
200
+ kind: shell
201
+ options:
202
+ command: 'test -z "${vars.feature}" && test "${vars.merge}" != true'
203
+
204
+ # ── feature-transition: -> branch-cleanup (if merge) or done ──
205
+ - from: feature-transition
206
+ to: branch-cleanup
207
+ description: Feature advanced — merge is requested.
208
+ guard:
209
+ kind: shell
210
+ options:
211
+ command: 'test "${vars.merge}" = true'
212
+ - from: feature-transition
213
+ to: done
214
+ description: Feature advanced — no merge requested.
215
+ guard:
216
+ kind: shell
217
+ options:
218
+ command: 'test "${vars.merge}" != true'
219
+
220
+ # ── branch-cleanup: HITL confirmed -> done ──
221
+ - from: branch-cleanup
222
+ to: done
223
+ description: Branch cleanup confirmed — wrap-up complete.
224
+ guard:
225
+ kind: always
package/spur.js CHANGED
@@ -58665,18 +58665,16 @@ var init_task_check = __esm(() => {
58665
58665
  runL3(doc2, entry, findings) {
58666
58666
  const reqBody = doc2.getSection("Requirements");
58667
58667
  if (reqBody !== null && !isPlaceholderBody(reqBody)) {
58668
- const rLines = reqBody.trim().split(`
58669
- `);
58668
+ const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
58670
58669
  let numbered = 0;
58671
- let allLines = 0;
58672
- for (const l of rLines) {
58673
- if (l.trim().length > 0) {
58674
- allLines++;
58675
- if (/^\s*[-*]?\s*(?:\[[ xX]\]\s*)?R\d+\.?\s/.test(l))
58676
- numbered++;
58670
+ for (const block of blocks) {
58671
+ const firstLine = block.trimStart().split(`
58672
+ `)[0] ?? "";
58673
+ if (/^\s*[-*]?\s*(?:\[[ xX]\]\s*)?R\d+\.?\s/.test(firstLine)) {
58674
+ numbered++;
58677
58675
  }
58678
58676
  }
58679
- if (numbered === 0 || numbered < allLines * 0.5) {
58677
+ if (numbered === 0 || numbered < blocks.length * 0.5) {
58680
58678
  findings.push({
58681
58679
  layer: "L3",
58682
58680
  severity: "warning",
@@ -59966,6 +59964,7 @@ var init_team_service = __esm(() => {
59966
59964
  });
59967
59965
 
59968
59966
  // ../../packages/app/src/workflow/actions/agent-run.ts
59967
+ import { existsSync as existsSync4 } from "fs";
59969
59968
  import { mkdir, writeFile } from "fs/promises";
59970
59969
  import { dirname as dirname9, isAbsolute as isAbsolute2, join as join10 } from "path";
59971
59970
 
@@ -60012,6 +60011,7 @@ class AgentRunActionRunner {
60012
60011
  if (continueFlag !== undefined)
60013
60012
  flags.continue = continueFlag;
60014
60013
  const answerFile = asOptionalString(options.answerFile);
60014
+ const expectFile = asOptionalString(options.expectFile);
60015
60015
  const capture = asOptionalBoolean(options.capture) || answerFile !== undefined;
60016
60016
  const agentLabel = agent ?? "<default>";
60017
60017
  if (capture) {
@@ -60022,6 +60022,16 @@ class AgentRunActionRunner {
60022
60022
  await mkdir(dirname9(target), { recursive: true });
60023
60023
  await writeFile(target, answer, "utf8");
60024
60024
  }
60025
+ if (ok2 && expectFile !== undefined) {
60026
+ const target = isAbsolute2(expectFile) ? expectFile : join10(cwd, expectFile);
60027
+ if (!existsSync4(target)) {
60028
+ return {
60029
+ ok: false,
60030
+ data: { exitCode: exitCode2, agent: agentLabel, answer },
60031
+ error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
60032
+ };
60033
+ }
60034
+ }
60025
60035
  return {
60026
60036
  ok: ok2,
60027
60037
  data: { exitCode: exitCode2, agent: agentLabel, answer },
@@ -60031,6 +60041,16 @@ class AgentRunActionRunner {
60031
60041
  }
60032
60042
  const exitCode = await this.agentService.run(input, flags);
60033
60043
  const ok = exitCode === 0;
60044
+ if (ok && expectFile !== undefined) {
60045
+ const target = isAbsolute2(expectFile) ? expectFile : join10(cwd, expectFile);
60046
+ if (!existsSync4(target)) {
60047
+ return {
60048
+ ok: false,
60049
+ data: { exitCode, agent: agentLabel },
60050
+ error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
60051
+ };
60052
+ }
60053
+ }
60034
60054
  return {
60035
60055
  ok,
60036
60056
  data: { exitCode, agent: agentLabel },
@@ -68685,7 +68705,7 @@ init_src3();
68685
68705
  init_src3();
68686
68706
  init_loader();
68687
68707
  init_src2();
68688
- import { existsSync as existsSync4 } from "fs";
68708
+ import { existsSync as existsSync5 } from "fs";
68689
68709
  import { join as join12 } from "path";
68690
68710
 
68691
68711
  // src/workflow/resolve-spur-bin.ts
@@ -68708,11 +68728,11 @@ function resolveWorkflowPath(context3, profile) {
68708
68728
  const bundledRoot = bundledConfigRoot();
68709
68729
  if (bundledRoot !== null) {
68710
68730
  const bundledPath = join12(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
68711
- if (existsSync4(bundledPath))
68731
+ if (existsSync5(bundledPath))
68712
68732
  return bundledPath;
68713
68733
  }
68714
68734
  const projectPath = join12(context3.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
68715
- if (existsSync4(projectPath))
68735
+ if (existsSync5(projectPath))
68716
68736
  return projectPath;
68717
68737
  return null;
68718
68738
  }
@@ -68967,7 +68987,7 @@ import { join as join13, resolve as resolve4 } from "path";
68967
68987
  var CLI_CONFIG = {
68968
68988
  binaryName: "spur",
68969
68989
  binaryLabel: "spur",
68970
- binaryVersion: "0.2.11",
68990
+ binaryVersion: "0.2.12",
68971
68991
  configDir: ".spur",
68972
68992
  configFile: ".spur/config.yaml",
68973
68993
  databaseFile: ".spur/spur.db"
@@ -77672,7 +77692,7 @@ async function readTargetStatus(context3, targetPath) {
77672
77692
  init_src3();
77673
77693
  init_loader();
77674
77694
  init_src2();
77675
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
77695
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
77676
77696
  import { join as join18 } from "path";
77677
77697
  // schemas/section-matrix.schema.json
77678
77698
  var section_matrix_schema_default = {
@@ -78495,7 +78515,7 @@ function loadTemplateContent(projectRoot, variant) {
78495
78515
  if (templateMissSet.has(variant))
78496
78516
  return;
78497
78517
  const localPath = join18(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
78498
- if (existsSync5(localPath)) {
78518
+ if (existsSync6(localPath)) {
78499
78519
  const content = readFileSync6(localPath, "utf8");
78500
78520
  templateContentCache.set(variant, content);
78501
78521
  return content;
@@ -78503,7 +78523,7 @@ function loadTemplateContent(projectRoot, variant) {
78503
78523
  const root = bundledConfigRoot();
78504
78524
  if (root !== null) {
78505
78525
  const templatePath = join18(root, "templates", "task", `${variant}.md`);
78506
- if (existsSync5(templatePath)) {
78526
+ if (existsSync6(templatePath)) {
78507
78527
  const content = readFileSync6(templatePath, "utf8");
78508
78528
  templateContentCache.set(variant, content);
78509
78529
  return content;
@@ -78519,13 +78539,13 @@ function loadTemplateBodies(projectRoot, variant) {
78519
78539
  return cached2;
78520
78540
  let bodies = {};
78521
78541
  const localPath = join18(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
78522
- if (existsSync5(localPath)) {
78542
+ if (existsSync6(localPath)) {
78523
78543
  bodies = extractTemplateBodies(readFileSync6(localPath, "utf8"));
78524
78544
  } else {
78525
78545
  const root = bundledConfigRoot();
78526
78546
  if (root !== null) {
78527
78547
  const templatePath = join18(root, "templates", "task", `${variant}.md`);
78528
- if (existsSync5(templatePath)) {
78548
+ if (existsSync6(templatePath)) {
78529
78549
  bodies = extractTemplateBodies(readFileSync6(templatePath, "utf8"));
78530
78550
  }
78531
78551
  }
@@ -78560,7 +78580,7 @@ async function loadSectionMatrix(projectRoot) {
78560
78580
  }
78561
78581
  async function loadSectionMatrixUncached(projectRoot) {
78562
78582
  const localPath = join18(projectRoot, ".spur", "tasks", "section-matrix.yaml");
78563
- if (existsSync5(localPath)) {
78583
+ if (existsSync6(localPath)) {
78564
78584
  const data = await loadStructuredSpurConfig(localPath, {
78565
78585
  validateJsonSchema: true,
78566
78586
  embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
@@ -78570,7 +78590,7 @@ async function loadSectionMatrixUncached(projectRoot) {
78570
78590
  const root = bundledConfigRoot();
78571
78591
  if (root !== null) {
78572
78592
  const matrixPath = join18(root, "tasks", "section-matrix.yaml");
78573
- if (existsSync5(matrixPath)) {
78593
+ if (existsSync6(matrixPath)) {
78574
78594
  const data = await loadStructuredSpurConfig(matrixPath, {
78575
78595
  validateJsonSchema: true,
78576
78596
  embeddedSchemas: EMBEDDED_SPUR_SCHEMAS