@gobing-ai/spur 0.3.80 → 0.3.82

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 +31 -18
  3. package/config/config.global.yaml +13 -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/docs/99_PROJECT_CONSTITUTION.md +75 -13
  33. package/config/transition-shims.json +0 -7
  34. package/config/workflows/basic.yaml +4 -0
  35. package/config/workflows/docs-pipeline.yaml +13 -14
  36. package/config/workflows/feature-dev.yaml +20 -65
  37. package/config/workflows/history-anatomy.yaml +22 -1
  38. package/config/workflows/idea-pipeline.yaml +53 -97
  39. package/config/workflows/pr-review.yaml +21 -33
  40. package/config/workflows/task-pipeline.yaml +87 -330
  41. package/config/workflows/wayfinder-resolution.yaml +12 -26
  42. package/config/workflows/wrapup-pipeline.yaml +48 -189
  43. package/package.json +9 -9
  44. package/plugins/sp/README.md +12 -3
  45. package/plugins/sp/agents/expert-spur.md +41 -19
  46. package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
  47. package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
  48. package/plugins/sp/plugin.json +1 -1
  49. package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
  50. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  51. package/plugins/sp/scripts/idea-handoff.mjs +27 -0
  52. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  53. package/plugins/sp/scripts/inline-run-setup.ts +69 -1
  54. package/plugins/sp/scripts/quality-gate.mjs +216 -0
  55. package/plugins/sp/scripts/quality-gate.ts +287 -0
  56. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -2
  57. package/plugins/sp/scripts/task-size-precheck.ts +44 -16
  58. package/plugins/sp/scripts/verify-answer-lint.ts +17 -1
  59. package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
  60. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  61. package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
  62. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
  63. package/plugins/sp/skills/code-review/references/review-lenses.md +3 -0
  64. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  65. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
  66. package/plugins/sp/skills/spur-cli/SKILL.md +4 -8
  67. package/plugins/sp/skills/spur-cli/references/agent.md +44 -69
  68. package/plugins/sp/skills/spur-cli/references/message.md +30 -3
  69. package/plugins/sp/skills/spur-cli/references/projects.md +25 -1
  70. package/plugins/sp/skills/spur-cli/references/self.md +6 -5
  71. package/plugins/sp/skills/spur-cli/references/serve.md +8 -7
  72. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
  73. package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
  74. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
  75. package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
  76. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +10 -0
  77. package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
  78. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +4 -6
  79. package/plugins/sp/skills/spur-dev/references/glossary.md +1 -1
  80. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +11 -1
  81. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
  82. package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
  83. package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
  84. package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
  85. package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
  86. package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
  87. package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
  88. package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
  89. package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
  90. package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
  91. package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
  92. package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
  93. package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
  94. package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
  95. package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
  96. package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
  97. package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
  98. package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
  99. package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
  100. package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
  101. package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
  102. package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
  103. package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
  104. package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
  105. package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
  106. package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
  107. package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
  108. package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
  109. package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
  110. package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
  111. package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
  112. package/schemas/spur-config.schema.json +105 -85
  113. package/spur.js +44616 -43320
  114. package/web/_astro/BoardApp.D-WlxiN2.js +1 -0
  115. package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.D8bM9pKL.js} +97 -95
  116. package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.BPRqgVUE.js} +1 -1
  117. package/web/_astro/{arc.DWEtA3Tx.js → arc.BPrPES3z.js} +1 -1
  118. package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.qX_7q02P.js} +1 -1
  119. package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.CUZfj5V7.js} +1 -1
  120. package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CTaOr8hH.js} +1 -1
  121. package/web/_astro/channel.DGZaFHZx.js +1 -0
  122. package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.Dt-9wf3h.js} +1 -1
  123. package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.CTC2sdoN.js} +1 -1
  124. package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.DQcxt2_g.js} +1 -1
  125. package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DXFPSn-a.js} +1 -1
  126. package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.BCx3U4bT.js} +1 -1
  127. package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.DL2tJkdO.js} +1 -1
  128. package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.DZyflMro.js} +1 -1
  129. package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.CUI2mT09.js} +1 -1
  130. package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.g4rX4Fr1.js} +1 -1
  131. package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.g4rX4Fr1.js} +1 -1
  132. package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.CzWJLqp0.js} +1 -1
  133. package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.WgsvQeCR.js} +1 -1
  134. package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
  135. package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.Dzv6ngql.js} +1 -1
  136. package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.CpJ4a9rU.js} +1 -1
  137. package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.CzPlF_dq.js} +1 -1
  138. package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.TAkZNTcQ.js} +1 -1
  139. package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.uCjoKSag.js} +1 -1
  140. package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.eMplIjoK.js} +1 -1
  141. package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.Bf7zoXGz.js} +1 -1
  142. package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.B_bHj3gN.js} +1 -1
  143. package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BasrHRMj.js} +1 -1
  144. package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.C6iphq1x.js} +1 -1
  145. package/web/_astro/index.DayyIngm.css +1 -0
  146. package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.HXmDMhW4.js} +1 -1
  147. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BSmW8NiU.js} +1 -1
  148. package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.DEQow5fo.js} +1 -1
  149. package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.IVm9cTdc.js} +1 -1
  150. package/web/_astro/{linear.CHXgcIbN.js → linear.CrsM73_9.js} +1 -1
  151. package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.CfBeDJls.js} +6 -6
  152. package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.C3j60Y-0.js} +1 -1
  153. package/web/_astro/ordinal.BYWQX77i.js +1 -0
  154. package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.B-aCMeEA.js} +1 -1
  155. package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.Cib965yq.js} +1 -1
  156. package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.D61cS4O-.js} +1 -1
  157. package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.GKF2qVPy.js} +1 -1
  158. package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DZnq8F2h.js} +1 -1
  159. package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DXUFmdgM.js} +1 -1
  160. package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.BtHmhLEz.js} +1 -1
  161. package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.Cy-WW2ln.js} +1 -1
  162. package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.SLp5b9KI.js} +1 -1
  163. package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.Bww45mWV.js} +1 -1
  164. package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DR4swI6a.js} +1 -1
  165. package/web/apple-touch-icon.png +0 -0
  166. package/web/favicon.ico +0 -0
  167. package/web/favicon.svg +17 -4
  168. package/web/icon-192.png +0 -0
  169. package/web/icon-512.png +0 -0
  170. package/web/index.html +2 -2
  171. package/web/site.webmanifest +31 -0
  172. package/web/spur_logo.svg +1 -0
  173. package/plugins/sp/skills/spur-cli/references/team.md +0 -145
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.80",
3
+ "version": "0.3.82",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // plugins/sp/scripts/feature-dev-precheck.ts
5
+ import { spawnSync } from "child_process";
6
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
7
+ import { join } from "path";
8
+ var BLOCKING_STATUSES = ["backlog", "wip", "testing", "blocked"];
9
+ var KNOWN_STATUSES = ["todo", "done", "cancelled", ...BLOCKING_STATUSES];
10
+ function identityRc(featureId, runId) {
11
+ return featureId && runId ? 0 : 1;
12
+ }
13
+ function featureIdentityMatches(raw, featureId) {
14
+ let parsed;
15
+ try {
16
+ parsed = JSON.parse(raw);
17
+ } catch {
18
+ return false;
19
+ }
20
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && parsed.id === featureId;
21
+ }
22
+ function rosterIsNonEmptyArray(raw) {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(raw);
26
+ } catch {
27
+ return false;
28
+ }
29
+ return Array.isArray(parsed) && parsed.length > 0;
30
+ }
31
+ function rosterContractHolds(rows) {
32
+ const seen = new Set;
33
+ for (const row of rows) {
34
+ if (row === null || typeof row !== "object")
35
+ return false;
36
+ if (typeof row.wbs !== "string" || row.wbs.length === 0)
37
+ return false;
38
+ if (seen.has(row.wbs))
39
+ return false;
40
+ seen.add(row.wbs);
41
+ if (typeof row.status !== "string" || !KNOWN_STATUSES.includes(row.status)) {
42
+ return false;
43
+ }
44
+ }
45
+ return true;
46
+ }
47
+ function hasBlockingStatus(rows) {
48
+ return rows.some((row) => typeof row.status === "string" && BLOCKING_STATUSES.includes(row.status));
49
+ }
50
+ function freezeTodoList(rows) {
51
+ return rows.filter((row) => row.status === "todo" && typeof row.wbs === "string").map((row) => row.wbs).sort().join(",");
52
+ }
53
+ function spurCommand(spurBin) {
54
+ const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
55
+ return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
56
+ }
57
+ function readFileSyncRaw(path) {
58
+ try {
59
+ return readFileSync(path, "utf8");
60
+ } catch {
61
+ return "";
62
+ }
63
+ }
64
+ function runFeatureDevPrecheck(env, options = {}) {
65
+ const cwd = options.cwd;
66
+ const runDir = join(".spur", "run");
67
+ mkdirSync(cwd ? join(cwd, runDir) : runDir, { recursive: true });
68
+ const abs = (p) => cwd ? join(cwd, p) : p;
69
+ const featureFile = join(runDir, `${env.__runId ?? ""}-feature-dev-feature.json`);
70
+ const rosterFile = join(runDir, `${env.__runId ?? ""}-feature-dev-roster.json`);
71
+ const tasksFile = join(runDir, `${env.__runId ?? ""}-feature-dev-tasks.txt`);
72
+ const statusFile = join(runDir, `${env.__runId ?? ""}-feature-dev-precheck.status`);
73
+ const featureId = env.featureId ?? "";
74
+ rmSync(abs(statusFile), { force: true });
75
+ const { cmd, prefix } = spurCommand(env.spurBin);
76
+ const read = (args, target) => {
77
+ const result = spawnSync(cmd, [...prefix, ...args], {
78
+ cwd,
79
+ encoding: "utf8",
80
+ ...options.env ? { env: { ...process.env, ...options.env } } : {}
81
+ });
82
+ if (result.error !== undefined) {
83
+ writeFileSync(abs(target), `spawn failed: ${result.error.message}
84
+ `);
85
+ return 127;
86
+ }
87
+ writeFileSync(abs(target), `${result.stdout ?? ""}${result.stderr ?? ""}`);
88
+ return result.status ?? 1;
89
+ };
90
+ const idRc = identityRc(featureId, env.__runId);
91
+ const showRc = idRc === 0 ? read(["feature", "show", featureId, "--json"], featureFile) : 1;
92
+ const listRc = idRc === 0 && showRc === 0 && featureIdentityMatches(readFileSyncRaw(abs(featureFile)), featureId) ? read(["task", "list", "--feature", featureId, "--json"], rosterFile) : 1;
93
+ const fail = (message) => {
94
+ process.stderr.write(`${message}
95
+ `);
96
+ writeFileSync(abs(statusFile), `FAIL
97
+ `);
98
+ return { status: "FAIL", featureFile, rosterFile, tasksFile, statusFile, tasks: "" };
99
+ };
100
+ if (idRc !== 0 || showRc !== 0 || listRc !== 0) {
101
+ return fail(`feature-dev precheck: missing featureId/runId, unknown feature '${featureId}', or unreadable roster (rc ${idRc}/${showRc}/${listRc}) \u2014 supply an existing planned feature via /sp:dev-plan or /sp:dev-idea; nothing was auto-created or re-planned`);
102
+ }
103
+ const rosterRaw = readFileSyncRaw(abs(rosterFile));
104
+ if (!rosterIsNonEmptyArray(rosterRaw)) {
105
+ return fail(`feature-dev precheck: roster at ${rosterFile} is malformed, not an array, or empty \u2014 plan the feature first via /sp:dev-plan; refusing to replan or run an empty batch`);
106
+ }
107
+ const rows = JSON.parse(rosterRaw);
108
+ if (!rosterContractHolds(rows)) {
109
+ return fail(`feature-dev precheck: roster has empty/duplicate/mismatched WBS identities or unknown statuses at ${rosterFile} \u2014 repair the task corpus; refusing to batch a broken roster`);
110
+ }
111
+ if (hasBlockingStatus(rows)) {
112
+ return fail("feature-dev precheck: linked task(s) are backlog/wip/testing/blocked \u2014 refine or resume them through their own task pipelines before batching; refusing to launch overlapping work");
113
+ }
114
+ const tasks = freezeTodoList(rows);
115
+ writeFileSync(abs(tasksFile), tasks);
116
+ writeFileSync(abs(statusFile), `PASS
117
+ `);
118
+ return { status: "PASS", featureFile, rosterFile, tasksFile, statusFile, tasks };
119
+ }
120
+ var FEATURE_DEV_PRECHECK_USAGE = "usage: feature-dev-precheck.ts (env: featureId, __runId, optional spurBin) \u2014 no subcommands";
121
+ function main(argv, env = process.env) {
122
+ if (argv.length > 0) {
123
+ process.stderr.write(`${FEATURE_DEV_PRECHECK_USAGE}
124
+ `);
125
+ return 2;
126
+ }
127
+ runFeatureDevPrecheck(env);
128
+ return 0;
129
+ }
130
+ {
131
+ process.exit(main(process.argv.slice(2)));
132
+ }
133
+ export {
134
+ spurCommand,
135
+ runFeatureDevPrecheck,
136
+ rosterIsNonEmptyArray,
137
+ rosterContractHolds,
138
+ main,
139
+ identityRc,
140
+ hasBlockingStatus,
141
+ freezeTodoList,
142
+ featureIdentityMatches,
143
+ KNOWN_STATUSES,
144
+ FEATURE_DEV_PRECHECK_USAGE,
145
+ BLOCKING_STATUSES
146
+ };
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * feature-dev-precheck — portable identity/roster gate behind the feature-dev pipeline
4
+ * precheck (task 0825, feature I21, governance §1.1 composition budgets).
5
+ *
6
+ * Ports the former `feature-dev.yaml` `precheck:onEnter:0` shell program one-for-one so the
7
+ * workflow stays inside the shell-program caps while writing the same `.spur/run` artifacts:
8
+ * - `<runId>-feature-dev-feature.json` merged `feature show` capture (stdout + stderr)
9
+ * - `<runId>-feature-dev-roster.json` merged `task list --feature` capture
10
+ * - `<runId>-feature-dev-tasks.txt` frozen sorted todo WBS list, comma-joined
11
+ * - `<runId>-feature-dev-precheck.status` `PASS`/`FAIL`
12
+ *
13
+ * Failure ladder (first failing check wins, message to stderr, FAIL status):
14
+ * 1. identity/roster read failure — missing featureId/runId, unknown feature, or a
15
+ * non-zero rc from any of the three reads (message carries the rc triple);
16
+ * 2. roster not a non-empty JSON array;
17
+ * 3. roster contract breach — empty/duplicate/non-string WBS identities or a status
18
+ * outside todo/done/cancelled/backlog/wip/testing/blocked;
19
+ * 4. refinement-blocking statuses present (backlog/wip/testing/blocked).
20
+ *
21
+ * Environment: `featureId`, `__runId` (required), `spurBin` (split on whitespace into a
22
+ * command plus prefix args). Soft-fail contract: the process always exits 0; the verdict
23
+ * lives in the status file. Node-builtin imports only; pure helpers are exported for unit
24
+ * testing (ADR-065).
25
+ */
26
+
27
+ import { spawnSync } from 'node:child_process';
28
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
29
+ import { join } from 'node:path';
30
+
31
+ /** Statuses that must be refined/resumed through their own pipelines before batching. */
32
+ export const BLOCKING_STATUSES = ['backlog', 'wip', 'testing', 'blocked'] as const;
33
+
34
+ /** Every status a roster row may carry (anything else fails the structural contract). */
35
+ export const KNOWN_STATUSES = ['todo', 'done', 'cancelled', ...BLOCKING_STATUSES] as const;
36
+
37
+ export interface FeatureDevPrecheckEnv {
38
+ featureId?: string;
39
+ __runId?: string;
40
+ spurBin?: string;
41
+ [key: string]: string | undefined;
42
+ }
43
+
44
+ export interface FeatureDevPrecheckOptions {
45
+ /** Base directory for `.spur/run`; defaults to the process cwd (CLI behavior). */
46
+ cwd?: string;
47
+ /** Extra environment for the spawned CLI reads (merged over process.env). */
48
+ env?: Record<string, string>;
49
+ }
50
+
51
+ export interface FeatureDevPrecheckResult {
52
+ status: 'PASS' | 'FAIL';
53
+ featureFile: string;
54
+ rosterFile: string;
55
+ tasksFile: string;
56
+ statusFile: string;
57
+ /** Frozen todo list; empty string on FAIL. */
58
+ tasks: string;
59
+ }
60
+
61
+ export interface RosterRow {
62
+ wbs: unknown;
63
+ status: unknown;
64
+ }
65
+
66
+ /** Identity gate: rc 0 when featureId and __runId are both non-empty, else 1. */
67
+ export function identityRc(featureId: string | undefined, runId: string | undefined): number {
68
+ return featureId && runId ? 0 : 1;
69
+ }
70
+
71
+ /**
72
+ * jq `type == "object" and .id == $id` port: parsed JSON must be a non-array object whose
73
+ * `.id` equals the supplied feature id.
74
+ */
75
+ export function featureIdentityMatches(raw: string, featureId: string): boolean {
76
+ let parsed: unknown;
77
+ try {
78
+ parsed = JSON.parse(raw);
79
+ } catch {
80
+ return false;
81
+ }
82
+ return (
83
+ typeof parsed === 'object' &&
84
+ parsed !== null &&
85
+ !Array.isArray(parsed) &&
86
+ (parsed as { id?: unknown }).id === featureId
87
+ );
88
+ }
89
+
90
+ /** jq roster-shape port: parsed JSON must be a non-empty array. */
91
+ export function rosterIsNonEmptyArray(raw: string): boolean {
92
+ let parsed: unknown;
93
+ try {
94
+ parsed = JSON.parse(raw);
95
+ } catch {
96
+ return false;
97
+ }
98
+ return Array.isArray(parsed) && parsed.length > 0;
99
+ }
100
+
101
+ /** jq WBS-identity/status contract port: non-empty string wbs, unique, known statuses. */
102
+ export function rosterContractHolds(rows: RosterRow[]): boolean {
103
+ const seen = new Set<string>();
104
+ for (const row of rows) {
105
+ if (row === null || typeof row !== 'object') return false;
106
+ if (typeof row.wbs !== 'string' || row.wbs.length === 0) return false;
107
+ if (seen.has(row.wbs)) return false;
108
+ seen.add(row.wbs);
109
+ if (typeof row.status !== 'string' || !(KNOWN_STATUSES as readonly string[]).includes(row.status)) {
110
+ return false;
111
+ }
112
+ }
113
+ return true;
114
+ }
115
+
116
+ /** jq blocking-status port: any backlog/wip/testing/blocked member. */
117
+ export function hasBlockingStatus(rows: RosterRow[]): boolean {
118
+ return rows.some(
119
+ (row) => typeof row.status === 'string' && (BLOCKING_STATUSES as readonly string[]).includes(row.status),
120
+ );
121
+ }
122
+
123
+ /** jq `sort | join(",")` port over the todo subset: codepoint-sorted, comma-joined, no newline. */
124
+ export function freezeTodoList(rows: RosterRow[]): string {
125
+ return rows
126
+ .filter((row) => row.status === 'todo' && typeof row.wbs === 'string')
127
+ .map((row) => row.wbs as string)
128
+ .sort()
129
+ .join(',');
130
+ }
131
+
132
+ /** `spurBin` splits on whitespace into a command plus prefix args (PATH-independent runs). */
133
+ export function spurCommand(spurBin: string | undefined): { cmd: string; prefix: string[] } {
134
+ const parts = (spurBin ?? 'spur')
135
+ .trim()
136
+ .split(/\s+/)
137
+ .filter((p) => p.length > 0);
138
+ return { cmd: parts[0] ?? 'spur', prefix: parts.slice(1) };
139
+ }
140
+
141
+ function readFileSyncRaw(path: string): string {
142
+ try {
143
+ return readFileSync(path, 'utf8');
144
+ } catch {
145
+ return '';
146
+ }
147
+ }
148
+
149
+ export function runFeatureDevPrecheck(
150
+ env: FeatureDevPrecheckEnv,
151
+ options: FeatureDevPrecheckOptions = {},
152
+ ): FeatureDevPrecheckResult {
153
+ const cwd = options.cwd;
154
+ const runDir = join('.spur', 'run');
155
+ mkdirSync(cwd ? join(cwd, runDir) : runDir, { recursive: true });
156
+ const abs = (p: string): string => (cwd ? join(cwd, p) : p);
157
+ const featureFile = join(runDir, `${env.__runId ?? ''}-feature-dev-feature.json`);
158
+ const rosterFile = join(runDir, `${env.__runId ?? ''}-feature-dev-roster.json`);
159
+ const tasksFile = join(runDir, `${env.__runId ?? ''}-feature-dev-tasks.txt`);
160
+ const statusFile = join(runDir, `${env.__runId ?? ''}-feature-dev-precheck.status`);
161
+ const featureId = env.featureId ?? '';
162
+
163
+ rmSync(abs(statusFile), { force: true });
164
+
165
+ const { cmd, prefix } = spurCommand(env.spurBin);
166
+ /** Spawn one CLI read; merged output into the artifact, 127 on a spawn error (exec loss). */
167
+ const read = (args: string[], target: string): number => {
168
+ const result = spawnSync(cmd, [...prefix, ...args], {
169
+ cwd,
170
+ encoding: 'utf8',
171
+ ...(options.env ? { env: { ...process.env, ...options.env } } : {}),
172
+ });
173
+ if (result.error !== undefined) {
174
+ writeFileSync(abs(target), `spawn failed: ${result.error.message}\n`);
175
+ return 127;
176
+ }
177
+ writeFileSync(abs(target), `${result.stdout ?? ''}${result.stderr ?? ''}`);
178
+ return result.status ?? 1;
179
+ };
180
+
181
+ const idRc = identityRc(featureId, env.__runId);
182
+ const showRc = idRc === 0 ? read(['feature', 'show', featureId, '--json'], featureFile) : 1;
183
+ const listRc =
184
+ idRc === 0 && showRc === 0 && featureIdentityMatches(readFileSyncRaw(abs(featureFile)), featureId)
185
+ ? read(['task', 'list', '--feature', featureId, '--json'], rosterFile)
186
+ : 1;
187
+
188
+ const fail = (message: string): FeatureDevPrecheckResult => {
189
+ process.stderr.write(`${message}\n`);
190
+ writeFileSync(abs(statusFile), 'FAIL\n');
191
+ return { status: 'FAIL', featureFile, rosterFile, tasksFile, statusFile, tasks: '' };
192
+ };
193
+
194
+ if (idRc !== 0 || showRc !== 0 || listRc !== 0) {
195
+ return fail(
196
+ `feature-dev precheck: missing featureId/runId, unknown feature '${featureId}', or unreadable roster (rc ${idRc}/${showRc}/${listRc}) — supply an existing planned feature via /sp:dev-plan or /sp:dev-idea; nothing was auto-created or re-planned`,
197
+ );
198
+ }
199
+
200
+ const rosterRaw = readFileSyncRaw(abs(rosterFile));
201
+ if (!rosterIsNonEmptyArray(rosterRaw)) {
202
+ return fail(
203
+ `feature-dev precheck: roster at ${rosterFile} is malformed, not an array, or empty — plan the feature first via /sp:dev-plan; refusing to replan or run an empty batch`,
204
+ );
205
+ }
206
+ const rows = JSON.parse(rosterRaw) as RosterRow[];
207
+ if (!rosterContractHolds(rows)) {
208
+ return fail(
209
+ `feature-dev precheck: roster has empty/duplicate/mismatched WBS identities or unknown statuses at ${rosterFile} — repair the task corpus; refusing to batch a broken roster`,
210
+ );
211
+ }
212
+ if (hasBlockingStatus(rows)) {
213
+ return fail(
214
+ 'feature-dev precheck: linked task(s) are backlog/wip/testing/blocked — refine or resume them through their own task pipelines before batching; refusing to launch overlapping work',
215
+ );
216
+ }
217
+
218
+ const tasks = freezeTodoList(rows);
219
+ writeFileSync(abs(tasksFile), tasks);
220
+ writeFileSync(abs(statusFile), 'PASS\n');
221
+ return { status: 'PASS', featureFile, rosterFile, tasksFile, statusFile, tasks };
222
+ }
223
+
224
+ export const FEATURE_DEV_PRECHECK_USAGE =
225
+ 'usage: feature-dev-precheck.ts (env: featureId, __runId, optional spurBin) — no subcommands';
226
+
227
+ export function main(argv: string[], env: FeatureDevPrecheckEnv = process.env): number {
228
+ if (argv.length > 0) {
229
+ process.stderr.write(`${FEATURE_DEV_PRECHECK_USAGE}\n`);
230
+ return 2;
231
+ }
232
+ runFeatureDevPrecheck(env);
233
+ return 0;
234
+ }
235
+
236
+ if (import.meta.main) {
237
+ process.exit(main(process.argv.slice(2)));
238
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // plugins/sp/scripts/idea-handoff.ts
5
+ var libUrl = new URL("../lib/idea-handoff.generated.mjs", import.meta.url).href;
6
+ async function loadHandoffLib() {
7
+ return await import(libUrl);
8
+ }
9
+ var IDEA_HANDOFF_USAGE = "usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) \u2014 no subcommands";
10
+ async function main(argv, env = process.env) {
11
+ if (argv.length > 0) {
12
+ process.stderr.write(`${IDEA_HANDOFF_USAGE}
13
+ `);
14
+ return 2;
15
+ }
16
+ const { runIdeaHandoffCli } = await loadHandoffLib();
17
+ const outcome = await runIdeaHandoffCli(env);
18
+ return outcome.exitCode;
19
+ }
20
+ {
21
+ process.exit(await main(process.argv.slice(2)));
22
+ }
23
+ export {
24
+ main,
25
+ loadHandoffLib,
26
+ IDEA_HANDOFF_USAGE
27
+ };
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * idea-handoff — portable entrypoint for the idea-pipeline finalization (task 0824, feature I21).
4
+ *
5
+ * ADR-065 standard plugin script: the workflow wrapper prefers the monorepo writer
6
+ * (`bun packages/app/src/workflow/idea-handoff-cli.ts`) and falls back to this script's
7
+ * registered `.mjs` twin under bare `node`, so a seeded project with no monorepo checkout
8
+ * still finalizes the handoff (run-scoped report + task-deps ordering + roster refresh) and
9
+ * fails closed (exit 1) when neither writer resolves.
10
+ *
11
+ * The behavior lives in the generated bundle `../lib/idea-handoff.generated.mjs`, produced by
12
+ * `bun run build:plugin-lib` from `packages/app/src/workflow/idea-handoff-cli.ts` — the same
13
+ * source the monorepo branch executes, so both branches share one contract (0604/0824).
14
+ * Declarations: `../lib/idea-handoff.generated.d.mts`. The bundle is imported by computed URL
15
+ * on purpose: `superskill script convert` must NOT inline it (its tree carries one
16
+ * `Bun.spawn` site in a ts-runtime streaming path this workflow never exercises), so the
17
+ * twin stays a thin node-runnable loader over the committed runtime asset.
18
+ *
19
+ * Environment: `__runId`, `featureId`, optional `spurBin`. Missing required vars exit 1
20
+ * (mis-invocation is failed-closed, never a silent skip).
21
+ */
22
+
23
+ const libUrl = new URL('../lib/idea-handoff.generated.mjs', import.meta.url).href;
24
+
25
+ export async function loadHandoffLib(): Promise<typeof import('../lib/idea-handoff.generated.mjs')> {
26
+ return (await import(libUrl)) as typeof import('../lib/idea-handoff.generated.mjs');
27
+ }
28
+
29
+ export const IDEA_HANDOFF_USAGE =
30
+ 'usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) — no subcommands';
31
+
32
+ export async function main(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
33
+ if (argv.length > 0) {
34
+ process.stderr.write(`${IDEA_HANDOFF_USAGE}\n`);
35
+ return 2;
36
+ }
37
+ const { runIdeaHandoffCli } = await loadHandoffLib();
38
+ const outcome = await runIdeaHandoffCli(env as { __runId?: string; featureId?: string; spurBin?: string });
39
+ return outcome.exitCode;
40
+ }
41
+
42
+ if (import.meta.main) {
43
+ process.exit(await main(process.argv.slice(2)));
44
+ }
@@ -29,6 +29,10 @@
29
29
  *
30
30
  * Usage:
31
31
  * bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
32
+ * bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]
33
+ *
34
+ * The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
35
+ * creates nothing (task 0862 R5).
32
36
  *
