@gobing-ai/spur 0.3.78 → 0.3.81

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 (177) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +29 -18
  3. package/config/config.global.yaml +10 -11
  4. package/config/pipeline-budgets.json +34 -2
  5. package/config/plugin-scripts.json +25 -0
  6. package/config/rules/boundary/config-loading-ownership.yaml +0 -3
  7. package/config/rules/boundary/dao-boundary.yaml +4 -17
  8. package/config/rules/boundary/planning-folder-hardcode.yaml +0 -1
  9. package/config/rules/boundary/sp-no-vendor-refs.yaml +3 -2
  10. package/config/rules/boundary/sp-runtime-path.yaml +3 -14
  11. package/config/rules/quality/coverage-gate.yaml +3 -14
  12. package/config/rules/quality/tsdoc-exports.yaml +4 -7
  13. package/config/rules/strict/http-boundaries.yaml +5 -8
  14. package/config/rules/strict/runtime-boundaries.yaml +1 -5
  15. package/config/rules/structure/protected-files.yaml +9 -3
  16. package/config/rules/structure/test-focus-skip.yaml +0 -2
  17. package/config/rules/structure/test-location.yaml +0 -5
  18. package/config/rules/surface/check-cli-surface.yaml +3 -2
  19. package/config/rules/typescript/bun-tooling.yaml +5 -7
  20. package/config/rules/typescript/guarded-happy-dom-register.yaml +0 -2
  21. package/config/rules/typescript/happy-dom-teardown.yaml +0 -2
  22. package/config/rules/typescript/no-biome-suppressions.yaml +0 -2
  23. package/config/rules/typescript/no-debugger.yaml +0 -2
  24. package/config/rules/typescript/no-eslint-suppressions.yaml +0 -4
  25. package/config/rules/typescript/no-leaky-module-mocks.yaml +6 -13
  26. package/config/rules/typescript/no-module-scope-import-calls.yaml +0 -2
  27. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +0 -3
  28. package/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +0 -3
  29. package/config/rules/typescript/output-boundaries.yaml +0 -3
  30. package/config/rules/typescript/prefer-accessible-role-for-button-queries.yaml +0 -3
  31. package/config/rules/ui/ui-import-boundary.yaml +1 -5
  32. package/config/templates/AGENTS.md +26 -23
  33. package/config/templates/docs/00_ADR.md +13 -23
  34. package/config/templates/docs/01_PRD.md +5 -2
  35. package/config/templates/docs/02_ROADMAP.md +9 -13
  36. package/config/templates/docs/03_ARCHITECTURE.md +2 -2
  37. package/config/templates/docs/04_DESIGN.md +12 -31
  38. package/config/templates/docs/05_FEATURES.md +6 -18
  39. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +162 -394
  40. package/config/transition-shims.json +7 -7
  41. package/config/workflows/basic.yaml +4 -0
  42. package/config/workflows/docs-pipeline.yaml +13 -14
  43. package/config/workflows/feature-dev.yaml +20 -65
  44. package/config/workflows/history-anatomy.yaml +22 -1
  45. package/config/workflows/idea-pipeline.yaml +53 -97
  46. package/config/workflows/pr-review.yaml +21 -33
  47. package/config/workflows/task-pipeline.yaml +87 -330
  48. package/config/workflows/wayfinder-resolution.yaml +12 -26
  49. package/config/workflows/wrapup-pipeline.yaml +48 -189
  50. package/package.json +9 -9
  51. package/plugins/sp/README.md +22 -8
  52. package/plugins/sp/agents/expert-spur.md +41 -19
  53. package/plugins/sp/agents/super-reviewer.md +43 -8
  54. package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
  55. package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
  56. package/plugins/sp/plugin.json +1 -1
  57. package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
  58. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  59. package/plugins/sp/scripts/idea-handoff.mjs +27 -0
  60. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  61. package/plugins/sp/scripts/quality-gate.mjs +165 -0
  62. package/plugins/sp/scripts/quality-gate.ts +217 -0
  63. package/plugins/sp/scripts/verify-answer-lint.ts +21 -3
  64. package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
  65. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  66. package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
  67. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
  68. package/plugins/sp/skills/conflict-finding/SKILL.md +6 -0
  69. package/plugins/sp/skills/daily-summary/SKILL.md +1 -1
  70. package/plugins/sp/skills/doc-evolve/SKILL.md +26 -40
  71. package/plugins/sp/skills/doc-evolve/references/operations.md +17 -30
  72. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  73. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
  74. package/plugins/sp/skills/spur-cli/references/agent.md +56 -14
  75. package/plugins/sp/skills/spur-cli/references/message.md +30 -3
  76. package/plugins/sp/skills/spur-cli/references/projects.md +45 -1
  77. package/plugins/sp/skills/spur-cli/references/self.md +5 -4
  78. package/plugins/sp/skills/spur-cli/references/serve.md +5 -4
  79. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +17 -1
  80. package/plugins/sp/skills/spur-cli/references/tasks.md +32 -2
  81. package/plugins/sp/skills/spur-cli/references/team.md +21 -1
  82. package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
  83. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
  84. package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
  85. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +14 -0
  86. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +3 -3
  87. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +12 -0
  88. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +46 -4
  89. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
  90. package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
  91. package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
  92. package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
  93. package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
  94. package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
  95. package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
  96. package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
  97. package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
  98. package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
  99. package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
  100. package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
  101. package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
  102. package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
  103. package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
  104. package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
  105. package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
  106. package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
  107. package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
  108. package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
  109. package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
  110. package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
  111. package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
  112. package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
  113. package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
  114. package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
  115. package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
  116. package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
  117. package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
  118. package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
  119. package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
  120. package/schemas/spur-config.schema.json +49 -0
  121. package/spur.js +46936 -44198
  122. package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.B1U26g3I.js} +97 -95
  123. package/web/_astro/BoardApp.Csgyg-lS.js +1 -0
  124. package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.DwPqpq7v.js} +1 -1
  125. package/web/_astro/{arc.DWEtA3Tx.js → arc.CweZEjN2.js} +1 -1
  126. package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.D89pbDuv.js} +1 -1
  127. package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.BOuTeEpX.js} +1 -1
  128. package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CASbkWZF.js} +1 -1
  129. package/web/_astro/channel.Cx6sXxhq.js +1 -0
  130. package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.BKQYtOvY.js} +1 -1
  131. package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.9sHLdMtG.js} +1 -1
  132. package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.wOLXWlPs.js} +1 -1
  133. package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DovFbwg3.js} +1 -1
  134. package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.B1Weod1X.js} +1 -1
  135. package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.TEMS04st.js} +1 -1
  136. package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.Cp8VT1wQ.js} +1 -1
  137. package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.BzATdEcP.js} +1 -1
  138. package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.C9BOCfAO.js} +1 -1
  139. package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.C9BOCfAO.js} +1 -1
  140. package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.DUnr4UAw.js} +1 -1
  141. package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.rYq5uM3D.js} +1 -1
  142. package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
  143. package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.CWeNKe3I.js} +1 -1
  144. package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.DCkfls10.js} +1 -1
  145. package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.D5U4JCka.js} +1 -1
  146. package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.BZJgqaqG.js} +1 -1
  147. package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.DoMeHvPR.js} +1 -1
  148. package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.B50qwwWX.js} +1 -1
  149. package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.DdGPG6LK.js} +1 -1
  150. package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.QP2MJ12u.js} +1 -1
  151. package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BI6LgKSy.js} +1 -1
  152. package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.npPZiC2G.js} +1 -1
  153. package/web/_astro/index.DayyIngm.css +1 -0
  154. package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.DCJCBVbp.js} +1 -1
  155. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BMLV-3I1.js} +1 -1
  156. package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.LE58crde.js} +1 -1
  157. package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.BPbz8rH9.js} +1 -1
  158. package/web/_astro/{linear.CHXgcIbN.js → linear.DhZaBtYh.js} +1 -1
  159. package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.BD5-jXum.js} +6 -6
  160. package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.MTJyrQ65.js} +1 -1
  161. package/web/_astro/ordinal.BYWQX77i.js +1 -0
  162. package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.BrDhDvIS.js} +1 -1
  163. package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.71d73_5N.js} +1 -1
  164. package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.Bga6UF-z.js} +1 -1
  165. package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.BnHs4K82.js} +1 -1
  166. package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DsfY2gnj.js} +1 -1
  167. package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DvsTSc9a.js} +1 -1
  168. package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.DxzzmHUR.js} +1 -1
  169. package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.4ZuQmOTt.js} +1 -1
  170. package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.Ck5Q86SG.js} +1 -1
  171. package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.BK7k2hXr.js} +1 -1
  172. package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DfCrgauK.js} +1 -1
  173. package/web/index.html +2 -2
  174. package/web/_astro/BoardApp.DV9kx0wo.js +0 -1
  175. package/web/_astro/channel.BAI6xLeV.js +0 -1
  176. package/web/_astro/index.Dcr_8fiK.css +0 -1
  177. package/web/_astro/ordinal.DBvzRdQf.js +0 -1
