@gobing-ai/spur 0.3.81 → 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 (92) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +19 -17
  3. package/config/config.global.yaml +6 -3
  4. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +75 -13
  5. package/config/transition-shims.json +0 -7
  6. package/package.json +1 -1
  7. package/plugins/sp/README.md +2 -2
  8. package/plugins/sp/agents/expert-spur.md +3 -3
  9. package/plugins/sp/lib/idea-handoff.generated.mjs +43 -43
  10. package/plugins/sp/plugin.json +1 -1
  11. package/plugins/sp/scripts/inline-run-setup.ts +69 -1
  12. package/plugins/sp/scripts/quality-gate.mjs +52 -1
  13. package/plugins/sp/scripts/quality-gate.ts +70 -0
  14. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -2
  15. package/plugins/sp/scripts/task-size-precheck.ts +44 -16
  16. package/plugins/sp/scripts/verify-answer-lint.ts +17 -1
  17. package/plugins/sp/skills/code-review/references/review-lenses.md +3 -0
  18. package/plugins/sp/skills/spur-cli/SKILL.md +4 -8
  19. package/plugins/sp/skills/spur-cli/references/agent.md +30 -97
  20. package/plugins/sp/skills/spur-cli/references/message.md +1 -1
  21. package/plugins/sp/skills/spur-cli/references/projects.md +7 -27
  22. package/plugins/sp/skills/spur-cli/references/self.md +2 -2
  23. package/plugins/sp/skills/spur-cli/references/serve.md +4 -4
  24. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
  25. package/plugins/sp/skills/spur-composer/SKILL.md +3 -3
  26. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +10 -0
  27. package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
  28. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +4 -6
  29. package/plugins/sp/skills/spur-dev/references/glossary.md +1 -1
  30. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +11 -1
  31. package/plugins/sp/skills/spur-doctor/SKILL.md +2 -2
  32. package/schemas/spur-config.schema.json +56 -85
  33. package/spur.js +3498 -4835
  34. package/web/_astro/BoardApp.D-WlxiN2.js +1 -0
  35. package/web/_astro/{BoardApp.B1U26g3I.js → BoardApp.D8bM9pKL.js} +73 -73
  36. package/web/_astro/{TaskDetail.DwPqpq7v.js → TaskDetail.BPRqgVUE.js} +1 -1
  37. package/web/_astro/{arc.CweZEjN2.js → arc.BPrPES3z.js} +1 -1
  38. package/web/_astro/{architectureDiagram-3BPJPVTR.D89pbDuv.js → architectureDiagram-3BPJPVTR.qX_7q02P.js} +1 -1
  39. package/web/_astro/{blockDiagram-GPEHLZMM.BOuTeEpX.js → blockDiagram-GPEHLZMM.CUZfj5V7.js} +1 -1
  40. package/web/_astro/{c4Diagram-AAUBKEIU.CASbkWZF.js → c4Diagram-AAUBKEIU.CTaOr8hH.js} +1 -1
  41. package/web/_astro/channel.DGZaFHZx.js +1 -0
  42. package/web/_astro/{chunk-2J33WTMH.BKQYtOvY.js → chunk-2J33WTMH.Dt-9wf3h.js} +1 -1
  43. package/web/_astro/{chunk-4BX2VUAB.9sHLdMtG.js → chunk-4BX2VUAB.CTC2sdoN.js} +1 -1
  44. package/web/_astro/{chunk-55IACEB6.wOLXWlPs.js → chunk-55IACEB6.DQcxt2_g.js} +1 -1
  45. package/web/_astro/{chunk-727SXJPM.DovFbwg3.js → chunk-727SXJPM.DXFPSn-a.js} +1 -1
  46. package/web/_astro/{chunk-AQP2D5EJ.B1Weod1X.js → chunk-AQP2D5EJ.BCx3U4bT.js} +1 -1
  47. package/web/_astro/{chunk-FMBD7UC4.TEMS04st.js → chunk-FMBD7UC4.DL2tJkdO.js} +1 -1
  48. package/web/_astro/{chunk-ND2GUHAM.Cp8VT1wQ.js → chunk-ND2GUHAM.DZyflMro.js} +1 -1
  49. package/web/_astro/{chunk-QZHKN3VN.BzATdEcP.js → chunk-QZHKN3VN.CUI2mT09.js} +1 -1
  50. package/web/_astro/{classDiagram-4FO5ZUOK.C9BOCfAO.js → classDiagram-4FO5ZUOK.g4rX4Fr1.js} +1 -1
  51. package/web/_astro/{classDiagram-v2-Q7XG4LA2.C9BOCfAO.js → classDiagram-v2-Q7XG4LA2.g4rX4Fr1.js} +1 -1
  52. package/web/_astro/{cose-bilkent-S5V4N54A.DUnr4UAw.js → cose-bilkent-S5V4N54A.CzWJLqp0.js} +1 -1
  53. package/web/_astro/{cynefin-OW5HDTMX.rYq5uM3D.js → cynefin-OW5HDTMX.WgsvQeCR.js} +1 -1
  54. package/web/_astro/{dagre-BM42HDAG.CWeNKe3I.js → dagre-BM42HDAG.Dzv6ngql.js} +1 -1
  55. package/web/_astro/{diagram-2AECGRRQ.DCkfls10.js → diagram-2AECGRRQ.CpJ4a9rU.js} +1 -1
  56. package/web/_astro/{diagram-5GNKFQAL.D5U4JCka.js → diagram-5GNKFQAL.CzPlF_dq.js} +1 -1
  57. package/web/_astro/{diagram-KO2AKTUF.BZJgqaqG.js → diagram-KO2AKTUF.TAkZNTcQ.js} +1 -1
  58. package/web/_astro/{diagram-LMA3HP47.DoMeHvPR.js → diagram-LMA3HP47.uCjoKSag.js} +1 -1
  59. package/web/_astro/{diagram-OG6HWLK6.B50qwwWX.js → diagram-OG6HWLK6.eMplIjoK.js} +1 -1
  60. package/web/_astro/{erDiagram-TEJ5UH35.DdGPG6LK.js → erDiagram-TEJ5UH35.Bf7zoXGz.js} +1 -1
  61. package/web/_astro/{flowDiagram-I6XJVG4X.QP2MJ12u.js → flowDiagram-I6XJVG4X.B_bHj3gN.js} +1 -1
  62. package/web/_astro/{ganttDiagram-6RSMTGT7.BI6LgKSy.js → ganttDiagram-6RSMTGT7.BasrHRMj.js} +1 -1
  63. package/web/_astro/{gitGraphDiagram-PVQCEYII.npPZiC2G.js → gitGraphDiagram-PVQCEYII.C6iphq1x.js} +1 -1
  64. package/web/_astro/{infoDiagram-5YYISTIA.DCJCBVbp.js → infoDiagram-5YYISTIA.HXmDMhW4.js} +1 -1
  65. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BMLV-3I1.js → ishikawaDiagram-YF4QCWOH.BSmW8NiU.js} +1 -1
  66. package/web/_astro/{journeyDiagram-JHISSGLW.LE58crde.js → journeyDiagram-JHISSGLW.DEQow5fo.js} +1 -1
  67. package/web/_astro/{kanban-definition-UN3LZRKU.BPbz8rH9.js → kanban-definition-UN3LZRKU.IVm9cTdc.js} +1 -1
  68. package/web/_astro/{linear.DhZaBtYh.js → linear.CrsM73_9.js} +1 -1
  69. package/web/_astro/{mermaid.core.BD5-jXum.js → mermaid.core.CfBeDJls.js} +4 -4
  70. package/web/_astro/{mindmap-definition-RKZ34NQL.MTJyrQ65.js → mindmap-definition-RKZ34NQL.C3j60Y-0.js} +1 -1
  71. package/web/_astro/{pieDiagram-4H26LBE5.BrDhDvIS.js → pieDiagram-4H26LBE5.B-aCMeEA.js} +1 -1
  72. package/web/_astro/{quadrantDiagram-W4KKPZXB.71d73_5N.js → quadrantDiagram-W4KKPZXB.Cib965yq.js} +1 -1
  73. package/web/_astro/{requirementDiagram-4Y6WPE33.Bga6UF-z.js → requirementDiagram-4Y6WPE33.D61cS4O-.js} +1 -1
  74. package/web/_astro/{sankeyDiagram-5OEKKPKP.BnHs4K82.js → sankeyDiagram-5OEKKPKP.GKF2qVPy.js} +1 -1
  75. package/web/_astro/{sequenceDiagram-3UESZ5HK.DsfY2gnj.js → sequenceDiagram-3UESZ5HK.DZnq8F2h.js} +1 -1
  76. package/web/_astro/{stateDiagram-AJRCARHV.DvsTSc9a.js → stateDiagram-AJRCARHV.DXUFmdgM.js} +1 -1
  77. package/web/_astro/{stateDiagram-v2-BHNVJYJU.DxzzmHUR.js → stateDiagram-v2-BHNVJYJU.BtHmhLEz.js} +1 -1
  78. package/web/_astro/{timeline-definition-PNZ67QCA.4ZuQmOTt.js → timeline-definition-PNZ67QCA.Cy-WW2ln.js} +1 -1
  79. package/web/_astro/{vennDiagram-CIIHVFJN.Ck5Q86SG.js → vennDiagram-CIIHVFJN.SLp5b9KI.js} +1 -1
  80. package/web/_astro/{wardleyDiagram-YWT4CUSO.BK7k2hXr.js → wardleyDiagram-YWT4CUSO.Bww45mWV.js} +1 -1
  81. package/web/_astro/{xychartDiagram-2RQKCTM6.DfCrgauK.js → xychartDiagram-2RQKCTM6.DR4swI6a.js} +1 -1
  82. package/web/apple-touch-icon.png +0 -0
  83. package/web/favicon.ico +0 -0
  84. package/web/favicon.svg +17 -4
  85. package/web/icon-192.png +0 -0
  86. package/web/icon-512.png +0 -0
  87. package/web/index.html +2 -2
  88. package/web/site.webmanifest +31 -0
  89. package/web/spur_logo.svg +1 -0
  90. package/plugins/sp/skills/spur-cli/references/team.md +0 -165
  91. package/web/_astro/BoardApp.Csgyg-lS.js +0 -1
  92. package/web/_astro/channel.Cx6sXxhq.js +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.81",
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"]
@@ -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
 
