@gobing-ai/spur 0.3.74 → 0.3.76

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 (78) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +6 -0
  3. package/config/rules/README.md +1 -1
  4. package/config/rules/strict/runtime-boundaries.yaml +1 -0
  5. package/config/rules/typescript/no-eslint-suppressions.yaml +30 -0
  6. package/config/workflows/docs-pipeline.yaml +26 -1
  7. package/config/workflows/feature-dev.yaml +143 -102
  8. package/config/workflows/idea-pipeline.yaml +58 -11
  9. package/config/workflows/task-pipeline.yaml +76 -40
  10. package/config/workflows/wrapup-pipeline.yaml +183 -111
  11. package/package.json +9 -9
  12. package/plugins/sp/agents/expert-spur.md +7 -4
  13. package/plugins/sp/commands/dev-idea.md +1 -1
  14. package/plugins/sp/commands/dev-refineall.md +2 -2
  15. package/plugins/sp/plugin.json +1 -1
  16. package/plugins/sp/skills/dogfood-testing/SKILL.md +7 -4
  17. package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +9 -3
  18. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +5 -3
  19. package/plugins/sp/skills/spur-cli/references/tasks.md +3 -1
  20. package/plugins/sp/skills/spur-cli/references/workflows.md +3 -3
  21. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +23 -8
  22. package/plugins/sp/skills/spur-dev/references/dev-operations.md +3 -3
  23. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +4 -1
  24. package/schemas/spur-config.schema.json +4 -0
  25. package/spur.js +3675 -838
  26. package/web/_astro/{BoardApp.C60RJZRj.js → BoardApp.CHQ1lycZ.js} +92 -92
  27. package/web/_astro/BoardApp.DV9kx0wo.js +1 -0
  28. package/web/_astro/{TaskDetail.Guk8VuNu.js → TaskDetail.GKfQJ60c.js} +1 -1
  29. package/web/_astro/{arc.CAZtlWJM.js → arc.DWEtA3Tx.js} +1 -1
  30. package/web/_astro/{architectureDiagram-3BPJPVTR.9-XbF_Tz.js → architectureDiagram-3BPJPVTR.DB42oWmP.js} +1 -1
  31. package/web/_astro/{blockDiagram-GPEHLZMM.DC4WLx3L.js → blockDiagram-GPEHLZMM.rhv-zNQV.js} +1 -1
  32. package/web/_astro/{c4Diagram-AAUBKEIU.JgjUQMgG.js → c4Diagram-AAUBKEIU.Ci4-4VvY.js} +1 -1
  33. package/web/_astro/channel.BAI6xLeV.js +1 -0
  34. package/web/_astro/{chunk-2J33WTMH.D_JFiXa-.js → chunk-2J33WTMH.Cc9veUgf.js} +1 -1
  35. package/web/_astro/{chunk-4BX2VUAB.Bk_RxoeT.js → chunk-4BX2VUAB.Bec9c4eI.js} +1 -1
  36. package/web/_astro/{chunk-55IACEB6.a_Rj2VxL.js → chunk-55IACEB6.DoV8S1iB.js} +1 -1
  37. package/web/_astro/{chunk-727SXJPM.C2NoS1U6.js → chunk-727SXJPM.DwR-Qlyj.js} +1 -1
  38. package/web/_astro/{chunk-AQP2D5EJ.D1Xn6CNa.js → chunk-AQP2D5EJ.ND_a81WY.js} +1 -1
  39. package/web/_astro/{chunk-FMBD7UC4.BhJrSBsX.js → chunk-FMBD7UC4.Wv_jwG48.js} +1 -1
  40. package/web/_astro/{chunk-ND2GUHAM.ByVGduYQ.js → chunk-ND2GUHAM.CXKXCMmp.js} +1 -1
  41. package/web/_astro/{chunk-QZHKN3VN.B47Paunq.js → chunk-QZHKN3VN.nkaoNYQq.js} +1 -1
  42. package/web/_astro/{classDiagram-4FO5ZUOK.BERMSD5C.js → classDiagram-4FO5ZUOK.cMQcVlQu.js} +1 -1
  43. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BERMSD5C.js → classDiagram-v2-Q7XG4LA2.cMQcVlQu.js} +1 -1
  44. package/web/_astro/{cose-bilkent-S5V4N54A.O_rHGlhU.js → cose-bilkent-S5V4N54A.OaDJ7Mr2.js} +1 -1
  45. package/web/_astro/{cynefin-OW5HDTMX.qCW2GdNp.js → cynefin-OW5HDTMX.Chi8IphF.js} +1 -1
  46. package/web/_astro/{dagre-BM42HDAG.C5Y4lH_u.js → dagre-BM42HDAG.CzK2t_Fp.js} +1 -1
  47. package/web/_astro/{diagram-2AECGRRQ.DxZmRXxF.js → diagram-2AECGRRQ.DRvxlVS7.js} +1 -1
  48. package/web/_astro/{diagram-5GNKFQAL.qSxZeYSK.js → diagram-5GNKFQAL.CnYvNdwA.js} +1 -1
  49. package/web/_astro/{diagram-KO2AKTUF.-7vl3bXs.js → diagram-KO2AKTUF.CpLpMw5R.js} +1 -1
  50. package/web/_astro/{diagram-LMA3HP47.BHVV-UH3.js → diagram-LMA3HP47.JTb78qUA.js} +1 -1
  51. package/web/_astro/{diagram-OG6HWLK6.DHAuW9sK.js → diagram-OG6HWLK6.Bk-1jDIb.js} +1 -1
  52. package/web/_astro/{erDiagram-TEJ5UH35.DMRLQwPN.js → erDiagram-TEJ5UH35.D8hN9GZq.js} +1 -1
  53. package/web/_astro/{flowDiagram-I6XJVG4X.B3EpPp_8.js → flowDiagram-I6XJVG4X.-6zQr6m5.js} +1 -1
  54. package/web/_astro/{ganttDiagram-6RSMTGT7.BgUrExXM.js → ganttDiagram-6RSMTGT7.DboLQ9ca.js} +1 -1
  55. package/web/_astro/{gitGraphDiagram-PVQCEYII.CU-9yVN4.js → gitGraphDiagram-PVQCEYII.4tYvJKGR.js} +1 -1
  56. package/web/_astro/index.Dcr_8fiK.css +1 -0
  57. package/web/_astro/{infoDiagram-5YYISTIA.CvVTCRLe.js → infoDiagram-5YYISTIA.Bd9rXpsB.js} +1 -1
  58. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUzOfKPf.js → ishikawaDiagram-YF4QCWOH.CvMoaf67.js} +1 -1
  59. package/web/_astro/{journeyDiagram-JHISSGLW.BBTb7ziz.js → journeyDiagram-JHISSGLW.Ccy1CA7y.js} +1 -1
  60. package/web/_astro/{kanban-definition-UN3LZRKU.CTQQr70i.js → kanban-definition-UN3LZRKU.0MaMqHNS.js} +1 -1
  61. package/web/_astro/{linear.CLKlJPiS.js → linear.CHXgcIbN.js} +1 -1
  62. package/web/_astro/{mermaid.core.BA4wFhLP.js → mermaid.core.Ca-kcelG.js} +4 -4
  63. package/web/_astro/{mindmap-definition-RKZ34NQL.t1vG2l5e.js → mindmap-definition-RKZ34NQL.BUIDlHa0.js} +1 -1
  64. package/web/_astro/{pieDiagram-4H26LBE5.C8zJhyTu.js → pieDiagram-4H26LBE5.2dX3CU1s.js} +1 -1
  65. package/web/_astro/{quadrantDiagram-W4KKPZXB.DqYYvb7T.js → quadrantDiagram-W4KKPZXB.B3LBlRiv.js} +1 -1
  66. package/web/_astro/{requirementDiagram-4Y6WPE33.lzfDZ9nI.js → requirementDiagram-4Y6WPE33.X12I2uNx.js} +1 -1
  67. package/web/_astro/{sankeyDiagram-5OEKKPKP.lm6N5ORM.js → sankeyDiagram-5OEKKPKP.BXohIHqx.js} +1 -1
  68. package/web/_astro/{sequenceDiagram-3UESZ5HK.E6_IV4Dj.js → sequenceDiagram-3UESZ5HK.C37ZIUzg.js} +1 -1
  69. package/web/_astro/{stateDiagram-AJRCARHV.Bnv6Ok2p.js → stateDiagram-AJRCARHV.BRgz317z.js} +1 -1
  70. package/web/_astro/{stateDiagram-v2-BHNVJYJU.mJHH_Ng_.js → stateDiagram-v2-BHNVJYJU.7VYSXN9-.js} +1 -1
  71. package/web/_astro/{timeline-definition-PNZ67QCA.BhRgqSLa.js → timeline-definition-PNZ67QCA.BVNz_HiN.js} +1 -1
  72. package/web/_astro/{vennDiagram-CIIHVFJN.pjb5DMC9.js → vennDiagram-CIIHVFJN.CHVDkPX4.js} +1 -1
  73. package/web/_astro/{wardleyDiagram-YWT4CUSO.CW20KUng.js → wardleyDiagram-YWT4CUSO.EQQ_qT9v.js} +1 -1
  74. package/web/_astro/{xychartDiagram-2RQKCTM6.vX9_yuMl.js → xychartDiagram-2RQKCTM6.DrAT9WoP.js} +1 -1
  75. package/web/index.html +2 -2
  76. package/web/_astro/BoardApp.ymfj1EHA.js +0 -1
  77. package/web/_astro/channel.Bxxe2Byc.js +0 -1
  78. package/web/_astro/index.BhvM3djt.css +0 -1