@@ -163,142 +163,38 @@ states:
163
163
  Fast deterministic task readiness and size checks. Failures route to the
164
164
  `failed` terminal state (not a raw lifecycle abort mid-enter).
165
165
  onEnter:
166
- # R6 (0487): pre-launch hygiene WARNING (never a block) — starting a task on
167
- # a tree already dirty with another task's implementation is how 0485's diff
168
- # got swept into 0486's run. Corpus dirs are excluded: the pipeline writes
169
- # those itself.
170
- # R1 (0511): task-corpus dirt gets its own advisory NOTE — docs/tasks* is
171
- # excluded from the non-corpus warning, so pre-existing task-corpus edits are
172
- # invisible at launch yet can be accidentally staged with the current task
173
- # later. Advisory only (never a block): the pipeline legitimately updates
174
- # task files, so corpus dirt is launch-time visibility, not a lifecycle guard.
166
+ # (e) git-status hygiene advisory; never fails the run (0487 R6 / 0511 R1).
175
167
  - kind: shell
176
168
  options:
177
169
  command: >-
178
- DIRTY=$(git status --porcelain -- . ':(exclude)docs/tasks*' ':(exclude)docs/features' 2>/dev/null);
179
- if [ -n "$DIRTY" ]; then
180
- echo "precheck: WARNING - working tree has uncommitted non-corpus changes; commit or stash before starting a new task:";
181
- echo "$DIRTY";
182
- fi;
183
- CORPUS_DIRTY=$(git status --porcelain -- ':(glob)docs/tasks*/**' 2>/dev/null);
184
- if [ -n "$CORPUS_DIRTY" ]; then
185
- echo "precheck: NOTE - task corpus has uncommitted changes; review before staging with this task:";
186
- echo "$CORPUS_DIRTY";
187
- fi;
188
- exit 0
170
+ D=$(git status --porcelain -- . ':(exclude)docs/tasks*' ':(exclude)docs/features' 2>/dev/null); [ -z "$D" ] || printf 'precheck: WARNING - working tree has uncommitted non-corpus changes — commit or stash before starting a new task:\n%s\n' "$D"; C=$(git status --porcelain -- ':(glob)docs/tasks*/**' 2>/dev/null); [ -z "$C" ] || printf 'precheck: NOTE - task corpus has uncommitted changes — review before staging with this task:\n%s\n' "$C"; exit 0
189
171
  - kind: note
190
172
  options:
191
173
  # R4 task_run_links linkage (kind=pipeline) is written by a WorkflowService
192
174
  # hook at run start — see the 0062 follow-up note; there is no link-writing
193
175
  # CLI verb to call from a shell step, so it cannot live in pure YAML.
194
176
  message: "Pipeline start for task ${vars.wbs}."
195
- # R1 (0453): auto-profile precheck reopens a done feature before task check.
196
- # Under profile=auto, resolve feature_id, sync (preferred) or update to active.
197
- # Under non-auto, leave R4 message to guide the operator.
198
- # R3 (0723): a real reactivation failure is surfaced, not swallowed —
199
- # the default 'fail' onEnter policy halts the sequence and routes the
200
- # run to `failed` before implementation. Verbs stay single-shot:
201
- # one sync, then one update fallback, never retried in a loop.
177
+ # (e) auto-profile feature reopen; exit 1 when reactivation fails (0723 R3).
202
178
  - kind: shell
203
179
  options:
204
180
  command: >-