@@ -23,6 +23,7 @@ var RETRY_DELAY_MS_DEFAULT = 1e4;
23
23
  var RETRY_DELAY_MS_ENV = "SPUR_QUALITY_GATE_RETRY_DELAY_MS";
24
24
  var LOCKED_PATTERN = /SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY/;
25
25
  var FINDINGS_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
26
+ var COVERAGE_ROW_PATTERN = /^\s*(\S+\.[A-Za-z]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*(\S*)/;
26
27
  function isTransientLock(attemptOutput) {
27
28
  return LOCKED_PATTERN.test(attemptOutput);
28
29
  }
@@ -46,6 +47,38 @@ function retryMessage(attempt) {
46
47
  return `quality gate: database is locked; retrying (${attempt}/${MAX_GATE_ATTEMPTS}) in 10s
47
48
  `;
48
49
  }
50
+ function parseCoverageThreshold(bunfigText) {
51
+ const block = bunfigText.match(/coverageThreshold\s*=\s*\{([^}]*)\}/);
52
+ if (!block)
53
+ return null;
54
+ const threshold = {};
55
+ for (const axis of ["functions", "lines"]) {
56
+ const raw = block[1]?.match(new RegExp(`\\b${axis}\\s*=\\s*([\\d.]+)`))?.[1];
57
+ const value = raw === undefined ? Number.NaN : Number.parseFloat(raw);
58
+ if (Number.isFinite(value))
59
+ threshold[axis] = value;
60
+ }
61
+ return threshold.functions === undefined && threshold.lines === undefined ? null : threshold;
62
+ }
63
+ function scanCoverageShortfalls(logText, threshold) {
64
+ const shortfalls = new Map;
65
+ for (const line of logText.split(`
66
+ `)) {
67
+ const row = line.match(COVERAGE_ROW_PATTERN);
68
+ if (!row)
69
+ continue;
70
+ const [, path, funcs, lines, uncovered] = row;
71
+ if (!path || path === "All files" || shortfalls.has(path))
72
+ continue;
73
+ const belowFunctions = threshold.functions !== undefined && Number.parseFloat(funcs ?? "") < threshold.functions * 100;
74
+ const belowLines = threshold.lines !== undefined && Number.parseFloat(lines ?? "") < threshold.lines * 100;
75
+ if (!belowFunctions && !belowLines)
76
+ continue;
77
+ const firstUncovered = uncovered?.match(/\d+/)?.[0] ?? "1";
78
+ shortfalls.set(path, `quality gate: coverage shortfall ${path}:${firstUncovered} funcs=${funcs} lines=${lines}`);
79
+ }
80
+ return [...shortfalls.values()];
81
+ }
49
82
  function retryDelayMs(env) {
50
83
  const raw = Number.parseInt(env[RETRY_DELAY_MS_ENV] ?? "", 10);
51
84
  return Number.isFinite(raw) && raw >= 0 ? raw : RETRY_DELAY_MS_DEFAULT;
@@ -111,6 +144,21 @@ function runQualityGate(mode, env, options = {}) {
111
144
  gateSleep(delayMs);
112
145
  }
113
146
  }