@@ -7,7 +7,7 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "sp",
10
- "version": "0.3.74",
10
+ "version": "0.3.76",
11
11
  "source": "./plugins/sp"
12
12
  }
13
13
  ]
@@ -130,6 +130,12 @@ agent:
130
130
  # agent: omp
131
131
  # model: minimax/MiniMax-M3
132
132
  # tier: cheap
133
+ # A disabled profile stays visible to `spur agent doctor` but never routes
134
+ # (no role, team, stage, or explicit selection). Flip back to enabled by
135
+ # removing the line or setting disabled: false.
136
+ # - name: retired-profile
137
+ # agent: omp
138
+ # disabled: true
133
139
  - name: pi-dsv4-flash-volc
134
140
  agent: pi
135
141
  # tier: standard
@@ -9,7 +9,7 @@ install or to ts-libs.
9
9
 
10
10
  | Category | Dir | Purpose |
11
11
  |---|---|---|
12
- | `typescript` | `typescript/` | TypeScript tooling, output boundaries, biome-suppression ban, no `debugger` |
12
+ | `typescript` | `typescript/` | TypeScript tooling, output boundaries, biome/eslint-suppression ban, no `debugger` |
13
13
  | `strict` | `strict/` | Opt-in strict rules (runtime boundaries, HTTP boundaries, structural) |
14
14
  | `boundary` | `boundary/` | DB/DAO boundary enforcement |
15
15
  | `structure` | `structure/` | File layout, protected files, no focused/skipped tests |
@@ -69,6 +69,7 @@ rules:
69
69
  # Synchronous bootstrap & path resolution:
70
70
  - "packages/config/src/bundled-config.ts" # sync bundledConfigRoot() directory walk
71
71
  - "packages/config/src/loader.ts" # sync resolveConfigFile() / schema specifier lookup
72
+ - "packages/config/src/executor-update.ts" # atomic single-entry config update (task 0797): O_EXCL lock, fsync'd tmp+rename commit, lstat symlink refusal, mode preservation — ts-runtime FileSystem seam has no lstat/fsync/chmod/O_EXCL
72
73
  - "packages/app/src/services/project-start.ts" # sync CLI binary path checks before daemon spawn
73
74
 
74
75
  # Synchronous template & workflow resolvers:
@@ -0,0 +1,30 @@
1
+ $schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
2
+ # No ESLint suppression comments. Spur lints with Biome, not ESLint; leftover
3
+ # `eslint-disable*` comments are dead suppressions that hide the real fix and
4
+ # would not even silence the live gate. Sibling of `no-biome-suppressions`
5
+ # (AGENTS.md: never suppress a gate to force green).
6
+ #
7
+ # Tests ARE in scope: an eslint-disable in a test is the same bypass.
8
+ include:
9
+ - "apps/**/*.ts"
10
+ - "apps/**/*.tsx"
11
+ - "packages/**/*.ts"
12
+ - "packages/**/*.tsx"
13
+ - "plugins/**/*.ts"
14
+ - "scripts/**/*.ts"
15
+ exclude:
16
+ - "**/node_modules/**"
17
+ - "**/dist/**"
18
+ - "**/vendors/**"
19
+ rules:
20
+ - id: no-eslint-suppressions
21
+ description: >
22
+ Do not use ESLint disable comments (file, line, or next-line forms).
23
+ Spur lints with Biome; an ESLint suppression does not silence the live
24
+ gate and hides the real fix. Remove the comment and fix the underlying
25
+ issue (AGENTS.md: no suppressions to force green).
26
+ severity: error
27
+ evaluator:
28
+ type: rg
29
+ config:
30
+ pattern: "eslint-disable"
@@ -16,7 +16,7 @@
16
16
  # fail-closed on the captured record result + re-asserted verdict — never on a stub
17
17
 
18
18
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
19
- version: "1"
19
+ version: "2"
20
20
  kind: state-machine
21
21
  name: docs-pipeline
22
22
  description: "Docs-only execution pipeline (precheck -> draft -> docs-review -> verify -> record -> done)"
@@ -43,6 +43,10 @@ vars:
43
43
  # `taskSpecPath` carries the task file path because `docs/tasks*` is excluded from the digest's
44
44
  # git-tree half (task 0704 R4); resolved per-run by the verify state's taskpath shell step.
45
45
  taskSpecPath: ""
46
+ # 0785 R2: linked feature spec path, resolved at verify entry. Empty (orphan task) means the
47
+ # feature spec is legitimately omitted from the digest inputs (empty-string compatibility);
48
+ # a declared feature whose path fails to resolve fails closed.
49
+ featureSpecPath: ""
46
50
  __hitlAnswer: ""
47
51
  # R8 (0366): injected by WorkflowAppService.run(); run-scopes every temporary capture.
48
52
  __runId: ""
@@ -149,12 +153,32 @@ states:
149
153
  options:
150
154
  path: .spur/run/${vars.__runId}-docs-taskpath.txt
151
155
  var: taskSpecPath
156
+ # 0785 R2: resolve the linked feature spec path so the digest covers it too. An orphan task
157
+ # (no feature_id) resolves to empty — legitimately omitted via empty-string compatibility;
158
+ # a declared feature whose spec path does not resolve fails closed instead of silently
159
+ # folding a stale spec into the proof.
160
+ - kind: shell
161
+ options:
162
+ command: >-
163
+ set -e;
164
+ FID="$($spurBin task show $wbs --json 2>/dev/null | jq -r '.feature_id // .frontmatter.feature_id // empty' 2>/dev/null)";
165
+ FPATH_OUT="";
166
+ if [ -n "$FID" ]; then
167
+ FPATH_OUT="$($spurBin feature show "$FID" --json | jq -r '.filePath // .path // empty')";
168
+ if [ -z "$FPATH_OUT" ]; then echo "docs-pipeline: feature $FID linked to task $wbs but its spec path did not resolve" >&2; exit 1; fi;
169
+ fi;
170
+ printf '%s' "$FPATH_OUT" > ".spur/run/$__runId-docs-featurepath.txt"
171
+ - kind: file.read.into-var
172
+ options:
173
+ path: .spur/run/${vars.__runId}-docs-featurepath.txt
174
+ var: featureSpecPath
152
175
  # R4: canonical capture BEFORE the verifier runs. Same fingerprint input as the
153
176
  # re-capture below so the bracket compares like for like.
154
177
  - kind: proof.fingerprint
155
178
  options:
156
179
  var: proofDigest
157
180
  taskFile: ${vars.taskSpecPath}
181
+ featureFile: ${vars.featureSpecPath}
158
182
  input: /sp:dev-verify ${vars.wbs} --auto --fix none --focus all
159
183
  timeoutMs: ${vars.stepTimeoutMs}
160
184
  - kind: agent.run
@@ -191,6 +215,7 @@ states:
191
215
  options:
192
216
  var: proofDigestNow
193
217
  taskFile: ${vars.taskSpecPath}
218
+ featureFile: ${vars.featureSpecPath}
194
219
  input: /sp:dev-verify ${vars.wbs} --auto --fix none --focus all
195
220
  timeoutMs: ${vars.stepTimeoutMs}
196
221
 
@@ -1,37 +1,57 @@
1
- # Feature development loop — brainstorm plan → execute-tasks → feature-verify → done.
1
+ # Feature development loop — existing-feature reuse: precheck → execute-tasks →
2
+ # feature-verify → integration-review → done.
3
+ #
4
+ # 0782 (feature D6): the caller supplies a featureId whose AC are already accepted and whose
5
+ # task roster already exists. This definition REUSES them — the brainstorm/plan states and
6
+ # their edges are deleted, so no model hop ever re-plans or re-decomposes an existing feature
7
+ # (explicit planning stays at /sp:dev-plan and /sp:dev-idea, outside this consumer).
2
8
  #
3
9
  # Reliability (aligned with task-pipeline / ADR-043):
4
- # - Soft precheck (featureId non-empty + agent doctor) → failed via transitions
10
+ # - Soft precheck (feature identity + roster structural contract) → failed via transitions,
11
+ # never a raw lifecycle abort. The doctor probe is gone: the precheck now performs the
12
+ # essential feature/roster validation itself; task execution owns executor availability.
5
13
  # - Every agent.run input is a pure slash command (no free-form pipeline essays)
6
14
  # - featureId must be supplied by the caller (workflow vars are not agent-mutated)
7
- # - feature-verify is fail-closed on `spur feature check --strict`
15
+ # - feature-verify runs `feature check --as done --json` EXACTLY ONCE (0782 R3): the shell
16
+ # captures the exit code and JSON result, writes the decision to a .status sibling, and
17
+ # the guards only read that status — no sibling guard re-runs the check.
18
+ # - A nonempty all-terminal roster routes straight to feature-verify with zero execution
19
+ # model calls (0782 R2).
8
20
 
