@gobing-ai/spur 0.3.74 → 0.3.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/workflows/docs-pipeline.yaml +26 -1
  3. package/config/workflows/feature-dev.yaml +143 -102
  4. package/config/workflows/idea-pipeline.yaml +58 -11
  5. package/config/workflows/task-pipeline.yaml +76 -40
  6. package/config/workflows/wrapup-pipeline.yaml +183 -111
  7. package/package.json +1 -1
  8. package/plugins/sp/agents/expert-spur.md +7 -4
  9. package/plugins/sp/commands/dev-idea.md +1 -1
  10. package/plugins/sp/plugin.json +1 -1
  11. package/plugins/sp/skills/spur-cli/references/tasks.md +3 -1
  12. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +23 -8
  13. package/plugins/sp/skills/spur-dev/references/dev-operations.md +1 -1
  14. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +4 -1
  15. package/spur.js +2010 -362
  16. package/web/_astro/{BoardApp.C60RJZRj.js → BoardApp.BOW1815F.js} +97 -97
  17. package/web/_astro/BoardApp.C4zimv1U.js +1 -0
  18. package/web/_astro/{TaskDetail.Guk8VuNu.js → TaskDetail.CX6C7dSJ.js} +1 -1
  19. package/web/_astro/{arc.CAZtlWJM.js → arc.CwSvH1ji.js} +1 -1
  20. package/web/_astro/{architectureDiagram-3BPJPVTR.9-XbF_Tz.js → architectureDiagram-3BPJPVTR.Dxne1RvP.js} +1 -1
  21. package/web/_astro/{blockDiagram-GPEHLZMM.DC4WLx3L.js → blockDiagram-GPEHLZMM.D9Fb14rC.js} +1 -1
  22. package/web/_astro/{c4Diagram-AAUBKEIU.JgjUQMgG.js → c4Diagram-AAUBKEIU.DmvWSFvv.js} +1 -1
  23. package/web/_astro/channel.CQHdDVp9.js +1 -0
  24. package/web/_astro/{chunk-2J33WTMH.D_JFiXa-.js → chunk-2J33WTMH.BCCktIrE.js} +1 -1
  25. package/web/_astro/{chunk-4BX2VUAB.Bk_RxoeT.js → chunk-4BX2VUAB.BzCk9q27.js} +1 -1
  26. package/web/_astro/{chunk-55IACEB6.a_Rj2VxL.js → chunk-55IACEB6.CZXVgTk4.js} +1 -1
  27. package/web/_astro/{chunk-727SXJPM.C2NoS1U6.js → chunk-727SXJPM.CJL8UIpX.js} +1 -1
  28. package/web/_astro/{chunk-AQP2D5EJ.D1Xn6CNa.js → chunk-AQP2D5EJ.Dz8MDEdw.js} +1 -1
  29. package/web/_astro/{chunk-FMBD7UC4.BhJrSBsX.js → chunk-FMBD7UC4.CkQeYYUw.js} +1 -1
  30. package/web/_astro/{chunk-ND2GUHAM.ByVGduYQ.js → chunk-ND2GUHAM.KiC1QgzH.js} +1 -1
  31. package/web/_astro/{chunk-QZHKN3VN.B47Paunq.js → chunk-QZHKN3VN.AJ08mw2e.js} +1 -1
  32. package/web/_astro/{classDiagram-4FO5ZUOK.BERMSD5C.js → classDiagram-4FO5ZUOK.DRWRDKzK.js} +1 -1
  33. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BERMSD5C.js → classDiagram-v2-Q7XG4LA2.DRWRDKzK.js} +1 -1
  34. package/web/_astro/{cose-bilkent-S5V4N54A.O_rHGlhU.js → cose-bilkent-S5V4N54A.CavydfLP.js} +1 -1
  35. package/web/_astro/{cynefin-OW5HDTMX.qCW2GdNp.js → cynefin-OW5HDTMX.D_7o_a0B.js} +1 -1
  36. package/web/_astro/{dagre-BM42HDAG.C5Y4lH_u.js → dagre-BM42HDAG.LWk2dKgg.js} +1 -1
  37. package/web/_astro/{diagram-2AECGRRQ.DxZmRXxF.js → diagram-2AECGRRQ.82lbq6aB.js} +1 -1
  38. package/web/_astro/{diagram-5GNKFQAL.qSxZeYSK.js → diagram-5GNKFQAL.hyNL1pwY.js} +1 -1
  39. package/web/_astro/{diagram-KO2AKTUF.-7vl3bXs.js → diagram-KO2AKTUF.BaLqVf-b.js} +1 -1
  40. package/web/_astro/{diagram-LMA3HP47.BHVV-UH3.js → diagram-LMA3HP47.DQJxj0oz.js} +1 -1
  41. package/web/_astro/{diagram-OG6HWLK6.DHAuW9sK.js → diagram-OG6HWLK6.DhlL8cG2.js} +1 -1
  42. package/web/_astro/{erDiagram-TEJ5UH35.DMRLQwPN.js → erDiagram-TEJ5UH35.HoTnXwkF.js} +1 -1
  43. package/web/_astro/{flowDiagram-I6XJVG4X.B3EpPp_8.js → flowDiagram-I6XJVG4X.DkFKlZIZ.js} +1 -1
  44. package/web/_astro/{ganttDiagram-6RSMTGT7.BgUrExXM.js → ganttDiagram-6RSMTGT7.9qPfOTDb.js} +1 -1
  45. package/web/_astro/{gitGraphDiagram-PVQCEYII.CU-9yVN4.js → gitGraphDiagram-PVQCEYII.B4G18Dwc.js} +1 -1
  46. package/web/_astro/index.Dcr_8fiK.css +1 -0
  47. package/web/_astro/{infoDiagram-5YYISTIA.CvVTCRLe.js → infoDiagram-5YYISTIA.e3KJkXAM.js} +1 -1
  48. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUzOfKPf.js → ishikawaDiagram-YF4QCWOH.CY3yddhD.js} +1 -1
  49. package/web/_astro/{journeyDiagram-JHISSGLW.BBTb7ziz.js → journeyDiagram-JHISSGLW.DaAKIO1t.js} +1 -1
  50. package/web/_astro/{kanban-definition-UN3LZRKU.CTQQr70i.js → kanban-definition-UN3LZRKU.DGkDddtc.js} +1 -1
  51. package/web/_astro/{linear.CLKlJPiS.js → linear.DnDPcpd1.js} +1 -1
  52. package/web/_astro/{mermaid.core.BA4wFhLP.js → mermaid.core.1uBmxa9t.js} +4 -4
  53. package/web/_astro/{mindmap-definition-RKZ34NQL.t1vG2l5e.js → mindmap-definition-RKZ34NQL.XFJVayxw.js} +1 -1
  54. package/web/_astro/{pieDiagram-4H26LBE5.C8zJhyTu.js → pieDiagram-4H26LBE5.BXm2OgHh.js} +1 -1
  55. package/web/_astro/{quadrantDiagram-W4KKPZXB.DqYYvb7T.js → quadrantDiagram-W4KKPZXB.DgB2p5fc.js} +1 -1
  56. package/web/_astro/{requirementDiagram-4Y6WPE33.lzfDZ9nI.js → requirementDiagram-4Y6WPE33.jBR39bnS.js} +1 -1
  57. package/web/_astro/{sankeyDiagram-5OEKKPKP.lm6N5ORM.js → sankeyDiagram-5OEKKPKP.N3u9OhE3.js} +1 -1
  58. package/web/_astro/{sequenceDiagram-3UESZ5HK.E6_IV4Dj.js → sequenceDiagram-3UESZ5HK.CZhqZXNL.js} +1 -1
  59. package/web/_astro/{stateDiagram-AJRCARHV.Bnv6Ok2p.js → stateDiagram-AJRCARHV.DlNu1VEa.js} +1 -1
  60. package/web/_astro/{stateDiagram-v2-BHNVJYJU.mJHH_Ng_.js → stateDiagram-v2-BHNVJYJU.BEqi8NQV.js} +1 -1
  61. package/web/_astro/{timeline-definition-PNZ67QCA.BhRgqSLa.js → timeline-definition-PNZ67QCA.BPcexclc.js} +1 -1
  62. package/web/_astro/{vennDiagram-CIIHVFJN.pjb5DMC9.js → vennDiagram-CIIHVFJN.BZalxKGQ.js} +1 -1
  63. package/web/_astro/{wardleyDiagram-YWT4CUSO.CW20KUng.js → wardleyDiagram-YWT4CUSO.BqOljod9.js} +1 -1
  64. package/web/_astro/{xychartDiagram-2RQKCTM6.vX9_yuMl.js → xychartDiagram-2RQKCTM6.tkO4ppN5.js} +1 -1
  65. package/web/index.html +2 -2
  66. package/web/_astro/BoardApp.ymfj1EHA.js +0 -1
  67. package/web/_astro/channel.Bxxe2Byc.js +0 -1
  68. package/web/_astro/index.BhvM3djt.css +0 -1
