@gobing-ai/spur 0.4.1 → 0.4.3

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 (159) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +4 -2
  3. package/config/decisions/maker-liveness.yaml +39 -0
  4. package/config/plugin-scripts.json +0 -4
  5. package/config/rules/README.md +1 -0
  6. package/config/rules/docs/sourced-api-claims.yaml +56 -12
  7. package/config/rules/strict/runtime-boundaries.yaml +7 -0
  8. package/config/script-placement-baseline.json +1 -19
  9. package/config/templates/AGENTS.md +1 -1
  10. package/config/workflows/feature-lifecycle.yaml +46 -1
  11. package/config/workflows/feature-verification.yaml +11 -0
  12. package/config/workflows/wrapup-pipeline.yaml +1 -1
  13. package/package.json +10 -10
  14. package/plugins/sp/README.md +23 -18
  15. package/plugins/sp/agents/super-planner.md +5 -6
  16. package/plugins/sp/agents/super-reviewer.md +43 -0
  17. package/plugins/sp/bunfig.toml +8 -5
  18. package/plugins/sp/commands/dev-review-session.md +2 -1
  19. package/plugins/sp/commands/dev-runall.md +18 -9
  20. package/plugins/sp/hooks/careful-guard.ts +1 -1
  21. package/plugins/sp/hooks/context-post-tool.ts +7 -404
  22. package/plugins/sp/hooks/context-session-start.ts +6 -192
  23. package/plugins/sp/hooks/context-session-stop.ts +6 -91
  24. package/plugins/sp/hooks/pi/guard-extension.ts +26 -12
  25. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  26. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  27. package/plugins/sp/lib/context-owner.ts +56 -0
  28. package/plugins/sp/lib/context-post-tool.ts +432 -0
  29. package/plugins/sp/lib/context-session-start.ts +214 -0
  30. package/plugins/sp/lib/context-session-stop.ts +196 -0
  31. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  32. package/plugins/sp/lib/dogfood-report.ts +278 -0
  33. package/plugins/sp/lib/idea-handoff.generated.mjs +6 -4
  34. package/plugins/sp/lib/inline-run-app.ts +40 -0
  35. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  36. package/plugins/sp/lib/inline-run.generated.mjs +1361 -87
  37. package/plugins/sp/lib/plan-projection.generated.d.mts +169 -0
  38. package/plugins/sp/lib/plan-projection.generated.mjs +704 -1
  39. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -12
  40. package/plugins/sp/lib/quality-gate.generated.mjs +62 -231
  41. package/plugins/sp/lib/residual-scan.generated.d.mts +7 -1
  42. package/plugins/sp/lib/residual-scan.generated.mjs +71 -6
  43. package/plugins/sp/lib/run-summary-core.ts +34 -370
  44. package/plugins/sp/lib/transcript.ts +37 -200
  45. package/plugins/sp/plugin.json +1 -1
  46. package/plugins/sp/references/roles.md +1 -1
  47. package/plugins/sp/scripts/batch-plan.mjs +740 -1
  48. package/plugins/sp/scripts/batch-plan.ts +48 -3
  49. package/plugins/sp/scripts/batch-preflight.mjs +23 -1
  50. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  51. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +108 -87
  52. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  53. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -1
  54. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  55. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +3 -0
  56. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  57. package/plugins/sp/scripts/feature-verification-steps.mjs +7 -123
  58. package/plugins/sp/scripts/feature-verification-steps.ts +29 -181
  59. package/plugins/sp/scripts/inline-run-dispatch.mjs +27 -26
  60. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  61. package/plugins/sp/scripts/inline-run-setup.mjs +158 -120
  62. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  63. package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
  64. package/plugins/sp/scripts/pr-reviewing.ts +34 -9
  65. package/plugins/sp/scripts/quality-gate.mjs +38 -208
  66. package/plugins/sp/scripts/residual-scan.mjs +73 -6
  67. package/plugins/sp/scripts/residual-scan.ts +8 -5
  68. package/plugins/sp/scripts/run-summary.mjs +160 -95
  69. package/plugins/sp/scripts/run-summary.ts +7 -5
  70. package/plugins/sp/scripts/session-timeline.mjs +57 -29
  71. package/plugins/sp/scripts/session-timeline.ts +4 -18
  72. package/plugins/sp/scripts/task-diffstat.mjs +43 -30
  73. package/plugins/sp/scripts/task-diffstat.ts +40 -71
  74. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  75. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  76. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  77. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  78. package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
  79. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  80. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  81. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +2 -2
  82. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
  83. package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
  84. package/plugins/sp/skills/session-review/SKILL.md +5 -3
  85. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  86. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  87. package/plugins/sp/skills/spur-cli/references/decision.md +40 -8
  88. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  89. package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
  90. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  91. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +27 -0
  92. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -3
  93. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +6 -1
  94. package/plugins/sp/skills/spur-dev/references/execution-batch.md +56 -7
  95. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +4 -1
  96. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  97. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  98. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +13 -11
  99. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +37 -25
  100. package/spur.js +89792 -76992
  101. package/web/_astro/{BoardApp.D66k_s_T.js → BoardApp.5IIl2O-z.js} +1 -1
  102. package/web/_astro/BoardApp.Drnx-KeM.js +324 -0
  103. package/web/_astro/TaskDetail.Ci4LNn9S.js +1 -0
  104. package/web/_astro/{arc._BUadrdH.js → arc.j2EVkYVd.js} +1 -1
  105. package/web/_astro/{architectureDiagram-3BPJPVTR.Dh7xS1Pp.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
  106. package/web/_astro/{blockDiagram-GPEHLZMM.DFDVpfXm.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
  107. package/web/_astro/{c4Diagram-AAUBKEIU.V7_3_glh.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
  108. package/web/_astro/channel.DmdDxGsq.js +1 -0
  109. package/web/_astro/{chunk-2J33WTMH.De1yx7Jz.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
  110. package/web/_astro/{chunk-4BX2VUAB.BDhHFuxx.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
  111. package/web/_astro/{chunk-55IACEB6.DDLkWnmC.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
  112. package/web/_astro/{chunk-727SXJPM.CwfiLnA7.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
  113. package/web/_astro/{chunk-AQP2D5EJ.CtGDmUnM.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
  114. package/web/_astro/{chunk-FMBD7UC4.BOLjpw73.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
  115. package/web/_astro/{chunk-ND2GUHAM.BDhQls6O.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
  116. package/web/_astro/{chunk-QZHKN3VN.76qBynDF.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
  117. package/web/_astro/{classDiagram-4FO5ZUOK.N0mpPHT0.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
  118. package/web/_astro/{classDiagram-v2-Q7XG4LA2.N0mpPHT0.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
  119. package/web/_astro/{cose-bilkent-S5V4N54A.CKknFfjB.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
  120. package/web/_astro/{cynefin-OW5HDTMX.Cw3ZWBAh.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
  121. package/web/_astro/{dagre-BM42HDAG.D3yIKCRD.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
  122. package/web/_astro/{diagram-2AECGRRQ.D51dO83Z.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
  123. package/web/_astro/{diagram-5GNKFQAL.Bb-FViDj.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
  124. package/web/_astro/{diagram-KO2AKTUF.D3mN8S6x.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
  125. package/web/_astro/{diagram-LMA3HP47.dMMyPcbJ.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
  126. package/web/_astro/{diagram-OG6HWLK6.AzbvzY4C.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
  127. package/web/_astro/{erDiagram-TEJ5UH35.BR5I4OZb.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
  128. package/web/_astro/{flowDiagram-I6XJVG4X.DFqvhGkn.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
  129. package/web/_astro/{ganttDiagram-6RSMTGT7.ByJbhH6J.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
  130. package/web/_astro/{gitGraphDiagram-PVQCEYII.CInHjQR3.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
  131. package/web/_astro/index.Do2lLdgo.css +1 -0
  132. package/web/_astro/{infoDiagram-5YYISTIA.Cm0dGVjJ.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
  133. package/web/_astro/{ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
  134. package/web/_astro/{journeyDiagram-JHISSGLW.4IvLa2MW.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
  135. package/web/_astro/{kanban-definition-UN3LZRKU.DS87Cl-k.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
  136. package/web/_astro/{linear.MzmRrNER.js → linear.CI6djNi7.js} +1 -1
  137. package/web/_astro/{mermaid.core.fK-WNswS.js → mermaid.core.CuQPxS8b.js} +4 -4
  138. package/web/_astro/{mindmap-definition-RKZ34NQL.BqipOHhY.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
  139. package/web/_astro/{pieDiagram-4H26LBE5.0nBiPqFG.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
  140. package/web/_astro/{quadrantDiagram-W4KKPZXB.Szj7Zpx5.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
  141. package/web/_astro/{requirementDiagram-4Y6WPE33.D4UR6TWo.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
  142. package/web/_astro/{sankeyDiagram-5OEKKPKP.DAE7BdOs.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
  143. package/web/_astro/{sequenceDiagram-3UESZ5HK.BP9ZVq25.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
  144. package/web/_astro/{stateDiagram-AJRCARHV.BQW7mIxk.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
  145. package/web/_astro/{stateDiagram-v2-BHNVJYJU.PbK4nDih.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
  146. package/web/_astro/{timeline-definition-PNZ67QCA.Ci_GvUpo.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
  147. package/web/_astro/{vennDiagram-CIIHVFJN.DNw1Y5qx.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
  148. package/web/_astro/{wardleyDiagram-YWT4CUSO.CwlkaaNe.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
  149. package/web/_astro/{xychartDiagram-2RQKCTM6.MI6ZuiH_.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
  150. package/web/index.html +2 -2
  151. package/config/proportional-route-table.ts +0 -155
  152. package/plugins/sp/commands/dev-parallel.md +0 -37
  153. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  154. package/web/_astro/BoardApp.D6sjAJr_.js +0 -324
  155. package/web/_astro/TaskDetail.BynLnMNc.js +0 -1
  156. package/web/_astro/channel.DZVfto-r.js +0 -1
  157. package/web/_astro/index.COh_kBxN.css +0 -1
  158. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  159. /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
@@ -2,20 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // plugins/sp/scripts/feature-verification-steps.ts
5
- import { spawnSync } from "child_process";
6
- import {
7
- closeSync,
8
- existsSync,
9
- mkdirSync,
10
- openSync,
11
- readdirSync,
12
- readFileSync,
13
- realpathSync,
14
- renameSync,
15
- statSync,
16
- writeFileSync
17
- } from "fs";
18
- import { join } from "path";
5
+ import { existsSync } from "fs";
19
6
  import { fileURLToPath } from "url";
20
7
 
21
8
  // plugins/sp/lib/env.ts
@@ -25,12 +12,11 @@ function getEnvVar(name, fallback) {
25
12
  }
26
13
 
27
14
  // plugins/sp/scripts/feature-verification-steps.ts
28
- var SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
29
15
  function bundleEntry() {
30
16
  return fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
31
17
  }
32
18
  function sourceModeError(spurBin) {
33
- return new Error(`source mode: refusing to load packages/app/src/index.ts (spurBin=${JSON.stringify(spurBin)}). ` + "That entry is not the feature-verification contract surface \u2014 it does not export " + "splitLaunchCommand / ArtifactDao. Fix: run in bundle mode so " + "plugins/sp/lib/inline-run.generated.mjs is loaded (omit --module-mode, or pass --module-mode bundle) " + "and regenerate that bundle if it is missing. Reinstalling the sp plugin does not add these seams to the app source entry.");
19
+ return new Error(`source mode: refusing to load packages/app/src/index.ts (spurBin=${JSON.stringify(spurBin)}). ` + "That entry is not the feature-verification contract surface \u2014 it does not export " + "runFeatureVerification. Fix: run in bundle mode so " + "plugins/sp/lib/inline-run.generated.mjs is loaded (omit --module-mode, or pass --module-mode bundle) " + "and regenerate that bundle if it is missing. Reinstalling the sp plugin does not add the operation to the app source entry.");
34
20
  }
35
21
  function resolveModuleMode(raw) {
36
22
  if (raw === "" || raw === "bundle")
@@ -50,123 +36,21 @@ async function loadModule(spurBin, mode) {
50
36
  }
51
37
  async function requireModule(entry, mode) {
52
38
  const mod = await import(entry);
53
- const missing = [
54
- "startFeatureVerificationReceipt",
55
- "completeFeatureVerificationReceipt",
56
- "captureFeatureReceiptDigest",
57
- "resolveWorkflowDefinition",
58
- "splitLaunchCommand",
59
- "openInlineRunProjectDb",
60
- "ArtifactDao"
61
- ].filter((k) => typeof mod[k] !== "function");
39
+ const missing = ["runFeatureVerification"].filter((k) => typeof mod[k] !== "function");
62
40
  if (missing.length > 0) {
63
41
  throw new Error(`feature-verification-steps: mode=${mode} \u2014 application entry ${entry} is missing feature-verification seams (${missing.join(", ")}). ` + "Fix: the generated inline bundle is stale or incomplete \u2014 run `bun run build:bundle` in a source checkout, or reinstall the sp plugin.");
64
42
  }
65
43
  return mod;
66
44
  }
67
- function assertSafeId(kind, id) {
68
- if (!SAFE_ID_RE.test(id) || id.includes("..")) {
69
- throw new Error(`refusing unsafe ${kind}: ${id}`);
70
- }
71
- }
72
- function findFeatureFile(featureDir, featureId) {
73
- if (!existsSync(featureDir))
74
- return;
75
- const name = readdirSync(featureDir).find((n) => n.startsWith(`${featureId}_`) && n.endsWith(".md"));
76
- return name === undefined ? undefined : join(featureDir, name);
77
- }
78
- var nodeFsShim = {
79
- realPath: realpathSync,
80
- readDir: readdirSync,
81
- stat: (path) => {
82
- try {
83
- return statSync(path);
84
- } catch (error) {
85
- if (error.code === "ENOENT")
86
- return null;
87
- throw error;
88
- }
89
- },
90
- ensureDir: (dir) => {
91
- mkdirSync(dir, { recursive: true });
92
- },
93
- writeFile: (path, body) => {
94
- writeFileSync(path, body);
95
- },
96
- rename: (src, dest) => {
97
- renameSync(src, dest);
98
- },
99
- readFile: (path) => {
100
- return readFileSync(path, "utf8");
101
- }
102
- };
103
45
  async function verify(featureId, runId, cmdOverride, spurBin, mode) {
104
- assertSafeId("feature id", featureId);
105
- assertSafeId("run id", runId);
106
- const cwd = process.cwd();
107
46
  const mod = await loadModule(spurBin, mode);
108
- const runDir = join(cwd, ".spur", "run");
109
- mkdirSync(runDir, { recursive: true });
110
- const selected = await mod.resolveWorkflowDefinition(cwd, "feature-verification");
111
- const vars = selected.workflow.vars;
112
- const configuredCmd = typeof vars?.verificationCmd === "string" && vars.verificationCmd.length > 0 ? vars.verificationCmd : "bun run spur-check-feature";
113
- const effectiveCmd = cmdOverride !== "" ? cmdOverride : configuredCmd;
114
- const featureFile = findFeatureFile(join(cwd, "docs", "features"), featureId);
115
- if (featureFile === undefined) {
116
- throw new Error(`feature ${featureId} not found under ${join(cwd, "docs", "features")}`);
117
- }
118
- const featureContent = readFileSync(featureFile, "utf8");
119
- const learningsPath = join(cwd, ".spur", "context", "learnings.md");
120
- const learningsContent = existsSync(learningsPath) ? readFileSync(learningsPath, "utf8") : undefined;
121
- const beforeDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
122
- const receipt = await mod.startFeatureVerificationReceipt(nodeFsShim, runDir, {
47
+ const result = await mod.runFeatureVerification({
48
+ workdir: process.cwd(),
123
49
  featureId,
124
50
  runId,
125
- workdir: cwd,
126
- verifier: {
127
- name: "feature-verification",
128
- sourcePath: selected.path,
129
- layer: selected.layer,
130
- definitionDigest: selected.digest
131
- },
132
- verificationCmd: effectiveCmd,
133
- inputDigest: beforeDigest
51
+ cmdOverride
134
52
  });
135
- const db = await mod.openInlineRunProjectDb(cwd);
136
- try {
137
- await new mod.ArtifactDao(db.adapter).record({
138
- runId,
139
- path: mod.featureReceiptPaths(runDir, featureId, runId).runScoped,
140
- kind: "feature-verification"
141
- });
142
- } finally {
143
- db.close();
144
- }
145
- const logPath = join(runDir, `${runId}-feature-verification.log`);
146
- const launch = mod.splitLaunchCommand(effectiveCmd, "feature-verification verificationCmd");
147
- if ("error" in launch) {
148
- await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, {
149
- status: "FAIL",
150
- inputDigest: beforeDigest
151
- });
152
- console.log(`feature-verification-steps: ${featureId} verification FAIL (${launch.error})`);
153
- return;
154
- }
155
- const logFd = openSync(logPath, "a");
156
- try {
157
- const result = spawnSync(launch.command, launch.leadingArgs, {
158
- cwd,
159
- stdio: ["ignore", logFd, logFd],
160
- timeout: 14400000
161
- });
162
- const exit = result.status ?? 1;
163
- const afterDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
164
- const status = exit === 0 && afterDigest === beforeDigest ? "PASS" : "FAIL";
165
- await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, { status, inputDigest: afterDigest });
166
- console.log(`feature-verification-steps: ${featureId} verification ${status} (run ${runId}, log ${logPath})`);
167
- } finally {
168
- closeSync(logFd);
169
- }
53
+ console.log(`feature-verification-steps: ${featureId} ${result.message}`);
170
54
  }
171
55
  function main() {
172
56
  const args = process.argv.slice(2);
@@ -1,62 +1,34 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * feature-verification-steps — the single writer of feature verification
4
- * receipts behind the `feature-verification` workflow (task 0915, remediated
5
- * to the frozen v1 contract; no public CLI verb exists by design).
3
+ * feature-verification-steps — argv adapter for the feature-verification pass (task 0915; reduced to
4
+ * an adapter in task 1176).
5
+ *
6
+ * The pass itself — resolve the selected `feature-verification` definition, capture the before
7
+ * proof-input digest, record and register the RUNNING receipt, run the effective command through the
8
+ * safe argv splitter (never `sh -c`), compare the after digest, complete PASS/FAIL plus the coarse
9
+ * status file — lives in the application operation
10
+ * `packages/app/src/workflow/feature-verification-run.ts`, reached through the generated inline
11
+ * bundle. This entry owns argv, usage and exit codes only: it opens no project DB, constructs no
12
+ * `ArtifactDao`, and scans no feature directory (that lookup is app-owned too).
6
13
  *
7
14
  * Usage (workflow onEnter, vars: featureId, verificationCmd, spurBin, __runId):
8
15
  * bun plugins/sp/scripts/feature-verification-steps.ts verify \
9
- * --feature-id D63 --run-id <runId> --cmd "bun run spur-check-feature"
16
+ * --feature-id D63 --run-id <runId> [--cmd "bun run spur-check-feature"] [--module-mode bundle]
10
17
  *
11
- * One pass, in order:
12
- * 1. resolve the selected `feature-verification` definition (project override
13
- * wins) for the verifier identity + configured command,
14
- * 2. capture the before proof-input digest, record the RUNNING receipt (both
15
- * copies) and register the run-scoped receipt as a run artifact,
16
- * 3. run the effective command via the safe launch splitter (never `sh -c`
17
- * interpolation), output to the run-scoped log,
18
- * 4. capture the after digest — a mismatch records FAIL,
19
- * 5. complete the receipt PASS/FAIL (both copies) and the coarse status file.
18
+ * Always exits 0 after a completed pass; the transition guard reads the status file and the
19
+ * completion boundary validates the receipt (fail-closed). Unsafe ids are refused by the operation.
20
20
  *
21
- * Always exits 0 after verify; the transition guard reads the status file and
22
- * the completion boundary validates the receipt (fail-closed). Unsafe ids are
23
- * refused outright. Node-builtin imports only (ADR-065); the application seams come
24
- * from the generated inline bundle (`plugins/sp/lib/inline-run.generated.mjs`) — never
25
- * from `packages/app/src/index.ts`, which is not a plugin-script contract surface (0948 R3).
21
+ * Node-builtin imports only (ADR-065); the application seams come from the generated inline bundle,
22
+ * never from `packages/app/src/index.ts`, which is not a plugin-script contract surface (0948 R3).
26
23
  */
27
24
 
28
- import { spawnSync } from 'node:child_process';
29
- import {
30
- closeSync,
31
- existsSync,
32
- mkdirSync,
33
- openSync,
34
- readdirSync,
35
- readFileSync,
36
- realpathSync,
37
- renameSync,
38
- statSync,
39
- writeFileSync,
40
- } from 'node:fs';
41
- import { join } from 'node:path';
25
+ import { existsSync } from 'node:fs';
42
26
  import { fileURLToPath } from 'node:url';
43
27
  import { getEnvVar } from '../lib/env';
44
28
 
45
- /** Single safe filename component — same class as the inline-run-setup run-id guard. */
46
- const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
47
-
48
29
  /** Shape of the generated inline bundle / repo app source this script consumes. */
49
30
  interface VerificationModule {
50
- featureReceiptPaths: typeof import('../../../packages/app/src/workflow/feature-verification-receipt')['featureReceiptPaths'];
51
- startFeatureVerificationReceipt: typeof import('../../../packages/app/src/workflow/feature-verification-receipt')['startFeatureVerificationReceipt'];
52
- completeFeatureVerificationReceipt: typeof import('../../../packages/app/src/workflow/feature-verification-receipt')['completeFeatureVerificationReceipt'];
53
- captureFeatureReceiptDigest: typeof import('../../../packages/app/src/workflow/feature-verification-receipt')['captureFeatureReceiptDigest'];
54
- resolveWorkflowDefinition: typeof import('../../../packages/app/src/workflow/workflow-resolver')['resolveWorkflowDefinition'];
55
- splitLaunchCommand: typeof import('../../../packages/app/src/workflow/split-launch-command')['splitLaunchCommand'];
56
- openInlineRunProjectDb: typeof import('../../../packages/app/src/services/inline-run-setup')['openInlineRunProjectDb'];
57
- ArtifactDao: new (
58
- adapter: never,
59
- ) => { record: (input: { runId: string; path: string; kind: string }) => Promise<unknown> };
31
+ runFeatureVerification: typeof import('../../../packages/app/src/workflow/feature-verification-run')['runFeatureVerification'];
60
32
  }
61
33
 
62
34
  /** Explicit module surface. Source is not the contract; the generated bundle is (0948 R3). */
@@ -67,16 +39,16 @@ function bundleEntry(): string {
67
39
  }
68
40
 
69
41
  /**
70
- * Source mode names the mode and the fix. The app source entry does not export
71
- * the verification seams, and "rebuild/install the sp plugin" cannot add them.
42
+ * Source mode names the mode and the fix. The app source entry does not export the verification
43
+ * operation, and "rebuild/install the sp plugin" cannot add it.
72
44
  */
73
45
  function sourceModeError(spurBin: string): Error {
74
46
  return new Error(
75
47
  `source mode: refusing to load packages/app/src/index.ts (spurBin=${JSON.stringify(spurBin)}). ` +
76
48
  'That entry is not the feature-verification contract surface — it does not export ' +
77
- 'splitLaunchCommand / ArtifactDao. Fix: run in bundle mode so ' +
49
+ 'runFeatureVerification. Fix: run in bundle mode so ' +
78
50
  'plugins/sp/lib/inline-run.generated.mjs is loaded (omit --module-mode, or pass --module-mode bundle) ' +
79
- 'and regenerate that bundle if it is missing. Reinstalling the sp plugin does not add these seams to the app source entry.',
51
+ 'and regenerate that bundle if it is missing. Reinstalling the sp plugin does not add the operation to the app source entry.',
80
52
  );
81
53
  }
82
54
 
@@ -89,8 +61,8 @@ function resolveModuleMode(raw: string): ModuleMode {
89
61
  /**
90
62
  * Bundle mode loads the generated inline bundle. Source mode fails closed with a named fix.
91
63
  *
92
- * `spurBin` no longer selects the module (it named the *caller's* CLI entry, not this
93
- * script's runtime surface); it is kept only to make the failure message actionable.
64
+ * `spurBin` no longer selects the module (it named the *caller's* CLI entry, not this script's runtime
65
+ * surface); it is kept only to make the failure message actionable.
94
66
  */
95
67
  async function loadModule(spurBin: string, mode: ModuleMode): Promise<VerificationModule> {
96
68
  if (mode === 'source') throw sourceModeError(spurBin);
@@ -105,20 +77,10 @@ async function loadModule(spurBin: string, mode: ModuleMode): Promise<Verificati
105
77
  return requireModule(bundle, mode);
106
78
  }
107
79
 
108
- /** Dynamic-import the entry and refuse modules missing the verification seams. */
80
+ /** Dynamic-import the entry and refuse a module missing the verification operation. */
109
81
  async function requireModule(entry: string, mode: ModuleMode): Promise<VerificationModule> {
110
82
  const mod = (await import(entry)) as Partial<VerificationModule> & Record<string, unknown>;
111
- const missing = (
112
- [
113
- 'startFeatureVerificationReceipt',
114
- 'completeFeatureVerificationReceipt',
115
- 'captureFeatureReceiptDigest',
116
- 'resolveWorkflowDefinition',
117
- 'splitLaunchCommand',
118
- 'openInlineRunProjectDb',
119
- 'ArtifactDao',
120
- ] as const
121
- ).filter((k) => typeof mod[k] !== 'function');
83
+ const missing = (['runFeatureVerification'] as const).filter((k) => typeof mod[k] !== 'function');
122
84
  if (missing.length > 0) {
123
85
  throw new Error(
124
86
  `feature-verification-steps: mode=${mode} — application entry ${entry} is missing feature-verification seams (${missing.join(', ')}). ` +
@@ -128,48 +90,6 @@ async function requireModule(entry: string, mode: ModuleMode): Promise<Verificat
128
90
  return mod as VerificationModule;
129
91
  }
130
92
 
131
- /** Refuse ids that could escape the run directory through receipt filenames. */
132
- function assertSafeId(kind: string, id: string): void {
133
- if (!SAFE_ID_RE.test(id) || id.includes('..')) {
134
- throw new Error(`refusing unsafe ${kind}: ${id}`);
135
- }
136
- }
137
-
138
- /** Locate `<id>_<slug>.md` under the features directory. */
139
- function findFeatureFile(featureDir: string, featureId: string): string | undefined {
140
- if (!existsSync(featureDir)) return undefined;
141
- const name = readdirSync(featureDir).find((n) => n.startsWith(`${featureId}_`) && n.endsWith('.md'));
142
- return name === undefined ? undefined : join(featureDir, name);
143
- }
144
-
145
- // ponytail: the receipt service is filesystem-pure over the ts-runtime
146
- // FileSystem port; only ensureDir/writeFile/rename are exercised on the
147
- // start/complete paths. Swap to createNodeFileSystem if more surface is needed.
148
- const nodeFsShim = {
149
- realPath: realpathSync,
150
- readDir: readdirSync,
151
- stat: (path: string) => {
152
- try {
153
- return statSync(path);
154
- } catch (error) {
155
- if ((error as { code?: string }).code === 'ENOENT') return null;
156
- throw error;
157
- }
158
- },
159
- ensureDir: (dir: string) => {
160
- mkdirSync(dir, { recursive: true });
161
- },
162
- writeFile: (path: string, body: string) => {
163
- writeFileSync(path, body);
164
- },
165
- rename: (src: string, dest: string) => {
166
- renameSync(src, dest);
167
- },
168
- readFile: (path: string) => {
169
- return readFileSync(path, 'utf8');
170
- },
171
- } as never;
172
-
173
93
  /** One full verification pass for one feature. Always exits 0. */
174
94
  async function verify(
175
95
  featureId: string,
@@ -178,86 +98,14 @@ async function verify(
178
98
  spurBin: string,
179
99
  mode: ModuleMode,
180
100
  ): Promise<void> {
181
- assertSafeId('feature id', featureId);
182
- assertSafeId('run id', runId);
183
- const cwd = process.cwd();
184
101
  const mod = await loadModule(spurBin, mode);
185
- const runDir = join(cwd, '.spur', 'run');
186
- mkdirSync(runDir, { recursive: true });
187
-
188
- // 1. Selected verifier definition (project override wins); its configured
189
- // command is the completion contract — an override records but can't complete.
190
- const selected = await mod.resolveWorkflowDefinition(cwd, 'feature-verification');
191
- const vars = selected.workflow.vars as { verificationCmd?: unknown } | undefined;
192
- const configuredCmd =
193
- typeof vars?.verificationCmd === 'string' && vars.verificationCmd.length > 0
194
- ? vars.verificationCmd
195
- : 'bun run spur-check-feature';
196
- const effectiveCmd = cmdOverride !== '' ? cmdOverride : configuredCmd;
197
-
198
- const featureFile = findFeatureFile(join(cwd, 'docs', 'features'), featureId);
199
- if (featureFile === undefined) {
200
- throw new Error(`feature ${featureId} not found under ${join(cwd, 'docs', 'features')}`);
201
- }
202
- const featureContent = readFileSync(featureFile, 'utf8');
203
- const learningsPath = join(cwd, '.spur', 'context', 'learnings.md');
204
- const learningsContent = existsSync(learningsPath) ? readFileSync(learningsPath, 'utf8') : undefined;
205
-
206
- // 2. Before digest + RUNNING receipt (both copies) + artifact registration.
207
- const beforeDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
208
- const receipt = await mod.startFeatureVerificationReceipt(nodeFsShim, runDir, {
102
+ const result = await mod.runFeatureVerification({
103
+ workdir: process.cwd(),
209
104
  featureId,
210
105
  runId,
211
- workdir: cwd,
212
- verifier: {
213
- name: 'feature-verification',
214
- sourcePath: selected.path,
215
- layer: selected.layer,
216
- definitionDigest: selected.digest,
217
- },
218
- verificationCmd: effectiveCmd,
219
- inputDigest: beforeDigest,
106
+ cmdOverride,
220
107
  });
221
- const db = await mod.openInlineRunProjectDb(cwd);
222
- try {
223
- await new mod.ArtifactDao(db.adapter as never).record({
224
- runId,
225
- path: mod.featureReceiptPaths(runDir, featureId, runId).runScoped,
226
- kind: 'feature-verification',
227
- });
228
- } finally {
229
- db.close();
230
- }
231
-
232
- // 3. Run the effective command via the safe splitter (no `sh -c`).
233
- const logPath = join(runDir, `${runId}-feature-verification.log`);
234
- const launch = mod.splitLaunchCommand(effectiveCmd, 'feature-verification verificationCmd');
235
- if ('error' in launch) {
236
- await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, {
237
- status: 'FAIL',
238
- inputDigest: beforeDigest,
239
- });
240
- console.log(`feature-verification-steps: ${featureId} verification FAIL (${launch.error})`);
241
- return;
242
- }
243
- const logFd = openSync(logPath, 'a');
244
- try {
245
- const result = spawnSync(launch.command, launch.leadingArgs, {
246
- cwd,
247
- stdio: ['ignore', logFd, logFd],
248
- timeout: 4 * 60 * 60 * 1000,
249
- });
250
- const exit = result.status ?? 1;
251
-
252
- // 4/5. After digest — a mismatch records FAIL (the pass did not verify
253
- // the tree it left behind); otherwise the exit code decides.
254
- const afterDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
255
- const status: 'PASS' | 'FAIL' = exit === 0 && afterDigest === beforeDigest ? 'PASS' : 'FAIL';
256
- await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, { status, inputDigest: afterDigest });
257
- console.log(`feature-verification-steps: ${featureId} verification ${status} (run ${runId}, log ${logPath})`);
258
- } finally {
259
- closeSync(logFd);
260
- }
108
+ console.log(`feature-verification-steps: ${featureId} ${result.message}`);
261
109
  }
262
110
 
263
111
  function main(): void {
@@ -3,9 +3,7 @@
3
3
 
4
4
  // plugins/sp/scripts/inline-run-dispatch.ts
5
5
  import { spawnSync } from "child_process";
6
- import { existsSync } from "fs";
7
- import { dirname, join, resolve } from "path";
8
- import { fileURLToPath } from "url";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
9
7
 
10
8
  // plugins/sp/lib/env.ts
11
9
  function getEnvVar(name, fallback) {
@@ -14,8 +12,30 @@ function getEnvVar(name, fallback) {
14
12
  }
15
13
 
16
14
  // plugins/sp/lib/inline-run-app.ts
17
- async function loadInlineApp(spurBin, resolveAppEntry, required) {
18
- const { entry, portable } = resolveAppEntry(spurBin);
15
+ import { existsSync } from "node:fs";
16
+ import { dirname, join, resolve } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ function resolveAppEntry(spurBin) {
19
+ const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
20
+ const candidates = spurBin !== "" ? [spurBin] : [fallback];
21
+ for (const candidate of candidates) {
22
+ const tokens = candidate.split(/\s+/).filter(Boolean);
23
+ const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
24
+ if (mainModule === undefined || !existsSync(mainModule))
25
+ continue;
26
+ const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
27
+ const appEntry = join(repoRoot, "packages", "app", "src", "index.ts");
28
+ if (existsSync(appEntry))
29
+ return { entry: appEntry, portable: false };
30
+ }
31
+ const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
32
+ if (!existsSync(entry)) {
33
+ throw new Error("inline application bundle is missing — rebuild/install the sp plugin before running inline");
34
+ }
35
+ return { entry, portable: true };
36
+ }
37
+ async function loadInlineApp(spurBin, resolveAppEntry2, required) {
38
+ const { entry, portable } = resolveAppEntry2(spurBin);
19
39
  const app = await import(entry);
20
40
  const missing = portable ? required.filter((name) => typeof app[name] !== "function") : [];
21
41
  if (missing.length > 0) {
@@ -36,28 +56,9 @@ function usage() {
36
56
  process.exit(2);
37
57
  }
38
58
  var SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
39
- function resolveAppEntry(spurBin) {
40
- const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
41
- const candidates = spurBin !== "" ? [spurBin] : [fallback];
42
- for (const candidate of candidates) {
43
- const tokens = candidate.split(/\s+/).filter(Boolean);
44
- const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
45
- if (mainModule === undefined || !existsSync(mainModule))
46
- continue;
47
- const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
48
- const appEntry = join(repoRoot, "packages", "app", "src", "index.ts");
49
- if (existsSync(appEntry))
50
- return { entry: appEntry, portable: false };
51
- }
52
- const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
53
- if (!existsSync(entry)) {
54
- throw new Error("inline application bundle is missing \u2014 rebuild/install the sp plugin before running inline");
55
- }
56
- return { entry, portable: true };
57
- }
58
59
  async function main() {
59
60
  if (!process.versions.bun) {
60
- const child = spawnSync("bun", [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
61
+ const child = spawnSync("bun", [fileURLToPath2(import.meta.url), ...process.argv.slice(2)], {
61
62
  stdio: "inherit"
62
63
  });
63
64
  if (child.error)
@@ -113,7 +114,7 @@ async function main() {
113
114
  }
114
115
  if (decision.trim() === "" || text === "" === (textFile === ""))
115
116
  usage();
116
- const { app: lib } = await loadInlineApp(spurBin, () => ({ entry: fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url)), portable }), ["loadSpurConfig"]);
117
+ const { app: lib } = await loadInlineApp(spurBin, () => ({ entry: fileURLToPath2(new URL("../lib/inline-run.generated.mjs", import.meta.url)), portable }), ["loadSpurConfig"]);
117
118
  const spurConfig = await lib.loadSpurConfig(process.cwd(), portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined);
118
119
  process.exit(await app.runInlineRunDispatchFailure({
119
120
  runId,
@@ -5,13 +5,11 @@
5
5
  * `loadInlineApp` seam the setup facade uses. It is a SEPARATE script from `inline-run-setup.ts`
6
6
  * because it is a separate operation and the ADR-130 glue budget is per script. */
7
7
  import { spawnSync } from 'node:child_process';
8
- import { existsSync } from 'node:fs';
9
- import { dirname, join, resolve } from 'node:path';
10
8
  import { fileURLToPath } from 'node:url';
11
9
  // Type-only namespace import (erased before bundling; the smoke scanner exempts `import type`).
12
10
  import type * as spurApp from '@gobing-ai/spur-app';
13
11
  import { getEnvVar } from '../lib/env';
14
- import { loadInlineApp } from '../lib/inline-run-app';
12
+ import { loadInlineApp, resolveAppEntry } from '../lib/inline-run-app';
15
13
 
16
14
  // EMBEDDED_SPUR_SCHEMAS is declared by the bundle twin, never exported by the barrel.
17
15
  type InlineApp = typeof spurApp & { EMBEDDED_SPUR_SCHEMAS?: ReadonlyMap<string, string> };
@@ -32,24 +30,6 @@ function usage(): never {
32
30
  const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
33
31
 
34
32
  /** Resolve the app entry: --spur-bin/SPUR_BIN main module if it proves a repo checkout, else the bundle. */
35
- function resolveAppEntry(spurBin: string): { entry: string; portable: boolean } {
36
- const fallback = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
37
- const candidates = spurBin !== '' ? [spurBin] : [fallback];
38
- for (const candidate of candidates) {
39
- const tokens = candidate.split(/\s+/).filter(Boolean);
40
- // Main module = last path-like token; only a .ts source entry proves a repo checkout.
41
- const mainModule = [...tokens].reverse().find((t) => t.endsWith('.ts'));
42
- if (mainModule === undefined || !existsSync(mainModule)) continue;
43
- const repoRoot = resolve(dirname(mainModule), '..', '..', '..');
44
- const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
45
- if (existsSync(appEntry)) return { entry: appEntry, portable: false };
46
- }
47
- const entry = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
48
- if (!existsSync(entry)) {
49
- throw new Error('inline application bundle is missing — rebuild/install the sp plugin before running inline');
50
- }
51
- return { entry, portable: true };
52
- }
53
33
 
54
34
  async function main(): Promise<void> {
55
35
  // The portable Node twin has no workspace imports; the app bundle still uses Spur's Bun runtime.