@gobing-ai/spur 0.3.99 → 0.4.1

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 (172) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/gates.yaml +36 -0
  4. package/config/decisions/history-anatomy.yaml +35 -0
  5. package/config/decisions/idea-pipeline.yaml +38 -0
  6. package/config/decisions/task-pipeline.yaml +55 -0
  7. package/config/plugin-scripts.json +25 -0
  8. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  9. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  10. package/config/rules/quality/coverage-gate.yaml +5 -3
  11. package/config/rules/recommended-pre-check.yaml +8 -3
  12. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  13. package/config/rules/structure/protected-files.yaml +3 -0
  14. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  15. package/config/script-placement-baseline.json +2 -2
  16. package/config/workflows/feature-lifecycle.yaml +9 -2
  17. package/config/workflows/history-anatomy.yaml +22 -0
  18. package/config/workflows/idea-pipeline.yaml +75 -0
  19. package/config/workflows/task-pipeline.yaml +368 -49
  20. package/config/workflows/wrapup-pipeline.yaml +106 -13
  21. package/package.json +9 -9
  22. package/plugins/sp/README.md +25 -2
  23. package/plugins/sp/agents/super-planner.md +11 -0
  24. package/plugins/sp/agents/super-reviewer.md +39 -11
  25. package/plugins/sp/bunfig.toml +8 -0
  26. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  27. package/plugins/sp/commands/dev-changelog.md +5 -4
  28. package/plugins/sp/commands/dev-find-way.md +80 -0
  29. package/plugins/sp/commands/dev-idea.md +6 -0
  30. package/plugins/sp/commands/dev-parallel.md +8 -2
  31. package/plugins/sp/commands/dev-plan.md +6 -0
  32. package/plugins/sp/commands/dev-review-session.md +14 -7
  33. package/plugins/sp/commands/dev-run.md +17 -4
  34. package/plugins/sp/commands/dev-runall.md +21 -10
  35. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  36. package/plugins/sp/hooks/context-session-start.ts +1 -1
  37. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  38. package/plugins/sp/hooks/hooks.json +45 -1
  39. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
  42. package/plugins/sp/lib/inline-run-app.ts +28 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
  44. package/plugins/sp/lib/inline-run.generated.mjs +364 -46
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
  48. package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  50. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  51. package/plugins/sp/lib/run-summary-core.ts +561 -0
  52. package/plugins/sp/lib/transcript.ts +275 -0
  53. package/plugins/sp/plugin.json +1 -1
  54. package/plugins/sp/references/roles.md +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +335 -0
  56. package/plugins/sp/scripts/batch-plan.ts +100 -0
  57. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  58. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  59. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  60. package/plugins/sp/scripts/commit-guard.ts +249 -0
  61. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  62. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  63. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  64. package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
  65. package/plugins/sp/scripts/inline-run-setup.ts +65 -97
  66. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  67. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  68. package/plugins/sp/scripts/quality-gate.mjs +30407 -73
  69. package/plugins/sp/scripts/quality-gate.ts +21 -5
  70. package/plugins/sp/scripts/residual-scan.mjs +88 -38
  71. package/plugins/sp/scripts/residual-scan.ts +35 -38
  72. package/plugins/sp/scripts/run-summary.mjs +693 -0
  73. package/plugins/sp/scripts/run-summary.ts +105 -0
  74. package/plugins/sp/scripts/session-timeline.mjs +331 -0
  75. package/plugins/sp/scripts/session-timeline.ts +204 -0
  76. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  77. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  78. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  79. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  80. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  81. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  82. package/plugins/sp/skills/code-verification/SKILL.md +8 -4
  83. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  84. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  85. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  86. package/plugins/sp/skills/session-review/SKILL.md +77 -24
  87. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  88. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  89. package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
  90. package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
  91. package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
  92. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  93. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
  94. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  95. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  96. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  97. package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
  98. package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
  99. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  103. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
  104. package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
  105. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
  106. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
  107. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  108. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  109. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
  110. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  111. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
  112. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  113. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  114. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  115. package/schemas/decision-catalog.schema.json +226 -0
  116. package/schemas/spur-config.schema.json +25 -0
  117. package/schemas/state-machine-workflow.schema.json +23 -0
  118. package/spur.js +28165 -19051
  119. package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
  120. package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
  121. package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
  122. package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
  123. package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
  124. package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
  125. package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
  126. package/web/_astro/channel.DZVfto-r.js +1 -0
  127. package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
  128. package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
  129. package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
  130. package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
  131. package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
  132. package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
  133. package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
  134. package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
  135. package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
  136. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
  137. package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
  138. package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
  139. package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
  140. package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
  141. package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
  142. package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
  143. package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
  144. package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
  145. package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
  146. package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
  147. package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
  148. package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
  149. package/web/_astro/index.COh_kBxN.css +1 -0
  150. package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
  151. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
  152. package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
  153. package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
  154. package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
  155. package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
  156. package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
  157. package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
  158. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
  159. package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
  160. package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
  161. package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
  162. package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
  163. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
  164. package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
  165. package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
  166. package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
  167. package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
  168. package/web/board-runtime.json +1 -1
  169. package/web/index.html +2 -2
  170. package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
  171. package/web/_astro/channel.6IFIKBhu.js +0 -1
  172. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * batch-plan — letters-first batch plan surface (task 1104, feature I13).