@@ -52,6 +52,19 @@
52
52
  # longer be silently re-interpreted as an empty list). All captures are
53
53
  # run-scoped under .spur/run/<runId>-wrapup-*; a lookup failure is
54
54
  # recorded, never silently omitted as success.
55
+ # - 0783 validated consumers (audit 0781 F-04): validation accepts only
56
+ # canonical four-digit WBS strings (whitespace is rejected, not trimmed);
57
+ # the route writer, every guard, model prompt and operator note after
58
+ # resolution read the run-scoped capture, never raw vars.tasks, and a
59
+ # missing/corrupted capture or status refuses progression. Metrics
60
+ # revalidate the capture, require well-shaped task lookups, serialize rows
61
+ # with jq (never interpolated printf), and write PASS only after every
62
+ # required append succeeds. Required feature sync succeeds only for a
63
+ # valid matching proposal with no gateBlocked/requiresConfirm condition
64
+ # whose target status is freshly observed; applied:false is a successful
65
+ # no-op only when from == to and that status is observed — nonzero,
66
+ # malformed, partial, blocked or unreadable outcomes fail explicitly, and
67
+ # an affected-feature check can never convert a failed sync into success.
55
68
  # - branch-cleanup HITL is exhaustive (yes/no/cancel → done; missing answer
56
69
  # defense) and consent-only: it records the decision, performs no git op.
57
70
  # - Wrap-up never mutates task status (consumes completed work only).
@@ -59,7 +72,7 @@
59
72
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
60
73
  kind: state-machine
61
74
  name: wrapup-pipeline
62
- version: "1"
75
+ version: "3"
63
76
  description: "Post-execution wrap-up: doc-sync (doc drift + learning capture), metrics, feature-transition, branch-cleanup"
64
77
  iterationBound: 10
65
78
  initialState: start
@@ -99,54 +112,33 @@ states:
99
112
  - id: task-resolve
100
113
  description: >
101
114
  Validate the task list ONCE and evaluate closed proportional routing (0758
102
- R1-R4). Validation (0770): vars.tasks must be a JSON array of non-empty
103
- WBS strings, deduplicated in first-seen order, and every member must
104
- resolve via `spur task show` to a completed status (done/cancelled).
105
- Invalid input or an unresolved/non-completed task records FAIL (reason
106
- into the run-scoped reason file) and routes to `failed`; siblings consume
107
- the normalized run-scoped artifact .spur/run/<runId>-wrapup-tasks.json
108
- and never re-parse raw input. A validated empty list -> skipped;
109
- tasks > 0 && mode == fast -> fast-path (metrics-record, bypassing
110
- doc-sync); missing/unknown/conflict -> doc-sync (safety-path). Writes
111
- bounded machine-readable reason to reasonFile.
115
+ R1-R4). Validation (0770 + 0783): vars.tasks must be a JSON array of
116
+ canonical four-digit WBS strings non-arrays, non-strings, whitespace or
117
+ other shapes are rejected, not trimmed deduplicated in first-seen
118
+ order, with __runId present, and every member must resolve via `spur task
119
+ show` to a completed status (done/cancelled); failed or malformed
120
+ lookups fail. Invalid input or an unresolved/non-completed task records
121
+ FAIL (reason into the run-scoped reason file) and routes to `failed`;
122
+ the route writer and every guard, prompt and note after resolution read
123
+ the normalized run-scoped artifact
124
+ .spur/run/<runId>-wrapup-tasks.json and never re-parse raw input — a
125
+ missing or corrupted capture refuses progression. A validated empty list
126
+ -> skipped; length > 0 && mode == fast -> fast-path (metrics-record,
127
+ bypassing doc-sync); missing/unknown/conflict -> doc-sync (safety-path).
128
+ Writes bounded machine-readable reason to reasonFile.
112
129
  onEnter:
113
130
  - kind: note
114
131
  options:
115
132
  message: "Resolving task list: ${vars.tasks} (proportional mode: ${vars.mode})"
116
- # Run-id confinement (0758 R3). The run-scoped reason file is the only route artifact: a
117
- # second copy at the fixed path `.spur/run/wrapup-route-reason.txt` had no reader and was
118
- # overwritten by whichever run finished last, so a route claim read from it belonged to no
119
- # particular run. The log append carries the run id for the same reason — an unattributed
120
- # line is log scraping, which R5 rejects as evidence.
121
- - kind: shell
122
- options:
123
- command: >-
124
- mkdir -p .spur/run .spur/memory &&
125
- RUN_ID="$__runId" &&
126
- if [ -z "$RUN_ID" ]; then RUN_ID="wrapup"; fi &&
127
- REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
128
- if [ "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -eq 0 ]; then
129
- echo "skipped:empty task list" > "$REASON_FILE";
130
- elif [ "$mode" = "fast" ]; then
131
- echo "fast:evidence complete+consistent" > "$REASON_FILE";
132
- elif [ -z "$mode" ]; then
133
- echo "safety:missing evidence (mode empty)" > "$REASON_FILE";
134
- elif [ "$mode" = "unknown" ]; then
135
- echo "safety:unknown evidence quality" > "$REASON_FILE";
136
- elif [ "$mode" = "conflict" ]; then
137
- echo "safety:conflicting evidence" > "$REASON_FILE";
138
- else
139
- echo "safety:unrecognized evidence (mode=$mode)" > "$REASON_FILE";
140
- fi &&
141
- printf '%s %s\n' "$RUN_ID" "$(cat "$REASON_FILE")"
142
- >> .spur/memory/wrapup-routes.log &&
143
- exit 0
144
- # 0770 truthful input: parse and validate vars.tasks exactly once, here.
145
- # Malformed JSON, a non-array, a non-string or empty entry, an unresolved
146
- # task, or a task not at a completed status records FAIL (routed to
147
- # `failed` by the first declared edge) and is never re-interpreted as an
148
- # empty list by a later sibling guard. The normalized, deduplicated list
149
- # is persisted run-scoped; metrics/doc siblings only ever read it.
133
+ # 0770/0783 truthful input: parse and validate vars.tasks exactly once,
134
+ # here and only here. Malformed JSON, a non-array, a non-string entry,
135
+ # whitespace or a non-canonical WBS (anything but four digits), a missing
136
+ # __runId, an unresolved task, a failed or malformed lookup, or a task
137
+ # not at a completed status records FAIL (routed to `failed` by the first
138
+ # declared edge) and is never re-interpreted as an empty list by a later
139
+ # sibling guard. The normalized, deduplicated list is persisted
140
+ # run-scoped; the route writer, metrics, prompts, notes and guards only
141
+ # ever read that capture.
150
142
  - kind: shell
151
143
  options:
152
144
  command: >-
@@ -159,9 +151,9 @@ states:
159
151
  REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
160
152
  STATUS_FILE=".spur/run/$RUN_ID-wrapup-resolve.status" &&
161
153
  TASKS_FILE=".spur/run/$RUN_ID-wrapup-tasks.json" &&
162
- if ! printf '%s' "$tasks" | jq -e 'type == "array" and all(.[]; type == "string" and length > 0)' > /dev/null 2>&1; then
163
- echo "task-resolve: tasks must be a JSON array of non-empty WBS strings" >&2;
164
- echo "failed:tasks is not a JSON array of non-empty WBS strings" > "$REASON_FILE";
154
+ if ! printf '%s' "$tasks" | jq -e 'type == "array" and all(.[]; type == "string" and test("^[0-9]{4}$"))' > /dev/null 2>&1; then
155
+ echo "task-resolve: tasks must be a JSON array of canonical four-digit WBS strings (whitespace is rejected, not trimmed)" >&2;
156
+ echo "failed:tasks is not a JSON array of canonical four-digit WBS strings" > "$REASON_FILE";
165
157
  printf 'FAIL\n' > "$STATUS_FILE";
166
158
  else
167
159
  printf '%s' "$tasks" | jq -c 'reduce .[] as $w ([]; if any(.[]; . == $w) then . else . + [$w] end)' > "$TASKS_FILE";
@@ -184,6 +176,45 @@ states:
184
176
  fi;
185
177
  fi;
186
178
  exit 0
179
+ # Route reason writer (0758 R3/R4 run-attribution pins intact). Runs AFTER
180
+ # validation and reads ONLY the validated run-scoped capture — never raw
181
+ # vars.tasks (0783 R2/R5); the fixed-path fallback is gone because
182
+ # validation refuses an empty __runId above. A FAIL resolve exits without
183
+ # writing: the failed reason written by validation stands, and the skip
184
+ # reason can never mask it. A missing/corrupted capture yields N=-1 and
185
+ # no skipped claim; the failed defense edge owns the run.
186
+ - kind: shell
187
+ options:
188
+ command: >-
189
+ mkdir -p .spur/run .spur/memory &&
190
+ RUN_ID="$__runId" &&
191
+ if [ -z "$RUN_ID" ]; then
192
+ echo "task-resolve: __runId is empty — refusing to write a route reason" >&2;
193
+ exit 1;
194
+ fi &&
195
+ REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
196
+ STATUS_FILE=".spur/run/$RUN_ID-wrapup-resolve.status" &&
197
+ TASKS_FILE=".spur/run/$RUN_ID-wrapup-tasks.json" &&
198
+ if [ "$(cat "$STATUS_FILE" 2>/dev/null)" = FAIL ]; then
199
+ exit 0;
200
+ fi &&
201
+ N=$(jq length "$TASKS_FILE" 2>/dev/null || echo -1) &&
202
+ if [ "$N" -eq 0 ]; then
203
+ echo "skipped:empty task list" > "$REASON_FILE";
204
+ elif [ "$mode" = "fast" ]; then
205
+ echo "fast:evidence complete+consistent" > "$REASON_FILE";
206
+ elif [ -z "$mode" ]; then
207
+ echo "safety:missing evidence (mode empty)" > "$REASON_FILE";
208
+ elif [ "$mode" = "unknown" ]; then
209
+ echo "safety:unknown evidence quality" > "$REASON_FILE";
210
+ elif [ "$mode" = "conflict" ]; then
211
+ echo "safety:conflicting evidence" > "$REASON_FILE";
212
+ else
213
+ echo "safety:unrecognized evidence (mode=$mode)" > "$REASON_FILE";
214
+ fi &&
215
+ printf '%s %s\n' "$RUN_ID" "$(cat "$REASON_FILE")"
216
+ >> .spur/memory/wrapup-routes.log &&
217
+ exit 0
187
218
 
188
219
  - id: doc-sync
189
220
  description: >
@@ -200,9 +231,11 @@ states:
200
231
  options:
201
232
  agent: ${vars.agent}
202
233
  input: >-
203
- First run Skill(skill="sp:doc-evolve", args="wrapup tasks=${vars.tasks}") to repair drift in
234
+ First run Skill(skill="sp:doc-evolve", args="wrapup") to repair drift in
204
235
  docs/00_ADR.md, docs/03_ARCHITECTURE.md, docs/04_DESIGN.md, docs/design/* per constitution edit
205
- rules; do not write task/feature corpus. THEN extract working learnings from tasks ${vars.tasks}
236
+ rules; do not write task/feature corpus. The batch task list is the normalized, validated
237
+ capture at .spur/run/${vars.__runId}-wrapup-tasks.json — read it for the WBS ids; never
238
+ re-parse raw input. THEN extract working learnings from those tasks —
206
239
  conventions, errors fixed, patterns, gotchas, grouped by date and task WBS — as raw markdown
207
240
  (no fences) and END your final message with that markdown (it is captured to the
208
241
  run-scoped .spur/run/${vars.__runId}-wrapup-learnings.md).
@@ -228,9 +261,14 @@ states:
228
261
  - id: metrics-record
229
262
  description: >
230
263
  Append one JSONL row per task to .spur/memory/wrapup-metrics.jsonl deterministically.
231
- Reads ONLY the normalized run-scoped task list written by task-resolve (0770):
232
- a lookup failure for any member records FAIL and routes to `failed` — a
233
- missing metrics row is never silently absorbed as success.
264
+ Revalidates the normalized run-scoped capture from task-resolve (0770 + 0783):
265
+ a missing, corrupted or non-canonical capture records FAIL; every member must
266
+ produce a successful, well-shaped `task show` lookup (a missing metrics row is
267
+ never silently absorbed as success); rows are serialized with jq — never
268
+ interpolated printf JSON — so escaped fields stay parseable; and PASS is
269
+ written only after every required append succeeds. A missing per-task verdict
270
+ is UNKNOWN telemetry, never proof of completion; previously written valid rows
271
+ survive a failure.
234
272
  onEnter:
235
273
  - kind: shell
236
274
  options:
@@ -239,63 +277,69 @@ states:
239
277
  STATUS_FILE=".spur/run/$__runId-wrapup-metrics.status" &&
240
278
  TASKS_FILE=".spur/run/$__runId-wrapup-tasks.json" &&
241
279
  METRICS_RC=0 &&
242
- for wbs in $(jq -r '.[]' "$TASKS_FILE" 2>/dev/null); do
243
- task_json=$($spurBin task show "$wbs" --json 2>/dev/null || true);
244
- if [ -n "$task_json" ]; then
245
- feat=$(printf '%s' "$task_json" | jq -r '.frontmatter.feature_id // .feature_id // empty' 2>/dev/null || true);
246
- stat=$(printf '%s' "$task_json" | jq -r '.frontmatter.status // .status // "unknown"' 2>/dev/null || true);
247
- verdict="UNKNOWN";
248
- if [ -f ".spur/run/$wbs-verdict.json" ]; then
249
- v=$(jq -r '.verdict // "UNKNOWN"' ".spur/run/$wbs-verdict.json" 2>/dev/null || true);
250
- if [ -n "$v" ]; then verdict="$v"; fi;
280
+ if ! jq -e 'type == "array" and all(.[]; type == "string" and test("^[0-9]{4}$"))' "$TASKS_FILE" > /dev/null 2>&1; then
281
+ echo "metrics-record: run-scoped task capture missing, corrupted or non-canonical — refusing to record metrics" >&2;
282
+ printf 'FAIL\n' > "$STATUS_FILE";
283
+ else
284
+ for wbs in $(jq -r '.[]' "$TASKS_FILE"); do
285
+ task_json=$($spurBin task show "$wbs" --json 2>/dev/null || true);
286
+ if printf '%s' "$task_json" | jq -e '(.frontmatter.status // .status) != null' > /dev/null 2>&1; then
287
+ feat=$(printf '%s' "$task_json" | jq -r '.frontmatter.feature_id // .feature_id // ""');
288
+ stat=$(printf '%s' "$task_json" | jq -r '.frontmatter.status // .status // "unknown"');
289
+ verdict="UNKNOWN";
290
+ if [ -f ".spur/run/$wbs-verdict.json" ]; then
291
+ v=$(jq -r '.verdict // "UNKNOWN"' ".spur/run/$wbs-verdict.json" 2>/dev/null || true);
292
+ if [ -n "$v" ]; then verdict="$v"; fi;
293
+ fi;
294
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ");
295
+ if ! jq -cn --arg wbs "$wbs" --arg feature_id "$feat" --arg status "$stat" --arg verdict "$verdict" --arg timestamp "$ts" '{wbs:$wbs, feature_id:$feature_id, status:$status, verdict:$verdict, timestamp:$timestamp}' >> .spur/memory/wrapup-metrics.jsonl; then
296
+ echo "metrics-record: metrics append failed for task $wbs — recording FAIL instead of claiming the row landed" >&2;
297
+ METRICS_RC=1;
298
+ fi;
299
+ else
300
+ echo "metrics-record: task $wbs lookup failed or was malformed — recording FAIL instead of silently omitting its metrics row" >&2;
301
+ METRICS_RC=1;
251
302
  fi;
252
- ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ");
253
- printf '{"wbs":"%s","feature_id":"%s","status":"%s","verdict":"%s","timestamp":"%s"}\n' "$wbs" "$feat" "$stat" "$verdict" "$ts" >> .spur/memory/wrapup-metrics.jsonl;
254
- else
255
- echo "metrics-record: task $wbs did not resolve — recording FAIL instead of silently omitting its metrics row" >&2;
256
- METRICS_RC=1;
257
- fi;
258
- done;
259
- if [ "$METRICS_RC" -eq 0 ]; then printf 'PASS\n' > "$STATUS_FILE"; else printf 'FAIL\n' > "$STATUS_FILE"; fi;
303
+ done;
304
+ if [ "$METRICS_RC" -eq 0 ]; then printf 'PASS\n' > "$STATUS_FILE"; else printf 'FAIL\n' > "$STATUS_FILE"; fi;
305
+ fi;
260
306
  exit 0
261
307
 
262
308
  - id: feature-transition
263
309
  description: >
264
310
  If vars.feature is set, sync feature status via bounded feature-sync-bounded
265
- (0411). Soft shell the run is never aborted after learnings/metrics
266
- landed; instead (0770) a failed required sync or a failed feature gate
267
- records FAIL in the run-scoped sync status and routes to `failed`, and a
268
- successful sync that applied no transition is reported as an explicit
269
- no-change. Empty vars.feature is NOT a blocked sync but a mis-invocation
311
+ (0411). The shell terminates `exit 0` so artifacts already landed are never
312
+ discarded, but the outcome is truthful (0770 + 0783 R4): sync succeeds only
313
+ with a valid proposal matching vars.feature, no gateBlocked/requiresConfirm
314
+ condition, and a freshly observed feature status equal to proposal.to;
315
+ applied:false is a successful explicit no-change only when from == to and
316
+ that status is observed. A nonzero, malformed, partial, blocked or
317
+ unreadable outcome records FAIL in the run-scoped sync status and routes
318
+ to `failed` — an affected-feature check cannot convert a failed sync into
319
+ success. Empty vars.feature is NOT a blocked sync but a mis-invocation
270
320
  (the engine only enters this state when vars.feature is set) and fails
271
- loud (dogfood 2026-08-15, feature I3: a silent exit 0 made the wrap look
272
- complete with no transition).
321
+ loud (dogfood 2026-08-15, feature I3).
273
322
  onEnter:
274
- # Genuinely soft: the terminating `exit 0` must be reached on BOTH
275
- # branches, so
276
- # the sync is chained with `;` with `&&` a non-zero sync skipped `exit
277
- # 0` and
278
- # aborted the run at this state, discarding the learnings/metrics that
279
- # already
280
- # landed and never reaching branch-cleanup/done (the exact failure the
281
- # state
282
- # description promises against). `feature-sync-bounded.ts` is a
283
- # Spur-monorepo
284
- # path (`spur init` does not scaffold `plugins/sp/`), so seeded projects
285
- # fall
286
- # back to the plain `spur feature sync` verb.
323
+ # The terminating `exit 0` must be reached on every branch — the sync is
324
+ # chained with `;`, because `&&` would abort the state, discarding the
325
+ # learnings/metrics that already landed. Soft TERMINATION is not soft
326
+ # TRUTH: the result is classified per 0783 R4 and only a verified sync
327
+ # writes PASS. `feature-sync-bounded.ts` is a Spur-monorepo path (`spur
328
+ # init` does not scaffold `plugins/sp/`), so seeded projects fall back to
329
+ # the plain `spur feature sync` verb.
287
330
  # R1 (0625): a feature transition that changed state may have left
288
- # cross-task fallout the per-task fast gates never see. Run the
289
- # affected-feature gate (featureGateCmd) after an
290
- # applied sync OR a non-zero exit that may follow a partial multi-hop
291
- # transition, then report PASS/FAIL. Soft by design: the operator
292
- # decides, and wrap-up never hard-fails here.
331
+ # cross-task fallout the per-task fast gates never see. The
332
+ # affected-feature gate (featureGateCmd) runs after an applied sync OR a
333
+ # non-zero exit that may follow a partial multi-hop transition. It is a
334
+ # diagnostic only: a gate PASS can never convert a failed sync into
335
+ # success, and a gate FAIL fails the step (0770 failure routing).
293
336
  # NOTE: do not add
294
337
  # `#` comments INSIDE the folded shell string — YAML `>-` folds them onto
295
338
  # the following statement line and comments it out.
296
339
  - kind: shell
297
340
  options:
298
341
  command: >-
342
+ mkdir -p .spur/run &&
299
343
  if [ -z "$feature" ]; then
300
344
  echo "feature-transition: vars.feature is empty — refusing no-op feature sync (mis-invocation, not a blocked sync)" >&2;
301
345
  exit 1;
@@ -309,10 +353,32 @@ states:
309
353
  fi);
310
354
  SYNC_RC=$?;
311
355
  printf '%s\n' "$SYNC_OUTPUT";
312
- if printf '%s' "$SYNC_OUTPUT" | jq -e 'has("applied")' > /dev/null 2>&1; then
313
- APPLIED=$(printf '%s' "$SYNC_OUTPUT" | jq -r '.applied // false' 2>/dev/null || echo false);
356
+ OBSERVED=$($spurBin feature show "$feature" --json 2>/dev/null | jq -r '.status // .frontmatter.status // empty' 2>/dev/null || true);
357
+ if [ -z "$OBSERVED" ]; then OBSERVED=unreadable; fi;
358
+ APPLIED=unreadable;
359
+ SYNC_OK=0;
360
+ REASON="";
361
+ if [ "$SYNC_RC" -ne 0 ]; then
362
+ REASON="sync exited nonzero (rc=$SYNC_RC)";
363
+ elif ! printf '%s' "$SYNC_OUTPUT" | jq -e 'type == "object" and (.proposal | type == "object") and (.proposal.featureId | type == "string") and (.proposal.from | type == "string") and (.proposal.to | type == "string") and (.applied | type == "boolean")' > /dev/null 2>&1; then
364
+ REASON="malformed or unreadable sync result";
314
365
  else
315
- APPLIED="invalid";
366
+ P_FROM=$(printf '%s' "$SYNC_OUTPUT" | jq -r '.proposal.from // ""');
367
+ P_TO=$(printf '%s' "$SYNC_OUTPUT" | jq -r '.proposal.to // ""');
368
+ APPLIED=$(printf '%s' "$SYNC_OUTPUT" | jq -r '.applied');
369
+ if [ "$(printf '%s' "$SYNC_OUTPUT" | jq -r '.proposal.featureId // ""')" != "$feature" ]; then
370
+ REASON="sync proposal does not match feature $feature";
371
+ elif [ "$(printf '%s' "$SYNC_OUTPUT" | jq -r '.proposal.gateBlocked // false')" = "true" ]; then
372
+ REASON="sync proposal is gate-blocked — a blocked sync is not a no-change success";
373
+ elif [ "$(printf '%s' "$SYNC_OUTPUT" | jq -r '.proposal.requiresConfirm // false')" = "true" ]; then
374
+ REASON="sync proposal requires operator confirmation";
375
+ elif [ "$APPLIED" = "true" ] && [ "$OBSERVED" != "$P_TO" ]; then
376
+ REASON="applied sync did not land on the proposal target (observed=$OBSERVED, to=$P_TO)";
377
+ elif [ "$APPLIED" = "false" ] && { [ "$P_FROM" != "$P_TO" ] || [ "$OBSERVED" != "$P_TO" ]; }; then
378
+ REASON="sync applied nothing without a from==to observed no-op (from=$P_FROM, to=$P_TO, observed=$OBSERVED)";
379
+ else
380
+ SYNC_OK=1;
381
+ fi;
316
382
  fi;
317
383
  GATE="skipped";
318
384
  if [ "$APPLIED" = "true" ] || [ "$SYNC_RC" -ne 0 ]; then
@@ -325,10 +391,17 @@ states:
325
391
  echo "feature-transition: feature gate FAIL for feature $feature — inspect findings before reporting the transition complete" >&2;
326
392
  fi;
327
393
  else
328
- echo "feature-transition: sync did not apply a transition (rc=$SYNC_RC, applied=$APPLIED) — feature gate skipped (explicit no-change)";
394
+ echo "feature-transition: sync did not apply a transition (rc=$SYNC_RC, applied=$APPLIED) — feature gate skipped";
329
395
  fi;
330
396
  SYNC_STATUS="PASS";
331
- if [ "$SYNC_RC" -ne 0 ] || [ "$GATE" = "FAIL" ] || [ "$APPLIED" = "invalid" ]; then SYNC_STATUS="FAIL"; fi;
397
+ if [ "$SYNC_OK" -ne 1 ] || [ "$GATE" = "FAIL" ]; then
398
+ SYNC_STATUS="FAIL";
399
+ echo "feature-transition: required synchronization failed for $feature — $REASON; gate=$GATE" >&2;
400
+ elif [ "$APPLIED" = "false" ]; then
401
+ echo "feature-transition: feature sync verified for $feature (from==to observed at $OBSERVED, gate=$GATE) — explicit no-change";
402
+ else
403
+ echo "feature-transition: feature sync verified for $feature (applied, observed=$OBSERVED, gate=$GATE)";
404
+ fi;
332
405
  if [ "$SYNC_STATUS" = "PASS" ]; then
333
406
  printf 'PASS\n' > ".spur/run/$__runId-wrapup-sync.status";
334
407
  else
@@ -348,7 +421,7 @@ states:
348
421
  onEnter:
349
422
  - kind: hitl.confirm
350
423
  options:
351
- prompt: "Branch cleanup for tasks ${vars.tasks}. This is IRREVERSIBLE (merge or delete). Confirm to proceed?"
424
+ prompt: "Branch cleanup for the validated task list (.spur/run/${vars.__runId}-wrapup-tasks.json). This is IRREVERSIBLE (merge or delete). Confirm to proceed?"
352
425
 
353
426
  - id: done
354
427
  description: >
@@ -358,12 +431,7 @@ states:
358
431
  onEnter:
359
432
  - kind: note
360
433
  options:
361
- message: "Wrap-up pipeline complete for tasks: ${vars.tasks}. Learnings at .spur/memory/learnings.md, metrics at .spur/memory/wrapup-metrics.jsonl. Branch cleanup: consent recorded only (merge=${vars.merge}); no git operation performed by wrap-up."
362
- # Checkpoint write: record session state for resume (Phase 4, task 0171
363
- # R3)
364
- - kind: shell
365
- options:
366
- command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: wrapup-pipeline done tasks=$tasks ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/wrapup-checkpoint.md'
434
+ message: "Wrap-up pipeline complete for the validated task list at .spur/run/${vars.__runId}-wrapup-tasks.json. Learnings at .spur/memory/learnings.md, metrics at .spur/memory/wrapup-metrics.jsonl. Branch cleanup: consent recorded only (merge=${vars.merge}); no git operation performed by wrap-up."
367
435
 
368
436
  - id: skipped
369
437
  description: Terminal — wrap-up skipped (empty task list or operator abort).
@@ -394,27 +462,31 @@ transitions:
394
462
  kind: shell
395
463
  options:
396
464
  command: 'test "$(cat .spur/run/$__runId-wrapup-resolve.status 2>/dev/null)" = FAIL'
465
+ # Route edges key on the validated run-scoped capture, never raw vars.tasks
466
+ # (0783 R2). A missing or corrupted capture yields -1, which satisfies none
467
+ # of the numeric edges, so the run falls through to the always-defense
468
+ # `failed` edge instead of claiming a skip or a route.
397
469
  - from: task-resolve
398
470
  to: skipped
399
- description: Task list is empty — skip wrap-up.
471
+ description: Validated task list is empty — skip wrap-up (only a validated [] may skip).
400
472
  guard:
401
473
  kind: shell
402
474
  options:
403
- command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -eq 0'
475
+ command: 'test "$(jq length .spur/run/$__runId-wrapup-tasks.json 2>/dev/null || echo -1)" -eq 0'
404
476
  - from: task-resolve
405
477
  to: metrics-record
406
478
  description: Proportional fast path — complete and consistent evidence bypasses doc-sync.
407
479
  guard:
408
480
  kind: shell
409
481
  options:
410
- command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -gt 0 && test "$mode" = fast'
482
+ command: 'test "$(jq length .spur/run/$__runId-wrapup-tasks.json 2>/dev/null || echo -1)" -gt 0 && test "$mode" = fast'
411
483
  - from: task-resolve
412
484
  to: doc-sync
413
485
  description: Proportional safety path — missing/unknown/conflicting evidence routes to full doc-sync.
414
486
  guard:
415
487
  kind: shell
416
488
  options:
417
- command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -gt 0 && test "$mode" != fast'
489
+ command: 'test "$(jq length .spur/run/$__runId-wrapup-tasks.json 2>/dev/null || echo -1)" -gt 0 && test "$mode" != fast'
418
490
  - from: task-resolve
419
491
  to: failed
420
492
  description: >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.74",
3
+ "version": "0.3.75",
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",
@@ -46,8 +46,10 @@ CLI nouns for direct use, but they are not this corpus specialist's scope.
46
46
  3. Run the noun's read/check/validate path before mutation where available.
47
47
  4. Mutate only through `spur`; parse `--json` output when the verb advertises it.
48
48
  5. Inspect each result before the next dependent operation; stop on structural or validation failure.
49
- 6. Run the scoped check/validate/refresh path after mutation. After task/feature batch writes, run
50
- `spur task check --corpus --json` once.
49
+ 6. Run affected-input checks after mutation (constitution T11): after task/feature batch writes,
50
+ run `spur task check <wbs>` / `spur feature check <id>` for each changed document and its
51
+ required linked evidence — not a corpus sweep. The explicit unsuppressed audit
52
+ (`spur task check --corpus --json`) is reserved for checker-policy changes (T10).
51
53
 
52
54
  Workflow fit, mode selection, simplicity budgets, authoring, and tuning live in the workflow
53
55
  references under `plugins/sp/skills/spur-cli/references/workflows/`; load them rather than copying
@@ -59,7 +61,7 @@ their runbook here.
59
61
 
60
62
  - Use the source-local CLI when working in the Spur repository.
61
63
  - Use `spur task update --section --from-file` for task section writes.
62
- - Keep check-before/write/check-after evidence and the final scoped refresh result.
64
+ - Keep check-before/write/check-after evidence and the final scoped validation result.
63
65
  - Preserve declaration order and currently executing runs when changing workflows.
64
66
 
65
67
  ### Never
@@ -87,7 +89,8 @@ their runbook here.
87
89
  ### Gates
88
90
  - pre-check: <result>
89
91
  - post-check/validate: <result>
90
- - refresh/corpus sweep: <result or n/a>
92
+ - scoped validation: <affected task/feature checks + linked evidence, or n/a; explicit T10
93
+ corpus audit only when checker policy changed>
91
94
  ```
92
95
 
93
96
  ## Platform Notes
@@ -46,4 +46,4 @@ vars as subsets of `--approve-taste` (`idea_approved` / `design_approved`). Pref
46
46
  - `auto`/name: launch `spur workflow run idea-pipeline.yaml --async`, observe with one `workflow trace --follow`, and only report cancellation as stopped when `workflow cancel --json` returns `killed: true`.
47
47
  - `Skill(skill="sp:spur-dev", args="idea $ARGUMENTS")`
48
48
  - Stage contract (discovery → idea-eval → feature-create → AC → feature-check → system-design →
49
- decompose → batch-create → handoff): `plugins/sp/skills/spur-dev/references/dev-operations.md` § idea.
49
+ decompose → batch-create → ready-prepare → handoff): `plugins/sp/skills/spur-dev/references/dev-operations.md` § idea.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.74",
3
+ "version": "0.3.75",
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"]
@@ -187,7 +187,9 @@ spur task record 0040 --verdict-file .spur/run/0040-verdict.json --solution-from
187
187
  - **`--solution-from-diff`** backfills `Solution` from `git diff -U0` hunk headers **only when the
188
188
  Solution section is still bare** — a safety net, not an overwrite.
189
189
  - **`--transition <status>`** optionally advances the lifecycle after writing (e.g. `testing`).
190
- `record` **never transitions to `done`** the `testing→done` gate stays in the workflow (0108).
190
+ `--transition done` is a **guarded** completion path: it requires a PASS verdict, auto-walks
191
+ `wip → testing → done`, and auto-creates the pipeline run-link (0436 R4); a non-PASS verdict to
192
+ `done` errors instead of transitioning, and the normal done gates still apply (0108).
191
193
 
192
194
  The verdict shape (`wbs`, `verdict`, `requirements[]`, `checks[]`) and the rendered tables are
193
195
  documented in [tasks/verbs.md](tasks/verbs.md).
@@ -652,21 +652,35 @@ Field semantics (enforced by `parseCheckpointMetadata` / `checkpointStaleness`):
652
652
  up once retention expires.
653
653
  - `source_commit` pins repository HEAD at write time; drift staleness is reported, not hidden.
654
654
  - `artifacts` lists referenced run files; a missing artifact makes the checkpoint stale.
655
- - Writers: write after every HITL gate decision, phase transition, and terminal state; overwrite
656
- the same file on resume (`session_id` = `<date>-<wbs-or-feature>`).
655
+
656
+ **Writer cadence (0784 R4).** There is exactly one canonical writer: the `task-pipeline` done
657
+ state's terminal checkpoint (`status: done`, real HEAD, run id from `$__runId`, and `$wbs`-expanded
658
+ artifact paths). It is a plain `shell` step — checkpoints are working memory, not CLI-gated corpus.
659
+ The `feature-dev`, `wrapup-pipeline`, and `idea-pipeline` pipelines used to echo pseudo-checkpoints
660
+ ("checkpoint: <workflow> done ...") that violated the canonical schema; those writers were removed
661
+ in 0784 — the persisted run row is the authoritative terminal record, and a non-canonical echo
662
+ cannot be resumed, routed, or reclaimed safely.
663
+
664
+ **Engine state vs checkpoint status (0784 R3).** The persisted run row's status is authoritative;
665
+ a checkpoint's `status` is an advisory projection written by consumers. On resume
666
+ (`workflow continue`), a checkpoint associated with the paused run must project a nonterminal
667
+ engine state — `pending`, `running`, or `approved` are accepted; `done`/`failed`/`cancelled`/
668
+ `skipped`, a missing status, or any unknown value refuse the resume with a named reason. The
669
+ engine owns no persisted `paused` checkpoint status.
657
670
 
658
671
  **Write checkpoints after:**
659
672
 
660
- - Every HITL gate decision (approved/rejected/deferred).
661
- - Every phase transition in `task-pipeline`, `feature-dev`, `idea-pipeline`, and
662
- `wrapup-pipeline`.
663
- - Every terminal state (`done`, `failed`, `cancelled`, `skipped`).
673
+ - Terminal task completion in `task-pipeline` (the canonical writer above).
674
+ - Any additional consumer-specific checkpoints you genuinely need for local resume; keep the
675
+ canonical schema — non-canonical files are ignored by routing and cleanup.
664
676
 
665
677
  **Read checkpoints when:**
666
678
 
667
679
  - `/sp:dev-run --continue` or `/sp:dev-runall --continue` is used.
668
680
  - The operator asks to resume a task or feature.
669
- - A workflow run is paused and later continued (`spur workflow continue <run-id>`).
681
+ - A workflow run is paused and later continued (`spur workflow continue <run-id>`). Resume
682
+ validates freshness in the run's recorded launch workdir (0784 R1/R3): artifacts resolve relative
683
+ to that workdir, and git HEAD is probed there — never in the ambient process cwd.
670
684
 
671
685
  **Rules:**
672
686
 
@@ -678,7 +692,8 @@ Field semantics (enforced by `parseCheckpointMetadata` / `checkpointStaleness`):
678
692
  - **Retention.** Terminal checkpoints older than `workflowLogRetentionDays` with no active run are
679
693
  reclaimed by `spur workflow clean` (task 0711 R5–R8); non-terminal checkpoints are always kept.
680
694
  - **Not a validated corpus.** Checkpoints are working memory. They are overwritten when a session
681
- resumes and re-checkpoints. They are NOT authoritative task state — the task file is.
695
+ resumes and re-checkpoints. They are NOT authoritative task state — the task file and the
696
+ persisted run row are.
682
697
  - **One file per session.** The `session_id` is `<date>-<wbs-or-feature>`. A resumed session
683
698
  overwrites the same file.
684
699
  - **Operator-readable.** The YAML frontmatter is machine-parseable; the body is free-form markdown
@@ -341,7 +341,7 @@ must not be changed without updating the backing skill.
341
341
  - `--approve-taste` — with `--auto`, skip **all** remaining taste pauses this run (idea-eval + design-approval). Sets `idea_approved=true` and `design_approved=true`.
342
342
  Aliases (prefer `--approve-taste`): `--idea-approved` → `idea_approved`; `--design-approved` → `design_approved`. There is **no** `--design` force flag.
343
343
  - **Backing:** `idea-pipeline.yaml` through the inline driver for omitted/`inline`, or `spur workflow run idea-pipeline.yaml --async` for `auto`/name.
344
- - **Behavior:** Builds vars from the table above and drives the idea pipeline. Flow: discovery → **idea-eval** (taste; reject → cancelled) → feature-create → ac-generate → feature-check → system-design (conditional) → design-approval (taste) → decompose → batch-create → handoff. STOPS at handoff — no task execution, no pipeline nesting. Headless runs use one `trace --follow`; cancellation is reported stopped only when `workflow cancel --json` returns `killed: true`.
344
+ - **Behavior:** Builds vars from the table above and drives the idea pipeline. Flow: discovery → **idea-eval** (taste; reject → cancelled) → feature-create → ac-generate → feature-check → system-design (conditional) → design-approval (taste) → decompose → batch-create (`--skip-ready`) ready-prepare (ready checklist per created task + ready-evidence sidecar, 0788) → handoff. STOPS at handoff — no task execution, no pipeline nesting. Headless runs use one `trace --follow`; cancellation is reported stopped only when `workflow cancel --json` returns `killed: true`.
345
345
  - **Delegation:** Host-session inline driver by default; explicit executor selection uses the async workflow worker.
346
346
  - **Idea-evaluation gate:** After discovery, operator reviews `.spur/run/idea-eval-report.md` ([idea-evaluation.md](idea-evaluation.md)). Approve continues; reject/cancel → no feature. Under `--auto`, still pauses unless taste pre-cleared (`--approve-taste` / alias). Enhanced idea is a sidecar — `vars.idea` is not overwritten.
347
347
  - **Design package (`--skip-design` only):**
@@ -149,7 +149,10 @@ passed, so provenance denied first and Review L3 denied on the retry.
149
149
 
150
150
  | # | Gate layer | Triggers denial when | Remediation |
151
151
  |---|------------|----------------------|-------------|
152
- | 1 | **Strict-core + verdict artifact** (`spur task check <wbs> --strict-core` + `done-transition-guard.ts`) | The strict-core check fails, or `.spur/run/<wbs>-verdict.json` is **missing** or has a non-PASS aggregate. **Missing artifact is a deny** (not a silent allow — closes the 0349 "done without verdict" class). The aggregate is recomputed from requirement/AC rows; the harsher of stored and computed wins. | Re-run `/sp:dev-verify <wbs>` until PASS (writes the artifact), or explicitly override with `spur task update <wbs> done --force-done --reason "<why>"`. Docs-only pipelines write a docs PASS stub under `.spur/run/` before `done` (see `docs-pipeline.yaml`). |
152
+ | 1 | **Strict-core + verdict artifact** (`spur task check <wbs> --strict-core` + `done-transition-guard.ts`) | The strict-core check fails, or `.spur/run/<wbs>-verdict.json` is **missing** or has a non-PASS aggregate. **Missing artifact is a deny** (not a silent allow — closes the 0349 "done without verdict" class). The aggregate is recomputed from requirement/AC rows; the harsher of stored and computed wins. | Re-run `/sp:dev-verify <wbs>` until PASS (writes the artifact), or explicitly override with `spur task update <wbs> done --force-done --reason "<why>"`. Docs-only pipelines (`docs-pipeline.yaml`) meet the same layer: read-only measured verification
153
+ (answer file + `spur task verdict`) writes the standard `.spur/run/<wbs>-verdict.json` artifact
154
+ under proof-input digest bracketing; missing or non-PASS evidence is a refusal, never a synthetic
155
+ PASS stub. |
153
156
  | 2 | **Provenance guard** (`lifecycle-adapter.ts`) | No pipeline-kind run link exists for `<wbs>`. | Run `/sp:dev-run <wbs>` through the full pipeline, use `/sp:dev-run <wbs> --mode implement --auto --next` for the explicit step chain, or record the audited bypass with `SPUR_PROVENANCE_OVERRIDE=1`. |
154
157
  | 3 | **Review L3** (`task-check.ts`) | `### Review` is empty, placeholder-only, or lacks a populated P1–P4 findings table. | Run `/sp:dev-review <wbs>`; verify cannot write Review because of the Step 10 prohibition above. |
155
158