9
21
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
10
22
  kind: state-machine
11
23
  name: feature-dev
12
- version: "1"
24
+ version: "3"
13
25
  description: >
14
- End-to-end FEATURE development loop: precheck → brainstormplan (decompose)
15
- execute-tasks feature-verify done. Drives a whole feature (one description a
16
- feature with BDD AC many tasks all tasks done → feature verified), NOT a single
17
- task for one deliverable use task-pipeline.yaml directly.
26
+ Existing-feature execution loop: precheck → execute-tasksfeature-verify done.
27
+ Drives a WHOLE feature whose accepted AC and task roster already exist the caller
28
+ supplies featureId, this loop reuses the roster and runs it to verified completion.
29
+ For planning a NEW feature use /sp:dev-plan / /sp:dev-idea; for one deliverable use
30
+ task-pipeline.yaml directly.
18
31
 
19
- The engine has no native for-each over a dynamic task list, so the execute-tasks step
20
- DELEGATES the fan-out: a single agent.run instructs the agent to run every pending task
21
- under the feature through task-pipeline.yaml, enumerated via `spur task list --feature`.
22
- This mirrors how /sp:dev-run --mode full delegates its inner work loop (ADR-022: orchestration
23
- is configuration zero new engine code).
32
+ The engine has no native for-each over a dynamic task list, so execution DELEGATES the
33
+ fan-out: the precheck resolves the linked roster via `spur feature show` + `spur task
34
+ list --feature` (captured once under .spur/run/<runId>-feature-dev-{feature,roster}.json),
35
+ validates the essential structural contract (identity, roster shape, duplicate/mismatched
36
+ identities, refinement-blocking statuses), and freezes the sorted todo WBS list into
37
+ .spur/run/<runId>-feature-dev-tasks.txt. The execute-tasks step reads that frozen list
38
+ into a var (file.read.into-var) and dispatches ONE pure-slash /sp:dev-runall --tasks run
39
+ with it — the child never re-enumerates membership (0782 R2).
24
40
 
25
- Shape: precheck → brainstorm → plan → execute-tasks[-auto](delegated loop) → feature-verify → done
26
- (precheck/plan failure or a non-PASS feature check short-circuits to `failed`).
41
+ Shape: precheck → execute-tasks[-auto](delegated frozen batch) → feature-verify →
42
+ integration-review done
43
+ (precheck failure, a non-PASS completion check, or a blocking review short-circuits
44
+ to `failed`; a nonempty all-terminal roster skips execution entirely).
27
45
 
28
46
  Vars (passed as a JSON object via `--vars`):
29
- featureId — the feature ID this loop drives (required), e.g. `--vars '{"featureId":"B3"}'`
47
+ featureId — the existing feature ID this loop drives (required), e.g. `--vars '{"featureId":"B3"}'`
30
48
  agent — agent the agent.run steps invoke; override `--vars '{"agent":"claude"}'`
31
- profile — `auto` selects execute-tasks-auto (`/sp:dev-runall … --auto`); otherwise
49
+ profile — `auto` selects execute-tasks-auto (`/sp:dev-runall --tasks … --auto`); otherwise
32
50
  interactive execute-tasks (no --auto, per-task HITL remains)
51
+ featureTaskIds — frozen todo WBS list projected from the roster file at run time; declared
52
+ empty so template resolution cannot throw before the read lands (0782 design)
33
53
  spurBin — PATH-independent spur invocation (CLI overrides at run start; literal fallback)
34
- stepTimeoutMs — agent.run budget (ms) for brainstorm/plan/runall
54
+ stepTimeoutMs — agent.run budget (ms) for the runall dispatch
35
55
 
36
56
  Reliability (ADR-043): every agent.run input is a pure slash command. featureId must be
37
57
  supplied by the caller (workflow vars cannot be mutated by the agent). profile is wired
@@ -47,6 +67,7 @@ vars:
47
67
  featureId: ""
48
68
  agent: "auto"
49
69
  profile: "standard"
70
+ featureTaskIds: ""
50
71
  spurBin: "spur"
51
72
  stepTimeoutMs: "1800000"
52
73
  # Integration review (D5-P, feature R11). Advisory by default: a pending, timed-out, or
@@ -60,87 +81,116 @@ vars:
60
81
  states:
61
82
  - id: precheck
62
83
  description: >
63
- Soft precheck: non-empty featureId + agent doctor. Always exit 0 so failures
64
- route to `failed` via transitions (not a raw lifecycle abort).
84
+ Soft precheck (0782 R1): resolve the supplied feature and its linked task roster via
85
+ existing CLI reads, then validate the essential structural contract — non-empty
86
+ featureId/runId, a feature-show JSON whose identity matches, a nonempty array roster
87
+ with unique non-empty WBS identities and known statuses, no backlog/wip/testing/blocked
88
+ member (planning/refine/resume handoff), and at least one linked task. Rejects fail
89
+ closed with an actionable handoff BEFORE any model dispatch; nothing is auto-created
90
+ or re-planned. Always exit 0 so failures route to `failed` via transitions.
65
91
  onEnter:
66
92
  - kind: shell
67
93
  options:
68
94
  command: >-