205
- if [ "$profile" = auto ]; then
206
- FID=$($spurBin task show $wbs --json 2>/dev/null |
207
- jq -r '.feature_id // .frontmatter.feature_id // empty' 2>/dev/null);
208
- if [ -n "$FID" ]; then
209
- if ! $spurBin feature sync "$FID" --force 2>/dev/null; then
210
- if ! $spurBin feature update "$FID" active 2>/dev/null; then
211
- echo "precheck: FAIL - feature reactivation $FID failed;" >&2;
212
- echo "precheck: feature sync + feature update both errored" >&2;
213
- exit 1;
214
- fi;
215
- fi;
216
- fi;
217
- fi;
218
- exit 0
219
- # R2 (0454, 0723): task size precheck — deterministic count-only
220
- # evaluation of R-item and Plan-item counts. No executor-tier policy:
221
- # dispatch-time requiresCapabilities at `agent.run` is the
222
- # authoritative capability check. Writes PASS/FAIL to
223
- # .spur/run/<wbs>-precheck-size.status. Always exit 0 (soft action);
224
- # the precheck→implement guard reads the file, so a missing checker
225
- # fails closed (writes FAIL, never PASS).
181
+ [ "$profile" = auto ] || exit 0; FID=$($spurBin task show $wbs --json 2>/dev/null | jq -r '.feature_id // .frontmatter.feature_id // empty' 2>/dev/null); [ -z "$FID" ] || $spurBin feature sync "$FID" --force 2>/dev/null || $spurBin feature update "$FID" active 2>/dev/null || { echo "precheck: FAIL - feature reactivation $FID failed — feature sync + feature update both errored" >&2; exit 1; }; exit 0
182
+ # (d) task-size-precheck.ts owns the check; shell only resolves it and fails closed (0454 R2 / 0723 R2).
226
183
  - kind: shell
227
184
  options:
228
185
  command: >-
229
- SIZE_FILE=".spur/run/$wbs-precheck-size.status" &&
230
- mkdir -p .spur/run &&
231
- SIZE_SCRIPT="plugins/sp/scripts/task-size-precheck.ts";
232
- [ -f "$SIZE_SCRIPT" ] ||
233
- SIZE_SCRIPT="$(superskill script path sp task-size-precheck.ts 2>/dev/null)";
234
- if [ -n "$SIZE_SCRIPT" ] && [ -f "$SIZE_SCRIPT" ]; then
235
- bun "$SIZE_SCRIPT" "$wbs"
236
- --spur-bin "$spurBin" --max-reqs "$maxImplementReqs"
237
- --max-plan-items "$maxImplementPlanItems";
238
- else
239
- echo "task-size-precheck failed closed — checker not found in" >&2 &&
240
- echo "plugins/sp/scripts/ nor staged; run 'superskill install sp'." >&2 &&
241
- echo "FAIL" > "$SIZE_FILE";
242
- fi &&
243
- exit 0
244
- # 0726 R2: task evidence precheck — deterministic live-data
245
- # evidence-channel proof before implement dispatch. Writes PASS/FAIL to
246
- # .spur/run/<wbs>-precheck-evidence.status. Always exit 0 (soft action);
247
- # the precheck→implement guard reads the file, so a missing checker
248
- # fails closed (writes FAIL, never PASS).
186
+ mkdir -p .spur/run; S=plugins/sp/scripts/task-size-precheck.ts; [ -f "$S" ] || S="$(superskill script path sp task-size-precheck.ts 2>/dev/null)"; if [ -f "$S" ]; then bun "$S" "$wbs" --spur-bin "$spurBin" --max-reqs "$maxImplementReqs" --max-plan-items "$maxImplementPlanItems"; else echo "task-size-precheck failed closed — checker not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'." >&2; echo "FAIL" > ".spur/run/$wbs-precheck-size.status"; fi; exit 0
187
+ # (d) task-evidence-precheck.ts owns the check; shell only resolves it and fails closed (0726 R2).
249
188
  - kind: shell
250
189
  options:
251
190
  command: >-
252
- EVID_FILE=".spur/run/$wbs-precheck-evidence.status" &&
253
- mkdir -p .spur/run &&
254
- EVID_SCRIPT="plugins/sp/scripts/task-evidence-precheck.ts";
255
- [ -f "$EVID_SCRIPT" ] ||
256
- EVID_SCRIPT="$(superskill script path sp task-evidence-precheck.ts 2>/dev/null)";
257
- if [ -n "$EVID_SCRIPT" ] && [ -f "$EVID_SCRIPT" ]; then
258
- bun "$EVID_SCRIPT" "$wbs"
259
- --spur-bin "$spurBin";
260
- else
261
- echo "task-evidence-precheck failed closed — checker not found" >&2 &&
262
- echo "in plugins/sp/scripts/ nor staged; run 'superskill install sp'." >&2 &&
263
- echo "FAIL" > "$EVID_FILE";
264
- fi &&
265
- exit 0
266
- # Proportional route table evaluation (0759 R1/R4). The reason artifact is RUN-scoped, not
267
- # wbs-scoped: ADR-107 names `.spur/run/<runId>-route-reason.txt`, and a wbs-scoped path lets
268
- # a re-run of the same task overwrite the earlier run's route claim, so the artifact could
269
- # not attribute a route to the run that took it (0759 R5). `__runId` is injected by
270
- # WorkflowAppService.run(); the wbs fallback keeps a driver-less invocation from writing to
271
- # a bare "-route-reason.txt". The log line carries the run id for the same reason — an
272
- # unattributed append is log scraping, which R5 explicitly rejects as evidence.
273
- # 0804 R8: the run id must be a single safe filename component before it
274
- # becomes REASON_FILE — unresolved interpolation ($ { } vars.), path
275
- # separators and dot traversal are a failed action (nonzero, no artifact).
276
- # NOTE: no `#` comments inside the folded scalar — `>-` joins all same-indent
277
- # lines into ONE shell line, so an in-scalar `#` comments out the rest of the
278
- # command (the original R8 draft broke exactly this way; the case statement
279
- # must stay one line).
191
+ mkdir -p .spur/run; S=plugins/sp/scripts/task-evidence-precheck.ts; [ -f "$S" ] || S="$(superskill script path sp task-evidence-precheck.ts 2>/dev/null)"; if [ -f "$S" ]; then bun "$S" "$wbs" --spur-bin "$spurBin"; else echo "task-evidence-precheck failed closed — checker not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'." >&2; echo "FAIL" > ".spur/run/$wbs-precheck-evidence.status"; fi; exit 0
192
+ # (e) route-reason lookup and routes log; proportional-routing tests locate it
193
+ # (0759 R1/R5 run-scoped artifact; 0804 R8 run-id safety — one line, no in-scalar `#`).
280
194
  - kind: shell
