@gobing-ai/spur 0.3.94 → 0.3.95

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 (140) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +42 -0
  3. package/board/index.d.ts +38 -0
  4. package/config/plugin-scripts.json +5 -0
  5. package/config/workflows/feature-verification.yaml +7 -4
  6. package/config/workflows/idea-pipeline.yaml +10 -4
  7. package/config/workflows/task-pipeline.yaml +54 -26
  8. package/config/workflows/wrapup-pipeline.yaml +92 -17
  9. package/package.json +15 -11
  10. package/plugins/sp/commands/dev-verify.md +7 -4
  11. package/plugins/sp/commands/dev-verifyall.md +4 -3
  12. package/plugins/sp/commands/dev-wrap.md +1 -1
  13. package/plugins/sp/hooks/context-post-tool.ts +2 -2
  14. package/plugins/sp/hooks/context-session-start.ts +2 -1
  15. package/plugins/sp/hooks/context-session-stop.ts +22 -14
  16. package/plugins/sp/hooks/pi/guard-extension.ts +109 -150
  17. package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
  18. package/plugins/sp/lib/inline-run.generated.d.mts +2 -2
  19. package/plugins/sp/lib/inline-run.generated.mjs +10 -10
  20. package/plugins/sp/plugin.json +1 -1
  21. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
  22. package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
  23. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
  24. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
  25. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
  26. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
  27. package/plugins/sp/scripts/feature-sync-bounded.mjs +5 -2
  28. package/plugins/sp/scripts/feature-sync-bounded.ts +3 -1
  29. package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
  30. package/plugins/sp/scripts/history-anatomy-cache.mjs +3 -3
  31. package/plugins/sp/scripts/history-anatomy-cache.ts +4 -4
  32. package/plugins/sp/scripts/inline-run-setup.mjs +23 -13
  33. package/plugins/sp/scripts/inline-run-setup.ts +60 -27
  34. package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
  35. package/plugins/sp/scripts/pr-reviewing.ts +1 -0
  36. package/plugins/sp/scripts/quality-gate.mjs +4 -2
  37. package/plugins/sp/scripts/quality-gate.ts +8 -2
  38. package/plugins/sp/scripts/residual-scan.mjs +3 -3
  39. package/plugins/sp/scripts/residual-scan.ts +3 -3
  40. package/plugins/sp/scripts/script-contract-check.ts +175 -46
  41. package/plugins/sp/scripts/script-root.mjs +125 -0
  42. package/plugins/sp/scripts/script-root.ts +195 -0
  43. package/plugins/sp/scripts/stage-registry-adapter.ts +1 -1
  44. package/plugins/sp/scripts/task-evidence-precheck.ts +4 -2
  45. package/plugins/sp/scripts/task-size-precheck.ts +3 -1
  46. package/plugins/sp/scripts/validate-commands.ts +12 -12
  47. package/plugins/sp/scripts/validate-flag-contracts.ts +42 -30
  48. package/plugins/sp/scripts/verify-answer-lint.ts +3 -1
  49. package/plugins/sp/scripts/workflow-step-profile.mjs +5 -2
  50. package/plugins/sp/scripts/workflow-step-profile.ts +3 -1
  51. package/plugins/sp/scripts/wrapup-drift-probe.mjs +5 -2
  52. package/plugins/sp/scripts/wrapup-drift-probe.ts +4 -2
  53. package/plugins/sp/scripts/wrapup-steps.mjs +32 -8
  54. package/plugins/sp/scripts/wrapup-steps.ts +56 -10
  55. package/plugins/sp/skills/code-verification/SKILL.md +12 -7
  56. package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -0
  57. package/plugins/sp/skills/functional-review/SKILL.md +1 -1
  58. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  59. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +9 -1
  60. package/plugins/sp/skills/spur-dev/references/execution-batch.md +30 -10
  61. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +6 -3
  62. package/plugins/sp/tsconfig.json +8 -0
  63. package/spur.js +5194 -4457
  64. package/web/_astro/{BoardApp.FTEs3-N8.js → BoardApp.BxJuwD7I.js} +150 -153
  65. package/web/_astro/BoardApp.Cpxntzad.js +1 -0
  66. package/web/_astro/TaskDetail.C5bW4WGV.js +1 -0
  67. package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
  68. package/web/_astro/{arc.uuAf51IT.js → arc.CyjRvNMY.js} +1 -1
  69. package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
  70. package/web/_astro/{blockDiagram-GPEHLZMM.D1mGCq3p.js → blockDiagram-GPEHLZMM.Be38USQ2.js} +1 -1
  71. package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
  72. package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
  73. package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
  74. package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
  75. package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
  76. package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
  77. package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
  78. package/web/_astro/{c4Diagram-AAUBKEIU.CMsolcde.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
  79. package/web/_astro/channel.CI6N_tCg.js +1 -0
  80. package/web/_astro/{chunk-2J33WTMH.CrGA3fik.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
  81. package/web/_astro/{chunk-4BX2VUAB.DsLVVla0.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
  82. package/web/_astro/{chunk-55IACEB6.Dxc59Tfi.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
  83. package/web/_astro/{chunk-727SXJPM.CaEVE2Wy.js → chunk-727SXJPM.Bi-TUb_V.js} +1 -1
  84. package/web/_astro/{chunk-AQP2D5EJ.BiJ4HXeI.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
  85. package/web/_astro/{chunk-FMBD7UC4.Mxf1fru5.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
  86. package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
  87. package/web/_astro/{chunk-ND2GUHAM.pyOWQixH.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
  88. package/web/_astro/{chunk-QZHKN3VN.BmEsg4vr.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
  89. package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
  90. package/web/_astro/{classDiagram-4FO5ZUOK.BHhhMFTO.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
  91. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BHhhMFTO.js → classDiagram-v2-Q7XG4LA2.D_apfHS7.js} +1 -1
  92. package/web/_astro/client.BhluLQqe.js +1 -0
  93. package/web/_astro/client.DBpSRLnx.js +9 -0
  94. package/web/_astro/cose-bilkent-S5V4N54A.DNLU_L-x.js +1 -0
  95. package/web/_astro/{cynefin-OW5HDTMX.C2j1_lKL.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
  96. package/web/_astro/{dagre-BM42HDAG.hZ2NCTdT.js → dagre-BM42HDAG.Uc0lvjcV.js} +1 -1
  97. package/web/_astro/{diagram-2AECGRRQ.Bfw5_EzK.js → diagram-2AECGRRQ.DDIxtDBo.js} +1 -1
  98. package/web/_astro/{diagram-5GNKFQAL.Bt1V_Tmk.js → diagram-5GNKFQAL.DVoDnD3H.js} +1 -1
  99. package/web/_astro/{diagram-KO2AKTUF.DEk-YFwp.js → diagram-KO2AKTUF.CRubTJ-0.js} +1 -1
  100. package/web/_astro/{diagram-LMA3HP47.CDjndtQm.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
  101. package/web/_astro/{diagram-OG6HWLK6.cFnUHScG.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
  102. package/web/_astro/{erDiagram-TEJ5UH35.DlhYp7NV.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +1 -1
  103. package/web/_astro/{flowDiagram-I6XJVG4X.DNTpsxfx.js → flowDiagram-I6XJVG4X.DrZcys16.js} +1 -1
  104. package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
  105. package/web/_astro/{gitGraphDiagram-PVQCEYII.DkEqNI0P.js → gitGraphDiagram-PVQCEYII.BWc9oJ4l.js} +1 -1
  106. package/web/_astro/index.5dn-Zm-2.js +1 -0
  107. package/web/_astro/index.C-IqPpW2.js +1 -0
  108. package/web/_astro/index.CENnIEqT.css +1 -0
  109. package/web/_astro/index.Cyc_GA4f.js +1 -0
  110. package/web/_astro/index.tZfqwSut.js +1 -0
  111. package/web/_astro/{infoDiagram-5YYISTIA.CrjioCTG.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
  112. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BcK0CN8n.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +1 -1
  113. package/web/_astro/{journeyDiagram-JHISSGLW.p0CbBDX1.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
  114. package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
  115. package/web/_astro/{kanban-definition-UN3LZRKU.puHIFt6J.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +1 -1
  116. package/web/_astro/{linear.Bb-3a1d7.js → linear.zRsuuDTE.js} +1 -1
  117. package/web/_astro/{mermaid.core.CJDgXOJs.js → mermaid.core.jAJTcMKc.js} +4 -4
  118. package/web/_astro/{mindmap-definition-RKZ34NQL.BoAZEI9t.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +1 -1
  119. package/web/_astro/{pieDiagram-4H26LBE5.CJNSdqY7.js → pieDiagram-4H26LBE5.CacwWaE3.js} +1 -1
  120. package/web/_astro/{quadrantDiagram-W4KKPZXB.DtTy7_0Z.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
  121. package/web/_astro/{requirementDiagram-4Y6WPE33.BNYV98Xg.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +1 -1
  122. package/web/_astro/{sankeyDiagram-5OEKKPKP.DRq9DGHp.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +1 -1
  123. package/web/_astro/{sequenceDiagram-3UESZ5HK.B7KdBFCm.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
  124. package/web/_astro/{stateDiagram-AJRCARHV.CD62ZJ2H.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
  125. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CeVk1TAj.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
  126. package/web/_astro/{timeline-definition-PNZ67QCA.C_ZPA5tT.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +1 -1
  127. package/web/_astro/{vennDiagram-CIIHVFJN.CrThO_FQ.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +1 -1
  128. package/web/_astro/{wardleyDiagram-YWT4CUSO.DSZCA5nl.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +1 -1
  129. package/web/_astro/{xychartDiagram-2RQKCTM6.KI7baTuk.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
  130. package/web/board-runtime.json +69 -0
  131. package/web/index.html +2 -2
  132. package/web/_astro/BoardApp.C02hAHPO.js +0 -1
  133. package/web/_astro/TaskDetail.C-GdsS-t.js +0 -1
  134. package/web/_astro/architectureDiagram-3BPJPVTR.CGe629A1.js +0 -36
  135. package/web/_astro/channel.fsgl7o5j.js +0 -1
  136. package/web/_astro/client.yhYJvxCU.js +0 -9
  137. package/web/_astro/cose-bilkent-S5V4N54A.2fH4YOlp.js +0 -1
  138. package/web/_astro/ganttDiagram-6RSMTGT7.DC_p36PI.js +0 -292
  139. package/web/_astro/index.De90oHcH.js +0 -1
  140. package/web/_astro/index.Hjbr15fG.css +0 -1
@@ -7,7 +7,7 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "sp",
10
- "version": "0.3.94",
10
+ "version": "0.3.95",
11
11
  "source": "./plugins/sp"
12
12
  }
13
13
  ]
package/README.md CHANGED
@@ -59,6 +59,21 @@ If `~/.local/bin` isn't already on your `PATH`, add it (e.g. `export PATH="$HOME
59
59
  in your shell profile). Supported targets: `darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`.
60
60
  Windows: use the Bun path under WSL.
61
61
 
62
+ **Verification.** The installer downloads to a temp file, checks it against the release's
63
+ `SHA256SUMS`, and only then atomically replaces `spur`. A failed download, a checksum mismatch, or a
64
+ missing `SHA256SUMS` leaves an existing install untouched and exits non-zero. `SPUR_RELEASE_URL`
65
+ overrides the download base URL (a mirror, or a `file://` directory in tests).
66
+
67
+ The checksum is a **code-execution** boundary, not just an integrity check: having verified the
68
+ download, the installer *runs* it (`spur init`), so a release whose `SHA256SUMS` cannot be verified
69
+ is a release you cannot safely install. `SPUR_SKIP_VERIFY=1` is the single explicit bypass and
70
+ prints a warning — keep it for local or air-gapped installs, and pipe the script into `sh` only from
71
+ a source you trust.
72
+
73
+ **Releasing standalone binaries.** Run `bun run build:binaries` in `apps/cli`, then upload **all**
74
+ of `dist/cli/spur-*` **and** `dist/cli/SHA256SUMS` to the GitHub Release for the tag. A release
75
+ without `SHA256SUMS` makes the installer fail by default.
76
+
62
77
  ### Package layout (what ships)
63
78
 
64
79
  The published tarball is a self-contained Bun bundle plus static assets:
@@ -98,6 +113,33 @@ spur status
98
113
 
99
114
  Every command supports `--json` for machine-readable output.
100
115
 
116
+ ### Extending the Spur Board
117
+
118
+ A project can add its own Board modules — a compiled React tool or an embedded URL app — without
119
+ editing this package. An installed Board reads them from a fresh project's `.spur/config.yaml`:
120
+
121
+ ```yaml
122
+ bootstrap:
123
+ modules:
124
+ - id: team-board
125
+ name: Team board
126
+ icon: "🧭"
127
+ type: react
128
+ directory: board/team-board/dist
129
+ entry: index.js
130
+ styles: [index.css]
131
+ - id: docs
132
+ name: Documentation
133
+ icon: "📚"
134
+ type: iframe
135
+ url: https://docs.example.test/
136
+ ```
137
+
138
+ `spur init` seeds `modules: []` (built-ins only); change selection with a config edit, then restart
139
+ `spur serve` and reload. The full authoring contract — the declaration union, the
140
+ `@gobing-ai/spur/board` type export, the Vite library setup and the frame/trust limits — is in the
141
+ [downstream Board modules design](https://github.com/gobing-ai/spur/blob/main/docs/design/downstream-board-modules.md).
142
+
101
143
  ## Documentation
102
144
 
103
145
  Full docs, architecture, and the complete command surface live in the
@@ -0,0 +1,38 @@
1
+ // Generated by scripts/commands/emit-board-types.ts from
2
+ // apps/web/src/modules/contribution.ts — edit the authoring source, then regenerate.
3
+ import type { ComponentType } from 'react';
4
+ /**
5
+ * Public authoring contract for a native (compiled React) Board module contribution
6
+ * (A8 / task 0988 R1).
7
+ *
8
+ * This file is the ONE authoring source for the declaration-only `@gobing-ai/spur/board`
9
+ * package export: `scripts/commands/emit-board-types.ts` emits `apps/cli/board/index.d.ts`
10
+ * from it, so the published ABI cannot drift from the Board's own types. Keep it
11
+ * type-only — no React *value* import may reach the CLI or plugin artifacts (the CLI
12
+ * ships no React; the contributor supplies compatible React types through its own
13
+ * devDependencies).
14
+ *
15
+ * Module metadata (id/name/icon/route) is deliberately absent: it is project
16
+ * configuration (one selection authority), while this shape carries only what the
17
+ * renderer needs to mount a contribution.
18
+ */
19
+ export interface BoardModuleContribution {
20
+ /** Contribution protocol version this Board accepts. */
21
+ readonly apiVersion: 1;
22
+ /**
23
+ * Workspace component. Rendered inside the Board's React tree with the Board's own
24
+ * React instance — it must not call `createRoot`, and no private Board context is
25
+ * exported to it.
26
+ */
27
+ readonly component: ComponentType;
28
+ /** Optional companion the Board's right panel renders while this module is active. */
29
+ readonly rightPanelComponent?: ComponentType;
30
+ }
31
+ /**
32
+ * The named export a native Board module entry must publish.
33
+ *
34
+ * Declared rather than implemented: `@gobing-ai/spur/board` is a declaration-only export,
35
+ * not a runtime SDK (consumers import its types; their compiled ESM entry exports this
36
+ * name for the renderer).
37
+ */
38
+ export declare const webModule: BoardModuleContribution;
@@ -93,6 +93,11 @@
93
93
  "rel": "script-contract-check.ts",
94
94
  "contract": "repo-only"
95
95
  },
96
+ {
97
+ "rel": "script-root.ts",
98
+ "contract": "standard",
99
+ "twin": "script-root.mjs"
100
+ },
96
101
  {
97
102
  "rel": "stage-registry-adapter.ts",
98
103
  "contract": "repo-only"
@@ -50,13 +50,16 @@ states:
50
50
  digest, run identity) and the coarse status file, always exit 0 so the
51
51
  transition guard routes missing/corrupt/FAIL to `failed`.
52
52
  onEnter:
53
+ # 0960 R2: run-start script-resolution identity — record which tree this run
54
+ # resolved (source-repo vs installed) before the verification step runs. Never aborts.
53
55
  - kind: shell
54
56
  options:
55
57
  command: >-
56
- bun plugins/sp/scripts/feature-verification-steps.ts verify
57
- --feature-id "$featureId"
58
- --run-id "$__runId"
59
- --spur-bin "$spurBin"
58
+ S=plugins/sp/scripts/script-root.ts; [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp script-root.mjs 2>/dev/null)"; RUNNER=bun; case "$S" in *.mjs) RUNNER=node ;; esac; [ -f "$S" ] && "$RUNNER" "$S" --run-id "$__runId" --spur-bin "$spurBin" || echo "script-root failed closed — run 'superskill install sp --marketplace gobing-ai/spur'" >&2; exit 0
59
+ - kind: shell
60
+ options:
61
+ command: >-
62
+ mkdir -p .spur/run; S=plugins/sp/scripts/feature-verification-steps.ts; [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp feature-verification-steps.mjs 2>/dev/null)"; RUNNER=bun; case "$S" in *.mjs) RUNNER=node ;; esac; [ -f "$S" ] && "$RUNNER" "$S" verify --feature-id "$featureId" --run-id "$__runId" --spur-bin "$spurBin" || printf 'FAIL feature-verification failed closed — script not found — run superskill install sp --marketplace gobing-ai/spur\n' > ".spur/run/$__runId-feature-verification.status"; exit 0
60
63
 
61
64
  - id: done
62
65
  description: Terminal — repo-wide checks passed and the receipt is bound.
@@ -84,6 +84,12 @@ states:
84
84
  handoff — no task execution. Soft agent doctor (status file) so doctor red
85
85
  routes to `failed` via transitions instead of a raw lifecycle abort.
86
86
  onEnter:
87
+ # 0960 R2: run-start script-resolution identity — record which tree this run
88
+ # resolved (source-repo vs installed) before any step executes. Never aborts.
89
+ - kind: shell
90
+ options:
91
+ command: >-
92
+ S=plugins/sp/scripts/script-root.ts; [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp script-root.mjs 2>/dev/null)"; RUNNER=bun; case "$S" in *.mjs) RUNNER=node ;; esac; [ -f "$S" ] && "$RUNNER" "$S" --run-id "$__runId" --spur-bin "$spurBin" || echo "script-root failed closed — run 'superskill install sp --marketplace gobing-ai/spur'" >&2; exit 0
87
93
  - kind: note
88
94
  options:
89
95
  message: "Idea pipeline start for idea: ${vars.idea}"
@@ -254,12 +260,12 @@ states:
254
260
  options:
255
261
  command: >-
256
262
  mkdir -p .spur/run &&
257
- S=plugins/sp/scripts/idea-coverage-check.ts &&
258
- if [ ! -f "$S" ]; then S="$(superskill script path sp idea-coverage-check.ts 2>/dev/null)"; fi &&
263
+ S=plugins/sp/scripts/idea-coverage-check.ts;
264
+ [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp idea-coverage-check.ts 2>/dev/null)";
259
265
  if [ -n "$S" ] && [ -f "$S" ]; then
260
266
  bun "$S" --run-id "$__runId" --report ".spur/run/$__runId-idea-eval-report.md" --ac ".spur/run/$__runId-idea-ac-content.md" || printf 'FAIL run=%s checker exited nonzero (bun missing or checker crash)\n' "$__runId" > ".spur/run/$__runId-idea-coverage.reason";
261
267
  else
262
- printf 'FAIL run=%s checker not found — run superskill install sp\n' "$__runId" | tee ".spur/run/$__runId-idea-coverage.reason" >&2;
268
+ printf 'FAIL run=%s checker not found — run superskill install sp --marketplace gobing-ai/spur\n' "$__runId" | tee ".spur/run/$__runId-idea-coverage.reason" >&2;
263
269
  printf 'FAIL\n' > ".spur/run/$__runId-idea-coverage.status";
264
270
  fi
265
271
  # 0945 R2: derive the route facts ONCE at this boundary (0769 pattern) — guards read these
@@ -558,7 +564,7 @@ states:
558
564
  elif H="$(superskill script path sp idea-handoff.mjs 2>/dev/null)" && [ -f "$H" ]; then
559
565
  node "$H";
560
566
  else
561
- echo "idea handoff failed closed — idea-handoff script not found — run 'superskill install sp'" >&2;
567
+ echo "idea handoff failed closed — idea-handoff script not found — run 'superskill install sp --marketplace gobing-ai/spur'" >&2;
562
568
  exit 1;
563
569
  fi
564
570
  - id: handoff
@@ -13,9 +13,10 @@
13
13
  # `test` is the project quality gate (shell + bounded /sp:dev-fixall), not
14
14
  # /sp:dev-unit (coverage gap-fill; router C3/C5).
15
15
  #
16
- # F96 residual sweep (0950): precheck captures the resume-safe base commit; verify
17
- # scans and folds residuals between the verdict and the proof bind (blocking leftovers
18
- # downgrade PASS → PARTIAL and take the existing remediation edge); done settles
16
+ # F96 residual sweep (0950, reordered 0983): precheck captures the resume-safe base
17
+ # commit; record scans and folds residuals AFTER `task record` flips the verdict-proven
18
+ # boxes (a post-record blocking leftover downgrades PASS → PARTIAL, fails the record →
19
+ # done guard closed, and re-records Testing before the failed edge); done settles
19
20
  # deferrals; failed renders the recovery report. No new state, edge, or model query.
20
21
  #
21
22
  # Vars (passed as a JSON object via `--vars`):
@@ -187,6 +188,12 @@ states:
187
188
  Fast deterministic task readiness and size checks. Failures route to the
188
189
  `failed` terminal state (not a raw lifecycle abort mid-enter).
189
190
  onEnter:
191
+ # 0960 R2: run-start script-resolution identity — record which tree this run
192
+ # resolved (source-repo vs installed) before any step executes. Never aborts.
193
+ - kind: shell
194
+ options:
195
+ command: >-
196
+ S=plugins/sp/scripts/script-root.ts; [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp script-root.mjs 2>/dev/null)"; RUNNER=bun; case "$S" in *.mjs) RUNNER=node ;; esac; [ -f "$S" ] && "$RUNNER" "$S" --run-id "$__runId" --spur-bin "$spurBin" || echo "script-root failed closed — run 'superskill install sp --marketplace gobing-ai/spur'" >&2; exit 0
190
197
  # (e) git-status hygiene advisory; never fails the run (0487 R6 / 0511 R1).
191
198
  - kind: shell
192
199
  options:
@@ -207,12 +214,12 @@ states:
207
214
  - kind: shell
208
215
  options:
209
216
  command: >-
210
- 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
217
+ mkdir -p .spur/run; S=plugins/sp/scripts/task-size-precheck.ts; [ -f config/plugin-scripts.json -a -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 --marketplace gobing-ai/spur'." >&2; echo "FAIL" > ".spur/run/$wbs-precheck-size.status"; fi; exit 0
211
218
  # (d) task-evidence-precheck.ts owns the check; shell only resolves it and fails closed (0726 R2).
212
219
  - kind: shell
213
220
  options:
214
221
  command: >-
215
- 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
222
+ mkdir -p .spur/run; S=plugins/sp/scripts/task-evidence-precheck.ts; [ -f config/plugin-scripts.json -a -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 --marketplace gobing-ai/spur'." >&2; echo "FAIL" > ".spur/run/$wbs-precheck-evidence.status"; fi; exit 0
216
223
  # (e) F96 R1 (0950): capture the run's base commit once — a resumed or re-entered run
217
224
  # keeps its original base so residual scans stay anchored to the same diff.
218
225
  - kind: shell
@@ -432,7 +439,7 @@ states:
432
439
  - kind: shell
433
440
  options:
434
441
  command: >-
435
- 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
442
+ mkdir -p .spur/run; if [ -f config/plugin-scripts.json -a -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 --marketplace gobing-ai/spur'" >&2; printf 'FAIL\n' > ".spur/run/$wbs-test-gate.status"; fi; exit 0
436
443
 
437
444
  - id: test-fix
438
445
  description: >
@@ -515,7 +522,7 @@ states:
515
522
  - kind: shell
516
523
  options:
517
524
  command: >-
518
- 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
525
+ mkdir -p .spur/run; if [ -f config/plugin-scripts.json -a -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 --marketplace gobing-ai/spur'" >&2; printf 'FAIL\n' > ".spur/run/$wbs-test-gate.status"; fi; exit 0
519
526
 
520
527
  - id: triage
521
528
  description: >-
@@ -535,7 +542,7 @@ states:
535
542
  - kind: shell
536
543
  options:
537
544
  command: >-
538
- mkdir -p .spur/run; if [ -f plugins/sp/scripts/task-diffstat.ts ]; then bun plugins/sp/scripts/task-diffstat.ts --spur-bin "$spurBin"; elif D="$(superskill script path sp task-diffstat.mjs 2>/dev/null)" && [ -f "$D" ]; then node "$D"; else echo "task-diffstat failed closed — task-diffstat script not found — run 'superskill install sp'" >&2; printf '{"files":0,"insertions":0,"deletions":0,"paths":[],"sensitive":true}\n' > ".spur/run/$wbs-diffstat.json"; fi; exit 0
545
+ mkdir -p .spur/run; if [ -f config/plugin-scripts.json -a -f plugins/sp/scripts/task-diffstat.ts ]; then bun plugins/sp/scripts/task-diffstat.ts --spur-bin "$spurBin"; elif D="$(superskill script path sp task-diffstat.mjs 2>/dev/null)" && [ -f "$D" ]; then node "$D"; else echo "task-diffstat failed closed — task-diffstat script not found — run 'superskill install sp --marketplace gobing-ai/spur'" >&2; printf '{"files":0,"insertions":0,"deletions":0,"paths":[],"sensitive":true}\n' > ".spur/run/$wbs-diffstat.json"; fi; exit 0
539
546
  # (b)+(c) lane pre-guard BEFORE decide: a caller-set mode is projected verbatim (R2b);
540
547
  # a sensitive path or >400 changed lines pins safety (R2c) and the decide result below
541
548
  # is never consulted. A missing/malformed diffstat row (producer crashed or never ran,
@@ -706,10 +713,10 @@ states:
706
713
  options:
707
714
  command: >-
708
715
  LINT_SCRIPT="plugins/sp/scripts/verify-answer-lint.ts";
709
- [ -f "$LINT_SCRIPT" ] ||
716
+ [ -f config/plugin-scripts.json -a -f "$LINT_SCRIPT" ] ||
710
717
  LINT_SCRIPT="$(superskill script path sp verify-answer-lint.ts 2>/dev/null)";
711
718
  if [ -z "$LINT_SCRIPT" ] || [ ! -f "$LINT_SCRIPT" ]; then
712
- echo "verify-answer-lint: checker not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'" >&2;
719
+ echo "verify-answer-lint: checker not found in plugins/sp/scripts/ nor staged — run 'superskill install sp --marketplace gobing-ai/spur'" >&2;
713
720
  exit 1;
714
721
  fi;
715
722
  bun "$LINT_SCRIPT" "$wbs"
@@ -718,16 +725,6 @@ states:
718
725
  - kind: shell
719
726
  options:
720
727
  command: "$spurBin task verdict $wbs --from-answer .spur/run/$wbs-verify-answer.txt"
721
- # (d) F96 R2 (0950): residual-scan.ts owns the sweep; shell only resolves it.
722
- # ONE hard action (scan+fold): a scanner crash fails verify closed — the
723
- # verify→failed catch-all routes a missing/malformed verdict. Runs AFTER
724
- # `task verdict` and BEFORE the jq bind so a blocking residual turns PASS
725
- # into PARTIAL (fold rewrites the verdict artifact) and takes the existing
726
- # verify→test-fix edge while attempts remain.
727
- - kind: shell
728
- options:
729
- command: >-
730
- S=plugins/sp/scripts/residual-scan.ts; [ -f "$S" ] || S="$(superskill script path sp residual-scan.mjs 2>/dev/null)"; if [ ! -f "$S" ]; then echo "residual-scan failed closed — scanner not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'" >&2; exit 1; fi; case "$S" in *.mjs) RUNNER=node ;; *) RUNNER=bun ;; esac; "$RUNNER" "$S" scan "$wbs" --spur-bin "$spurBin" && "$RUNNER" "$S" fold "$wbs" --spur-bin "$spurBin"
731
728
  # (e) one jq mutation binds the verdict to the proof digest (0703 R3; runId 0730 §B.2 /
732
729
  # 0757 R4; definitionDigest 0759 R5; honest review stamp 0785 R4; soft action + hard guard).
733
730
  - kind: shell
@@ -745,7 +742,11 @@ states:
745
742
  evidence (0785 R4) — before any task record or status mutation. Post-record step
746
743
  conditionally syncs feature status via the bounded `feature-sync-bounded` wrapper
747
744
  (task 0411 retry-suppression) if `feature_id` is present, or appends an orphan
748
- link proposal to the run report if absent (task 0328 / ADR-0322).
745
+ link proposal to the run report if absent (task 0328 / ADR-0322). The post-record
746
+ residual sweep (0983) re-scans the task file after the verdict-proven box flips and
747
+ folds blocking leftovers into the verdict before `record → done`; a downgrade
748
+ re-records Testing from the final artifact so a failed task never keeps a stale
749
+ PASS narrative.
749
750
  onEnter:
750
751
  # 0785 R3: BOUND verify-verdict registration — FIRST action, before any task record or
751
752
  # status mutation, replacing the redundant proof.fingerprint compare (the action itself
@@ -771,7 +772,11 @@ states:
771
772
  options:
772
773
  id: task-record-transition
773
774
  executable: "${vars.spurBin}"
774
- args: ["task", "record", "${vars.wbs}", "--solution-from-diff", "--transition", "testing"]
775
+ # 0980: --no-lifecycle mirrors the wip/done hops — the pipeline run is
776
+ # already the lifecycle record, so a record-stage transition must not
777
+ # spawn a nested task-lifecycle run that outlives the pipeline as a
778
+ # `running` orphan. The target-aware check gate still runs (inline).
779
+ args: ["task", "record", "${vars.wbs}", "--solution-from-diff", "--transition", "testing", "--no-lifecycle"]
775
780
  retry:
776
781
  maxAttempts: 2
777
782
  delayMs: 2000
@@ -791,7 +796,30 @@ states:
791
796
  - kind: shell
792
797
  options:
793
798
  command: >-
794
- S=plugins/sp/scripts/record-feature-sync.ts; [ -f "$S" ] || S="$(superskill script path sp record-feature-sync.mjs 2>/dev/null)"; [ "$deferFeatureSync" = "true" ] && echo "feature sync deferred to batch integration" >> ".spur/run/$wbs-report.txt" || if [ -f "$S" ]; then bun "$S" --spur-bin "$spurBin"; else echo "feature sync skipped — record-feature-sync not found in plugins/sp/scripts/ nor staged" >> ".spur/run/$wbs-report.txt"; fi; exit 0
799
+ S=plugins/sp/scripts/record-feature-sync.ts; [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp record-feature-sync.mjs 2>/dev/null)"; [ "$deferFeatureSync" = "true" ] && echo "feature sync deferred to batch integration" >> ".spur/run/$wbs-report.txt" || if [ -f "$S" ]; then bun "$S" --spur-bin "$spurBin"; else echo "feature sync skipped — record-feature-sync not found in plugins/sp/scripts/ nor staged" >> ".spur/run/$wbs-report.txt"; fi; exit 0
800
+ # (d) F96 (0983): residual-scan.ts owns the sweep; shell only resolves it. The
801
+ # scan moved here from verify: it must read the task file AFTER `task record`
802
+ # flipped the verdict-proven Requirement/AC boxes (0692), or a proven-but-unticked
803
+ # box folds every good run to PARTIAL and the done guard is unreachable (0967).
804
+ # Classification unchanged: any box still unchecked post-record (unproven R/AC,
805
+ # unfinished Plan) stays blocking and fails the record → done guard closed.
806
+ # ONE hard action (scan+fold): a scanner crash fails record closed.
807
+ - kind: shell
808
+ options:
809
+ command: >-
810
+ S=plugins/sp/scripts/residual-scan.ts; [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp residual-scan.mjs 2>/dev/null)"; if [ ! -f "$S" ]; then echo "residual-scan failed closed — scanner not found in plugins/sp/scripts/ nor staged — run 'superskill install sp --marketplace gobing-ai/spur'" >&2; exit 1; fi; case "$S" in *.mjs) RUNNER=node ;; *) RUNNER=bun ;; esac; "$RUNNER" "$S" scan "$wbs" --spur-bin "$spurBin" && "$RUNNER" "$S" fold "$wbs" --spur-bin "$spurBin"
811
+ # (e) F96 R5 (0983): a sweep downgrade must reach the task record — re-record from
812
+ # the FINAL (post-fold) artifact so Testing carries the downgraded verdict; a failed
813
+ # task must not retain a stale PASS Testing narrative. Soft (exit 0): the downgrade
814
+ # itself already fails the record → done guard; a re-record failure prints its
815
+ # re-run command and the failed state renders the recovery report.
816
+ # 0980: --no-lifecycle for the same orphan reason as the main record command —
817
+ # inert today (the task is already at testing, so this is a same-status no-op),
818
+ # but it keeps the no-nested-lifecycle invariant if the flow shifts.
819
+ - kind: shell
820
+ options:
821
+ command: >-
822
+ verdict="$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)"; if [ -n "$verdict" ] && [ "$verdict" != PASS ]; then $spurBin task record $wbs --solution-from-diff --transition testing --no-lifecycle || echo "post-downgrade re-record failed — re-run: $spurBin task record $wbs" >&2; fi; exit 0
795
823
 
796
824
  - id: done
797
825
  description: >
@@ -805,7 +833,7 @@ states:
805
833
  - kind: shell
806
834
  options:
807
835
  command: >-
808
- S=plugins/sp/scripts/residual-scan.ts; [ -f "$S" ] || S="$(superskill script path sp residual-scan.mjs 2>/dev/null)"; if [ -f "$S" ]; then case "$S" in *.mjs) RUNNER=node ;; *) RUNNER=bun ;; esac; "$RUNNER" "$S" settle "$wbs" --spur-bin "$spurBin" || echo "residual-settle failed — re-run: residual-scan settle $wbs" >&2; else echo "residual-scan not staged — settle skipped — re-run: residual-scan settle $wbs" >&2; fi; exit 0
836
+ S=plugins/sp/scripts/residual-scan.ts; [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp residual-scan.mjs 2>/dev/null)"; if [ -f "$S" ]; then case "$S" in *.mjs) RUNNER=node ;; *) RUNNER=bun ;; esac; "$RUNNER" "$S" settle "$wbs" --spur-bin "$spurBin" || echo "residual-settle failed — re-run: residual-scan settle $wbs" >&2; else echo "residual-scan not staged — settle skipped — re-run: residual-scan settle $wbs" >&2; fi; exit 0
809
837
  # (c) command.gate owns the done transition with classified transient retry.
810
838
  - kind: command.gate
811
839
  options:
@@ -852,7 +880,7 @@ states:
852
880
  - kind: shell
853
881
  options:
854
882
  command: >-
855
- S=plugins/sp/scripts/residual-scan.ts; [ -f "$S" ] || S="$(superskill script path sp residual-scan.mjs 2>/dev/null)"; if [ -f "$S" ]; then case "$S" in *.mjs) RUNNER=node ;; *) RUNNER=bun ;; esac; "$RUNNER" "$S" report "$wbs" --spur-bin "$spurBin" || echo "residual-report failed — re-run: residual-scan report $wbs" >&2; else echo "residual-scan not staged — report skipped — re-run: residual-scan report $wbs" >&2; fi; exit 0
883
+ S=plugins/sp/scripts/residual-scan.ts; [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp residual-scan.mjs 2>/dev/null)"; if [ -f "$S" ]; then case "$S" in *.mjs) RUNNER=node ;; *) RUNNER=bun ;; esac; "$RUNNER" "$S" report "$wbs" --spur-bin "$spurBin" || echo "residual-report failed — re-run: residual-scan report $wbs" >&2; else echo "residual-scan not staged — report skipped — re-run: residual-scan report $wbs" >&2; fi; exit 0
856
884
 
857
885
  - id: cancelled
858
886
  description: Terminal — pipeline cancelled by operator at the approval gate (R1).
@@ -1163,6 +1191,6 @@ transitions:
1163
1191
  - from: record
1164
1192
  to: failed
1165
1193
  terminalReason: failed-check
1166
- description: Task check failed or proof evidence missing/malformed/mismatched — block before done.
1194
+ description: Task check failed, a post-record residual downgrade, or proof evidence missing/malformed/mismatched — block before done.
1167
1195
  guard:
1168
1196
  kind: always
@@ -13,13 +13,18 @@
13
13
  # pauses,
14
14
  # even under --auto (Iron Law #6: irreversible action -> surface to human).
15
15
  #
16
- # Shape: start -> task-resolve -> doc-sync -> learnings-append -> metrics-record
16
+ # Shape: start -> task-resolve -> doc-sync -> learnings-validate -> learnings-append
17
+ # -> metrics-record
17
18
  # -> feature-transition (conditional: if vars.feature set)
18
19
  # -> feature-verify (post-transition feature check gate, 0915 F1)
19
20
  # -> branch-cleanup (conditional: if vars.merge=true)
20
21
  # -> done
21
22
  # doc-sync routes a contract violation to `repair` (cheap, no
22
23
  # re-dispatch — ADR-118 pilot) and an executor failure to `failed`;
24
+ # learnings-validate (0986) rejects a nonempty narration-only capture to
25
+ # the same `repair` edge with a distinct invalid-learnings-shape status —
26
+ # expectFile proves existence, never shape — while empty/missing captures
27
+ # keep the soft skip by passing through to learnings-append;
23
28
  # task-resolve with a VALIDATED empty list short-circuits to `skipped`;
24
29
  # invalid wrap input, an unresolved/non-completed task, a failed metrics
25
30
  # capture, a failed required sync, or a failed feature gate routes to
@@ -75,7 +80,7 @@
75
80
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
76
81
  kind: state-machine
77
82
  name: wrapup-pipeline
78
- version: "5"
83
+ version: "6"
79
84
  description: "Post-execution wrap-up: doc-sync (doc drift + learning capture), metrics, feature-transition, branch-cleanup"
80
85
  iterationBound: 10
81
86
  initialState: start
@@ -108,6 +113,12 @@ states:
108
113
  Pipeline start. Task statuses are NOT mutated by wrap-up — wrap-up consumes
109
114
  completed tasks and produces learning/metrics/doc artifacts only.
110
115
  onEnter:
116
+ # 0960 R2: run-start script-resolution identity — record which tree this run
117
+ # resolved (source-repo vs installed) before any step executes. Never aborts.
118
+ - kind: shell
119
+ options:
120
+ command: >-
121
+ S=plugins/sp/scripts/script-root.ts; [ -f config/plugin-scripts.json -a -f "$S" ] || S="$(superskill script path sp script-root.mjs 2>/dev/null)"; RUNNER=bun; case "$S" in *.mjs) RUNNER=node ;; esac; [ -f "$S" ] && "$RUNNER" "$S" --run-id "$__runId" --spur-bin "$spurBin" || echo "script-root failed closed — run 'superskill install sp --marketplace gobing-ai/spur'" >&2; exit 0
111
122
  - kind: note
112
123
  options:
113
124
  message: "Wrap-up pipeline start for tasks: ${vars.tasks}. Task statuses are NOT mutated."
@@ -143,12 +154,12 @@ states:
143
154
  options:
144
155
  command: >-
145
156
  mkdir -p .spur/run &&
146
- if [ -f plugins/sp/scripts/wrapup-steps.ts ]; then
157
+ if [ -f config/plugin-scripts.json -a -f plugins/sp/scripts/wrapup-steps.ts ]; then
147
158
  bun plugins/sp/scripts/wrapup-steps.ts resolve;
148
159
  elif W="$(superskill script path sp wrapup-steps.mjs 2>/dev/null)" && [ -f "$W" ]; then
149
160
  node "$W" resolve;
150
161
  else
151
- echo "wrapup-steps failed closed — script not found — run 'superskill install sp'" >&2;
162
+ echo "wrapup-steps failed closed — script not found — run 'superskill install sp --marketplace gobing-ai/spur'" >&2;
152
163
  printf 'FAIL\n' > ".spur/run/$__runId-wrapup-resolve.status";
153
164
  fi
154
165
  # (0944 R1-R3) deterministic drift probe — runs ONLY when the caller left mode
@@ -163,7 +174,7 @@ states:
163
174
  command: >-
164
175
  if [ -n "$mode" ]; then
165
176
  printf '%s\n' "$mode" > ".spur/run/$__runId-mode.txt";
166
- elif [ -f plugins/sp/scripts/wrapup-drift-probe.ts ]; then
177
+ elif [ -f config/plugin-scripts.json -a -f plugins/sp/scripts/wrapup-drift-probe.ts ]; then
167
178
  bun plugins/sp/scripts/wrapup-drift-probe.ts;
168
179
  elif W="$(superskill script path sp wrapup-drift-probe.mjs 2>/dev/null)" && [ -f "$W" ]; then
169
180
  node "$W";
@@ -190,12 +201,12 @@ states:
190
201
  options:
191
202
  command: >-
192
203
  mkdir -p .spur/run .spur/memory &&
193
- if [ -f plugins/sp/scripts/wrapup-steps.ts ]; then
204
+ if [ -f config/plugin-scripts.json -a -f plugins/sp/scripts/wrapup-steps.ts ]; then
194
205
  bun plugins/sp/scripts/wrapup-steps.ts route-reason;
195
206
  elif W="$(superskill script path sp wrapup-steps.mjs 2>/dev/null)" && [ -f "$W" ]; then
196
207
  node "$W" route-reason;
197
208
  else
198
- echo "wrapup-steps failed closed — script not found — run 'superskill install sp'" >&2;
209
+ echo "wrapup-steps failed closed — script not found — run 'superskill install sp --marketplace gobing-ai/spur'" >&2;
199
210
  printf 'FAIL\n' > ".spur/run/$__runId-wrapup-resolve.status";
200
211
  fi
201
212
 
@@ -234,6 +245,34 @@ states:
234
245
  expectFile: .spur/run/${vars.__runId}-wrapup-learnings.md
235
246
  timeoutMs: ${vars.stepTimeoutMs}
236
247
 
248
+ - id: learnings-validate
249
+ description: >
250
+ Structural shape gate between doc-sync and the append (0986): expectFile
251
+ asserts the capture EXISTS and nothing about its shape, so a
252
+ stream-of-thought capture could be appended verbatim to the tracked
253
+ memory file. This state classifies a NONEMPTY capture — a date, a
254
+ four-digit task WBS, and at least one markdown bullet — and writes PASS
255
+ or invalid-learnings-shape to a run-scoped status. An empty or missing
256
+ capture keeps the existing soft skip (PASS) so learnings-append still
257
+ performs its no-op; an unclassifiable status fails loud instead of
258
+ appending it. Structural check only — the doc-sync prompt owns prose
259
+ quality; no new engine-level content contract.
260
+ onEnter:
261
+ # (warn) structural check only; the doc-sync prompt owns prose quality (0986 Design).
262
+ # The WBS predicate excludes digit/dash neighbours so a date's year cannot satisfy it.
263
+ - kind: shell
264
+ options:
265
+ command: >-
266
+ mkdir -p .spur/run &&
267
+ S=".spur/run/$__runId-wrapup-learnings.status" &&
268
+ F=".spur/run/$__runId-wrapup-learnings.md" &&
269
+ if [ ! -s "$F" ] || awk '/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/{d=1} /(^|[^0-9-])[0-9][0-9][0-9][0-9]([^0-9-]|$)/{w=1} /^[[:space:]]*[-*] /{b=1} END{exit !(d+w+b==3)}' "$F"; then
270
+ printf 'PASS\n' > "$S";
271
+ else
272
+ printf 'invalid-learnings-shape\n' > "$S";
273
+ fi &&
274
+ exit 0
275
+
237
276
  - id: learnings-append
238
277
  description: >
239
278
  Best-effort soft append of the captured learnings to .spur/memory/learnings.md
@@ -259,15 +298,22 @@ states:
259
298
  description: >
260
299
  Contract-violation repair (ADR-118 pilot, 0871): doc-sync's agent.run missed
261
300
  its declared post-condition after a clean exit, so wrap-up does NOT re-dispatch
262
- the expensive agent on this attempt. The miss is recorded to a run-scoped
263
- repair status and wrap-up proceeds to metrics-record — the run log and action
264
- trace already name the violated contract and the observed value.
301
+ the expensive agent on this attempt. Also the destination for a nonempty
302
+ capture that fails learnings-validate's shape gate (0986). The miss is
303
+ recorded to a run-scoped repair status — text distinguishing a narration-only
304
+ shape failure from the missing-file contract violation — and wrap-up proceeds
305
+ to metrics-record; the run log and action trace already name the violated
306
+ contract and the observed value.
265
307
  onEnter:
266
308
  - kind: shell
267
309
  options:
268
310
  command: >-
269
311
  mkdir -p .spur/run &&
270
- printf 'contract-violation: doc-sync learnings capture missed its declared contract — repair skipped re-dispatch (ADR-118 pilot)\n' > ".spur/run/$__runId-wrapup-repair.status" &&
312
+ R=".spur/run/$__runId-wrapup-repair.status" &&
313
+ V="$(cat .spur/run/$__runId-wrapup-learnings.status 2>/dev/null)" &&
314
+ if [ "$V" = invalid-learnings-shape ]; then printf 'invalid-learnings-shape: narration-only learnings capture — append skipped, repair recorded (0986)\n' > "$R";
315
+ else printf 'contract-violation: doc-sync learnings capture missed its declared contract — repair skipped re-dispatch (ADR-118 pilot)\n' > "$R";
316
+ fi &&
271
317
  exit 0
272
318
 
273
319
  - id: metrics-record
@@ -288,12 +334,12 @@ states:
288
334
  options:
289
335
  command: >-
290
336
  mkdir -p .spur/run &&
291
- if [ -f plugins/sp/scripts/wrapup-steps.ts ]; then
337
+ if [ -f config/plugin-scripts.json -a -f plugins/sp/scripts/wrapup-steps.ts ]; then
292
338
  bun plugins/sp/scripts/wrapup-steps.ts metrics;
293
339
  elif W="$(superskill script path sp wrapup-steps.mjs 2>/dev/null)" && [ -f "$W" ]; then
294
340
  node "$W" metrics;
295
341
  else
296
- echo "wrapup-steps failed closed — script not found — run 'superskill install sp'" >&2;
342
+ echo "wrapup-steps failed closed — script not found — run 'superskill install sp --marketplace gobing-ai/spur'" >&2;
297
343
  printf 'FAIL\n' > ".spur/run/$__runId-wrapup-metrics.status";
298
344
  fi
299
345
 
@@ -322,12 +368,12 @@ states:
322
368
  options:
323
369
  command: >-
324
370
  mkdir -p .spur/run &&
325
- if [ -f plugins/sp/scripts/wrapup-steps.ts ]; then
371
+ if [ -f config/plugin-scripts.json -a -f plugins/sp/scripts/wrapup-steps.ts ]; then
326
372
  bun plugins/sp/scripts/wrapup-steps.ts feature-transition;
327
373
  elif W="$(superskill script path sp wrapup-steps.mjs 2>/dev/null)" && [ -f "$W" ]; then
328
374
  node "$W" feature-transition;
329
375
  else
330
- echo "wrapup-steps failed closed — script not found — run 'superskill install sp'" >&2;
376
+ echo "wrapup-steps failed closed — script not found — run 'superskill install sp --marketplace gobing-ai/spur'" >&2;
331
377
  printf 'FAIL\n' > ".spur/run/$__runId-wrapup-sync.status";
332
378
  fi
333
379
 
@@ -460,8 +506,8 @@ transitions:
460
506
  guard:
461
507
  kind: contract-violation
462
508
  - from: doc-sync
463
- to: learnings-append
464
- description: Doc drift repair + learning capture succeeded — append learnings.
509
+ to: learnings-validate
510
+ description: Doc drift repair + learning capture succeeded — validate the capture's shape before appending.
465
511
  guard:
466
512
  kind: action-ok
467
513
  - from: doc-sync
@@ -475,6 +521,35 @@ transitions:
475
521
  guard:
476
522
  kind: always
477
523
 
524
+ # ── learnings-validate: shape gate (0986) — invalid-shape to repair, PASS to append ──
525
+ # Declaration order matters: the discriminating invalid-shape edge is tried
526
+ # first, then PASS; an unreadable status fails loud rather than appending an
527
+ # unclassified capture.
528
+ - from: learnings-validate
529
+ to: repair
530
+ description: >
531
+ Nonempty capture failed the structural shape gate (no date/WBS/bullet) —
532
+ route to the cheap repair edge and never append narration (0986 R2).
533
+ guard:
534
+ kind: shell
535
+ options:
536
+ command: 'test "$(cat .spur/run/$__runId-wrapup-learnings.status 2>/dev/null)" = invalid-learnings-shape'
537
+ - from: learnings-validate
538
+ to: learnings-append
539
+ description: Capture is shape-valid (or empty/missing, keeping the soft skip) — append.
540
+ guard:
541
+ kind: shell
542
+ options:
543
+ command: 'test "$(cat .spur/run/$__runId-wrapup-learnings.status 2>/dev/null)" = PASS'
544
+ - from: learnings-validate
545
+ to: failed
546
+ terminalReason: failed-check
547
+ description: >
548
+ Validation status missing or unreadable — fail loud instead of appending
549
+ a capture whose shape was never classified.
550
+ guard:
551
+ kind: always
552
+
478
553
  # ── linear body: learnings-append -> metrics-record ──
479
554
  - from: learnings-append
480
555
  to: metrics-record