69
- mkdir -p .spur/run &&
70
- set +e &&
71
- test -n "$featureId"; id_rc=$?;
72
- $spurBin agent doctor $agent; doctor_rc=$?;
73
- set -e &&
74
- if [ "$id_rc" -eq 0 ] && [ "$doctor_rc" -eq 0 ]; then
75
- printf 'PASS\n' > ".spur/run/$__runId-feature-dev-precheck.status";
95
+ mkdir -p .spur/run;
96
+ FEATURE_JSON=".spur/run/$__runId-feature-dev-feature.json";
97
+ ROSTER_JSON=".spur/run/$__runId-feature-dev-roster.json";
98
+ TASKS_TXT=".spur/run/$__runId-feature-dev-tasks.txt";
99
+ PRECHECK_STATUS=".spur/run/$__runId-feature-dev-precheck.status";
100
+ rm -f "$PRECHECK_STATUS";
101
+ set +e;
102
+ test -n "$featureId" && test -n "$__runId";
103
+ id_rc=$?;
104
+ if [ "$id_rc" -eq 0 ]; then $spurBin feature show "$featureId" --json > "$FEATURE_JSON" 2>&1; show_rc=$?; else show_rc=1; fi;
105
+ if [ "$id_rc" -eq 0 ] && [ "$show_rc" -eq 0 ] && jq -e --arg id "$featureId" 'type == "object" and .id == $id' "$FEATURE_JSON" > /dev/null 2>&1; then $spurBin task list --feature "$featureId" --json > "$ROSTER_JSON" 2>&1; list_rc=$?; else list_rc=1; fi;
106
+ if [ "$id_rc" -ne 0 ] || [ "$show_rc" -ne 0 ] || [ "$list_rc" -ne 0 ]; then
107
+ echo "feature-dev precheck: missing featureId/runId, unknown feature '$featureId', or unreadable roster (rc $id_rc/$show_rc/$list_rc) — supply an existing planned feature via /sp:dev-plan or /sp:dev-idea; nothing was auto-created or re-planned" >&2;
108
+ printf 'FAIL\n' > "$PRECHECK_STATUS";
109
+ elif ! jq -e 'type == "array" and length > 0' "$ROSTER_JSON" > /dev/null 2>&1; then
110
+ echo "feature-dev precheck: roster at $ROSTER_JSON is malformed, not an array, or empty — plan the feature first via /sp:dev-plan; refusing to replan or run an empty batch" >&2;
111
+ printf 'FAIL\n' > "$PRECHECK_STATUS";
112
+ elif ! jq -e 'all(.[]; (.wbs | type == "string" and length > 0)) and ([.[].wbs] | length == (unique | length)) and all(.[]; .status == "todo" or .status == "done" or .status == "cancelled" or .status == "backlog" or .status == "wip" or .status == "testing" or .status == "blocked")' "$ROSTER_JSON" > /dev/null 2>&1; then
113
+ echo "feature-dev precheck: roster has empty/duplicate/mismatched WBS identities or unknown statuses at $ROSTER_JSON — repair the task corpus; refusing to batch a broken roster" >&2;
114
+ printf 'FAIL\n' > "$PRECHECK_STATUS";
115
+ elif [ "$(jq '[.[] | select(.status == "backlog" or .status == "wip" or .status == "testing" or .status == "blocked")] | length' "$ROSTER_JSON")" -gt 0 ]; then
116
+ echo "feature-dev precheck: linked task(s) are backlog/wip/testing/blocked — refine or resume them through their own task pipelines before batching; refusing to launch overlapping work" >&2;
117
+ printf 'FAIL\n' > "$PRECHECK_STATUS";
76
118
  else
77
- printf 'FAIL\n' > ".spur/run/$__runId-feature-dev-precheck.status";
78
- fi &&
119
+ printf '%s' "$(jq -r '[.[] | select(.status == "todo") | .wbs] | sort | join(",")' "$ROSTER_JSON")" > "$TASKS_TXT";
120
+ printf 'PASS\n' > "$PRECHECK_STATUS";
121
+ fi;
79
122
  exit 0
80
123
  - kind: note
81
124
  options:
82
- message: "Feature-dev start for feature ${vars.featureId}."
83
-
84
- - id: brainstorm
85
- description: >
86
- Validate / expand the feature with BDD AC via pure slash /sp:dev-brainstorm.
87
- Caller must pass vars.featureId (required for plan/runall).
88
- onEnter:
89
- - kind: agent.run
90
- options:
91
- agent: ${vars.agent}
92
- input: /sp:dev-brainstorm --feature ${vars.featureId}
93
- # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
94
- role: planner
95
- timeoutMs: ${vars.stepTimeoutMs}
96
-
97
- - id: plan
98
- description: >
99
- Decompose the validated feature into a CLI-validated task batch via pure slash
100
- /sp:dev-plan. The batch-create gate rejects a malformed decomposition.
101
- onEnter:
102
- - kind: agent.run
103
- options:
104
- agent: ${vars.agent}
105
- input: /sp:dev-plan --feature ${vars.featureId}
106
- # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
107
- role: planner
108
- timeoutMs: ${vars.stepTimeoutMs}
125
+ message: "Feature-dev start for existing feature ${vars.featureId} (0782 reuse path): roster resolved under .spur/run/${vars.__runId}-feature-dev-roster.json; frozen todo list at .spur/run/${vars.__runId}-feature-dev-tasks.txt."
109
126
 
110
127
  # Two pure-slash execute hops so vars.profile actually controls --auto (ADR-043).
111
128
  # Engine templates cannot branch a single input string on profile; declaration-order
112
- # transitions pick the correct hop (auto first, then interactive fallback).
129
+ # transitions pick the correct hop (auto first, then interactive fallback). Each hop first
130
+ # reads the FROZEN todo list produced by precheck into featureTaskIds, then dispatches the
131
+ # explicit list — the runall child never re-enumerates membership (0782 R2).
113
132
  - id: execute-tasks-auto
114
133
  description: >
115
- profile=auto — drive every pending task via pure slash /sp:dev-runall --auto
116
- (skips per-task HITL inside task-pipeline).
134
+ profile=auto — dispatch the frozen todo WBS list via pure slash
135
+ /sp:dev-runall --tasks <list> --auto (skips per-task HITL inside task-pipeline).
117
136
  onEnter:
137
+ - kind: file.read.into-var
138
+ options:
139
+ path: .spur/run/${vars.__runId}-feature-dev-tasks.txt
140
+ var: featureTaskIds
141
+ trim: true
118
142
  - kind: agent.run
119
143
  options:
120
144
  agent: ${vars.agent}
121
- input: /sp:dev-runall --feature ${vars.featureId} --auto
145
+ input: /sp:dev-runall --tasks ${vars.featureTaskIds} --auto
122
146
  # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
123
147
  role: planner
124
148
  timeoutMs: ${vars.stepTimeoutMs}
125
149
 
126
150
  - id: execute-tasks
127
151
  description: >
128
- Interactive — drive every pending task via pure slash /sp:dev-runall without
129
- --auto (per-task HITL approve gates remain available).
152
+ Interactive — dispatch the frozen todo WBS list via pure slash
153
+ /sp:dev-runall --tasks <list> without --auto (per-task HITL approve gates remain
154
+ available).
130
155
  onEnter:
156
+ - kind: file.read.into-var
157
+ options:
158
+ path: .spur/run/${vars.__runId}-feature-dev-tasks.txt
159
+ var: featureTaskIds
160
+ trim: true
131
161
  - kind: agent.run
132
162
  options:
133
163
  agent: ${vars.agent}
134
- input: /sp:dev-runall --feature ${vars.featureId}
164
+ input: /sp:dev-runall --tasks ${vars.featureTaskIds}
135
165
  # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
136
166
  role: planner
137
167
  timeoutMs: ${vars.stepTimeoutMs}
138
168
 
139
169
  - id: feature-verify
140
170
  description: >
141
- Certify the feature is complete: `spur feature check ${featureId} --strict` validates the
142
- AC, traceability, and that all linked tasks are done. A clean strict check is the only
143
- proof the feature is shippable.
171
+ Certify the feature is complete (0782 R3): run `spur feature check --as done --json`
172
+ EXACTLY ONCE, capture the exit code and JSON result at
173
+ .spur/run/<runId>-feature-dev-verify.json, and decide PASS/FAIL into the .status
174
+ sibling. PASS requires exit 0, a nonempty result array, and every returned scope
175
+ pass === true. The guards below only read the captured status — no sibling guard
176
+ re-runs the check. No --strict elevation, no whole-corpus scan.
177
+ onEnter:
178
+ - kind: shell
179
+ options:
180
+ command: >-
181
+ mkdir -p .spur/run;
182
+ VERIFY_JSON=".spur/run/$__runId-feature-dev-verify.json";
183
+ VERIFY_STATUS=".spur/run/$__runId-feature-dev-verify.status";
184
+ VERIFY_TMP=".spur/run/$__runId-feature-dev-verify.status.tmp";
185
+ rm -f "$VERIFY_STATUS";
186
+ set +e;
187
+ $spurBin feature check "$featureId" --as done --json > "$VERIFY_JSON" 2>&1;
188
+ check_rc=$?;
189
+ set -e;
190
+ if [ "$check_rc" -eq 0 ] && jq -e 'type == "array" and length > 0 and all(.[]; .pass == true)' "$VERIFY_JSON" > /dev/null 2>&1; then printf 'PASS\n' > "$VERIFY_TMP" && mv -f "$VERIFY_TMP" "$VERIFY_STATUS"; else printf 'FAIL\n' > "$VERIFY_TMP" && mv -f "$VERIFY_TMP" "$VERIFY_STATUS"; fi
191
+ - kind: note
192
+ options:
193
+ message: "Feature verification for ${vars.featureId}: decision at .spur/run/${vars.__runId}-feature-dev-verify.status (evidence at .spur/run/${vars.__runId}-feature-dev-verify.json) — exactly one `feature check --as done --json` invocation per run (0782 R3)."
144
194
 
145
195
  - id: integration-review
146
196
  description: >
@@ -196,47 +246,40 @@ states:
196
246
  message: "Integration review for feature ${vars.featureId}: request verdict at .spur/run/${vars.__runId}-integration-review.status (request state only — REQUESTED/ALREADY_* are NEVER clean evidence); collected verdict at .spur/run/${vars.__runId}-integration-review-collect.status (CLEAN/FINDINGS/PENDING — the only clean evidence). Advisory unless requireCleanReview=true."
197
247
 
198
248
  - id: done
199
- description: Terminal — feature planned, all tasks executed, and the feature verified.
200
- # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
201
- onEnter:
202
- - kind: shell
203
- options:
204
- command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: feature-dev done featureId=$featureId ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/$featureId-checkpoint.md'
249
+ description: Terminal — the existing feature's roster executed and the feature verified.
205
250
 
206
251
  - id: failed
207
- description: Terminal — plan rejected, a task failed verification, or the feature check did not pass.
252
+ description: Terminal — the roster contract was rejected at precheck, a task failed verification, or the feature check did not pass.
208
253
 
209
254
  transitions:
255
+ # ── precheck routing (0782 R1/R2) — declaration order matters: the all-terminal
256
+ # direct-verify edge is tried FIRST so a fully-terminal roster reaches completion
257
+ # verification with zero execution model calls; then the profile hops; then the
258
+ # failure fallback. ──
210
259
  - from: precheck
211
- to: brainstorm
212
- description: Precheck PASS — begin brainstorm.
260
+ to: feature-verify
261
+ description: Roster PASS and every linked task is already terminal verify the feature directly, zero execution model calls.
213
262
  guard:
214
263
  kind: shell
215
264
  options:
216
- command: 'test "$(cat .spur/run/$__runId-feature-dev-precheck.status 2>/dev/null)" = PASS'
265
+ command: 'test "$(cat ".spur/run/$__runId-feature-dev-precheck.status" 2>/dev/null)" = PASS && test -f ".spur/run/$__runId-feature-dev-feature.json" && test ! -s ".spur/run/$__runId-feature-dev-tasks.txt"'
217
266
  - from: precheck
218
- to: failed
219
- description: Precheck FAIL (missing featureId and/or doctor red) — stop.
220
- guard:
221
- kind: always
222
-
223
- # Linear prefix: brainstorm → plan → execute-tasks
224
- - from: brainstorm
225
- to: plan
226
- description: Feature validated; decompose it into tasks
227
- guard:
228
- kind: always
229
-
230
- - from: plan
231
267
  to: execute-tasks-auto
232
- description: Tasks created; profile=auto — batch with --auto (no per-task HITL).
268
+ description: Roster PASS with todo work; profile=auto — batch the frozen list with --auto (no per-task HITL).
233
269
  guard:
234
270
  kind: shell
235
271
  options:
236
- command: 'test "$profile" = auto'
237
- - from: plan
272
+ command: 'test "$(cat ".spur/run/$__runId-feature-dev-precheck.status" 2>/dev/null)" = PASS && test -s ".spur/run/$__runId-feature-dev-tasks.txt" && test "$profile" = auto'
273
+ - from: precheck
238
274
  to: execute-tasks