4
+ *
5
+ * The projection logic lives in packages/app/src/workflow/plan-projection.ts (unit-tested
6
+ * there); this script is ADR-130 glue — argv/IO only — over the bundled core in
7
+ * ../lib/plan-projection.generated.* (regenerate: `bun run build:plugin-lib && bun run build:scripts`).
8
+ *
9
+ * Modes:
10
+ * waves --tasks <file> file: JSON array of {wbs, name}; prints `{ waves: [...] }` — waves
11
+ * of 24 tasks as A Prepare batch + A1..A4 + B..Y task rows + Z Batch report.
12
+ * task-children --letter <A-Z> --plan <file>
13
+ * file: a `workflow show --format todo --json` payload (or a bare plan
14
+ * array); prints the task's phase digits as a JSON array.
15
+ * Exit 0 on success, 1 on unreadable/invalid input, 2 on usage error. Runs under bare
16
+ * `node batch-plan.mjs` (committed twin) — keep the twin in lockstep.
17
+ */
18
+ import { readFileSync } from 'node:fs';
19
+ import type { BatchTask, PhasedPlanItem } from '../lib/plan-projection.generated.mjs';
20
+ import { buildBatchPlan, taskPhaseChildren } from '../lib/plan-projection.generated.mjs';
21
+
22
+ export * from '../lib/plan-projection.generated.mjs';
23
+
24
+ export const BATCH_PLAN_USAGE =
25
+ 'usage: batch-plan.ts waves --tasks <tasks.json> | task-children --letter <A-Z> --plan <plan.json>';
26
+
27
+ function readJsonFile(path: string): unknown {
28
+ return JSON.parse(readFileSync(path, 'utf8'));
29
+ }
30
+
31
+ function isBatchTask(value: unknown): value is BatchTask {
32
+ return (
33
+ typeof value === 'object' &&
34
+ value !== null &&
35
+ typeof (value as BatchTask).wbs === 'string' &&
36
+ typeof (value as BatchTask).name === 'string'
37
+ );
38
+ }
39
+
40
+ export function main(argv: string[]): number {
41
+ const mode = argv[0];
42
+ if (mode === 'waves') {
43
+ if (argv[1] !== '--tasks' || argv[2] === undefined) {
44
+ process.stderr.write(`${BATCH_PLAN_USAGE}\n`);
45
+ return 2;
46
+ }
47
+ let tasks: unknown;
48
+ try {
49
+ tasks = readJsonFile(argv[2]);
50
+ } catch (error) {
51
+ process.stderr.write(
52
+ `batch-plan: cannot read ${argv[2]} — ${error instanceof Error ? error.message : String(error)}\n`,
53
+ );
54
+ return 1;
55
+ }
56
+ if (!Array.isArray(tasks) || !tasks.every(isBatchTask)) {
57
+ process.stderr.write('batch-plan: --tasks file must be a JSON array of {wbs, name}\n');
58
+ return 1;
59
+ }
60
+ process.stdout.write(`${JSON.stringify({ waves: buildBatchPlan(tasks) })}\n`);
61
+ return 0;
62
+ }
63
+ if (mode === 'task-children') {
64
+ let letter = '';
65
+ let planPath = '';
66
+ for (let i = 1; i + 1 < argv.length; i += 2) {
67
+ if (argv[i] === '--letter') letter = argv[i + 1] ?? '';
68
+ else if (argv[i] === '--plan') planPath = argv[i + 1] ?? '';
69
+ }
70
+ if (!/^[A-Z]$/.test(letter) || planPath === '') {
71
+ process.stderr.write(`${BATCH_PLAN_USAGE}\n`);
72
+ return 2;
73
+ }
74
+ let payload: unknown;
75
+ try {
76
+ payload = readJsonFile(planPath);
77
+ } catch (error) {
78
+ process.stderr.write(
79
+ `batch-plan: cannot read ${planPath} — ${error instanceof Error ? error.message : String(error)}\n`,
80
+ );
81
+ return 1;
82
+ }
83
+ // Accept a bare plan array or a full `workflow show --format todo --json` payload.
84
+ const plan = Array.isArray(payload) ? payload : (payload as { plan?: PhasedPlanItem[] }).plan;
85
+ if (!Array.isArray(plan)) {
86
+ process.stderr.write(
87
+ 'batch-plan: --plan file must be a plan array or a todo --json payload with a `plan` field\n',
88
+ );
89
+ return 1;
90
+ }
91
+ process.stdout.write(`${JSON.stringify(taskPhaseChildren(letter, plan as PhasedPlanItem[]))}\n`);
92
+ return 0;
93
+ }
94
+ process.stderr.write(`${BATCH_PLAN_USAGE}\n`);
95
+ return 2;
96
+ }
97
+
98
+ if (import.meta.main) {
99
+ process.exit(main(process.argv.slice(2)));
100
+ }
@@ -3,6 +3,30 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // plugins/sp/scripts/batch-preflight.ts
6
+ var NON_ABORTING_PREFLIGHT_CODES = new Set([
7
+ "L4.scenario-unverified",
8
+ "L4.verifying-incomplete-tasks"
9
+ ]);
10
+ function classifyFeaturePreflightFindings(findings) {
11
+ const aborting = [];
12
+ const nonAborting = [];
13
+ for (const f of findings) {
14
+ if (f.severity === "error") {
15
+ if (NON_ABORTING_PREFLIGHT_CODES.has(f.code)) {
16
+ nonAborting.push(f);
17
+ } else {
18
+ aborting.push(f);
19
+ }
20
+ } else {
21
+ nonAborting.push(f);
22
+ }
23
+ }
24
+ return {
25
+ aborting,
26
+ nonAborting,
27
+ shouldAbort: aborting.length > 0
28
+ };
29
+ }
6
30
  function quickReadiness(input) {
7
31
  const status = (input.status ?? "").toLowerCase();
8
32
  const operation = input.operation;
@@ -190,6 +214,7 @@ function parsePreflightCliArgs(argv) {
190
214
  let filteredCount = null;
191
215
  let requiredSections = [];
192
216
  let presentSections = [];
217
+ let classifyFeature = null;
193
218
  for (let i = 0;i < argv.length; i++) {
194
219
  const a = argv[i];
195
220
  if (a === "--help" || a === "-h")
@@ -214,7 +239,9 @@ function parsePreflightCliArgs(argv) {
214
239
  presentSections = raw.length === 0 ? [] : raw.split(",").map((s) => s.trim()).filter(Boolean);
215
240
  } else if (a === "--wbs")
216
241
  wbs = argv[++i] ?? wbs;
217
- else if (a === "--status")
242
+ else if (a === "--classify-feature") {
243
+ classifyFeature = argv[++i] ?? "[]";
244
+ } else if (a === "--status")
218
245
  status = argv[++i] ?? null;
219
246
  else if (a === "--deps") {
220
247
  const raw = argv[++i] ?? "";
@@ -240,7 +267,8 @@ function parsePreflightCliArgs(argv) {
240
267
  force,
241
268
  filteredCount,
242
269
  requiredSections,
243
- presentSections
270
+ presentSections,
271
+ classifyFeature
244
272
  };
245
273
  }
246
274
  var PREFLIGHT_CLI_USAGE = `Usage:
@@ -253,6 +281,24 @@ function runPreflightCli(argv) {
253
281
  const args = parsePreflightCliArgs(argv);
254
282
  if (args.help)
255
283
  return { exitCode: 0, stdout: "", stderr: PREFLIGHT_CLI_USAGE };
284
+ if (args.classifyFeature !== null) {
285
+ try {
286
+ const findings = JSON.parse(args.classifyFeature);
287
+ const res = classifyFeaturePreflightFindings(Array.isArray(findings) ? findings : []);
288
+ return {
289
+ exitCode: res.shouldAbort ? 1 : 0,
290
+ stdout: `${JSON.stringify(res, null, 2)}
291
+ `,
292
+ stderr: ""
293
+ };
294
+ } catch (e) {
295
+ return {
296
+ exitCode: 1,
297
+ stdout: "",
298
+ stderr: `Invalid JSON for --classify-feature: ${String(e)}`
299
+ };
300
+ }
301
+ }
256
302
  if (!args.status)
257
303
  return { exitCode: 1, stdout: "", stderr: PREFLIGHT_CLI_USAGE };
258
304
  if (args.recovery) {
@@ -327,5 +373,7 @@ export {
327
373
  quickReadiness,
328
374
  preflightTask,
329
375
  parsePreflightCliArgs,
330
- PREFLIGHT_CLI_USAGE
376
+ classifyFeaturePreflightFindings,
377
+ PREFLIGHT_CLI_USAGE,
378
+ NON_ABORTING_PREFLIGHT_CODES
331
379
  };
@@ -25,6 +25,51 @@ export type PreflightResult =
25
25
  | { action: 'run'; code?: string; reason?: string }
26
26
  | { action: 'skip'; code: string; reason: string; unmetDeps?: string[] };
27
27
 
28
+ // ── Feature preflight classification (1132 R4) ──────────────────────────────
29
+ /**
30
+ * 1132 R4: Codes that are expected pre-run states under `feature check --strict`
31
+ * and must not abort the batch. Structured classification in code (replaces prose-only).
32
+ */
33
+ export const NON_ABORTING_PREFLIGHT_CODES = new Set<string>([
34
+ 'L4.scenario-unverified',
35
+ 'L4.verifying-incomplete-tasks',
36
+ ]);
37
+
38
+ export interface FeaturePreflightFinding {
39
+ layer: string;
40
+ code: string;
41
+ severity: string;
42
+ message: string;
43
+ section?: string;
44
+ }
45
+
46
+ export interface FeaturePreflightClassification {
47
+ aborting: FeaturePreflightFinding[];
48
+ nonAborting: FeaturePreflightFinding[];
49
+ shouldAbort: boolean;
50
+ }
51
+
52
+ export function classifyFeaturePreflightFindings(findings: FeaturePreflightFinding[]): FeaturePreflightClassification {
53
+ const aborting: FeaturePreflightFinding[] = [];
54
+ const nonAborting: FeaturePreflightFinding[] = [];
55
+ for (const f of findings) {
56
+ if (f.severity === 'error') {
57
+ if (NON_ABORTING_PREFLIGHT_CODES.has(f.code)) {
58
+ nonAborting.push(f);
59
+ } else {
60
+ aborting.push(f);
61
+ }
62
+ } else {
63
+ nonAborting.push(f);
64
+ }
65
+ }
66
+ return {
67
+ aborting,
68
+ nonAborting,
69
+ shouldAbort: aborting.length > 0,
70
+ };
71
+ }
72
+
28
73
  // ── Command-aware quick readiness (task 0814 R2) ──────────────────────────────
29
74
  // Read-only admission decision for the requested dev operation. Distinguishes
30
75
  // runnable / needs-refinement / blocked / skipped / invalid outcomes without
@@ -299,6 +344,8 @@ export interface PreflightCliArgs {
299
344
  requiredSections: string[];
300
345
  /** Sections actually present in the task. */
301
346
  presentSections: string[];
347
+ /** Feature findings to classify (1132 R4). */
348
+ classifyFeature: string | null;
302
349
  }
303
350
 
304
351
  export function parsePreflightCliArgs(argv: string[]): PreflightCliArgs {
@@ -314,6 +361,7 @@ export function parsePreflightCliArgs(argv: string[]): PreflightCliArgs {
314
361
  let filteredCount: number | null = null;
315
362
  let requiredSections: string[] = [];
316
363
  let presentSections: string[] = [];
364
+ let classifyFeature: string | null = null;
317
365
 
318
366
  for (let i = 0; i < argv.length; i++) {
319
367
  const a = argv[i];
@@ -346,7 +394,9 @@ export function parsePreflightCliArgs(argv: string[]): PreflightCliArgs {
346
394
  .map((s) => s.trim())
347
395
  .filter(Boolean);
348
396
  } else if (a === '--wbs') wbs = argv[++i] ?? wbs;
349
- else if (a === '--status') status = argv[++i] ?? null;
397
+ else if (a === '--classify-feature') {
398
+ classifyFeature = argv[++i] ?? '[]';
399
+ } else if (a === '--status') status = argv[++i] ?? null;
350
400
  else if (a === '--deps') {
351
401
  const raw = argv[++i] ?? '';
352
402
  deps =
@@ -378,6 +428,7 @@ export function parsePreflightCliArgs(argv: string[]): PreflightCliArgs {
378
428
  filteredCount,
379
429
  requiredSections,
380
430
  presentSections,
431
+ classifyFeature,
381
432
  };
382
433
  }
383
434
 
@@ -392,6 +443,26 @@ Exit: 0 = run (or recovery hint printed); 2 = skip; 1 = usage.`;
392
443
  export function runPreflightCli(argv: string[]): { exitCode: number; stdout: string; stderr: string } {
393
444
  const args = parsePreflightCliArgs(argv);
394
445
  if (args.help) return { exitCode: 0, stdout: '', stderr: PREFLIGHT_CLI_USAGE };
446
+
447
+ // Feature findings classification (1132 R4)
448
+ if (args.classifyFeature !== null) {
449
+ try {
450
+ const findings = JSON.parse(args.classifyFeature);
451
+ const res = classifyFeaturePreflightFindings(Array.isArray(findings) ? findings : []);
452
+ return {
453
+ exitCode: res.shouldAbort ? 1 : 0,
454
+ stdout: `${JSON.stringify(res, null, 2)}\n`,
455
+ stderr: '',
456
+ };
457
+ } catch (e) {
458
+ return {
459
+ exitCode: 1,
460
+ stdout: '',
461
+ stderr: `Invalid JSON for --classify-feature: ${String(e)}`,
462
+ };
463
+ }
464
+ }
465
+
395
466
  if (!args.status) return { exitCode: 1, stdout: '', stderr: PREFLIGHT_CLI_USAGE };
396
467
 
397
468
  if (args.recovery) {
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // plugins/sp/scripts/commit-guard.ts
5
+ import { spawnSync } from "child_process";
6
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
7
+ import { join } from "path";
8
+ var COMMIT_GUARD_USAGE = "usage: commit-guard start --run <id> | stage --run <id> [--base <sha>] -- <paths\u2026> | check --run <id>";
9
+ function isBookkeeping(path) {
10
+ return path === ".spur" || path.startsWith(".spur/");
11
+ }
12
+ function at(options, rel) {
13
+ return options.cwd ? join(options.cwd, rel) : rel;
14
+ }
15
+ function artifactPath(run) {
16
+ return join(".spur", "run", `${run}-tree-start.json`);
17
+ }
18
+ function gitOf(options) {
19
+ if (options.git)
20
+ return options.git;
21
+ return (args) => {
22
+ const run = spawnSync("git", args, { cwd: options.cwd, encoding: "utf8" });
23
+ return { status: run.status ?? 1, stdout: run.stdout ?? "" };
24
+ };
25
+ }
26
+ function hashPath(path, git) {
27
+ const run = git(["hash-object", "--", path]);
28
+ return run.status === 0 ? run.stdout.trim() : null;
29
+ }
30
+ function parsePorcelainZ(stdout) {
31
+ const tokens = stdout.split("\x00");
32
+ const paths = [];
33
+ for (let i = 0;i < tokens.length; i++) {
34
+ const entry = tokens[i] ?? "";
35
+ if (entry.length < 4)
36
+ continue;
37
+ const path = entry.slice(3);
38
+ if (!isBookkeeping(path))
39
+ paths.push(path);
40
+ if (/[RC]/.test(entry.slice(0, 2)))
41
+ i++;
42
+ }
43
+ return paths;
44
+ }
45
+ function conflictMarkerLine(content) {
46
+ let opener = false;
47
+ for (const line of content.split(`
48
+ `)) {
49
+ if (/^<{7}( |$)/.test(line))
50
+ opener = true;
51
+ else if (/^={7}(\s|$)/.test(line) && opener)
52
+ return true;
53
+ else if (/^>{7}( |$)/.test(line))
54
+ return true;
55
+ }
56
+ return opener;
57
+ }
58
+ function changedSinceBase(base, git) {
59
+ const tracked = git(["diff", "--name-only", base]);
60
+ const others = git(["ls-files", "--others", "--exclude-standard", "-z"]);
61
+ if (tracked.status !== 0 || others.status !== 0)
62
+ return new Set;
63
+ const clean = (out, sep) => out.split(sep).map((p) => p.trim()).filter((p) => p.length > 0 && !isBookkeeping(p));
64
+ return new Set([...clean(tracked.stdout, `
65
+ `), ...clean(others.stdout, "\x00")]);
66
+ }
67
+ function readTreeStart(run, options) {
68
+ try {
69
+ const parsed = JSON.parse(readFileSync(at(options, artifactPath(run)), "utf8"));
70
+ if (typeof parsed.head === "string" && Array.isArray(parsed.dirty))
71
+ return parsed;
72
+ } catch {}
73
+ return null;
74
+ }
75
+ function runContext(run, options) {
76
+ const git = gitOf(options);
77
+ const start = readTreeStart(run, options);
78
+ if (!start)
79
+ return null;
80
+ let base = options.base ?? "";
81
+ if (!base) {
82
+ try {
83
+ const anchored = readFileSync(at(options, join(".spur", "run", `${run}-base.sha`)), "utf8").trim();
84
+ if (/^[0-9a-f]{7,40}$/i.test(anchored))
85
+ base = anchored;
86
+ } catch {}
87
+ }
88
+ if (!base)
89
+ base = start.head.length > 0 ? start.head : "HEAD";
90
+ const written = changedSinceBase(base, git);
91
+ for (const entry of start.dirty)
92
+ if (hashPath(entry.path, git) === entry.sha)
93
+ written.delete(entry.path);
94
+ return { git, base, start, written };
95
+ }
96
+ function missingStart(run) {
97
+ process.stderr.write(`commit-guard: no ${artifactPath(run)} \u2014 run \`commit-guard start --run ${run}\` first
98
+ `);
99
+ return 1;
100
+ }
101
+ function runStart(run, options = {}) {
102
+ const git = gitOf(options);
103
+ const head = git(["rev-parse", "HEAD"]);
104
+ const status = git(["status", "--porcelain=v1", "-z"]);
105
+ if (head.status !== 0 || status.status !== 0) {
106
+ process.stderr.write(`commit-guard: git probe failed (rev-parse HEAD / status) \u2014 is this a work tree?
107
+ `);
108
+ return 1;
109
+ }
110
+ const snapshot = {
111
+ head: head.stdout.trim(),
112
+ dirty: parsePorcelainZ(status.stdout).map((path) => ({ path, sha: hashPath(path, git) }))
113
+ };
114
+ const abs = at(options, artifactPath(run));
115
+ mkdirSync(join(abs, ".."), { recursive: true });
116
+ writeFileSync(`${abs}.tmp`, `${JSON.stringify(snapshot, null, 2)}
117
+ `);
118
+ renameSync(`${abs}.tmp`, abs);
119
+ return 0;
120
+ }
121
+ function conflictingTarget(paths, options, git) {
122
+ const unmerged = new Set(git(["diff", "--name-only", "--diff-filter=U"]).stdout.split(`
123
+ `).map((p) => p.trim()));
124
+ for (const path of paths) {
125
+ if (unmerged.has(path))
126
+ return `${path} (unmerged)`;
127
+ try {
128
+ if (conflictMarkerLine(readFileSync(at(options, path), "utf8")))
129
+ return `${path} (conflict markers)`;
130
+ } catch {}
131
+ }
132
+ return null;
133
+ }
134
+ function runStage(run, paths, options = {}) {
135
+ if (paths.length === 0) {
136
+ process.stderr.write(`${COMMIT_GUARD_USAGE}
137
+ `);
138
+ return { staged: [], foreign: [], exitCode: 1 };
139
+ }
140
+ const context = runContext(run, options);
141
+ if (!context)
142
+ return { staged: [], foreign: [], exitCode: missingStart(run) };
143
+ const conflict = conflictingTarget(paths, options, context.git);
144
+ if (conflict) {
145
+ process.stderr.write(`commit-guard: refusing to stage ${conflict}; nothing staged
146
+ `);
147
+ return { staged: [], foreign: [], exitCode: 3 };
148
+ }
149
+ const dirtyAtStart = new Set(context.start.dirty.map((entry) => entry.path));
150
+ const staged = paths.filter((path) => context.written.has(path));
151
+ const refused = paths.filter((path) => !context.written.has(path));
152
+ for (const path of refused) {
153
+ const why = dirtyAtStart.has(path) ? "foreign \u2014 dirty at start, untouched by this run" : "unchanged since base";
154
+ process.stderr.write(`commit-guard: refused ${path} (${why})
155
+ `);
156
+ }
157
+ if (staged.length > 0 && context.git(["add", "--", ...staged]).status !== 0) {
158
+ process.stderr.write(`commit-guard: git add failed \u2014 nothing staged
159
+ `);
160
+ return { staged: [], foreign: [], exitCode: 1 };
161
+ }
162
+ const foreign = refused.filter((path) => dirtyAtStart.has(path));
163
+ process.stdout.write(`${JSON.stringify({ staged, foreign })}
164
+ `);
165
+ return { staged, foreign, exitCode: refused.length > 0 ? 2 : 0 };
166
+ }
167
+ function runCheck(run, options = {}) {
168
+ const context = runContext(run, options);
169
+ if (!context)
170
+ return missingStart(run);
171
+ const foreign = [...changedSinceBase(context.base, context.git)].filter((path) => !context.written.has(path)).sort();
172
+ process.stdout.write(`${JSON.stringify({ foreign })}
173
+ `);
174
+ return 0;
175
+ }
176
+ function takeFlag(args, name) {
177
+ const index = args.indexOf(name);
178
+ const value = index === -1 ? undefined : args[index + 1];
179
+ const missing = value === undefined || value.startsWith("--");
180
+ if (index !== -1)
181
+ args.splice(index, missing ? 1 : 2);
182
+ return missing ? undefined : value;
183
+ }
184
+ function main(argv, options = {}) {
185
+ const args = [...argv];
186
+ takeFlag(args, "--spur-bin");
187
+ const sub = args.shift();
188
+ const run = takeFlag(args, "--run");
189
+ const base = takeFlag(args, "--base");
190
+ const usage = () => {
191
+ process.stderr.write(`${COMMIT_GUARD_USAGE}
192
+ `);
193
+ return 1;
194
+ };
195
+ if (!run)
196
+ return usage();
197
+ if (sub === "start")
198
+ return args.length === 0 ? runStart(run, options) : usage();
199
+ if (sub === "check")
200
+ return args.length === 0 ? runCheck(run, { ...options, base }) : usage();
201
+ if (sub === "stage")
202
+ return runStage(run, args.filter((arg) => arg !== "--"), { ...options, base }).exitCode;
203
+ return usage();
204
+ }
205
+ {
206
+ process.exit(main(process.argv.slice(2)));
207
+ }
208
+ export {
209
+ runStart,
210
+ runStage,
211
+ runCheck,
212
+ parsePorcelainZ,
213
+ main,
214
+ conflictMarkerLine,
215
+ COMMIT_GUARD_USAGE
216
+ };