281
195
  options:
282
196
  command: >-
283
- mkdir -p .spur/run .spur/memory &&
284
- RUN_ID="$__runId" &&
285
- if [ -z "$RUN_ID" ]; then RUN_ID="pipeline-$wbs"; fi &&
286
- case "$RUN_ID" in *'$'*|*'{'*|*'}'*|*vars.*|*/*|*'\'*|*..*) echo "route-reason: refusing unsafe run id: $RUN_ID" >&2; exit 1 ;; *) : ;; esac &&
287
- REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
288
- if [ "$mode" = "fast" ]; then
289
- echo "fast:evidence complete+consistent" > "$REASON_FILE";
290
- elif [ -z "$mode" ]; then
291
- echo "safety:standard verification" > "$REASON_FILE";
292
- elif [ "$mode" = "unknown" ]; then
293
- echo "safety:unknown evidence quality" > "$REASON_FILE";
294
- elif [ "$mode" = "conflict" ]; then
295
- echo "safety:conflicting evidence" > "$REASON_FILE";
296
- else
297
- echo "safety:unrecognized evidence (mode=$mode)" > "$REASON_FILE";
298
- fi &&
299
- printf '%s %s %s\n' "$RUN_ID" "$wbs" "$(cat "$REASON_FILE")"
300
- >> .spur/memory/task-pipeline-routes.log &&
301
- exit 0
197
+ mkdir -p .spur/run .spur/memory; RUN_ID="$__runId"; [ -n "$RUN_ID" ] || RUN_ID="pipeline-$wbs"; case "$RUN_ID" in *'$'*|*'{'*|*'}'*|*vars.*|*/*|*'\'*|*..*) echo "route-reason: refusing unsafe run id: $RUN_ID" >&2; exit 1 ;; esac; REASON_FILE=".spur/run/$RUN_ID-route-reason.txt"; jq -rn --arg m "$mode" '{"fast":"fast:evidence complete+consistent","":"safety:standard verification","unknown":"safety:unknown evidence quality","conflict":"safety:conflicting evidence"}[$m] // "safety:unrecognized evidence (mode=\($m))"' > "$REASON_FILE"; printf '%s %s %s\n' "$RUN_ID" "$wbs" "$(cat "$REASON_FILE")" >> .spur/memory/task-pipeline-routes.log; exit 0
302
198
 
303
199
  - id: implement
304
200
  description: >
@@ -336,23 +232,19 @@ states:
336
232
  requiresCapabilities:
337
233
  fsWrite: available
338
234
  processSpawn: available
339
- - kind: shell
340
- options:
341
- command: >-
342
- retry_transient() {
343
- RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
344
- printf '%s\n' "$RETRY_OUTPUT";
345
- if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
346
- if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked|SQLite database .*is busy|SQLITE_BUSY'; then return "$RETRY_RC"; fi;
347
- sleep 2;
348
- RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
349
- printf '%s\n' "$RETRY_SECOND_OUTPUT";
350
- if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
351
- echo "node_modules link/dependency broken — run bun install and retry";
352
- fi;
353
- return "$RETRY_RC";
354
- };
355
- retry_transient $spurBin task update "$wbs" wip --no-lifecycle
235
+ # (c) command.gate owns the wip transition with classified transient retry.
236
+ - kind: command.gate
237
+ options:
238
+ id: task-implement-transition
239
+ executable: "${vars.spurBin}"
240
+ args: ["task", "update", "${vars.wbs}", "wip", "--no-lifecycle"]
241
+ retry:
242
+ maxAttempts: 2
243
+ delayMs: 2000
244
+ on: ["sqlite-busy", "ENOENT", "EBUSY", "ENOTEMPTY"]
245
+ resultFile: .spur/run/${vars.__runId}-implement-transition.status
246
+ softFail: false
247
+ timeoutMs: 120000
356
248
  # Post-implement cleanup: auto-format any unformatted output the agent produced.
357
249
  # The agent may leave files that don't pass `biome check`, which then fails
358
250
  # `bun run lint` downstream. Running format here is cheap (< 1 s) and prevents
@@ -388,6 +280,7 @@ states:
388
280
  onEnter:
389
281
  # R6/R2 (0703): resolve the task-spec path BEFORE the digest capture. `docs/tasks*` is excluded
390
282
  # from the digest's git-tree half, so the spec is folded in explicitly via `taskSpecPath`.
283
+ # (e) task path + priority resolution; fail-closed on an unresolvable spec (0751 R2, 0710 R4).
391
284
  - kind: shell
392
285
  options:
393
286
  # 0710 R4: resolve the spec path, then extract `priority:` from the TASK FILE itself (not the
@@ -410,6 +303,7 @@ states:
410
303
  # orphan task (no feature_id) resolves to empty — legitimately omitted via empty-string
411
304
  # compatibility; a declared feature whose spec path does not resolve fails closed instead
412
305
  # of silently folding a stale spec into the proof.
306
+ # (e) linked feature-spec path resolution; fail-closed when a declared feature does not resolve (0785 R2).
413
307
  - kind: shell
414
308
  options:
415
309
  command: 'mkdir -p .spur/run; FID=$($spurBin task show $wbs --json 2>/dev/null | jq -r ".feature_id // .frontmatter.feature_id // empty" 2>/dev/null); FPATH_OUT=""; if [ -n "$FID" ]; then FPATH_OUT=$($spurBin feature show "$FID" --json | jq -r ".filePath // .path // empty"); if [ -z "$FPATH_OUT" ]; then echo "fail-closed proof chain (0785 R2): feature $FID linked to task $wbs but its spec path did not resolve" >&2; exit 1; fi; fi; printf "%s" "$FPATH_OUT" > ".spur/run/$wbs-featurepath.txt"'
@@ -425,43 +319,11 @@ states:
425
319
  var: proofDigest
426
320
  taskFile: ${vars.taskSpecPath}
427
321
  featureFile: ${vars.featureSpecPath}
322
+ # (d) quality-gate.ts owns the gate; shell only resolves it and fails closed.
428
323
  - kind: shell
429
324
  options:
430
325
  command: >-