239
- description: Tasks created; interactive — batch without --auto.
275
+ description: Roster PASS with todo work; interactive — batch the frozen list without --auto.
276
+ guard:
277
+ kind: shell
278
+ options:
279
+ command: 'test "$(cat ".spur/run/$__runId-feature-dev-precheck.status" 2>/dev/null)" = PASS && test -s ".spur/run/$__runId-feature-dev-tasks.txt"'
280
+ - from: precheck
281
+ to: failed
282
+ description: Precheck FAIL (missing identity, malformed/empty roster, duplicate identities, or refinement-blocking statuses) — stop before any model dispatch.
240
283
  guard:
241
284
  kind: always
242
285
 
@@ -251,16 +294,16 @@ transitions:
251
294
  guard:
252
295
  kind: always
253
296
 
254
- # ── feature-verify gate: strict check certifies done, else fail ──
255
- # Declaration order: the pass guard is tried FIRST.
297
+ # ── feature-verify gate: the captured one-shot check decision certifies review, else
298
+ # fail. Declaration order: the pass guard is tried FIRST. The check itself is NOT
299
+ # re-run here (0782 R3) — the shell decision file is the only evidence. ──
256
300
  - from: feature-verify
257
301
  to: integration-review
258
- description: Strict feature check passed — spend one integration review on this stable HEAD.
302
+ description: Captured completion check is PASS — spend one integration review on this stable HEAD.
259
303
  guard:
260
304
  kind: shell
261
305
  options:
262
- command: "$spurBin feature check $featureId --strict"
263
-
306
+ command: 'test "$(cat ".spur/run/$__runId-feature-dev-verify.status" 2>/dev/null)" = PASS'
264
307
  - from: integration-review
265
308
  to: failed
266
309
  description: >
@@ -285,8 +328,6 @@ transitions:
285
328
  kind: always
286
329
  - from: feature-verify
287
330
  to: failed
288
- description: Strict feature check failed — block before done; investigate incomplete tasks or AC.
331
+ description: Completion check missing, malformed, or non-PASS (or its evidence failed to persist) — block BEFORE integration review; no review is requested (0782 R3).
289
332
  guard:
290
- kind: shell
291
- options:
292
- command: '! $spurBin feature check $featureId --strict'
333
+ kind: always
@@ -46,7 +46,7 @@
46
46
  # Seeded by `spur init`; adapt the agent.run inputs to your project's command set.
47
47
 
48
48
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
49
- version: "1"
49
+ version: "2"
50
50
  kind: state-machine
51
51
  name: idea-pipeline
52
52
  description: "Idea to feature + AC + task batch: discovery, idea-eval, feature-create, ac-generate, feature-check, system-design, decompose, batch-create, handoff"
@@ -327,7 +327,7 @@ states:
327
327
  onEnter:
328
328
  - kind: shell
329
329
  options:
330
- command: "mkdir -p .spur/run && count=$(cat .spur/run/$__runId-idea-decompose-retry-count 2>/dev/null || echo 0); echo $((count + 1)) > .spur/run/$__runId-idea-decompose-retry-count; rm -f .spur/run/$__runId-idea-task-batch.json .spur/run/$__runId-idea-task-order.json .spur/run/$__runId-idea-batch-create.done .spur/run/$__runId-idea-batch-create.failed .spur/run/$__runId-idea-batch-create-result.json .spur/run/$__runId-idea-handoff.md"
330
+ command: "mkdir -p .spur/run && count=$(cat .spur/run/$__runId-idea-decompose-retry-count 2>/dev/null || echo 0); echo $((count + 1)) > .spur/run/$__runId-idea-decompose-retry-count; rm -f .spur/run/$__runId-idea-task-batch.json .spur/run/$__runId-idea-task-order.json .spur/run/$__runId-idea-batch-create.done .spur/run/$__runId-idea-batch-create.failed .spur/run/$__runId-idea-batch-create-result.json .spur/run/$__runId-idea-handoff.md .spur/run/$__runId-idea-ready.json"
331
331
  - kind: agent.run
332
332
  options:
333
333
  agent: ${vars.planningAgent}
@@ -385,7 +385,7 @@ states:
385
385
  command: >-
386
386
  if test -f .spur/run/$__runId-idea-batch-create.done; then exit 0; fi;
387
387
  rm -f .spur/run/$__runId-idea-batch-create.failed .spur/run/$__runId-idea-batch-create-result.json .spur/run/$__runId-idea-batch-create-result.json.tmp;
388
- if $spurBin task batch-create --file .spur/run/$__runId-idea-task-batch.json --json > .spur/run/$__runId-idea-batch-create-result.json.tmp && jq -e ".created == (.wbs | length)" .spur/run/$__runId-idea-batch-create-result.json.tmp >/dev/null 2>&1; then
388
+ if $spurBin task batch-create --file .spur/run/$__runId-idea-task-batch.json --skip-ready --json > .spur/run/$__runId-idea-batch-create-result.json.tmp && jq -e ".created == (.wbs | length)" .spur/run/$__runId-idea-batch-create-result.json.tmp >/dev/null 2>&1; then
389
389
  mv .spur/run/$__runId-idea-batch-create-result.json.tmp .spur/run/$__runId-idea-batch-create-result.json &&
390
390
  date -u +%Y-%m-%dT%H:%M:%SZ > .spur/run/$__runId-idea-batch-create.done;
391
391
  else
@@ -393,6 +393,49 @@ states:
393
393
  date -u +%Y-%m-%dT%H:%M:%SZ > .spur/run/$__runId-idea-batch-create.failed;
394
394
  fi
395
395
 
