@gobing-ai/spur 0.3.92 → 0.3.93

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 (56) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +0 -7
  3. package/config/plugin-scripts.json +10 -0
  4. package/config/templates/AGENTS.md +4 -0
  5. package/config/templates/docs/02_ROADMAP.md +2 -0
  6. package/config/templates/docs/03_ARCHITECTURE.md +3 -1
  7. package/config/templates/docs/04_DESIGN.md +2 -0
  8. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +10 -2
  9. package/config/workflow-candidates.json +72 -1
  10. package/config/workflows/feature-verification.yaml +1 -0
  11. package/config/workflows/history-anatomy.yaml +2 -0
  12. package/config/workflows/idea-pipeline.yaml +67 -18
  13. package/config/workflows/pr-review.yaml +8 -0
  14. package/config/workflows/task-pipeline.yaml +180 -39
  15. package/config/workflows/wayfinder-resolution.yaml +5 -0
  16. package/config/workflows/wrapup-pipeline.yaml +55 -14
  17. package/package.json +9 -9
  18. package/plugins/sp/README.md +7 -2
  19. package/plugins/sp/commands/dev-run.md +2 -2
  20. package/plugins/sp/commands/dev-runall.md +2 -2
  21. package/plugins/sp/lib/idea-handoff.generated.mjs +8 -4
  22. package/plugins/sp/lib/inline-run.generated.d.mts +1 -0
  23. package/plugins/sp/lib/inline-run.generated.mjs +24 -16
  24. package/plugins/sp/plugin.json +1 -1
  25. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +1 -1
  26. package/plugins/sp/scripts/inline-run-setup.mjs +75 -3
  27. package/plugins/sp/scripts/inline-run-setup.ts +133 -3
  28. package/plugins/sp/scripts/quality-gate.mjs +248 -6
  29. package/plugins/sp/scripts/quality-gate.ts +410 -9
  30. package/plugins/sp/scripts/residual-scan.mjs +12 -4
  31. package/plugins/sp/scripts/residual-scan.ts +32 -6
  32. package/plugins/sp/scripts/task-diffstat.mjs +156 -0
  33. package/plugins/sp/scripts/task-diffstat.ts +229 -0
  34. package/plugins/sp/scripts/wrapup-drift-probe.mjs +181 -0
  35. package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
  36. package/plugins/sp/scripts/wrapup-steps.mjs +60 -1
  37. package/plugins/sp/scripts/wrapup-steps.ts +89 -4
  38. package/plugins/sp/skills/brainstorm/SKILL.md +2 -0
  39. package/plugins/sp/skills/brainstorm/references/workflows.md +17 -2
  40. package/plugins/sp/skills/code-verification/SKILL.md +2 -2
  41. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -2
  42. package/plugins/sp/skills/spur-check/SKILL.md +112 -0
  43. package/plugins/sp/skills/spur-dev/SKILL.md +2 -1
  44. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +14 -2
  45. package/plugins/sp/skills/spur-dev/references/document-authoring.md +85 -0
  46. package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
  47. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +8 -0
  48. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
  49. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +19 -1
  50. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +5 -5
  51. package/plugins/sp/skills/spur-dev/templates/design.md +31 -0
  52. package/plugins/sp/skills/spur-dev/templates/plan.md +32 -0
  53. package/plugins/sp/skills/spur-doctor/SKILL.md +60 -14
  54. package/schemas/state-machine-workflow.schema.json +4 -0
  55. package/spur.js +1117 -472
  56. package/config/workflows/decision-routing-example.yaml +0 -134
@@ -86,6 +86,190 @@ function scanCoverageShortfalls(logText, threshold) {
86
86
  }
87
87
  return [...shortfalls.values()];
88
88
  }