431
- mkdir -p .spur/run &&
432
- STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
433
- LOG_FILE=".spur/run/$wbs-test-gate.log" &&
434
- ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
435
- echo 0 > "$ATTEMPT_FILE" &&
436
- : > "$LOG_FILE" &&
437
- gate_attempt=1;
438
- while [ "$gate_attempt" -le 5 ]; do
439
- ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
440
- sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
441
- gate_locked=0;
442
- grep -Eq 'SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY' "$ATTEMPT_LOG" && gate_locked=1;
443
- cat "$ATTEMPT_LOG" >> "$LOG_FILE";
444
- rm -f "$ATTEMPT_LOG";
445
- if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
446
- printf 'quality gate: database is locked; retrying (%s/5) in 10s\n' "$gate_attempt" | tee -a "$LOG_FILE";
447
- sleep 10;
448
- gate_attempt=$((gate_attempt + 1));
449
- done &&
450
- if [ "$gate_rc" -eq 0 ]; then
451
- printf 'quality gate PASS (attempts: %s; log: %s; bytes: %s)\n' "$gate_attempt" "$LOG_FILE" "$(wc -c < "$LOG_FILE" | tr -d "[:space:]")";
452
- else
453
- printf 'quality gate FAIL — last 40 lines follow (full log: %s)\n' "$LOG_FILE";
454
- tail -n 40 "$LOG_FILE";
455
- fi &&
456
- FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
457
- 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 &&
458
- if [ "$gate_rc" -eq 0 ]; then
459
- printf 'PASS\n' > "$STATUS_FILE";
460
- else
461
- printf 'FAIL\n' > "$STATUS_FILE";
462
- fi &&
463
- printf 'proof-digest: %s\n' "$proofDigest" >> "$LOG_FILE" &&
464
- exit 0
326
+ mkdir -p .spur/run; if [ -f plugins/sp/scripts/quality-gate.ts ]; then bun plugins/sp/scripts/quality-gate.ts run; elif Q="$(superskill script path sp quality-gate.mjs 2>/dev/null)" && [ -f "$Q" ]; then node "$Q" run; else echo "quality gate failed closed — quality-gate script not found — run 'superskill install sp'" >&2; printf 'FAIL\n' > ".spur/run/$wbs-test-gate.status"; fi; exit 0
465
327
 
466
328
  - id: test-fix
467
329
  description: >
@@ -475,22 +337,12 @@ states:
475
337
  - kind: shell
476
338
  options:
477
339
  command: >-
478
- TASK_POLICY=$($spurBin task show "$wbs" --json |
479
- jq -er '.frontmatter.mutationPolicy // ([.content | scan("(?m)^mutationPolicy:[ \t]*([^ \r\n]+)[ \t]*$") | .[0]] | if length == 0 then "code" elif length == 1 then .[0] else "ambiguous" end)') || exit 1;
480
- if [ "$mutationPolicy:$TASK_POLICY" != "code:code" ]; then
481
- echo "test-fix: mutation policy forbids automatic code repair (run=$mutationPolicy, task=$TASK_POLICY); gate failure preserved, no agent dispatched" >&2;
482
- exit 1;
483
- fi;
484
- mkdir -p .spur/run &&
485
- ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
486
- n=$(cat "$ATTEMPT_FILE" 2>/dev/null || echo 0) &&
487
- printf '%s\n' "$((n + 1))" > "$ATTEMPT_FILE" &&
488
- if [ -f ".spur/run/$wbs-verdict.json" ]; then
489
- { echo '--- verify verdict (remediation input, task 0703 R4) ---';
490
- cat ".spur/run/$wbs-verdict.json";
491
- } >> ".spur/run/$wbs-test-gate.log";
492
- fi;
493
- exit 0
340
+ TASK_POLICY=$($spurBin task show "$wbs" --json | jq -er '.frontmatter.mutationPolicy // ([.content | scan("(?m)^mutationPolicy:[ \t]*([^ \r\n]+)[ \t]*$") | .[0]] | if length == 0 then "code" elif length == 1 then .[0] else "ambiguous" end)') || exit 1; [ "$mutationPolicy:$TASK_POLICY" = "code:code" ] || { echo "test-fix: mutation policy forbids automatic code repair (run=$mutationPolicy, task=$TASK_POLICY) — gate failure preserved, no agent dispatched" >&2; exit 1; }
341
+ # (e) fixall attempt counter and verdict hand-off to the remediation log.
342
+ - kind: shell
343
+ options:
344
+ command: >-
345
+ mkdir -p .spur/run; A=".spur/run/$wbs-test-fix-attempt"; n=$(cat "$A" 2>/dev/null || echo 0); printf '%s\n' "$((n + 1))" > "$A"; [ ! -f ".spur/run/$wbs-verdict.json" ] || { echo '--- verify verdict (remediation input, task 0703 R4) ---'; cat ".spur/run/$wbs-verdict.json"; } >> ".spur/run/$wbs-test-gate.log"; exit 0
494
346
  # R3 (0482): project the extracted gate anchors into a var so the dispatch input
495
347
  # can NAME the failing file:line, not merely point at a log. A vars template cannot
496
348
  # run a shell, so the read is an action, not an inline `$(cat ...)` substitution.
@@ -507,6 +359,11 @@ states:
507
359
  # the full-context escape hatch when the digest is not enough.
508
360
  input: /sp:dev-fixall "${vars.qualityGateCmd}" --gate-log .spur/run/${vars.wbs}-test-gate.log --findings "${vars.gateFindings}"
509
361
  timeoutMs: ${vars.stepTimeoutMs}
362
+ # The captured remediation answer is the run's dispatch evidence (0823 R1):
363
+ # expectFile fails the step when no answer was captured. Quality stays with
364
+ # the review proof digest and the verify verdict.
365
+ answerFile: .spur/run/${vars.__runId}-test-fix-answer.txt
366
+ expectFile: .spur/run/${vars.__runId}-test-fix-answer.txt
510
367
  # 0706 R6: bounded remediation hop — unattended and tree-mutating,
511
368
  # so it declares the same minimum requirements as `implement`.
512
369
  requiresCapabilities:
@@ -533,51 +390,12 @@ states:
533
390
  # a green probe (or empty gateProbeCmd) falls through to the full-gate loop unchanged.
534
391
  # Only the full gate writes PASS, so the `test-recheck → review` guard (reads PASS)
535
392
  # still means a full green qualityGateCmd ran — invariant preserved by construction.