396
+ - id: ready-prepare
397
+ description: >
398
+ Ready-by-default preparation (0788): the planning owner applies the
399
+ ready-refinement checklist to each created task (requirements, design,
400
+ plan, ac, decisions, dependencies, premises) so the deterministic task
401
+ check passes, then writes the run-scoped ready evidence sidecar
402
+ (.spur/run/${vars.__runId}-idea-ready.json) with one row per task: wbs,
403
+ status (ready|failed|skipped), planningDigest, and seven check rows.
404
+ Handoff-finalize verifies presence, digest and checklist evidence before
405
+ recommending auto runall; missing or failing evidence never fails the
406
+ run — it degrades the recommendation to ready-depth refineall.
407
+ onEnter:
408
+ - kind: agent.run
409
+ options:
410
+ agent: ${vars.planningAgent}
411
+ # Declared Layer-1 role (0538 R2), same planner executor as decompose.
412
+ role: planner
413
+ timeoutMs: ${vars.stepTimeoutMs}
414
+ input: >-
415
+ Ready-by-default preparation for feature ${vars.featureId} (0788). The created WBS
416
+ values are in .spur/run/${vars.__runId}-idea-batch-create-result.json under .wbs.
417
+ For EACH wbs: resolve the task file with `spur task path <wbs> --json`; open the
418
+ task doc and apply the ready-refinement checklist — make requirements, design,
419
+ plan, acceptance criteria, decisions, dependencies and premises present and
420
+ non-placeholder so `spur task check <wbs> --json` exits 0; edit only planning
421
+ sections, never Solution/Testing/Review/History. Record one checklist row per id
422
+ with concrete evidence of how you verified it. Compute the planning digest with
423
+ the project's own implementation when this is a monorepo checkout:
424
+ `bun -e 'const m = await import("./packages/app/src/services/task-readiness"); console.log(m.computePlanningDigest(await Bun.file(process.argv[1]).text()))' <task-file>`
425
+ — when that is impossible in this checkout, set status "skipped" instead of
426
+ guessing a digest. Finally write .spur/run/${vars.__runId}-idea-ready.json with
427
+ exactly this shape: {"runId":"${vars.__runId}","depth":"ready","tasks":[{"wbs":"<wbs>","status":"ready" or "failed" or "skipped","planningDigest":"<sha256 hex>","checks":[{"id":"requirements" or "design" or "plan" or "ac" or "decisions" or "dependencies" or "premises","pass":true or false,"evidence":"<how verified>"}]}]}. A task you cannot fully prepare gets status "failed" or "skipped" — never fabricate evidence; the handoff degrades to refineall.
428
+ # Fail-closed shape validation (mirrors the order-sidecar guard). Absence is
429
+ # normalized to an empty sidecar: finalize and the seeded fallback then degrade
430
+ # the recommendation to refineall instead of failing the run. A PRESENT but
431
+ # malformed sidecar fails the run here — it must not masquerade as evidence.
432
+ - kind: shell
433
+ options:
434
+ command: >-
435
+ READY=".spur/run/$__runId-idea-ready.json" &&
436
+ if ! test -f "$READY"; then printf '{"runId":"%s","depth":"ready","tasks":[]}\n' "$__runId" > "$READY"; fi &&
437
+ jq -e 'type == "object" and (.tasks | type == "array") and (all(.tasks[]; (.wbs | type == "string") and (.status == "ready" or .status == "failed" or .status == "skipped") and (.planningDigest | type == "string") and (.checks | type == "array") and (all(.checks[]; (.id | type == "string") and (.pass | type == "boolean") and (.evidence | type == "string")))))' "$READY" >/dev/null
438
+
396
439
  - id: handoff-finalize
397
440
  description: >
398
441
  Post-create finalization (0518): zip batch item names to the created WBS
@@ -458,7 +501,9 @@ states:
458
501
  CHECK_ROWS=$(wc -l < "$CHECKS" | tr -d ' ') &&
459
502
  WBS_COUNT=$(printf '%s\n' $WBS_LIST | wc -l | tr -d ' ') &&
460
503
  test "$CHECK_ROWS" = "$WBS_COUNT" &&
461
- NEXT=$(jq -r --arg feature "$featureId" --slurpfile c "$CHECKS" 'if ($c | any(.[]; .pass == false)) then "/sp:dev-refineall --feature \($feature) --auto --depth ready" else "/sp:dev-runall --feature \($feature) --auto" end' "$RESULT") &&
504
+ READY=".spur/run/$__runId-idea-ready.json" &&
505
+ (test -f "$READY" || printf '{"tasks":[]}\n' > "$READY") &&
506
+ NEXT=$(jq -r --arg feature "$featureId" --slurpfile c "$CHECKS" --slurpfile k "$READY" 'if (($c | any(.[]; .pass == false)) or (($k[0].tasks // []) | length == 0) or (($k[0].tasks // []) | any(.[]; .status != "ready"))) then "/sp:dev-refineall --feature \($feature) --auto --depth ready" else "/sp:dev-runall --feature \($feature) --auto" end' "$RESULT") &&
462
507
  {
463
508
  echo "# Idea pipeline handoff report";
464
509
  echo;
@@ -498,10 +543,6 @@ states:
498
543
  options:
499
544
  path: .spur/run/${vars.__runId}-idea-handoff.md
500
545
  artifactKind: idea-handoff
501
- # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
502
- - kind: shell
503
- options:
504
- command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: idea-pipeline handoff feature=$featureId ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/idea-checkpoint.md'
505
546
 
506
547
  - id: cancelled
507
548
  description: Terminal — pipeline cancelled by operator or error.
@@ -748,14 +789,20 @@ transitions:
748
789
  options:
749
790
  command: 'test "$__hitlAnswer" = cancel'
750
791
 
751
- # ── batch-create-run: success -> handoff-finalize, failure -> decompose (retry cap: 3), failure+cap -> failed ──
792
+ # ── batch-create-run: success -> ready-prepare, failure -> decompose (retry cap: 3), failure+cap -> failed ──
752
793
  - from: batch-create-run
753
- to: handoff-finalize
754
- description: Batch created and result captured — apply ordering, refresh roster, write handoff report.
794
+ to: ready-prepare
795
+ description: Batch created and result captured — apply ready-by-default preparation, then finalize the handoff.
755
796
  guard:
756
797
  kind: shell
757
798
  options:
758
799
  command: "test -f .spur/run/$__runId-idea-batch-create.done"
800
+ # ── ready-prepare: -> handoff-finalize (always; evidence absence degrades to refineall, never fails the run) ──
801
+ - from: ready-prepare
802
+ to: handoff-finalize
803
+ description: Ready evidence sidecar written (or normalized empty) — finalize the handoff report.
804
+ guard:
805
+ kind: always
759
806
  - from: batch-create-run
760
807
  to: decompose
761
808
  description: "Batch-create failed — revise decomposition (retry cap: 3)."