@gobing-ai/spur 0.3.52 → 0.3.53

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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "plugins": [
8
8
  {
9
- "version": "0.3.52",
9
+ "version": "0.3.53",
10
10
  "name": "sp",
11
11
  "source": "./plugins/sp",
12
12
  "description": "Spur - my harness toolkits"
@@ -239,14 +239,6 @@
239
239
  "reason": "BDD step-order advisory (\"When\" after \"Then\") — see note § L3.ac-bdd-warning.",
240
240
  "since": "2026-08-17"
241
241
  },
242
- {
243
- "kind": "feature",
244
- "id": "I",
245
- "code": "L4.orphan-scenarios",
246
- "severity": "warning",
247
- "reason": "Feature AC scenarios have no linked task (orphan scenarios) — see note § L4.orphan-scenarios.",
248
- "since": "2026-08-17"
249
- },
250
242
  {
251
243
  "kind": "feature",
252
244
  "id": "I1",
@@ -271,6 +263,14 @@
271
263
  "reason": "Linked task lacks a PASS verdict with MET requirement — verification pending, see note § L4.scenario-unverified.",
272
264
  "since": "2026-08-17"
273
265
  },
266
+ {
267
+ "kind": "feature",
268
+ "id": "I",
269
+ "code": "L4.uncovered-feature-scenario",
270
+ "severity": "warning",
271
+ "reason": "Feature I (sp plugin) ownership scenarios (\"Plugin work has one active root\", \"Runtime execution remains separate\", \"Legacy H does not receive new work\") are planning/ownership assertions not covered by any linked task AC. Task 0600 links feature I (superskill-entrypoint alignment) but its AC do not route ownership; the link changed the report from L4.orphan-scenarios to uncovered-feature-scenario (DD-09). Cover by a future feature-I ownership task.",
272
+ "since": "2026-08-19"
273
+ },
274
274
  {
275
275
  "kind": "feature",
276
276
  "id": "J",
@@ -0,0 +1,778 @@
1
+ # Task execution pipeline 2 — rival pipeline (feature I6, task 0596): task-pipeline.yaml
2
+ # plus a residual-sweep stage. NOT promoted — task-pipeline.yaml stays the default.
3
+ #
4
+ # Delta from task-pipeline.yaml:
5
+ # 1. `verify` PASS now routes to `residual-sweep` (operator's manual fallback, wired
6
+ # into the FSM) before `record`/commit — it no longer depends on the operator
7
+ # remembering to type the prompt.
8
+ # 2. The two-layer plan (Layer 1 = stages; Layer 2 = the active stage's onEnter steps)
9
+ # is sourced from `workflow run --dry-run` + the parsed definition and rendered by
10
+ # the inline driver (see inline-pipeline-driver.md § Two-layer plan) — zero engine
11
+ # change, consumption-only.
12
+ #
13
+ # Orchestration is configuration (ADR-022 / §3.2): this is YAML over the existing
14
+ # dual-workflow engine — zero new engine code. The pipeline NEVER touches files
15
+ # directly — `record` writes via `spur task update --section`, and status moves go
16
+ # through the normal `spur task update <wbs> <status>` verb so the lifecycle guards
17
+ # (0055) apply identically. Run linkage is written to `task_run_links` (kind=pipeline).
18
+ #
19
+ # Shape: precheck → implement → test[→test-fix↔test-recheck] → review → approve(HITL)
20
+ # → verify → record → done
21
+ # (precheck failure short-circuits to `failed`; approve routes to `failed` on
22
+ # operator rejection or `cancelled` on operator cancel — R1, bug-750).
23
+ # `test` is the project quality gate (shell + bounded /sp:dev-fixall), not
24
+ # /sp:dev-unit (coverage gap-fill; router C3/C5).
25
+ #
26
+ # Vars (passed as a JSON object via `--vars`):
27
+ # wbs — task WBS (required)
28
+ # profile — "auto" skips HITL approve (R4)
29
+ # agent / spurBin — executor + spur binary (CLI overrides spurBin)
30
+ # stepTimeoutMs — agent.run budget for review/verify/test-fix (ms)
31
+ # implementTimeoutMs — implement agent.run budget (ms)
32
+ # qualityGateCmd — project gate (default: bun run autofix && bun run spur-check)
33
+ # qualityGateMaxFixAttempts — max /sp:dev-fixall hops after a red gate (default: 2)
34
+ #
35
+ # Seeded by `spur init`. agent.run inputs are pure slash commands (ADR-043).
36
+
37
+ "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
38
+ kind: state-machine
39
+ name: task-pipeline2
40
+ description: "Rival task pipeline: precheck → implement → quality-gate (test/fix/recheck) → review → approve → verify → residual-sweep → record → done"
41
+ # Task entity lifecycle (owned by task-lifecycle.yaml):
42
+ # normal: backlog → todo → wip → testing → done
43
+ # blocked is bidirectional with todo/wip/testing; done may reopen to wip.
44
+ # any non-terminal state may transition to cancelled.
45
+ # Guards:
46
+ # wip → testing: spur task check <wbs>
47
+ # testing → done: spur task check <wbs> --strict-core, then PASS-verdict provenance
48
+ # Invalid: todo → testing, todo → done, and wip → done.
49
+ # Pipeline states below are orchestration states, not task statuses.
50
+ iterationBound: 20
51
+ initialState: precheck
52
+ terminalStates:
53
+ - done
54
+ - failed
55
+ - cancelled
56
+ failureStates:
57
+ - failed
58
+ - cancelled
59
+ vars:
60
+ wbs: "0000"
61
+ profile: "standard"
62
+ # PATH-independent spur invocation for shell guards/actions. The CLI overrides this
63
+ # at run start (resolveSpurBin); the literal default is a safe fallback so direct/dry
64
+ # runs and `workflow validate` resolve the template. (See AGENTS.md / ADR-026.)
65
+ spurBin: "spur"
66
+ # Agent the pipeline's agent.run steps invoke. Override per run with
67
+ # `--vars '{"agent":"claude"}'`. Pinned (not left to the AiRunner's <default>
68
+ # selection) so a broken/misconfigured agent on the box can't silently capture the run.
69
+ agent: "omp"
70
+ # Implement-only executor override (R1, task 0454). Resolves to this YAML literal
71
+ # unless overridden. The `--agent` flag (execution-batch.md §3.2) forwards into BOTH
72
+ # `agent` and `implementAgent`, so a pinned `--agent X` reaches implement too. To
73
+ # pin ONLY implement while other hops keep the default, pass
74
+ # `--vars '{"implementAgent":"omp-zai"}'`.
75
+ implementAgent: "omp"
76
+ # Step-level timeout for agentic hops (review / verify / test-fix) in ms.
77
+ # Soft quality-gate shells are unbounded by this var (host shell only).
78
+ # Raised 600s → 1800s (task 0398 R4 / H6 dogfood). Override:
79
+ # `--vars '{"stepTimeoutMs":"120000"}'`. 30 min is headroom, not a licence to
80
+ # run unbounded — if a hop still hits this wall, STOP and record it.
81
+ stepTimeoutMs: "1800000"
82
+ # Timeout for the `implement` step specifically (R2a). Implementation is the
83
+ # heaviest agent.run step (full read/write/test-probe loop) and has timed out
84
+ # at the 600s default in five consecutive dogfood runs (bugs 742/744/746/748),
85
+ # a 100% failure rate at that budget. 30 min gives real headroom; STOP and
86
+ # record honestly rather than raising it further without operator sign-off.
87
+ implementTimeoutMs: "1800000"
88
+ # Answer captured by the approve gate's hitl.confirm (R1): "yes" | "no" | "cancel".
89
+ # Empty by default; only meaningful once the approve state has been entered.
90
+ __hitlAnswer: ""
91
+ # Residual-sweep prompt subject (R4). The operator's verbatim fallback prompt is
92
+ # templated over this noun phrase so both forms stay verbatim. Single-task default
93
+ # "current task"; a feature batch overrides it via `--vars`:
94
+ # --vars '{"residualSweepTarget":"these tasks for current feature"}'
95
+ residualSweepTarget: "current task"
96
+ # Project quality gate for the `test` hop (probe + fixall + recheck). Override per project
97
+ # with the same package-manager surface (this monorepo is Bun-only):
98
+ # `--vars '{"qualityGateCmd":"bun run lint && bun run test"}'`. Soft probe, hard recheck,
99
+ # and the fixall slash input all use this same var so the command stays single-sourced.
100
+ # TRUSTED CONFIG ONLY — this string is executed via `sh -c` (see test/test-recheck). Never
101
+ # interpolate untrusted operator/LLM input into qualityGateCmd (task 0436 SECUA residual).
102
+ qualityGateCmd: "bun run format && bun run spur-check"
103
+ # Cheap red-detector run before the full gate on **recheck only**; empty ⇒ no probe
104
+ # (full gate every recheck — the pre-0587 behavior). A project overriding qualityGateCmd
105
+ # should override this too. TRUSTED CONFIG ONLY — executed via `sh -c` (same surface as
106
+ # qualityGateCmd). Invariant: `review` is only ever entered through a full green
107
+ # qualityGateCmd — only the full gate writes PASS to <wbs>-test-gate.status.
108
+ gateProbeCmd: "bun run lint"
109
+ # Max /sp:dev-fixall attempts after a red quality-gate probe/recheck (bounded; no thrash).
110
+ # Attempt counter: .spur/run/<wbs>-test-fix-attempt. Default 2 = two fixall hops before failed.
111
+ qualityGateMaxFixAttempts: "2"
112
+ # Post-implement auto-format. Overridable like qualityGateCmd so a non-Bun seeded
113
+ # project can point it at its own formatter; invoked best-effort (a missing or
114
+ # failing formatter must never abort a run — the quality gate is the real gate).
115
+ formatCmd: "bun run format"
116
+ # Bounded `file:line` anchors extracted from the failing quality-gate log by the
117
+ # `test` / `test-recheck` hops (deduped, max 20, space-separated on one line).
118
+ # `test-fix` reads `.spur/run/<wbs>-test-gate.findings` into this var and names the
119
+ # anchors directly in the /sp:dev-fixall input, so the fix agent starts AT the failing
120
+ # file:line instead of re-deriving it from a fresh gate run (0482 R3).
121
+ gateFindings: ""
122
+ # Max R-items in ## Requirements before size precheck fails (R2, task 0454).
123
+ # Override with `--vars '{"maxImplementReqs":"12"}'`.
124
+ maxImplementReqs: "5"
125
+ # Max checklist items under ## Plan before size precheck fails (R2, task 0454).
126
+ # Override with `--vars '{"maxImplementPlanItems":"15"}'`.
127
+ maxImplementPlanItems: "8"
128
+ # Diff-scope guard on the implement hop (R1, task 0487). When the target task
129
+ # body backticks at least one path, non-corpus changes outside those paths
130
+ # fail the step by name. New files beside a declared file are allowed. Empty
131
+ # (default) = on; set to "off" to bypass:
132
+ # `--vars '{"implementScopeGuard":"off"}'`.
133
+ implementScopeGuard: ""
134
+
135
+ states:
136
+ - id: precheck
137
+ description: >
138
+ Pre-flight: soft agent doctor + transition guards for doctor PASS and
139
+ `spur task check <wbs>`. Failures route to the `failed` terminal state
140
+ (not a raw lifecycle abort mid-enter).
141
+ onEnter:
142
+ # Soft doctor — write status and always exit 0 so transitions can branch to
143
+ # `failed` cleanly (same pattern as the quality-gate soft probe).
144
+ #
145
+ # R2 (0487): probe BOTH resolved executors ($agent and $implementAgent) and
146
+ # FAIL on `authenticated: unauthenticated`. Previously only $agent was probed
147
+ # and auth was informational, so a run whose implement executor had no
148
+ # provider key sailed through precheck into a guaranteed implement failure
149
+ # (runs e8cb00e7 / b16bfbf4: "auth: no … API key not found for provider
150
+ # 'volc'" → precheck ✓). `unknown` auth keeps the old soft behavior — some
151
+ # agents expose no auth-status verb. `spur agent doctor` CLI exit-code
152
+ # semantics are deliberately untouched; the gate lives here.
153
+ # R4 (0487): one divergence line when the two executors differ (legitimate
154
+ # when only implementAgent is pinned, but it must be visible in the log).
155
+ # R2 (0503): omp/pi env-key probe misses are soft because the CLI process
156
+ # cannot see relay-owned credentials; explicit non-omp auth failures remain hard.
157
+ - kind: shell
158
+ options:
159
+ command: >-
160
+ mkdir -p .spur/run &&
161
+ DOCTOR_FILE=".spur/run/$wbs-precheck-doctor.status" &&
162
+ classifyDoctorProbe() {
163
+ PROBE_DETAIL="$1";
164
+ PROBE_AGENT="$2";
165
+ case "$PROBE_AGENT" in
166
+ omp|omp-*|pi|pi-*)
167
+ if printf '%s' "$PROBE_DETAIL" | grep -Eiq 'API key not found for provider|no probe registered'; then
168
+ printf 'env-miss\n';
169
+ return;
170
+ fi;;
171
+ esac;
172
+ if printf '%s' "$PROBE_DETAIL" | grep -Eiq 'invalid (API )?key|unauthori[sz]ed|forbidden|authentication failed|credential(s)? rejected'; then
173
+ printf 'auth-fail\n';
174
+ else
175
+ printf 'unknown\n';
176
+ fi;
177
+ } &&
178
+ STATUS=PASS;
179
+ if [ -n "$implementAgent" ] && [ "$implementAgent" != "$agent" ]; then
180
+ echo "precheck: agent=$agent implementAgent=$implementAgent (executors diverge)";
181
+ EXECS="$agent $implementAgent";
182
+ else
183
+ EXECS="$agent";
184
+ fi;
185
+ for EXE in $EXECS; do
186
+ OUT=$($spurBin agent doctor "$EXE" --json 2>&1) || {
187
+ echo "precheck: FAIL - doctor exited non-zero for $EXE";
188
+ echo "$OUT";
189
+ STATUS=FAIL;
190
+ continue;
191
+ };
192
+ AUTH=$(printf '%s' "$OUT" | jq -r '.agents[0].authenticated // "unknown"' 2>/dev/null || echo unknown);
193
+ DETAIL=$(printf '%s' "$OUT" | jq -r '.agents[0].modelStatus.detail // ""' 2>/dev/null || echo "");
194
+ PROBE=$(classifyDoctorProbe "$DETAIL" "$EXE");
195
+ echo "precheck: $EXE auth=$AUTH probe=$PROBE $DETAIL";
196
+ if [ "$AUTH" = unauthenticated ]; then
197
+ case "$EXE:$PROBE" in
198
+ omp*:env-miss|pi*:env-miss|omp*:unknown|pi*:unknown)
199
+ echo "precheck: SOFT - executor $EXE auth probe cannot see agent-owned credentials";;
200
+ *)
201
+ echo "precheck: FAIL - executor $EXE is unauthenticated; fix agent.default or pass --vars '{\"agent\":\"<authenticated-executor>\"}' ($spurBin agent doctor $EXE --json); $DETAIL";
202
+ STATUS=FAIL;;
203
+ esac;
204
+ fi;
205
+ done;
206
+ echo "$STATUS" > "$DOCTOR_FILE";
207
+ exit 0
208
+ # R6 (0487): pre-launch hygiene WARNING (never a block) — starting a task on
209
+ # a tree already dirty with another task's implementation is how 0485's diff
210
+ # got swept into 0486's run. Corpus dirs are excluded: the pipeline writes
211
+ # those itself.
212
+ # R1 (0511): task-corpus dirt gets its own advisory NOTE — docs/tasks* is
213
+ # excluded from the non-corpus warning, so pre-existing task-corpus edits are
214
+ # invisible at launch yet can be accidentally staged with the current task
215
+ # later. Advisory only (never a block): the pipeline legitimately updates
216
+ # task files, so corpus dirt is launch-time visibility, not a lifecycle guard.
217
+ - kind: shell
218
+ options:
219
+ command: >-
220
+ DIRTY=$(git status --porcelain -- . ':(exclude)docs/tasks*' ':(exclude)docs/features' 2>/dev/null);
221
+ if [ -n "$DIRTY" ]; then
222
+ echo "precheck: WARNING - working tree has uncommitted non-corpus changes; commit or stash before starting a new task:";
223
+ echo "$DIRTY";
224
+ fi;
225
+ CORPUS_DIRTY=$(git status --porcelain -- ':(glob)docs/tasks*/**' 2>/dev/null);
226
+ if [ -n "$CORPUS_DIRTY" ]; then
227
+ echo "precheck: NOTE - task corpus has uncommitted changes; review before staging with this task:";
228
+ echo "$CORPUS_DIRTY";
229
+ fi;
230
+ exit 0
231
+ - kind: note
232
+ options:
233
+ # R4 task_run_links linkage (kind=pipeline) is written by a WorkflowService
234
+ # hook at run start — see the 0062 follow-up note; there is no link-writing
235
+ # CLI verb to call from a shell step, so it cannot live in pure YAML.
236
+ message: "Pipeline start for task ${vars.wbs}."
237
+ # R1 (0453): auto-profile precheck reopens a done feature before task check.
238
+ # Under profile=auto, resolve feature_id, sync (preferred) or update to active.
239
+ # Under non-auto, leave R4 message to guide the operator.
240
+ - kind: shell
241
+ options:
242
+ command: >-
243
+ if [ "$profile" = auto ]; then
244
+ FID=$($spurBin task show $wbs --json 2>/dev/null |
245
+ jq -r '.feature_id // .frontmatter.feature_id // empty' 2>/dev/null);
246
+ if [ -n "$FID" ]; then
247
+ $spurBin feature sync "$FID" --force --json 2>/dev/null ||
248
+ $spurBin feature update "$FID" active 2>/dev/null || true;
249
+ fi;
250
+ fi;
251
+ exit 0
252
+ # R2 (0454): task size precheck — evaluate R-item and Plan-item counts.
253
+ # Writes PASS/FAIL to .spur/run/<wbs>-precheck-size.status. Always exit 0
254
+ # (soft check, like doctor). The precheck→implement guard reads the file.
255
+ # R3 (0487): `--executor` adds the size-vs-capability gate — a task past the
256
+ # DEFAULT caps routed to a sub-`capable-1` executor blocks here instead of
257
+ # burning the full implementTimeoutMs and exiting 3 (run ca130182).
258
+ - kind: shell
259
+ options:
260
+ command: >-
261
+ SIZE_FILE=".spur/run/$wbs-precheck-size.status" &&
262
+ mkdir -p .spur/run &&
263
+ bun plugins/sp/scripts/task-size-precheck.ts "$wbs"
264
+ --spur-bin "$spurBin" --max-reqs "$maxImplementReqs" --max-plan-items "$maxImplementPlanItems"
265
+ --executor "$implementAgent" &&
266
+ exit 0
267
+
268
+ - id: implement
269
+ description: >
270
+ Run agent-driven implementation via /sp:dev-run --mode implement, THEN move the
271
+ task to `wip` via the normal verb. The lifecycle transition fires AFTER the
272
+ implement agent.run succeeds — onEnter runs in declaration order and the default
273
+ 'fail' policy halts the sequence (routing the run to `failed`) if the agent.run
274
+ fails or times out, so a halted implement never leaves the task at `wip` with an
275
+ empty Solution (the F3 half-state). The `--no-lifecycle` flag is correct here: the
276
+ pipeline owns this transition; the FSM guard would otherwise re-run `spur task
277
+ check`, which is precheck's job.
278
+ NOTE: this step must NOT call /sp:dev-run in full mode (the default) —
279
+ that command DRIVES this pipeline, so calling it here recurses.
280
+ --mode implement is the single-step implement entry.
281
+ onEnter:
282
+ - kind: agent.run
283
+ options:
284
+ agent: ${vars.implementAgent}
285
+ # Pure slash command only (ADR-043). Anti-recursion / implement discipline
286
+ # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
287
+ role: coder
288
+ # lives in /sp:dev-run --mode implement → sp:code-implementation, not YAML prose.
289
+ input: /sp:dev-run --mode implement ${vars.wbs} --auto
290
+ timeoutMs: ${vars.implementTimeoutMs}
291
+ # R3 (task 0424): empty-implement no-op guard — the agent.run action
292
+ # fails the step when exit 0 produced zero non-corpus file changes, so
293
+ # a silent no-op routes the run to `failed` here instead of drifting
294
+ # into test/review and being caught a full pass later.
295
+ requireDiff: true
296
+ - kind: shell
297
+ options:
298
+ command: >-
299
+ retry_transient() {
300
+ RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
301
+ printf '%s\n' "$RETRY_OUTPUT";
302
+ if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
303
+ if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
304
+ sleep 2;
305
+ RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
306
+ printf '%s\n' "$RETRY_SECOND_OUTPUT";
307
+ if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
308
+ echo "node_modules link/dependency broken — run bun install and retry";
309
+ fi;
310
+ return "$RETRY_RC";
311
+ };
312
+ retry_transient $spurBin task update "$wbs" wip --no-lifecycle
313
+ # Post-implement cleanup: auto-format any unformatted output the agent produced.
314
+ # The agent may leave files that don't pass `biome check`, which then fails
315
+ # `bun run lint` downstream. Running format here is cheap (< 1 s) and prevents
316
+ # the test stage from tripping on a purely mechanical formatting defect (dogfood
317
+ # bug-733 — omp left agent-run.test.ts unformatted, causing a spurious lint gate
318
+ # failure that the ## Testing section mis-attributed to "pre-existing gaps").
319
+ # Best-effort: `; exit 0` (not `&&`) so a missing/failing formatter in a seeded
320
+ # non-Bun project cannot abort a run mid-implement. Formatting is a convenience;
321
+ # `${vars.qualityGateCmd}` at `test` is the gate that actually decides.
322
+ - kind: shell
323
+ options:
324
+ command: "$formatCmd ; exit 0"
325
+
326
+ # ── test hop (quality gate + bounded auto-fix) ─────────────────────────────
327
+ # NOT /sp:dev-unit. That command (sp:code-testing) *extends/generates* tests toward
328
+ # a coverage target; it is not the project quality gate. Coverage gap-fill remains
329
+ # available via router C3/C5 (`/sp:dev-unit`) or inside implement — not this hop.
330
+ #
331
+ # Shape (engine has no per-action "run next only if previous failed" without
332
+ # `onError`, and Spur's action schema does not expose `onError`):
333
+ # test — soft probe: qualityGateCmd → PASS|FAIL status; always exit 0
334
+ # (green path: one gate run → review; no fixall/recheck tax)
335
+ # test-fix — increment attempt + pure slash /sp:dev-fixall (≤ max attempts)
336
+ # test-recheck — soft recheck → review | test-fix | failed (never silent lifecycle abort)
337
+ - id: test
338
+ description: >
339
+ Soft quality-gate probe (single logical gate on the green path; bounded retries only
340
+ for SQLite lock contention). Runs
341
+ `${vars.qualityGateCmd}`, records PASS|FAIL under
342
+ `.spur/run/<wbs>-test-gate.status`, resets the fix attempt counter, always exit 0.
343
+ onEnter:
344
+ - kind: shell
345
+ options:
346
+ command: >-
347
+ mkdir -p .spur/run &&
348
+ STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
349
+ LOG_FILE=".spur/run/$wbs-test-gate.log" &&
350
+ ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
351
+ echo 0 > "$ATTEMPT_FILE" &&
352
+ : > "$LOG_FILE" &&
353
+ gate_attempt=1;
354
+ while [ "$gate_attempt" -le 5 ]; do
355
+ ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
356
+ sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
357
+ gate_locked=0;
358
+ grep -q 'SQLiteError: database is locked' "$ATTEMPT_LOG" && gate_locked=1;
359
+ cat "$ATTEMPT_LOG" >> "$LOG_FILE";
360
+ rm -f "$ATTEMPT_LOG";
361
+ if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
362
+ printf 'quality gate: database is locked; retrying (%s/5) in 10s\n' "$gate_attempt" | tee -a "$LOG_FILE";
363
+ sleep 10;
364
+ gate_attempt=$((gate_attempt + 1));
365
+ done &&
366
+ cat "$LOG_FILE" &&
367
+ FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
368
+ set +e; grep -oE '[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+' "$LOG_FILE" | sort -u | head -20 | tr '\n' ' ' > "$FINDINGS_FILE"; set -e &&
369
+ if [ "$gate_rc" -eq 0 ]; then
370
+ printf 'PASS\n' > "$STATUS_FILE";
371
+ else
372
+ printf 'FAIL\n' > "$STATUS_FILE";
373
+ fi &&
374
+ exit 0
375
+
376
+ - id: test-fix
377
+ description: >
378
+ Bounded auto-fix hop when the quality gate is red. Increments
379
+ `.spur/run/<wbs>-test-fix-attempt`, then pure slash (ADR-043)
380
+ `/sp:dev-fixall` against `${vars.qualityGateCmd}`.
381
+ onEnter:
382
+ - kind: shell
383
+ options:
384
+ command: >-
385
+ mkdir -p .spur/run &&
386
+ ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
387
+ n=$(cat "$ATTEMPT_FILE" 2>/dev/null || echo 0) &&
388
+ printf '%s\n' "$((n + 1))" > "$ATTEMPT_FILE"
389
+ # R3 (0482): project the extracted gate anchors into a var so the dispatch input
390
+ # can NAME the failing file:line, not merely point at a log. A vars template cannot
391
+ # run a shell, so the read is an action, not an inline `$(cat ...)` substitution.
392
+ - kind: file.read.into-var
393
+ options:
394
+ path: .spur/run/${vars.wbs}-test-gate.findings
395
+ var: gateFindings
396
+ - kind: agent.run
397
+ options:
398
+ agent: ${vars.agent}
399
+ # R3 (0482): `--findings` names the failing anchors inline; `--gate-log` remains
400
+ # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
401
+ role: coder
402
+ # the full-context escape hatch when the digest is not enough.
403
+ input: /sp:dev-fixall "${vars.qualityGateCmd}" --gate-log .spur/run/${vars.wbs}-test-gate.log --findings "${vars.gateFindings}"
404
+ timeoutMs: ${vars.stepTimeoutMs}
405
+
406
+ - id: test-recheck
407
+ description: >
408
+ Soft recheck after fixall with bounded SQLite-lock retry. Writes PASS|FAIL (always exit 0). Transitions
409
+ branch to review (PASS), another test-fix (FAIL and under max attempts),
410
+ or the pipeline `failed` state (FAIL and attempts exhausted) — never a
411
+ raw lifecycle abort that skips the terminal `failed` state.
412
+ onEnter:
413
+ # 0587 R3: probe-then-full recheck. A red gateProbeCmd records FAIL and skips the full
414
+ # gate (the measured waste is re-running a 110–140s gate to learn the tree is still red);
415
+ # a green probe (or empty gateProbeCmd) falls through to the full-gate loop unchanged.
416
+ # Only the full gate writes PASS, so the `test-recheck → review` guard (reads PASS)
417
+ # still means a full green qualityGateCmd ran — invariant preserved by construction.
418
+ - kind: shell
419
+ options:
420
+ command: >-
421
+ mkdir -p .spur/run &&
422
+ STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
423
+ LOG_FILE=".spur/run/$wbs-test-gate.log" &&
424
+ FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
425
+ : > "$LOG_FILE" &&
426
+ probe_rc=0;
427
+ if [ -n "$gateProbeCmd" ]; then
428
+ sh -c "$gateProbeCmd" > "$LOG_FILE.probe" 2>&1; probe_rc=$?;
429
+ cat "$LOG_FILE.probe" >> "$LOG_FILE";
430
+ rm -f "$LOG_FILE.probe";
431
+ fi;
432
+ if [ "$probe_rc" -ne 0 ]; then
433
+ gate_rc=$probe_rc;
434
+ else
435
+ gate_attempt=1;
436
+ while [ "$gate_attempt" -le 5 ]; do
437
+ ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
438
+ sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
439
+ gate_locked=0;
440
+ grep -q 'SQLiteError: database is locked' "$ATTEMPT_LOG" && gate_locked=1;
441
+ cat "$ATTEMPT_LOG" >> "$LOG_FILE";
442
+ rm -f "$ATTEMPT_LOG";
443
+ if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
444
+ printf 'quality gate: database is locked; retrying (%s/5) in 10s\n' "$gate_attempt" | tee -a "$LOG_FILE";
445
+ sleep 10;
446
+ gate_attempt=$((gate_attempt + 1));
447
+ done;
448
+ fi &&
449
+ cat "$LOG_FILE" &&
450
+ set +e; grep -oE '[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+' "$LOG_FILE" | sort -u | head -20 | tr '\n' ' ' > "$FINDINGS_FILE"; set -e &&
451
+ if [ "$gate_rc" -eq 0 ]; then
452
+ printf 'PASS\n' > "$STATUS_FILE";
453
+ else
454
+ printf 'FAIL\n' > "$STATUS_FILE";
455
+ fi &&
456
+ exit 0
457
+
458
+ - id: review
459
+ 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`.
460
+ onEnter:
461
+ - kind: agent.run
462
+ options:
463
+ agent: ${vars.agent}
464
+ input: /sp:dev-review ${vars.wbs} --auto
465
+ # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
466
+ role: reviewer
467
+ timeoutMs: ${vars.stepTimeoutMs}
468
+
469
+ - id: approve
470
+ description: >
471
+ Human-in-the-loop approval gate. Under `profile=auto` this state is never entered
472
+ (review routes around it straight to verify). In interactive mode this state pauses
473
+ the run for `spur workflow continue` (E3), making approval an explicit operator action.
474
+ The operator's answer is routed (R1, bug-750): yes -> verify; no -> failed (rejection
475
+ recorded, not silently approved); cancel -> cancelled (a distinct terminal state).
476
+ pause: true
477
+ onEnter:
478
+ - kind: hitl.confirm
479
+ options:
480
+ prompt: "Approve task ${vars.wbs} to proceed to verification?"
481
+
482
+ - id: verify
483
+ description: >
484
+ Functional verification (BDD + traceability) via /sp:dev-verify. The agent's
485
+ captured answer is written to `.spur/run/<wbs>-verify-answer.txt` and MUST follow
486
+ the answer-file schema contract (explicit `Verdict: PASS|PARTIAL|FAIL` line plus
487
+ `| Req | Status | Evidence |` and `| AC | Status | Evidence Type | Evidence |` tables);
488
+ a deterministic shell step then derives the verdict and writes the gate artifact
489
+ `.spur/run/<wbs>-verdict.json` (so the completion gate reads a real verdict, not
490
+ agent discretion — R9). The verdict is PASS only if the agent both reported PASS
491
+ AND `spur task check` passes; otherwise FAIL.
492
+ onEnter:
493
+ - kind: agent.run
494
+ options:
495
+ agent: ${vars.agent}
496
+ input: /sp:dev-verify ${vars.wbs} --auto --fix all --focus all
497
+ # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
498
+ role: reviewer
499
+ timeoutMs: ${vars.stepTimeoutMs}
500
+ answerFile: .spur/run/${vars.wbs}-verify-answer.txt
501
+ - kind: shell
502
+ options:
503
+ command: "$spurBin task verdict $wbs --from-answer .spur/run/$wbs-verify-answer.txt"
504
+
505
+ - id: residual-sweep
506
+ description: >
507
+ Residual sweep (R4) — the operator's manual fallback, wired into the FSM as
508
+ its own stage (NOT a step inside verify). Decision: the PASS verdict is a
509
+ TRANSITION guard (`verify -> record`), evaluated only after verify's onEnter
510
+ finishes — a step inside verify would run before that guard and could not be
511
+ conditioned on the verdict. A separate stage reached only via the PASS guard
512
+ is the only way to hold the fixed position (after PASS, before record/commit).
513
+ It also makes the per-task sweep free in the `--feature` batch: the batch runs
514
+ this pipeline once per task, so each task sweeps itself (Q&A: per task, never
515
+ once per feature). The agent.run prompt is the operator's verbatim fallback,
516
+ templated over `residualSweepTarget` (single-task vs feature wording).
517
+ onEnter:
518
+ - kind: agent.run
519
+ options:
520
+ agent: ${vars.agent}
521
+ # Post-verification completeness judgment (catch what the per-requirement
522
+ # verdict structurally misses) — folds the `verify` stage, so it takes the
523
+ # reviewer role (capable-1), matching the operator's strong-model usage.
524
+ role: reviewer
525
+ input: "Anything remained in ${vars.residualSweepTarget}? if any, you should get all of them done before we commit all the changes in next step"
526
+ timeoutMs: ${vars.stepTimeoutMs}
527
+
528
+ - id: record
529
+ description: >
530
+ Record pipeline results into the task file via `spur task record` —
531
+ Testing/Review from the verdict, Solution backfilled from git diff as a
532
+ safety net, optional transition to testing. Post-record step conditionally syncs
533
+ feature status via the bounded `feature-sync-bounded` wrapper (task 0411
534
+ retry-suppression) if `feature_id` is present, or appends an orphan link proposal
535
+ to the run report if absent (task 0328 / ADR-0322).
536
+ onEnter:
537
+ - kind: shell
538
+ options:
539
+ command: >-
540
+ retry_transient() {
541
+ RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
542
+ printf '%s\n' "$RETRY_OUTPUT";
543
+ if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
544
+ if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
545
+ sleep 2;
546
+ RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
547
+ printf '%s\n' "$RETRY_SECOND_OUTPUT";
548
+ if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
549
+ echo "node_modules link/dependency broken — run bun install and retry";
550
+ fi;
551
+ return "$RETRY_RC";
552
+ };
553
+ retry_transient $spurBin task record "$wbs" --solution-from-diff --transition testing
554
+ # Best-effort, and portable across layouts. `feature-sync-bounded.ts` is a
555
+ # Spur-monorepo path — `spur init` never scaffolds `plugins/sp/`, so a seeded
556
+ # project must fall back to the plain `spur feature sync` verb rather than
557
+ # abort a run that already produced a PASS verdict. Trailing `exit 0` keeps the
558
+ # whole step advisory for the same reason (feature status sync is a follow-up,
559
+ # not a completion gate — `record → done` runs `spur task check`).
560
+ - kind: shell
561
+ options:
562
+ command: >-
563
+ FID=$($spurBin task show $wbs --json 2>/dev/null |
564
+ jq -r ".feature_id // .frontmatter.feature_id // empty" 2>/dev/null);
565
+ if [ -n "$FID" ]; then
566
+ if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then
567
+ bun plugins/sp/scripts/feature-sync-bounded.ts "$FID" --spur-bin "$spurBin" --json;
568
+ else
569
+ $spurBin feature sync "$FID" --json;
570
+ fi;
571
+ else
572
+ echo "Orphan task $wbs — no feature_id linked; proposal: consider linking to a parent feature." >> ".spur/run/$wbs-report.txt";
573
+ fi;
574
+ exit 0
575
+
576
+ - id: done
577
+ description: >
578
+ Terminal — task executed, verified, and recorded. The `record → done`
579
+ guard runs `spur task check` before certifying; a genuinely non-compliant
580
+ task routes to `failed` instead of a silent bad `done`.
581
+ onEnter:
582
+ - kind: shell
583
+ options:
584
+ command: >-
585
+ retry_transient() {
586
+ RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
587
+ printf '%s\n' "$RETRY_OUTPUT";
588
+ if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
589
+ if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
590
+ sleep 2;
591
+ RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
592
+ printf '%s\n' "$RETRY_SECOND_OUTPUT";
593
+ if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
594
+ echo "node_modules link/dependency broken — run bun install and retry";
595
+ fi;
596
+ return "$RETRY_RC";
597
+ };
598
+ retry_transient $spurBin task update "$wbs" done --no-lifecycle
599
+ - kind: note
600
+ options:
601
+ message: "Pipeline complete for task ${vars.wbs} (done gate cleared)."
602
+ # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
603
+ - kind: shell
604
+ options:
605
+ command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: task-pipeline done wbs=$wbs ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/$wbs-checkpoint.md'
606
+
607
+ - id: failed
608
+ description: >
609
+ Terminal — precheck, quality-gate exhaustion, verify non-PASS, record check
610
+ failure, or operator rejection; reported, not advanced.
611
+
612
+ - id: cancelled
613
+ description: Terminal — pipeline cancelled by operator at the approval gate (R1).
614
+
615
+ transitions:
616
+ # ── precheck: doctor PASS + task check → implement; else → failed ──
617
+ - from: precheck
618
+ to: implement
619
+ description: Agent doctor PASS and task check green — begin implementation.
620
+ guard:
621
+ kind: shell
622
+ options:
623
+ command: 'test "$(cat .spur/run/$wbs-precheck-doctor.status 2>/dev/null)" = PASS && test "$(cat .spur/run/$wbs-precheck-size.status 2>/dev/null)" = PASS && $spurBin task check $wbs'
624
+ - from: precheck
625
+ to: failed
626
+ description: Doctor FAIL and/or task check failed — stop before implement.
627
+ guard:
628
+ kind: always
629
+
630
+ # ── linear body ──
631
+ - from: implement
632
+ to: test
633
+ description: Implementation done — quality-gate probe.
634
+ guard:
635
+ kind: always
636
+ # Soft probe branching (declaration order: PASS first, then FAIL, then defense).
637
+ - from: test
638
+ to: review
639
+ description: Quality gate already green — one gate run only; skip fixall/recheck.
640
+ guard:
641
+ kind: shell
642
+ options:
643
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS'
644
+ - from: test
645
+ to: test-fix
646
+ description: Quality gate red — start bounded fixall loop.
647
+ guard:
648
+ kind: shell
649
+ options:
650
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL'
651
+ # Defense: missing/corrupt status — treat as FAIL path, not silent PASS.
652
+ - from: test
653
+ to: test-fix
654
+ description: Probe status missing/corrupt — attempt fixall then recheck.
655
+ guard:
656
+ kind: always
657
+ - from: test-fix
658
+ to: test-recheck
659
+ description: Fixall finished — soft recheck the same quality gate.
660
+ guard:
661
+ kind: always
662
+ # Recheck branching (PASS first; under-max FAIL → fixall again; exhausted → failed).
663
+ - from: test-recheck
664
+ to: review
665
+ description: Quality gate green after fixall — proceed to review.
666
+ guard:
667
+ kind: shell
668
+ options:
669
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS'
670
+ - from: test-recheck
671
+ to: test-fix
672
+ description: Still red and under qualityGateMaxFixAttempts — another fixall hop.
673
+ guard:
674
+ kind: shell
675
+ options:
676
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL && test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -lt "$qualityGateMaxFixAttempts"'
677
+ - from: test-recheck
678
+ to: failed
679
+ description: Still red after max fixall attempts — stop at failed (not silent abort).
680
+ guard:
681
+ kind: shell
682
+ options:
683
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = FAIL && test "$(cat .spur/run/$wbs-test-fix-attempt 2>/dev/null || echo 0)" -ge "$qualityGateMaxFixAttempts"'
684
+ # Defense: corrupt recheck status — failed, not review.
685
+ - from: test-recheck
686
+ to: failed
687
+ description: Recheck status missing/corrupt — stop at failed.
688
+ guard:
689
+ kind: always
690
+ # ── review → approve, OR skip the HITL gate entirely when profile=auto (R4) ──
691
+ # Declaration order matters: the auto-skip guard is tried FIRST, so under
692
+ # profile=auto the run routes straight to verify and never ENTERS approve
693
+ # (whose onEnter hitl.confirm would otherwise block). Otherwise fall through
694
+ # to approve for the human gate.
695
+ - from: review
696
+ to: verify
697
+ description: profile=auto — skip the human approval gate, go straight to verify.
698
+ guard:
699
+ kind: shell
700
+ options:
701
+ command: 'test "$profile" = auto'
702
+ - from: review
703
+ to: approve
704
+ description: Review done — gate on human approval.
705
+ guard:
706
+ kind: always
707
+
708
+ # ── approve: HITL gate (reached only when profile != auto). Routed by the
709
+ # operator's captured answer (R1, bug-750) — three ordered guards, declaration
710
+ # order matters (yes first, then no, then cancel), mirroring idea-pipeline's
711
+ # design-approval gate (.spur/workflows/idea-pipeline.yaml:352-366). No
712
+ # `always` edge remains out of `approve`. ──
713
+ - from: approve
714
+ to: verify
715
+ description: Approved — proceed to verification.
716
+ guard:
717
+ kind: shell
718
+ options:
719
+ command: 'test "$__hitlAnswer" = yes'
720
+ - from: approve
721
+ to: failed
722
+ description: Operator rejected at the approval gate — report and stop.
723
+ guard:
724
+ kind: shell
725
+ options:
726
+ command: 'test "$__hitlAnswer" = no'
727
+ - from: approve
728
+ to: cancelled
729
+ description: Operator cancelled at the approval gate.
730
+ guard:
731
+ kind: shell
732
+ options:
733
+ command: 'test "$__hitlAnswer" = cancel'
734
+
735
+ # ── completion gate (the YAML-native replacement for rd3's default-on --postflight-verify) ──
736
+ # The verify step emits .spur/run/<wbs>-verdict.json. Only `verdict: PASS` clears
737
+ # the gate to `residual-sweep` (thence `record`); any non-PASS (PARTIAL/FAIL), a
738
+ # missing file, or malformed JSON routes to `failed`. Declaration order: PASS guard
739
+ # tried FIRST. The residual sweep therefore runs ONLY on a PASS verdict, before
740
+ # record/commit — its fixed position is enforced by the FSM, not operator memory.
741
+ - from: verify
742
+ to: residual-sweep
743
+ description: Verification verdict is PASS — sweep residual work before record/commit.
744
+ guard:
745
+ kind: shell
746
+ options:
747
+ command: 'test "$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)" = PASS'
748
+ - from: verify
749
+ to: failed
750
+ description: Verification verdict is not PASS (PARTIAL/FAIL/missing) — block before done.
751
+ guard:
752
+ kind: shell
753
+ options:
754
+ command: 'test "$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)" != PASS'
755
+ - from: residual-sweep
756
+ to: record
757
+ description: Residual sweep done — record results and proceed to done.
758
+ guard:
759
+ kind: always
760
+ # ── done gate: record → done/failed gated on `spur task check` (ADR-026 amendment 2026-06-23) ──
761
+ # The record step guarantees every done-required section ([Solution, Testing, Review])
762
+ # has real content (each owned by its pipeline step). This guard is defense-in-depth —
763
+ # it certifies the matrix before done; a genuinely non-compliant task routes to failed.
764
+ # Declaration order: pass guard first.
765
+ - from: record
766
+ to: done
767
+ description: Task check passed — certify done.
768
+ guard:
769
+ kind: shell
770
+ options:
771
+ command: "$spurBin task check $wbs"
772
+ - from: record
773
+ to: failed
774
+ description: Task check failed — block before done; investigate missing sections.
775
+ guard:
776
+ kind: shell
777
+ options:
778
+ command: "! $spurBin task check $wbs"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.52",
3
+ "version": "0.3.53",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.52",
3
+ "version": "0.3.53",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -30,7 +30,16 @@ command, skill, script, or second workflow.
30
30
  `session` and the Codex key `session_id` (in that order). If neither is available, allocate
31
31
  `host-session-<run-id>` and record that fallback in the log; provenance must never be blank or
32
32
  guessed from an executor subprocess.
33
- 4. Record lifecycle provenance before entering the FSM:
33
+ 4. Render the two-layer plan into the host todo list (task 0596):
34
+ - **Layer 1** = the pipeline's `states` in declaration order, with the active state marked — the
35
+ same order `renderRunPlan` / `spur workflow run --dry-run` previews.
36
+ - **Layer 2** = the active state's `onEnter` actions (`kind` + resolved `input`/`command`), shown
37
+ only for the active state.
38
+ - **Refresh cadence** = stage boundaries only (when the current state changes after a transition),
39
+ never per action.
40
+ - **Source of truth** = the YAML parsed in step 1 plus the dry-run walk — never hand-copy the
41
+ state list into the driver, a command, a skill, or a script.
42
+ 5. Record lifecycle provenance before entering the FSM:
34
43
 
35
44
  ```bash
