@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/corpus-baseline.json +8 -8
- package/config/workflows/task-pipeline2.yaml +778 -0
- package/package.json +1 -1
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +10 -1
- package/spur.js +7 -6
|
@@ -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.
|
|
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",
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
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.
|
|
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));
|