@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.example.yaml +19 -17
- package/config/config.global.yaml +6 -3
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +75 -13
- package/config/transition-shims.json +0 -7
- package/package.json +1 -1
- package/plugins/sp/README.md +2 -2
- package/plugins/sp/agents/expert-spur.md +3 -3
- package/plugins/sp/lib/idea-handoff.generated.mjs +43 -43
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/inline-run-setup.ts +69 -1
- package/plugins/sp/scripts/quality-gate.mjs +52 -1
- package/plugins/sp/scripts/quality-gate.ts +70 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -2
- package/plugins/sp/scripts/task-size-precheck.ts +44 -16
- package/plugins/sp/scripts/verify-answer-lint.ts +17 -1
- package/plugins/sp/skills/code-review/references/review-lenses.md +3 -0
- package/plugins/sp/skills/spur-cli/SKILL.md +4 -8
- package/plugins/sp/skills/spur-cli/references/agent.md +30 -97
- package/plugins/sp/skills/spur-cli/references/message.md +1 -1
- package/plugins/sp/skills/spur-cli/references/projects.md +7 -27
- package/plugins/sp/skills/spur-cli/references/self.md +2 -2
- package/plugins/sp/skills/spur-cli/references/serve.md +4 -4
- package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
- package/plugins/sp/skills/spur-composer/SKILL.md +3 -3
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +10 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +4 -6
- package/plugins/sp/skills/spur-dev/references/glossary.md +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +11 -1
- package/plugins/sp/skills/spur-doctor/SKILL.md +2 -2
- package/schemas/spur-config.schema.json +56 -85
- package/spur.js +3498 -4835
- package/web/_astro/BoardApp.D-WlxiN2.js +1 -0
- package/web/_astro/{BoardApp.B1U26g3I.js → BoardApp.D8bM9pKL.js} +73 -73
- package/web/_astro/{TaskDetail.DwPqpq7v.js → TaskDetail.BPRqgVUE.js} +1 -1
- package/web/_astro/{arc.CweZEjN2.js → arc.BPrPES3z.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.D89pbDuv.js → architectureDiagram-3BPJPVTR.qX_7q02P.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.BOuTeEpX.js → blockDiagram-GPEHLZMM.CUZfj5V7.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.CASbkWZF.js → c4Diagram-AAUBKEIU.CTaOr8hH.js} +1 -1
- package/web/_astro/channel.DGZaFHZx.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BKQYtOvY.js → chunk-2J33WTMH.Dt-9wf3h.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.9sHLdMtG.js → chunk-4BX2VUAB.CTC2sdoN.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.wOLXWlPs.js → chunk-55IACEB6.DQcxt2_g.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DovFbwg3.js → chunk-727SXJPM.DXFPSn-a.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.B1Weod1X.js → chunk-AQP2D5EJ.BCx3U4bT.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.TEMS04st.js → chunk-FMBD7UC4.DL2tJkdO.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.Cp8VT1wQ.js → chunk-ND2GUHAM.DZyflMro.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BzATdEcP.js → chunk-QZHKN3VN.CUI2mT09.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.C9BOCfAO.js → classDiagram-4FO5ZUOK.g4rX4Fr1.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.C9BOCfAO.js → classDiagram-v2-Q7XG4LA2.g4rX4Fr1.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DUnr4UAw.js → cose-bilkent-S5V4N54A.CzWJLqp0.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.rYq5uM3D.js → cynefin-OW5HDTMX.WgsvQeCR.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.CWeNKe3I.js → dagre-BM42HDAG.Dzv6ngql.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DCkfls10.js → diagram-2AECGRRQ.CpJ4a9rU.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D5U4JCka.js → diagram-5GNKFQAL.CzPlF_dq.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.BZJgqaqG.js → diagram-KO2AKTUF.TAkZNTcQ.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.DoMeHvPR.js → diagram-LMA3HP47.uCjoKSag.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.B50qwwWX.js → diagram-OG6HWLK6.eMplIjoK.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DdGPG6LK.js → erDiagram-TEJ5UH35.Bf7zoXGz.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.QP2MJ12u.js → flowDiagram-I6XJVG4X.B_bHj3gN.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.BI6LgKSy.js → ganttDiagram-6RSMTGT7.BasrHRMj.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.npPZiC2G.js → gitGraphDiagram-PVQCEYII.C6iphq1x.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.DCJCBVbp.js → infoDiagram-5YYISTIA.HXmDMhW4.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BMLV-3I1.js → ishikawaDiagram-YF4QCWOH.BSmW8NiU.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.LE58crde.js → journeyDiagram-JHISSGLW.DEQow5fo.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BPbz8rH9.js → kanban-definition-UN3LZRKU.IVm9cTdc.js} +1 -1
- package/web/_astro/{linear.DhZaBtYh.js → linear.CrsM73_9.js} +1 -1
- package/web/_astro/{mermaid.core.BD5-jXum.js → mermaid.core.CfBeDJls.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.MTJyrQ65.js → mindmap-definition-RKZ34NQL.C3j60Y-0.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.BrDhDvIS.js → pieDiagram-4H26LBE5.B-aCMeEA.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.71d73_5N.js → quadrantDiagram-W4KKPZXB.Cib965yq.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.Bga6UF-z.js → requirementDiagram-4Y6WPE33.D61cS4O-.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BnHs4K82.js → sankeyDiagram-5OEKKPKP.GKF2qVPy.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DsfY2gnj.js → sequenceDiagram-3UESZ5HK.DZnq8F2h.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DvsTSc9a.js → stateDiagram-AJRCARHV.DXUFmdgM.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.DxzzmHUR.js → stateDiagram-v2-BHNVJYJU.BtHmhLEz.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.4ZuQmOTt.js → timeline-definition-PNZ67QCA.Cy-WW2ln.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.Ck5Q86SG.js → vennDiagram-CIIHVFJN.SLp5b9KI.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.BK7k2hXr.js → wardleyDiagram-YWT4CUSO.Bww45mWV.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DfCrgauK.js → xychartDiagram-2RQKCTM6.DR4swI6a.js} +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/favicon.ico +0 -0
- package/web/favicon.svg +17 -4
- package/web/icon-192.png +0 -0
- package/web/icon-512.png +0 -0
- package/web/index.html +2 -2
- package/web/site.webmanifest +31 -0
- package/web/spur_logo.svg +1 -0
- package/plugins/sp/skills/spur-cli/references/team.md +0 -165
- package/web/_astro/BoardApp.Csgyg-lS.js +0 -1
- package/web/_astro/channel.Cx6sXxhq.js +0 -1
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
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
|
-
|
|
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
|
-
// ───
|
|
30
|
+
// ─── Counting (sync with packages/app/src/services/task-size-precheck.ts) ────
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
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
|
-
/**
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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`, `
|
|
142
|
-
|
|
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).
|