33
37
  * Env: SPUR_BIN
34
38
  */
@@ -56,7 +60,10 @@ function usage(): never {
56
60
  console.error(
57
61
  'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
58
62
  );
59
- process.exit(1);
63
+ console.error(
64
+ ' bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]',
65
+ );
66
+ process.exit(2);
60
67
  }
61
68
 
62
69
  /**
@@ -122,16 +129,77 @@ function writeOutcome(runId: string, outcome: SetupOutcome): void {
122
129
  writeFileSync(join(runDir, `${runId}-inline-setup.json`), `${JSON.stringify(outcome, null, 4)}\n`);
123
130
  }
124
131
 
132
+ /**
133
+ * `--fingerprint` mode (task 0862 R5): print the engine's proof-input digest for a task file
134
+ * (plus optional feature file) instead of creating a run, so the inline driver can capture the
135
+ * fresh digest bound `run.artifact` registration checks against.
136
+ *
137
+ * The digest itself comes from the exported app functions — never reimplemented here — and the
138
+ * options mirror `ProofFingerprintActionRunner` (cwd, spec contents, fileSystem; no `--expect`).
139
+ * Returns the process exit code: 0 printed, 1 read/resolve failure.
140
+ */
141
+ async function printFingerprint(taskFile: string, featureFile: string, spurBin: string): Promise<number> {
142
+ const { entry, repoRoot, chain } = resolveAppEntry(spurBin);
143
+ if (entry === null || repoRoot === null) {
144
+ console.error(
145
+ `inline-run-setup: FAIL — no monorepo checkout of spur is reachable via ${chain}. ` +
146
+ 'The proof-input digest must be computed by the app service; a bundle-only install cannot do it.',
147
+ );
148
+ return 1;
149
+ }
150
+ const app = (await import(entry)) as {
151
+ computeProofInputFingerprint: (options: Record<string, unknown>) => Promise<string>;
152
+ readProofInputContents: (
153
+ fileSystem: unknown,
154
+ workdir: string,
155
+ options: { taskFile?: unknown; featureFile?: unknown },
156
+ ) => Promise<{ ok: true; taskContent?: string; featureContent?: string } | { ok: false; error: string }>;
157
+ };
158
+
159
+ const workdir = process.cwd();
160
+ // `undefined` fs takes readProofInputContents' node-filesystem default — the same default its
161
+ // sibling createGitAlternateTree applies, and the same Node FS the CLI's runner injects.
162
+ const inputs = await app.readProofInputContents(undefined, workdir, {
163
+ taskFile,
164
+ ...(featureFile.trim() !== '' ? { featureFile } : {}),
165
+ });
166
+ if (!inputs.ok) {
167
+ console.error(`inline-run-setup: FAIL — ${inputs.error}`);
168
+ return 1;
169
+ }
170
+
171
+ const digest = await app.computeProofInputFingerprint({
172
+ cwd: workdir,
173
+ ...(inputs.taskContent !== undefined ? { taskContent: inputs.taskContent } : {}),
174
+ ...(inputs.featureContent !== undefined ? { featureContent: inputs.featureContent } : {}),
175
+ });
176
+ process.stdout.write(`${digest}\n`);
177
+ return 0;
178
+ }
179
+
125
180
  async function main(): Promise<void> {
126
181
  let runId = '';
127
182
  let file = '';
183
+ let fingerprint = false;
184
+ let taskFile = '';
185
+ let featureFile = '';
128
186
  let spurBin = process.env.SPUR_BIN ?? '';
129
187
  const argv = process.argv.slice(2);
130
188
  for (let i = 0; i < argv.length; i++) {
131
189
  if (argv[i] === '--run-id') runId = argv[++i] ?? '';
132
190
  else if (argv[i] === '--file') file = argv[++i] ?? '';
191
+ else if (argv[i] === '--fingerprint') fingerprint = true;
192
+ else if (argv[i] === '--task-file') taskFile = argv[++i] ?? '';
193
+ else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
133
194
  else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
134
195
  }
196
+
197
+ // Two mutually exclusive modes share this entry point: create/attach a run (run-id + file), or
198
+ // print the proof digest for the inline driver (task 0862 R5). Mixing them is a usage error.
199
+ if (fingerprint) {
200
+ if (runId !== '' || file !== '' || taskFile.trim() === '') usage();
201
+ process.exit(await printFingerprint(taskFile, featureFile, spurBin));
202
+ }
135
203
  if (runId.trim() === '' || file.trim() === '') usage();
136
204
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
137
205