393
+ # (d) quality-gate.ts owns the recheck; shell only resolves it and fails closed
394
+ # (0587 R3 probe-then-full: a red gateProbeCmd is the recheck verdict, the full gate is skipped).
536
395
  - kind: shell
537
396
  options:
538
397
  command: >-
539
- mkdir -p .spur/run &&
540
- STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
541
- LOG_FILE=".spur/run/$wbs-test-gate.log" &&
542
- FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
543
- : > "$LOG_FILE" &&
544
- probe_rc=0;
545
- if [ -n "$gateProbeCmd" ]; then
546
- sh -c "$gateProbeCmd" > "$LOG_FILE.probe" 2>&1; probe_rc=$?;
547
- cat "$LOG_FILE.probe" >> "$LOG_FILE";
548
- rm -f "$LOG_FILE.probe";
549
- fi;
550
- if [ "$probe_rc" -ne 0 ]; then
551
- gate_rc=$probe_rc;
552
- else
553
- gate_attempt=1;
554
- while [ "$gate_attempt" -le 5 ]; do
555
- ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
556
- sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
557
- gate_locked=0;
558
- grep -Eq 'SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY' "$ATTEMPT_LOG" && gate_locked=1;
559
- cat "$ATTEMPT_LOG" >> "$LOG_FILE";
560
- rm -f "$ATTEMPT_LOG";
561
- if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
562
- printf 'quality gate: database is locked; retrying (%s/5) in 10s\n' "$gate_attempt" | tee -a "$LOG_FILE";
563
- sleep 10;
564
- gate_attempt=$((gate_attempt + 1));
565
- done;
566
- fi &&
567
- if [ "$gate_rc" -eq 0 ]; then
568
- printf 'quality gate PASS (attempts: %s; log: %s; bytes: %s)\n' "$gate_attempt" "$LOG_FILE" "$(wc -c < "$LOG_FILE" | tr -d "[:space:]")";
569
- else
570
- printf 'quality gate FAIL — last 40 lines follow (full log: %s)\n' "$LOG_FILE";
571
- tail -n 40 "$LOG_FILE";
572
- fi &&
573
- 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 &&
574
- if [ "$gate_rc" -eq 0 ]; then
575
- printf 'PASS\n' > "$STATUS_FILE";
576
- else
577
- printf 'FAIL\n' > "$STATUS_FILE";
578
- fi &&
579
- printf 'proof-digest: %s\n' "$proofDigest" >> "$LOG_FILE" &&
580
- exit 0
398
+ mkdir -p .spur/run; if [ -f plugins/sp/scripts/quality-gate.ts ]; then bun plugins/sp/scripts/quality-gate.ts recheck; elif Q="$(superskill script path sp quality-gate.mjs 2>/dev/null)" && [ -f "$Q" ]; then node "$Q" recheck; else echo "quality gate failed closed — quality-gate script not found — run 'superskill install sp'" >&2; printf 'FAIL\n' > ".spur/run/$wbs-test-gate.status"; fi; exit 0
581
399
 
582
400
  - id: review