147
+ if (gateRc !== 0) {
148
+ const gateLog = existsSync(abs(logFile)) ? readFileSync(abs(logFile), "utf8") : "";
149
+ if (/^\s*0 fail\b/m.test(gateLog) && !/^\s*[1-9]\d*\s+fail\b/m.test(gateLog)) {
150
+ const bunfigPath = cwd ? join(cwd, "bunfig.toml") : "bunfig.toml";
151
+ const threshold = existsSync(bunfigPath) ? parseCoverageThreshold(readFileSync(bunfigPath, "utf8")) : null;
152
+ if (threshold) {
153
+ for (const shortfall of scanCoverageShortfalls(gateLog, threshold)) {
154
+ process.stdout.write(`${shortfall}
155
+ `);
156
+ appendFileSync(abs(logFile), `${shortfall}
157
+ `);
158
+ }
159
+ }
160
+ }
161
+ }
114
162
  if (gateRc === 0) {
115
163
  const bytes = existsSync(abs(logFile)) ? statSync(abs(logFile)).size : 0;
116
164
  const attemptsLabel = gateAttempt > 0 ? String(gateAttempt) : "";
@@ -149,9 +197,11 @@ function main(argv, env = process.env) {
149
197
  }
150
198
  export {
151
199
  tailLines,
200
+ scanCoverageShortfalls,
152
201
  runShellCommand,
153
202
  runQualityGate,
154
203
  retryMessage,
204
+ parseCoverageThreshold,
155
205
  main,
156
206
  isTransientLock,
157
207
  extractFindings,
@@ -161,5 +211,6 @@ export {
161
211
  MAX_GATE_ATTEMPTS,
162
212
  MAX_FINDINGS,
163
213
  LOCKED_PATTERN,
164
- FINDINGS_PATTERN
214
+ FINDINGS_PATTERN,
215
+ COVERAGE_ROW_PATTERN
165
216
  };
@@ -53,6 +53,12 @@ export const LOCKED_PATTERN = /SQLiteError: database is locked|SQLite database .
53
53
  /** `file.ext:line` anchor extraction, mirroring the former grep -oE. */
54
54
  export const FINDINGS_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
55
55
 
56
+ /**
57
+ * bun's coverage table row: `File | % Funcs | % Lines | Uncovered Line #s` (0862 R2).
58
+ * Group 4 (uncovered line numbers) may be empty — a fully covered file has no entry.
59
+ */
60
+ export const COVERAGE_ROW_PATTERN = /^\s*(\S+\.[A-Za-z]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*(\S*)/;
61
+
56
62
  export interface QualityGateEnv {
57
63
  wbs: string;
58
64
  qualityGateCmd?: string;
@@ -102,6 +108,53 @@ export function retryMessage(attempt: number): string {
102
108
  return `quality gate: database is locked; retrying (${attempt}/${MAX_GATE_ATTEMPTS}) in 10s\n`;
103
109
  }
104
110
 
111
+ /** Per-axis coverage floor from `bunfig.toml`; a missing key leaves that axis unchecked. */
112
+ export interface CoverageThreshold {
113
+ functions?: number;
114
+ lines?: number;
115
+ }
116
+
117
+ /**
118
+ * Parse `coverageThreshold = { lines = 0.9, functions = 0.9 }` out of bunfig text.
119
+ * `null` when the setting is absent — the caller then skips the shortfall scan entirely.
120
+ */
121
+ export function parseCoverageThreshold(bunfigText: string): CoverageThreshold | null {
122
+ const block = bunfigText.match(/coverageThreshold\s*=\s*\{([^}]*)\}/);
123
+ if (!block) return null;
124
+ const threshold: CoverageThreshold = {};
125
+ for (const axis of ['functions', 'lines'] as const) {
126
+ const raw = block[1]?.match(new RegExp(`\\b${axis}\\s*=\\s*([\\d.]+)`))?.[1];
127
+ const value = raw === undefined ? Number.NaN : Number.parseFloat(raw);
128
+ if (Number.isFinite(value)) threshold[axis] = value;
129
+ }
130
+ return threshold.functions === undefined && threshold.lines === undefined ? null : threshold;
131
+ }
132
+
133
+ /**
134
+ * One `quality gate: coverage shortfall <path>:<line> funcs=<f> lines=<l>` line per distinct table
135
+ * row below either axis (thresholds are fractions, table numbers are percentages). `line` is the
136
+ * first number of the uncovered column, else `1`.
137
+ */
138
+ export function scanCoverageShortfalls(logText: string, threshold: CoverageThreshold): string[] {
139
+ const shortfalls = new Map<string, string>();
140
+ for (const line of logText.split('\n')) {
141
+ const row = line.match(COVERAGE_ROW_PATTERN);
142
+ if (!row) continue;
143
+ const [, path, funcs, lines, uncovered] = row;
144
+ if (!path || path === 'All files' || shortfalls.has(path)) continue;
145
+ const belowFunctions =
146
+ threshold.functions !== undefined && Number.parseFloat(funcs ?? '') < threshold.functions * 100;
147
+ const belowLines = threshold.lines !== undefined && Number.parseFloat(lines ?? '') < threshold.lines * 100;
148
+ if (!belowFunctions && !belowLines) continue;
149
+ const firstUncovered = uncovered?.match(/\d+/)?.[0] ?? '1';
150
+ shortfalls.set(
151
+ path,
152
+ `quality gate: coverage shortfall ${path}:${firstUncovered} funcs=${funcs} lines=${lines}`,
153
+ );
154
+ }
155
+ return [...shortfalls.values()];
156
+ }
157
+
105
158
  function retryDelayMs(env: QualityGateEnv): number {
106
159
  const raw = Number.parseInt(env[RETRY_DELAY_MS_ENV] ?? '', 10);
107
160
  return Number.isFinite(raw) && raw >= 0 ? raw : RETRY_DELAY_MS_DEFAULT;
@@ -178,6 +231,23 @@ export function runQualityGate(
178
231
  }
179
232
  }
180
233
 
234
+ // Coverage-only failures carry no `file.ext:line` anchor, so findings extraction would hand the
235
+ // fix hop an empty file. Name one shortfall line per under-threshold row instead (0862 R2) —
236
+ // real test failures, an absent threshold and the exit-0 contract are untouched.
237
+ if (gateRc !== 0) {
238
+ const gateLog = existsSync(abs(logFile)) ? readFileSync(abs(logFile), 'utf8') : '';
239
+ if (/^\s*0 fail\b/m.test(gateLog) && !/^\s*[1-9]\d*\s+fail\b/m.test(gateLog)) {
240
+ const bunfigPath = cwd ? join(cwd, 'bunfig.toml') : 'bunfig.toml';
241
+ const threshold = existsSync(bunfigPath) ? parseCoverageThreshold(readFileSync(bunfigPath, 'utf8')) : null;
242
+ if (threshold) {
243
+ for (const shortfall of scanCoverageShortfalls(gateLog, threshold)) {
244
+ process.stdout.write(`${shortfall}\n`); // tee: stdout and the log
245
+ appendFileSync(abs(logFile), `${shortfall}\n`);
246
+ }
247
+ }
248
+ }
249
+ }
250
+
181
251
  if (gateRc === 0) {
182
252
  const bytes = existsSync(abs(logFile)) ? statSync(abs(logFile)).size : 0;
183
253
  const attemptsLabel = gateAttempt > 0 ? String(gateAttempt) : '';
@@ -235,7 +235,6 @@ export function nounOfReference(file: string): string | null {
235
235
  workflows: 'workflow',
236
236
  agent: 'agent',
237
237
  message: 'message',
238
- team: 'team',
239
238
  init: 'init',
240
239
  serve: 'serve',
241
240
  };
@@ -641,7 +640,6 @@ const JSON_PROBES: string[][] = [
641
640
  ['workflow', 'list', '--json'],
642
641
  ['workflow', 'validate', 'basic.yaml', '--json'],
643
642
  ['status', '--json'],
644
- ['team', 'status', '--json'],
645
643
  ['projects', 'list', '--json'],
646
644
  ];
647
645
 
@@ -27,13 +27,50 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
27
27
  import { join } from 'node:path';
28
28
  import { fileURLToPath } from 'node:url';
29
29
 
30
- // ─── Regex (sync with packages/app/src/services/task-size-precheck.ts) ───────
30
+ // ─── Counting (sync with packages/app/src/services/task-size-precheck.ts) ────
31
31
 
32
- /** Matches `- [ ] **R1.**` or `- [x] R1.` etc. Requires period after digits. */
33
- const R_ITEM_RE = /^\s*-\s*\[[ xX]\]\s*(\*\*)?R\d+\./m;
32
+ /**
33
+ * Requirement item: list marker required, then an optional checkbox, then the R-number in the
34
+ * house `- **R1** — <text>` or legacy `- [ ] R1. <text>` form. A bare `R1 <text>` line is prose.
35
+ */
36
+ const R_ITEM_RE_SOURCE = '^\\s*[-*]\\s+(?:\\[[ xX]\\]\\s*)?[*_]{0,2}R(\\d+)\\.?[*_]{0,2}(?:\\s|$)';
37
+
38
+ /** Top-level numbered Plan step (`1. <text>`). */
39
+ const NUMBERED_PLAN_ITEM_RE_SOURCE = '^\\d+\\.\\s';
40
+
41
+ /** Checklist Plan item (`- [ ] <text>`, `- [x] <text>`). */
42
+ const CHECKLIST_ITEM_RE_SOURCE = '^\\s*[-*]\\s+\\[[ xX]\\]';
43
+
44
+ /**
45
+ * Body of a `##`/`###` section (heading line excluded), up to the next `##`/`###` heading.
46
+ * `null` when the heading is absent — callers decide the fallback.
47
+ */
48
+ function sectionBody(content: string, headingPattern: string): string | null {
49
+ const heading = content.match(new RegExp(headingPattern, 'm'));
50
+ if (!heading) return null;
51
+ const rest = content.slice((heading.index ?? 0) + heading[0].length);
52
+ const nextHeading = rest.match(/^#{2,3}\s+/m);
53
+ return nextHeading ? rest.slice(0, nextHeading.index ?? 0) : rest;
54
+ }
55
+
56
+ /** Distinct R-items in `## Requirements` (whole content when the heading is absent). */
57
+ function countRItems(taskContent: string): number {
58
+ const body = sectionBody(taskContent, '^#{2,3}\\s+Requirements\\s*$') ?? taskContent;
59
+ const numbers = new Set<string>();
60
+ for (const match of body.matchAll(new RegExp(R_ITEM_RE_SOURCE, 'gm'))) {
61
+ numbers.add(match[1] ?? '');
62
+ }
63
+ return numbers.size;
64
+ }
34
65
 
35
- /** Matches checklist items under the Plan section. */
36
- const CHECKLIST_ITEM_RE = /^\s*-\s*\[[ xX]\]/m;
66
+ /** Plan items: top-level numbered steps plus checklist items under `## Plan`. */
67
+ function countPlanItems(taskContent: string): number {
68
+ const body = sectionBody(taskContent, '^#{2,3}\\s+Plan\\s*$');
69
+ if (body === null) return 0;
70
+ const numbered = body.match(new RegExp(NUMBERED_PLAN_ITEM_RE_SOURCE, 'gm'))?.length ?? 0;
71
+ const checklist = body.match(new RegExp(CHECKLIST_ITEM_RE_SOURCE, 'gm'))?.length ?? 0;
72
+ return numbered + checklist;
73
+ }
37
74
 
38
75
  // ─── CLI ─────────────────────────────────────────────────────────────────────
39
76
 
@@ -128,17 +165,8 @@ function main(): void {
128
165
  process.exit(0);
129
166
  }
130
167
 
131
- const reqCount = taskContent.match(new RegExp(R_ITEM_RE.source, 'gm'))?.length ?? 0;
132
-
133
- const planMatch = taskContent.match(/^#{2,3}\s+Plan\s*$/m);
134
- const planBody = planMatch
135
- ? (() => {
136
- const rest = taskContent.slice((planMatch.index ?? 0) + planMatch[0].length);
137
- const nextSection = rest.match(/^#{2,3}\s+/m);
138
- return nextSection ? rest.slice(0, nextSection.index ?? 0) : rest;
139
- })()
140
- : '';
141
- const planItemCount = planBody.match(new RegExp(CHECKLIST_ITEM_RE.source, 'gm'))?.length ?? 0;
168
+ const reqCount = countRItems(taskContent);
169
+ const planItemCount = countPlanItems(taskContent);
142
170
 
143
171
  const reasons: string[] = [];
144
172
  if (reqCount > maxReqs) {
@@ -298,6 +298,11 @@ function normalizeAcTitle(title: string): string {
298
298
  // U+02BC stays a meaningful character, never removable punctuation.
299
299
  .replace(/[\u0027\u2018\u2019\u201c\u201d]/g, '')
300
300
  .replace(/\s+/g, ' ')
301
+ // A declared bold head keeps its sentence-final period (`- **AC1: … form.**`) while an
302
+ // answer row normally cites it without one. Both the declared spelling and the queried
303
+ // row normalize through here, so dropping it is symmetric — it can only merge titles
304
+ // that already differ by nothing but trailing periods.
305
+ .replace(/\s*\.+$/, '')
301
306
  .trim()
302
307
  );
303
308
  }
@@ -332,6 +337,16 @@ function buildAcIdentityIndex(taskContent: string, featureContent: string | null
332
337
  const leading = label.split(/\s+/)[0] ?? '';
333
338
  if (leading && leading !== label) declareIdentity(leading);
334
339
  }
340
+ // Bold head of a single-line criterion bullet (task 0862 R4): the bold span and its
341
+ // head before the first ` — ` / `:` are declared ids, so `- **AC2 — Title.** Given …`
342
+ // answers to both `AC2` and the full bold title.
343
+ for (const m of section.matchAll(/^[-*]\s+(?:\[[ xX]\]\s+)?\*\*(.+?)\*\*/gm)) {
344
+ const inner = (m[1] ?? '').trim();
345
+ if (!inner) continue;
346
+ declareIdentity(inner);
347
+ const head = inner.split(/\s+[—–]\s+|:/)[0]?.trim() ?? '';
348
+ if (head && head !== inner) declareIdentity(head);
349
+ }
335
350
  // Bold-trajectory form (task 0817 R3): answers may cite an AC by a bare
336
351
  // `**AC id**` paragraph (house style for long/complex ids). Only whole-line
337
352
  // bold spans count — the line-anchored lazy regex rejects lines with two
@@ -492,7 +507,8 @@ function main(): void {
492
507
  else
493
508
  add(
494
509
  `line ${row.line}: AC ID "${row.id.slice(0, 60)}" matches no task AC checklist label or scenario title ` +
495
- '(accepted forms: exact title, bare title, `Scenario:` prefix, bracket tags, declared AC-N alias)',
510
+ "(accepted forms: exact title, bare title, a criterion bullet's bold head or full bold span, " +
511
+ '`Scenario:` prefix, bracket tags, declared AC-N alias)',
496
512
  );
497
513
  } else if (canonicalKey !== null && seenAc.has(canonicalKey)) {
498
514
  const first = seenAc.get(canonicalKey) ?? '';
@@ -18,6 +18,9 @@ Five dimensions for structured code review. Each lens has diagnostic questions;
18
18
  - Are edge cases handled (empty, null, boundary, error)?
19
19
  - Are invariants preserved across the changed functions?
20
20
  - Is there a test that fails if the behavior is wrong?
21
+ - A changed route response shape or client parser is covered both ways: a server-side key-shape
22
+ assertion and a test that runs a real payload through the real parser (a dropped key like `teamId`
23
+ must fail something). Missing either → P2.
21
24
 
22
25
  **Severity guide:** Logic errors that produce wrong output → P1. Missing edge case that could cause wrong output → P2. Unclear control flow that might hide bugs → P3.
23
26
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: spur-cli
3
- description: "Reference and operate the complete `spur` CLI surface: task, feature, rule, workflow, builder, agent, message, team, self, history, and projects. Use for verb or flag lookup, machine-readable output and exit contracts, or CLI-gated corpus writes. Triggers: \"spur task\", \"spur history\", \"spur projects\", \"create a task\", \"task check\", and any Spur CLI convention. Not for planning or execution lifecycle orchestration (`sp:spur-dev`)."
3
+ description: "Reference and operate the complete `spur` CLI surface: task, feature, rule, workflow, builder, agent, message, self, history, and projects. Use for verb or flag lookup, machine-readable output and exit contracts, or CLI-gated corpus writes. Triggers: \"spur task\", \"spur history\", \"spur projects\", \"create a task\", \"task check\", and any Spur CLI convention. Not for planning or execution lifecycle orchestration (`sp:spur-dev`)."
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: spur
@@ -17,7 +17,6 @@ metadata:
17
17
  - builder
18
18
  - agent
19
19
  - message
20
- - team
21
20
  - self
22
21
  - history
23
22
  - projects
@@ -45,9 +44,8 @@ Pick the noun, read its reference. Each Tier A and Tier B reference owns that no
45
44
  | **Tier A** | **rule** | Constraint quality gate: run presets, author rules, fine-tune, validate rule files/presets, extend engine | [references/rules.md](references/rules.md) |
46
45
  | **Tier A** | **workflow** | Dual-mode workflow runtime: author state-machine / transition-flow workflows, validate, run, read traces | [references/workflows.md](references/workflows.md) |
47
46
  | **Tier A** | **builder** | Release plumbing: bump a package (or the `workspace:`-pinned set) with `bump-ver`, delete release tags with `drop-tags`, commit + tag + optional push | [references/builder.md](references/builder.md) |
48
- | **Tier B** | **agent** | Coding-agent execution surface: run prompts via detected/named agents, manage team agent specs, persistent self-draining loop, readiness check | [references/agent.md](references/agent.md) |
47
+ | **Tier B** | **agent** | Coding-agent execution surface: run prompts via detected/named agents, list agent specs, start/stop supervised processes, readiness check | [references/agent.md](references/agent.md) |
49
48
  | **Tier B** | **message** | Durable inter-agent messaging: send, inbox, reply, watch | [references/message.md](references/message.md) |
50
- | **Tier B** | **team** | Team coordination and supervision: assign, status, up/down rosters, start/stop supervised processes | [references/team.md](references/team.md) |
51
49
  | **Tier B** | **self** | Self-management verbs: scaffold (`init`), schema migrations (`migrate`), local web server (`serve`), status overview (`status`); `self init` runs post-scaffold validation probes & layout classification | [references/self.md](references/self.md) |
52
50
  | **Tier B** | **history** | Import agent histories, aggregate forensic artifacts, render reports, and run the checkpoint-resumed daily pipeline | [references/history.md](references/history.md) |
53
51
  | **Tier B** | **projects** | Manage the local multi-project registry and start/stop project servers | [references/projects.md](references/projects.md) |
@@ -138,13 +136,11 @@ and spreading it; full contract in `docs/04_DESIGN.md` §1.0.1.
138
136
 
139
137
  ## See also
140
138
 
141
- - **[references/agent.md](references/agent.md)** - coding-agent execution surface (`run`, `loop`,
142
- spec management). Cross-references the
139
+ - **[references/agent.md](references/agent.md)** - coding-agent execution surface (`run`, `wait`,
140
+ `list --specs`, `start`/`stop`). Cross-references the
143
141
  [dispatch-surface rule](../parallel-execution/references/dispatch-surface.md).
144
142
  - **[references/message.md](references/message.md)** - durable inter-agent messaging (`send`,
145
143
  `inbox`, `reply`, `watch`).
146
- - **[references/team.md](references/team.md)** - team coordination and supervision (`assign`,
147
- `status`, `up`/`down`, `start`/`stop`).
148
144
  - **[references/self.md](references/self.md)** - `spur self init|migrate|serve|status` CLI verbs
149
145
  (the four legacy top-level nouns remain hidden aliases). `self init` runs post-scaffold init
150
146
  validation (Phase 1.5/1.6 probes).