36
45
  spur task run-link <wbs> --source inline-full --run-id <run-id> --json
package/spur.js CHANGED
@@ -75146,7 +75146,8 @@ class TaskService {
75146
75146
  priority: true,
75147
75147
  done_forced: true,
75148
75148
  done_reason: true,
75149
- ac_numbering: true
75149
+ ac_numbering: true,
75150
+ ac_altitude: true
75150
75151
  };
75151
75152
  if (!(key2 in allowed)) {
75152
75153
  throw new Error(`Field "${key2}" is not settable via update; allowed: ${Object.keys(allowed).join(", ")}.`);
@@ -88006,7 +88007,7 @@ import { createRequire } from "module";
88006
88007
  var CLI_CONFIG = {
88007
88008
  binaryName: "spur",
88008
88009
  binaryLabel: "spur",
88009
- binaryVersion: "0.3.52",
88010
+ binaryVersion: "0.3.53",
88010
88011
  configDir: ".spur",
88011
88012
  configFile: ".spur/config.yaml",
88012
88013
  databaseFile: ".spur/spur.db"
@@ -100037,7 +100038,7 @@ ${result.content}`);
100037
100038
  "See the gate checklist (spur-dev/references/gate-checklists.md).",
100038
100039
  "Valid section names (no failed write): `spur task sections <wbs> list`."
100039
100040
  ].join(`
100040
- `)).option("--section <name>", "Section name to replace").option("--from-file <path>", "File to read section body from (requires --section)").option("--feature <id>", "Set the feature_id frontmatter field (traceability edge)").option("--priority <p>", "Set the priority frontmatter field (P0\u2013P3)").option("--ac-numbering <mode>", "Set the ac_numbering frontmatter field (task-local) \u2014 opts the task into the Requirements\u2194AC coverage check").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292). Waives the verdict only \u2014 the FSM path still applies, so from an earlier status walk the hops first: `todo` \u2192 `wip` \u2192 `testing` \u2192 `done` (each hop runs the structural `spur task check`)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, status, options) => {
100041
+ `)).option("--section <name>", "Section name to replace").option("--from-file <path>", "File to read section body from (requires --section)").option("--feature <id>", "Set the feature_id frontmatter field (traceability edge)").option("--priority <p>", "Set the priority frontmatter field (P0\u2013P3)").option("--ac-numbering <mode>", "Set the ac_numbering frontmatter field (task-local) \u2014 opts the task into the Requirements\u2194AC coverage check").option("--ac-altitude <mode>", "Set the ac_altitude frontmatter field. Valid: `graduating` (default; feature-AC subset rule enforced) or `task-local` (skip the DD-09 subset rule \u2014 task scenarios are intentionally not feature ship criteria). Mirrors the L1 schema enum (packages/domain/src/planning/schema.ts:304).").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292). Waives the verdict only \u2014 the FSM path still applies, so from an earlier status walk the hops first: `todo` \u2192 `wip` \u2192 `testing` \u2192 `done` (each hop runs the structural `spur task check`)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, status, options) => {
100041
100042
  const svc = await makeService2(context4, options.folder, options.lifecycle === false);
100042
100043
  try {
100043
100044
  if (options.section !== undefined) {
@@ -100055,9 +100056,9 @@ ${result.content}`);
100055
100056
  }
100056
100057
  context4.output.write(`Updated section '${options.section}' in task ${result.ref.id}`);
100057
100058
  }
100058
- } else if (options.feature !== undefined || options.priority !== undefined || options.acNumbering !== undefined) {
100059
- const key2 = options.feature !== undefined ? "feature_id" : options.priority !== undefined ? "priority" : "ac_numbering";
100060
- const value2 = options.feature ?? options.priority ?? options.acNumbering ?? "";
100059
+ } else if (options.feature !== undefined || options.priority !== undefined || options.acNumbering !== undefined || options.acAltitude !== undefined) {
100060
+ const key2 = options.feature !== undefined ? "feature_id" : options.priority !== undefined ? "priority" : options.acNumbering !== undefined ? "ac_numbering" : "ac_altitude";
100061
+ const value2 = options.feature ?? options.priority ?? options.acNumbering ?? options.acAltitude ?? "";
100061
100062
  const result = await svc.updateField(wbs, key2, value2);
100062
100063
  if (options.json) {
100063
100064
  context4.output.write(toJson2(result));