583
401
  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`.
@@ -592,6 +410,11 @@ states:
592
410
  # fail-closed) that a P0/P1 task's review resolves a DIFFERENT executor
593
411
  # spec than the implement stage recorded in __agentRouting_implement.
594
412
  input: /sp:dev-review ${vars.wbs} --auto
413
+ # The captured review answer is the run's dispatch evidence (0823 R1):
414
+ # expectFile fails the step when no answer was captured. Quality stays with
415
+ # the review proof digest and the verify verdict.
416
+ answerFile: .spur/run/${vars.__runId}-review-answer.txt
417
+ expectFile: .spur/run/${vars.__runId}-review-answer.txt
595
418
  # Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
596
419
  role: reviewer
597
420
  freshSession: true
@@ -663,6 +486,7 @@ states:
663
486
  # evidence-row identity, before the verdict derivation reads it.
664
487
  # Hard action: a malformed answer halts the sequence here instead of
665
488
  # poisoning the verdict parse downstream.
489
+ # (d) verify-answer-lint.ts owns the answer lint; shell only resolves it.
666
490
  - kind: shell
667
491
  options:
668
492
  command: >-
@@ -679,43 +503,12 @@ states:
679
503
  - kind: shell
680
504
  options:
681
505
  command: "$spurBin task verdict $wbs --from-answer .spur/run/$wbs-verify-answer.txt"
682
- # R3 (0703): write the required proof block into the verdict artifact — the digest, the
683
- # certifying run id, the capture point, and the named per-stage results, each stage carrying
684
- # the SAME digest value (prose asserting proof validity is insufficient). `runId` closes
685
- # 0730 §B.2 (task 0757 R4): without it the verified-outcome fold has to accept ANY linked
686
- # run as certifying, so a dry-run probe linked to the same wbs reads as proof of completion.
687
- # `definitionDigest` closes 0759 R5: the record binds to the certifying run AND the exact
688
- # workflow definition it executed — a stale-definition resume or a definition edited
689
- # between run and record cannot certify silently. `__definitionDigest` is injected at run
690
- # start (workflow-service.ts) and equals the digest stamped on the run row (task 0603).
691
- # 0785 R4: review is stamped `completed` only when the run-scoped marker written by the
692
- # review stage equals the current digest; otherwise `skipped` — an unexecuted review is
693
- # never reported completed, and the verify→record guard plus the bound record registration
694
- # refuse a skipped review (the D9 fast route stays dormant, honestly).
695
- # Also keeps the flat
696
- # `proof-input-digest` check row for consumers that read `checks[]`. Soft action + hard
697
- # guard: a missing/malformed stamp fails the `verify → record` guard below, not this step.
506
+ # (e) one jq mutation binds the verdict to the proof digest (0703 R3; runId 0730 §B.2 /
507
+ # 0757 R4; definitionDigest 0759 R5; honest review stamp 0785 R4; soft action + hard guard).
698
508
  - kind: shell
699
509
  options:
700
- # One folded line: every content line sits at the block's base indentation so
701
- # `>-` joins them with spaces. A more-indented continuation would preserve a
702
- # literal newline and split the jq invocation from its program argument.
703
510
  command: >-
704
- V=".spur/run/$wbs-verdict.json";
705
- RV="skipped";
706
- if [ -f ".spur/run/$__runId-review-proof.digest" ] && [ "$(cat ".spur/run/$__runId-review-proof.digest")" = "$proofDigest" ]; then
707
- RV="completed";
708
- fi;
709
- if [ -f "$V" ] && [ -n "$proofDigest" ]; then
710
- jq --arg d "$proofDigest" --arg r "$__runId" --arg dd "$__definitionDigest" --arg rv "$RV" --arg g "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null || echo UNKNOWN)"
711
- '. + {proof: {digest: $d, runId: $r, definitionDigest: $dd, capturePoint: "quality-gate-entry", stages: {
712
- qualityGate: {status: $g, digest: $d},
713
- review: {status: $rv, digest: $d},
714
- verification: {status: .verdict, digest: $d}}}}
715
- | .checks += [{name: "proof-input-digest", status: "pass", evidence: $d}]'
716
- "$V" > "$V.tmp" && mv "$V.tmp" "$V";
717
- fi;
718
- exit 0
511
+ V=".spur/run/$wbs-verdict.json"; [ -f "$V" ] && [ -n "$proofDigest" ] || exit 0; jq --arg d "$proofDigest" --arg r "$__runId" --arg dd "$__definitionDigest" --arg rp "$(cat .spur/run/$__runId-review-proof.digest 2>/dev/null)" --arg g "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null || echo UNKNOWN)" '. + {proof: {digest: $d, runId: $r, definitionDigest: $dd, capturePoint: "quality-gate-entry", stages: {qualityGate: {status: $g, digest: $d}, review: {status: (if $rp == $d then "completed" else "skipped" end), digest: $d}, verification: {status: .verdict, digest: $d}}}} | .checks += [{name: "proof-input-digest", status: "pass", evidence: $d}]' "$V" > "$V.tmp" && mv "$V.tmp" "$V"; exit 0
719
512
 
720
513
  - id: record
721
514
  description: >
@@ -748,46 +541,26 @@ states:
748
541
  proofBinding: current
749
542
  taskFile: ${vars.taskSpecPath}
750
543
  featureFile: ${vars.featureSpecPath}
544
+ # (c) command.gate owns the record write with classified transient retry.
545
+ - kind: command.gate
546
+ options:
547
+ id: task-record-transition
548
+ executable: "${vars.spurBin}"
549
+ args: ["task", "record", "${vars.wbs}", "--solution-from-diff", "--transition", "testing"]
550
+ retry:
551
+ maxAttempts: 2
552
+ delayMs: 2000
553
+ on: ["sqlite-busy", "ENOENT", "EBUSY", "ENOTEMPTY"]
554
+ resultFile: .spur/run/${vars.__runId}-record-transition.status
555
+ softFail: false
556
+ timeoutMs: 120000
557
+ # (d) feature-sync-bounded.ts owns the sync; shell adds the orphan note and fallbacks
558
+ # (0411 retry-suppression; 0328 / ADR-0322). Best-effort `exit 0` — feature status sync is a
559
+ # follow-up, not a completion gate; `record → done` runs `spur task check`.
751
560
  - kind: shell
752
561
  options:
753
562
  command: >-
754
- retry_transient() {
755
- RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
756
- printf '%s\n' "$RETRY_OUTPUT";
757
- if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
758
- if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked|SQLite database .*is busy|SQLITE_BUSY'; then return "$RETRY_RC"; fi;
759
- sleep 2;
760
- RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
761
- printf '%s\n' "$RETRY_SECOND_OUTPUT";
762
- if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
763
- echo "node_modules link/dependency broken — run bun install and retry";
764
- fi;
765
- return "$RETRY_RC";
766
- };
767
- retry_transient $spurBin task record "$wbs" --solution-from-diff --transition testing
768
- # Best-effort, and portable across layouts. `feature-sync-bounded.ts` is a
769
- # Spur-monorepo path — `spur init` never scaffolds `plugins/sp/`, so a seeded
770
- # project must fall back to the plain `spur feature sync` verb rather than
771
- # abort a run that already produced a PASS verdict. Trailing `exit 0` keeps the
772
- # whole step advisory for the same reason (feature status sync is a follow-up,
773
- # not a completion gate — `record → done` runs `spur task check`).
774
- - kind: shell
775
- options:
776
- command: >-
777
- FID=$($spurBin task show $wbs --json 2>/dev/null |
778
- jq -r ".feature_id // .frontmatter.feature_id // empty" 2>/dev/null);
779
- if [ -n "$FID" ]; then
780
- if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then
781
- bun plugins/sp/scripts/feature-sync-bounded.ts "$FID" --spur-bin "$spurBin" --json;
782
- elif SYNC_MJS="$(superskill script path sp feature-sync-bounded.mjs 2>/dev/null)" && [ -f "$SYNC_MJS" ]; then
783
- node "$SYNC_MJS" "$FID" --spur-bin "$spurBin" --json;
784
- else
785
- $spurBin feature sync "$FID" --json;
786
- fi;
787
- else
788
- echo "Orphan task $wbs — no feature_id linked; proposal: consider linking to a parent feature." >> ".spur/run/$wbs-report.txt";
789
- fi;
790
- exit 0
563
+ FID=$($spurBin task show $wbs --json 2>/dev/null | jq -r '.feature_id // .frontmatter.feature_id // empty' 2>/dev/null); if [ -z "$FID" ]; then echo "Orphan task $wbs — no feature_id linked — proposal: consider linking to a parent feature." >> ".spur/run/$wbs-report.txt"; elif [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then bun plugins/sp/scripts/feature-sync-bounded.ts "$FID" --spur-bin "$spurBin" --json; elif M="$(superskill script path sp feature-sync-bounded.mjs 2>/dev/null)" && [ -f "$M" ]; then node "$M" "$FID" --spur-bin "$spurBin" --json; else $spurBin feature sync "$FID" --json; fi; exit 0
791
564
 
792
565
  - id: done
793
566
  description: >
@@ -795,38 +568,27 @@ states:
795
568
  guard runs `spur task check` before certifying; a genuinely non-compliant
796
569
  task routes to `failed` instead of a silent bad `done`.
797
570
  onEnter:
798
- - kind: shell
799
- options:
800
- command: >-
801
- retry_transient() {
802
- RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
803
- printf '%s\n' "$RETRY_OUTPUT";
804
- if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
805
- if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked|SQLite database .*is busy|SQLITE_BUSY'; then return "$RETRY_RC"; fi;
806
- sleep 2;
807
- RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
808
- printf '%s\n' "$RETRY_SECOND_OUTPUT";
809
- if [ "$RETRY_RC" -ne 0 ] && printf '%s\n%s' "$RETRY_OUTPUT" "$RETRY_SECOND_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY'; then
810
- echo "node_modules link/dependency broken — run bun install and retry";
811
- fi;
812
- return "$RETRY_RC";
813
- };
814
- retry_transient $spurBin task update "$wbs" done --no-lifecycle
571
+ # (c) command.gate owns the done transition with classified transient retry.
572
+ - kind: command.gate
573
+ options:
574
+ id: task-done-transition
575
+ executable: "${vars.spurBin}"
576
+ args: ["task", "update", "${vars.wbs}", "done", "--no-lifecycle"]
577
+ retry:
578
+ maxAttempts: 2
579
+ delayMs: 2000
580
+ on: ["sqlite-busy", "ENOENT", "EBUSY", "ENOTEMPTY"]
581
+ resultFile: .spur/run/${vars.__runId}-done-transition.status
582
+ softFail: false
583
+ timeoutMs: 120000
815
584
  - kind: note
816
585
  options:
817
586
  message: "Pipeline complete for task ${vars.wbs} (done gate cleared)."
818
- # Checkpoint write: record session state for resume (0711 R1–R3;
819
- # run-id/artifact binding corrected by 0784 R4) — canonical frontmatter
820
- # contract; mirrors the Session Checkpoint Convention in
821
- # plugins/sp/skills/spur-dev/references/cross-cutting.md.
822
- # Advisory only: the task file and the persisted run row stay authoritative.
587
+ # (e) advisory terminal checkpoint, one printf (0711 R1–R3; binding corrected 0784 R4).
823
588
  - kind: shell
824
589
  options:
825
- # Single logical line: the composition-baseline argument-split lint flags any
826
- # command whose continuation lines look like argument lists (heredocs with
827
- # `- item` entries trip it), so the checkpoint body is one printf with \n escapes.
828
590
  command: >-
829
- mkdir -p .spur/memory/sessions; CP_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"; CP_COMMIT="$(git rev-parse HEAD 2>/dev/null || echo unknown)"; CP_DIGEST="$proofDigest"; CP_RUN="$__runId"; printf '%s\n' '---' 'schema_version: 1' "session_id: $(date -u +%Y-%m-%d)-$wbs" 'workflow: task-pipeline' "run_id: $CP_RUN" "task_wbs: $wbs" 'feature_id: ""' 'phase: done' 'status: done' 'last_gate: record' "source_commit: $CP_COMMIT" "digest: $CP_DIGEST" "generated_at: $CP_TS" "updated_at: $CP_TS" "next_action: none - task $wbs complete (terminal; advisory only)" 'artifacts:' " - .spur/run/$wbs-verdict.json" " - .spur/run/$wbs-test-gate.log" '---' '' '## Session Notes' '' "Terminal checkpoint for task $wbs (task-pipeline done)." 'Advisory only; the task file is authoritative.' > .spur/memory/sessions/$wbs-checkpoint.md; exit 0
591
+ mkdir -p .spur/memory/sessions; CP_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"; CP_COMMIT="$(git rev-parse HEAD 2>/dev/null || echo unknown)"; printf '%s\n' '---' 'schema_version: 1' "session_id: $(date -u +%Y-%m-%d)-$wbs" 'workflow: task-pipeline' "run_id: $__runId" "task_wbs: $wbs" 'feature_id: ""' 'phase: done' 'status: done' 'last_gate: record' "source_commit: $CP_COMMIT" "digest: $proofDigest" "generated_at: $CP_TS" "updated_at: $CP_TS" "next_action: none - task $wbs complete (terminal; advisory only)" 'artifacts:' " - .spur/run/$wbs-verdict.json" " - .spur/run/$wbs-test-gate.log" '---' '' '## Session Notes' '' "Terminal checkpoint for task $wbs (task-pipeline done)." 'Advisory only; the task file is authoritative.' > .spur/memory/sessions/$wbs-checkpoint.md; exit 0
830
592
 
831
593
  - id: failed
832
594
  description: >
@@ -981,18 +743,12 @@ transitions:
981
743
  to: record
982
744
  description: Verification verdict is PASS and its proof block names the captured digest on every stage — record results and proceed to done.
983
745
  guard:
746
+ # (one jq -e predicate) PASS verdict; proof, quality-gate, review and verification digests
747
+ # equal to proofDigest; review completed; matching run id and definition digest (0703 R5, 0785 R4).
984
748
  kind: shell
985
749
  options:
986
750
  command: >-
987
- V=".spur/run/$wbs-verdict.json";
988
- test "$(jq -r .verdict "$V" 2>/dev/null)" = PASS &&
989
- test "$(jq -r '.proof.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
990
- test "$(jq -r '.proof.stages.qualityGate.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
991
- test "$(jq -r '.proof.stages.review.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
992
- test "$(jq -r '.proof.stages.review.status // ""' "$V" 2>/dev/null)" = "completed" &&
993
- test "$(jq -r '.proof.stages.verification.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
994
- test "$(jq -r '.proof.runId // ""' "$V" 2>/dev/null)" = "$__runId" &&
995
- test "$(jq -r '.proof.definitionDigest // ""' "$V" 2>/dev/null)" = "$__definitionDigest"
751
+ jq -e --arg d "$proofDigest" --arg r "$__runId" --arg dd "$__definitionDigest" '.verdict == "PASS" and (.proof.digest // "") == $d and (.proof.stages.qualityGate.digest // "") == $d and (.proof.stages.review.digest // "") == $d and (.proof.stages.review.status // "") == "completed" and (.proof.stages.verification.digest // "") == $d and (.proof.runId // "") == $r and (.proof.definitionDigest // "") == $dd' ".spur/run/$wbs-verdict.json" >/dev/null 2>&1
996
752
  - from: verify
997
753
  to: test-fix
998
754
  description: >-
@@ -1000,6 +756,7 @@ transitions:
1000
756
  bounded remediation hop (task 0703 R4); the chain re-enters quality → review → verify on a
1001
757
  freshly captured digest. Never reached on PASS: remediation cannot follow certification.
1002
758
  guard:
759
+ # (e) remediation route: verdict not PASS and fix attempts below qualityGateMaxFixAttempts (0703 R4).
1003
760
  kind: shell
1004
761
  options:
1005
762
  command: >-