89
+ var RECEIPT_SCHEMA_VERSION = "check-receipt/v1";
90
+ function buildReceipt(input) {
91
+ return {
92
+ schemaVersion: RECEIPT_SCHEMA_VERSION,
93
+ wbs: input.wbs,
94
+ runId: input.runId,
95
+ tier: input.tier,
96
+ inputDigest: input.inputDigest,
97
+ checks: input.checks,
98
+ status: input.checks.every((row) => row.status === "PASS") ? "PASS" : "FAIL",
99
+ completedAt: input.completedAt
100
+ };
101
+ }
102
+ function readReceipt(receiptPath) {
103
+ try {
104
+ return JSON.parse(readFileSync(receiptPath, "utf8"));
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ function receiptFailsAtDigest(receipt, currentDigest) {
110
+ return receipt !== null && receipt.schemaVersion === RECEIPT_SCHEMA_VERSION && receipt.tier === "full" && receipt.status === "FAIL" && currentDigest.length > 0 && receipt.inputDigest === currentDigest;
111
+ }
112
+ function readReceiptStatus(receiptPath, currentDigest) {
113
+ const receipt = readReceipt(receiptPath);
114
+ if (receipt === null || receipt.schemaVersion !== RECEIPT_SCHEMA_VERSION) {
115
+ return { reuse: false, reason: "missing" };
116
+ }
117
+ if (receipt.status !== "PASS")
118
+ return { reuse: false, reason: "failed" };
119
+ if (currentDigest.length === 0 || receipt.inputDigest !== currentDigest) {
120
+ return { reuse: false, reason: "stale" };
121
+ }
122
+ if (receipt.tier !== "full")
123
+ return { reuse: false, reason: "light-only" };
124
+ return { reuse: true, reason: "ok" };
125
+ }
126
+ var TEST_FILE_PATTERN = /\.test\.tsx?$/;
127
+ function workspaceOf(file, exists) {
128
+ const segments = file.split("/");
129
+ for (let depth = 1;depth < segments.length; depth++) {
130
+ const prefix = segments.slice(0, depth).join("/");
131
+ if (exists(`${prefix}/package.json`))
132
+ return prefix;
133
+ }
134
+ return null;
135
+ }
136
+ function lightScope(changedFiles, exists = existsSync) {
137
+ const files = [];
138
+ const workspaces = new Set;
139
+ const tests = new Set;
140
+ for (const file of changedFiles) {
141
+ files.push(file);
142
+ const workspace = workspaceOf(file, exists);
143
+ if (workspace === null)
144
+ continue;
145
+ workspaces.add(workspace);
146
+ const rest = file.slice(workspace.length + 1);
147
+ if (rest.startsWith("src/")) {
148
+ const candidate = `${workspace}/tests/${rest.slice("src/".length).replace(/\.tsx?$/, (ext) => `.test${ext}`)}`;
149
+ if (exists(candidate))
150
+ tests.add(candidate);
151
+ } else if (rest.startsWith("tests/") && TEST_FILE_PATTERN.test(rest)) {
152
+ tests.add(file);
153
+ }
154
+ }
155
+ return { files, workspaces: [...workspaces].sort(), tests: [...tests].sort() };
156
+ }
157
+ function workspaceHasTypecheck(workspace) {
158
+ try {
159
+ const pkg = JSON.parse(readFileSync(join(workspace, "package.json"), "utf8"));
160
+ return typeof pkg.scripts?.typecheck === "string";
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+ function planLightChecks(scope, hasTypecheck = workspaceHasTypecheck) {
166
+ const plans = [];
167
+ if (scope.files.length > 0) {
168
+ plans.push({ id: "format-lint:changed", cmd: `bunx biome check ${scope.files.join(" ")}` });
169
+ }
170
+ for (const workspace of scope.workspaces) {
171
+ if (hasTypecheck(workspace)) {
172
+ plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${workspace} && bun run typecheck` });
173
+ }
174
+ }
175
+ const testsByWorkspace = new Map;
176
+ for (const test of scope.tests) {
177
+ const workspace = test.slice(0, test.indexOf("/tests/"));
178
+ const paths = testsByWorkspace.get(workspace) ?? [];
179
+ paths.push(test.slice(workspace.length + 1));
180
+ testsByWorkspace.set(workspace, paths);
181
+ }
182
+ for (const [workspace, paths] of testsByWorkspace) {
183
+ plans.push({ id: `test:${workspace}`, cmd: `cd ${workspace} && bun test ${paths.join(" ")}` });
184
+ }
185
+ return plans;
186
+ }
187
+ function gitChangedFiles(cwd) {
188
+ const abs = (p) => cwd ? join(cwd, p) : p;
189
+ const changed = new Set;
190
+ for (const cmd of ["git diff --name-only HEAD", "git ls-files --others --exclude-standard"]) {
191
+ const result = runShellCommand(cmd, cwd);
192
+ for (const line of result.output.split(`
193
+ `)) {
194
+ const file = line.trim();
195
+ if (file.length > 0 && existsSync(abs(file)))
196
+ changed.add(file);
197
+ }
198
+ }
199
+ return [...changed].sort();
200
+ }
201
+ function receiptRunId(env) {
202
+ return (env.runId ?? "").length > 0 ? env.runId : `pipeline-${env.wbs}`;
203
+ }
204
+ function runLightGate(env, options = {}) {
205
+ const cwd = options.cwd;
206
+ const abs = (p) => cwd ? join(cwd, p) : p;
207
+ const runDir = join(".spur", "run");
208
+ mkdirSync(abs(runDir), { recursive: true });
209
+ const logFile = join(runDir, `${env.wbs}-light-gate.log`);
210
+ const receiptFile = join(runDir, `${env.wbs}-check-receipt.json`);
211
+ writeFileSync(abs(logFile), "");
212
+ const digest = env.proofDigest ?? "";
213
+ const scope = lightScope(gitChangedFiles(cwd), (p) => existsSync(abs(p)));
214
+ const plans = planLightChecks(scope, (workspace) => workspaceHasTypecheck(abs(workspace)));
215
+ const reusable = new Map;
216
+ let preserveFullReceipt = false;
217
+ if (digest.length > 0) {
218
+ try {
219
+ const prior = JSON.parse(readFileSync(abs(receiptFile), "utf8"));
220
+ if (prior?.schemaVersion === RECEIPT_SCHEMA_VERSION && prior.inputDigest === digest) {
221
+ if (prior.tier === "full") {
222
+ preserveFullReceipt = true;
223
+ } else {
224
+ for (const row of prior.checks ?? [])
225
+ if (row.status === "PASS")
226
+ reusable.set(row.id, row);
227
+ }
228
+ }
229
+ } catch {}
230
+ }
231
+ const checks = [];
232
+ let skipped = 0;
233
+ for (const plan of plans) {
234
+ const priorRow = reusable.get(plan.id);
235
+ if (priorRow !== undefined) {
236
+ checks.push(priorRow);
237
+ skipped++;
238
+ const line = `--- light ${plan.id}: check.reused \u2014 skipped (PASS at the same input digest)
239
+ `;
240
+ process.stdout.write(line);
241
+ appendFileSync(abs(logFile), line);
242
+ continue;
243
+ }
244
+ const startedAtMs = Date.now();
245
+ const result = runShellCommand(plan.cmd, cwd);
246
+ const durationMs = Date.now() - startedAtMs;
247
+ const status = result.code === 0 ? "PASS" : "FAIL";
248
+ appendFileSync(abs(logFile), `--- light ${plan.id}: ${status} (${durationMs}ms)
249
+ ${result.output}`);
250
+ checks.push({ id: plan.id, cmd: plan.cmd, status, durationMs, logPath: logFile });
251
+ }
252
+ const receipt = buildReceipt({
253
+ wbs: env.wbs,
254
+ runId: receiptRunId(env),
255
+ tier: "light",
256
+ inputDigest: digest,
257
+ checks,
258
+ completedAt: new Date().toISOString()
259
+ });
260
+ if (preserveFullReceipt) {
261
+ appendFileSync(abs(logFile), `--- light receipt: not written \u2014 the full-tier receipt at the same input digest is preserved
262
+ `);
263
+ process.stdout.write(`light gate ${receipt.status} (${scope.files.length} changed files; checks: ${checks.length},` + ` skipped: ${skipped}; receipt: ${receiptFile} preserved (full tier); log: ${logFile})
264
+ `);
265
+ } else {
266
+ writeFileSync(abs(receiptFile), `${JSON.stringify(receipt, null, 2)}
267
+ `);
268
+ process.stdout.write(`light gate ${receipt.status} (${scope.files.length} changed files; checks: ${checks.length},` + ` skipped: ${skipped}; receipt: ${receiptFile}; log: ${logFile})
269
+ `);
270
+ }
271
+ return { status: receipt.status, scope, checks, logFile, receiptFile };
272
+ }
89
273
  function retryDelayMs(env) {
90
274
  const raw = Number.parseInt(env[RETRY_DELAY_MS_ENV] ?? "", 10);
91
275
  return Number.isFinite(raw) && raw >= 0 ? raw : RETRY_DELAY_MS_DEFAULT;
@@ -124,9 +308,18 @@ function runQualityGate(mode, env, options = {}) {
124
308
  if (mode === "run")
125
309
  writeFileSync(abs(attemptFile), `0
126
310
  `);
311
+ const gateStartedAtMs = Date.now();
127
312
  let gateRc = 0;
128
313
  let gateAttempt = 0;
129
- if (mode === "recheck" && (env.gateProbeCmd ?? "").length > 0) {
314
+ const noProgressSkip = mode === "recheck" && receiptFailsAtDigest(readReceipt(abs(rel("-check-receipt.json"))), env.proofDigest ?? "");
315
+ if (noProgressSkip) {
316
+ const line = `check.skipped-no-progress \u2014 full-tier FAIL receipt at input digest ${env.proofDigest ?? ""}; recheck skipped
317
+ `;
318
+ process.stdout.write(line);
319
+ appendFileSync(abs(logFile), line);
320
+ gateRc = 1;
321
+ }
322
+ if (mode === "recheck" && !noProgressSkip && (env.gateProbeCmd ?? "").length > 0) {
130
323
  const probe = runShellCommand(env.gateProbeCmd ?? "", cwd);
131
324
  writeFileSync(abs(`${logFile}.probe`), probe.output);
132
325
  gateRc = probe.code;
@@ -182,12 +375,38 @@ function runQualityGate(mode, env, options = {}) {
182
375
  `);
183
376
  appendFileSync(abs(logFile), `proof-digest: ${env.proofDigest ?? ""}
184
377
  `);
185
- return { status, attempts: gateAttempt, logFile, findingsFile, statusFile, attemptFile };
378
+ let receiptFile;
379
+ if (mode === "run") {
380
+ if ((env.proofDigest ?? "").length > 0) {
381
+ receiptFile = join(runDir, `${env.wbs}-check-receipt.json`);
382
+ const receipt = buildReceipt({
383
+ wbs: env.wbs,
384
+ runId: receiptRunId(env),
385
+ tier: "full",
386
+ inputDigest: env.proofDigest ?? "",
387
+ checks: [
388
+ {
389
+ id: "test",
390
+ cmd: env.qualityGateCmd ?? "",
391
+ status,
392
+ durationMs: Date.now() - gateStartedAtMs,
393
+ logPath: logFile
394
+ }
395
+ ],
396
+ completedAt: new Date().toISOString()
397
+ });
398
+ writeFileSync(abs(receiptFile), `${JSON.stringify(receipt, null, 2)}
399
+ `);
400
+ } else {
401
+ appendFileSync(abs(logFile), "check-receipt: not written \u2014 env `proofDigest` is not set\n");
402
+ }
403
+ }
404
+ return { status, attempts: gateAttempt, logFile, findingsFile, statusFile, attemptFile, receiptFile };
186
405
  }
187
- var QUALITY_GATE_USAGE = "usage: quality-gate.ts <run|recheck> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest)";
188
- function main(argv, env = getEnvVars()) {
406
+ var QUALITY_GATE_USAGE = "usage: quality-gate.ts <run|recheck|light|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)";
407
+ function main(argv, env = getEnvVars(), options = {}) {
189
408
  const mode = argv[0];
190
- if (mode !== "run" && mode !== "recheck") {
409
+ if (mode !== "run" && mode !== "recheck" && mode !== "light" && mode !== "status") {
191
410
  process.stderr.write(`${QUALITY_GATE_USAGE}
192
411
  `);
193
412
  return 2;
@@ -196,24 +415,47 @@ function main(argv, env = getEnvVars()) {
196
415
  process.stderr.write("quality-gate: env `wbs` is required\n");
197
416
  return 2;
198
417
  }
199
- runQualityGate(mode, env);
418
+ if (mode === "light") {
419
+ runLightGate(env);
420
+ } else if (mode === "status") {
421
+ const runDir = join(options.cwd ?? ".", ".spur", "run");
422
+ const verdict = readReceiptStatus(join(runDir, `${env.wbs}-check-receipt.json`), env.proofDigest ?? "");
423
+ if (verdict.reuse) {
424
+ const line = `check.reused \u2014 full-tier receipt reused for input digest ${env.proofDigest ?? ""}
425
+ `;
426
+ process.stdout.write(line);
427
+ appendFileSync(join(runDir, `${env.wbs}-test-gate.log`), line);
428
+ }
429
+ process.stdout.write(`${JSON.stringify(verdict)}
430
+ `);
431
+ } else {
432
+ runQualityGate(mode, env);
433
+ }
200
434
  return 0;
201
435
  }
202
436
  {
203
437
  process.exit(main(process.argv.slice(2)));
204
438
  }
205
439
  export {
440
+ workspaceHasTypecheck,
206
441
  tailLines,
207
442
  scanCoverageShortfalls,
208
443
  runShellCommand,
209
444
  runQualityGate,
445
+ runLightGate,
210
446
  retryMessage,
447
+ receiptFailsAtDigest,
448
+ readReceiptStatus,
449
+ planLightChecks,
211
450
  parseCoverageThreshold,
212
451
  main,
452
+ lightScope,
213
453
  isTransientLock,
214
454
  extractFindings,
455
+ buildReceipt,
215
456
  RETRY_DELAY_MS_ENV,
216
457
  RETRY_DELAY_MS_DEFAULT,
458
+ RECEIPT_SCHEMA_VERSION,
217
459
  QUALITY_GATE_USAGE,
218
460
  MAX_GATE_ATTEMPTS,
219
461
  MAX_FINDINGS,