@gobing-ai/spur 0.3.93 → 0.3.94
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/rules/boundary/test-subpath-boundary.yaml +48 -0
- package/config/rules/strict/runtime-boundaries.yaml +2 -1
- package/config/rules/structure/protected-files.yaml +4 -0
- package/config/workflows/task-pipeline.yaml +17 -26
- package/package.json +9 -9
- package/plugins/sp/README.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +4 -4
- package/plugins/sp/commands/dev-review.md +16 -5
- package/plugins/sp/hooks/pi/guard-extension.ts +17 -36
- package/plugins/sp/lib/idea-handoff.generated.mjs +1 -1
- package/plugins/sp/lib/inline-run.generated.d.mts +2 -0
- package/plugins/sp/lib/inline-run.generated.mjs +29 -8
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/inline-run-setup.mjs +42 -4
- package/plugins/sp/scripts/inline-run-setup.ts +78 -4
- package/plugins/sp/scripts/quality-gate.mjs +11 -4
- package/plugins/sp/scripts/quality-gate.ts +22 -7
- package/plugins/sp/scripts/residual-scan.mjs +1 -1
- package/plugins/sp/scripts/residual-scan.ts +2 -1
- package/plugins/sp/skills/session-review/SKILL.md +11 -9
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +8 -2
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +45 -17
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -3
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +16 -2
- package/spur.js +19288 -20178
- package/web/_astro/BoardApp.C02hAHPO.js +1 -0
- package/web/_astro/{BoardApp.CerSBgis.js → BoardApp.FTEs3-N8.js} +107 -105
- package/web/_astro/{TaskDetail.DCqiC-OZ.js → TaskDetail.C-GdsS-t.js} +1 -1
- package/web/_astro/arc.uuAf51IT.js +1 -0
- package/web/_astro/{architectureDiagram-3BPJPVTR.DM_vp_hO.js → architectureDiagram-3BPJPVTR.CGe629A1.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DXVIiv0p.js → blockDiagram-GPEHLZMM.D1mGCq3p.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.BbF_zCxW.js → c4Diagram-AAUBKEIU.CMsolcde.js} +1 -1
- package/web/_astro/channel.fsgl7o5j.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CAgQHpPC.js → chunk-2J33WTMH.CrGA3fik.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.BN-5tpw4.js → chunk-4BX2VUAB.DsLVVla0.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CnPkEEr0.js → chunk-55IACEB6.Dxc59Tfi.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.BQzQeMVm.js → chunk-727SXJPM.CaEVE2Wy.js} +4 -4
- package/web/_astro/{chunk-AQP2D5EJ.B6xNyDnL.js → chunk-AQP2D5EJ.BiJ4HXeI.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.C7f9Ih78.js → chunk-FMBD7UC4.Mxf1fru5.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.CNV1dFXT.js → chunk-ND2GUHAM.pyOWQixH.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.Cudn2TkJ.js → chunk-QZHKN3VN.BmEsg4vr.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.D1NwP50q.js → classDiagram-4FO5ZUOK.BHhhMFTO.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.D1NwP50q.js → classDiagram-v2-Q7XG4LA2.BHhhMFTO.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.B1wSL-Xb.js → cose-bilkent-S5V4N54A.2fH4YOlp.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BmK52w8G.js → cynefin-OW5HDTMX.C2j1_lKL.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.Bfy5CTDT.js → dagre-BM42HDAG.hZ2NCTdT.js} +2 -2
- package/web/_astro/diagram-2AECGRRQ.Bfw5_EzK.js +43 -0
- package/web/_astro/diagram-5GNKFQAL.Bt1V_Tmk.js +10 -0
- package/web/_astro/{diagram-KO2AKTUF.CW_vMJ4z.js → diagram-KO2AKTUF.DEk-YFwp.js} +3 -3
- package/web/_astro/{diagram-LMA3HP47.B_8ZGF67.js → diagram-LMA3HP47.CDjndtQm.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.BppnHsdS.js → diagram-OG6HWLK6.cFnUHScG.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BEuHXcjJ.js → erDiagram-TEJ5UH35.DlhYp7NV.js} +5 -5
- package/web/_astro/{flowDiagram-I6XJVG4X.CH-UlnGr.js → flowDiagram-I6XJVG4X.DNTpsxfx.js} +4 -4
- package/web/_astro/{ganttDiagram-6RSMTGT7.BO81S85v.js → ganttDiagram-6RSMTGT7.DC_p36PI.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.XnPxPPZN.js → gitGraphDiagram-PVQCEYII.DkEqNI0P.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.JyjYRu_T.js → infoDiagram-5YYISTIA.CrjioCTG.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js → ishikawaDiagram-YF4QCWOH.BcK0CN8n.js} +5 -5
- package/web/_astro/{journeyDiagram-JHISSGLW.C_iymSyp.js → journeyDiagram-JHISSGLW.p0CbBDX1.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DdfW-Oqt.js → kanban-definition-UN3LZRKU.puHIFt6J.js} +7 -7
- package/web/_astro/{linear.C2_IkbZT.js → linear.Bb-3a1d7.js} +1 -1
- package/web/_astro/mermaid.core.CJDgXOJs.js +301 -0
- package/web/_astro/{mindmap-definition-RKZ34NQL.DAZIxQSK.js → mindmap-definition-RKZ34NQL.BoAZEI9t.js} +2 -2
- package/web/_astro/{pieDiagram-4H26LBE5.CN8sIhKM.js → pieDiagram-4H26LBE5.CJNSdqY7.js} +3 -3
- package/web/_astro/{quadrantDiagram-W4KKPZXB.3dGcX5GP.js → quadrantDiagram-W4KKPZXB.DtTy7_0Z.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.BV2y4dd6.js → requirementDiagram-4Y6WPE33.BNYV98Xg.js} +3 -3
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Cqo15Tvo.js → sankeyDiagram-5OEKKPKP.DRq9DGHp.js} +4 -4
- package/web/_astro/{sequenceDiagram-3UESZ5HK.CROCPMJB.js → sequenceDiagram-3UESZ5HK.B7KdBFCm.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.RfXZrkFE.js → stateDiagram-AJRCARHV.CD62ZJ2H.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CPXmbBs9.js → stateDiagram-v2-BHNVJYJU.CeVk1TAj.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.DdgKTiO8.js → timeline-definition-PNZ67QCA.C_ZPA5tT.js} +3 -3
- package/web/_astro/{vennDiagram-CIIHVFJN.CPNVSHF1.js → vennDiagram-CIIHVFJN.CrThO_FQ.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CQhA0Jyr.js → wardleyDiagram-YWT4CUSO.DSZCA5nl.js} +3 -3
- package/web/_astro/{xychartDiagram-2RQKCTM6.n61BWyy4.js → xychartDiagram-2RQKCTM6.KI7baTuk.js} +1 -1
- package/web/index.html +1 -1
- package/web/_astro/BoardApp.eoTz0pZs.js +0 -1
- package/web/_astro/arc.CPwg6Rw0.js +0 -1
- package/web/_astro/channel.MYZLKNwy.js +0 -1
- package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +0 -43
- package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +0 -10
- package/web/_astro/mermaid.core.GAOYeSR0.js +0 -303
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.94",
|
|
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"]
|
|
@@ -30,6 +30,8 @@ function usage() {
|
|
|
30
30
|
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]");
|
|
31
31
|
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> " + "--status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]");
|
|
32
32
|
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]");
|
|
33
|
+
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--spur-bin <path>]");
|
|
34
|
+
console.error(" terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, " + "failed-timeout, failed-guard, cancelled, interrupted, retry-exhausted");
|
|
33
35
|
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]");
|
|
34
36
|
process.exit(2);
|
|
35
37
|
}
|
|
@@ -171,7 +173,7 @@ function runRecordLogPath(runDir, runId) {
|
|
|
171
173
|
return legacyLogPath;
|
|
172
174
|
return markdownPath;
|
|
173
175
|
}
|
|
174
|
-
function
|
|
176
|
+
function appendRunLogLine(runId, detail) {
|
|
175
177
|
try {
|
|
176
178
|
const runDir = join(process.cwd(), ".spur", "run");
|
|
177
179
|
if (!existsSync(runDir))
|
|
@@ -185,7 +187,7 @@ function appendTraceFailureLine(runId, detail) {
|
|
|
185
187
|
async function runTraceMode(input) {
|
|
186
188
|
const operation = input.close ? "run.close" : "action.finish";
|
|
187
189
|
const fail = (error) => {
|
|
188
|
-
|
|
190
|
+
appendRunLogLine(input.runId, `trace-emission-failed operation=${operation} run=${input.runId}${input.node === "" ? "" : ` node=${input.node}`}${input.kind === "" ? "" : ` kind=${input.kind}`}: ${error}`);
|
|
189
191
|
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}
|
|
190
192
|
`);
|
|
191
193
|
return input.close ? 1 : 0;
|
|
@@ -197,7 +199,7 @@ async function runTraceMode(input) {
|
|
|
197
199
|
projectDb = await app.openInlineRunProjectDb(process.cwd());
|
|
198
200
|
const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure) => {
|
|
199
201
|
const detail = failure;
|
|
200
|
-
|
|
202
|
+
appendRunLogLine(input.runId, `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? "unknown error"}`);
|
|
201
203
|
});
|
|
202
204
|
const result = input.close ? await writer.closeRun(input.runId, input.status, undefined, input.reason) : await writer.recordAction({
|
|
203
205
|
runId: input.runId,
|
|
@@ -211,13 +213,20 @@ async function runTraceMode(input) {
|
|
|
211
213
|
const failure = result.failure;
|
|
212
214
|
return fail(failure.error ?? "unknown trace emission failure");
|
|
213
215
|
}
|
|
216
|
+
if (input.close && input.status === "done" && result.actionRows === 0) {
|
|
217
|
+
const error = `run ${input.runId} closed done with zero action_runs rows`;
|
|
218
|
+
appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${error}`);
|
|
219
|
+
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error, code: "NO_ACTION_ROWS", actionRows: 0 })}
|
|
220
|
+
`);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
214
223
|
process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}
|
|
215
224
|
`);
|
|
216
225
|
return 0;
|
|
217
226
|
} catch (error) {
|
|
218
227
|
if (input.close && error.name === "RunRowNotFoundError") {
|
|
219
228
|
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
-
|
|
229
|
+
appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
|
|
221
230
|
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: message, code: "RUN_NOT_FOUND" })}
|
|
222
231
|
`);
|
|
223
232
|
return 1;
|
|
@@ -251,6 +260,7 @@ async function runDecideMode(input) {
|
|
|
251
260
|
return decideFailed(outcome.error ?? "decide failed without an error message");
|
|
252
261
|
process.stdout.write(`${JSON.stringify({ ok: true, runId: input.runId, node: input.node, ...outcome })}
|
|
253
262
|
`);
|
|
263
|
+
appendRunLogLine(input.runId, `decide node=${input.node} value=${outcome.value ?? ""} source=${outcome.source ?? "default"} reason=${outcome.reason ?? ""}`);
|
|
254
264
|
return await runTraceMode({
|
|
255
265
|
runId: input.runId,
|
|
256
266
|
close: false,
|
|
@@ -262,6 +272,21 @@ async function runDecideMode(input) {
|
|
|
262
272
|
spurBin: input.spurBin
|
|
263
273
|
});
|
|
264
274
|
}
|
|
275
|
+
async function runPersistOutMode(input) {
|
|
276
|
+
try {
|
|
277
|
+
const { entry } = resolveAppEntry(input.spurBin);
|
|
278
|
+
const app = await import(entry);
|
|
279
|
+
const result = await app.persistWorktreeRuns({ fromWorkdir: input.from, toWorkdir: process.cwd() });
|
|
280
|
+
process.stdout.write(`${JSON.stringify({ ok: true, persisted: result.persisted, skipped: result.skipped })}
|
|
281
|
+
`);
|
|
282
|
+
return 0;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
285
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: message })}
|
|
286
|
+
`);
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
265
290
|
async function main() {
|
|
266
291
|
if (!process.versions.bun) {
|
|
267
292
|
const child = spawnSync("bun", [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
@@ -279,6 +304,8 @@ async function main() {
|
|
|
279
304
|
let action = false;
|
|
280
305
|
let close = false;
|
|
281
306
|
let decide = false;
|
|
307
|
+
let persistOut = false;
|
|
308
|
+
let from = "";
|
|
282
309
|
let optionsJson = "";
|
|
283
310
|
let node = "";
|
|
284
311
|
let kind = "";
|
|
@@ -305,6 +332,10 @@ async function main() {
|
|
|
305
332
|
close = true;
|
|
306
333
|
else if (argv[i] === "--decide")
|
|
307
334
|
decide = true;
|
|
335
|
+
else if (argv[i] === "--persist-out")
|
|
336
|
+
persistOut = true;
|
|
337
|
+
else if (argv[i] === "--from")
|
|
338
|
+
from = argv[++i] ?? "";
|
|
308
339
|
else if (argv[i] === "--options-json")
|
|
309
340
|
optionsJson = argv[++i] ?? "";
|
|
310
341
|
else if (argv[i] === "--node")
|
|
@@ -336,6 +367,13 @@ async function main() {
|
|
|
336
367
|
refuseUnsafeRunId(runId);
|
|
337
368
|
process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
|
|
338
369
|
}
|
|
370
|
+
if (persistOut) {
|
|
371
|
+
if (fingerprint || decide || action || close || runId !== "" || file !== "" || taskFile !== "")
|
|
372
|
+
usage();
|
|
373
|
+
if (from.trim() === "")
|
|
374
|
+
usage();
|
|
375
|
+
process.exit(await runPersistOutMode({ from, spurBin }));
|
|
376
|
+
}
|
|
339
377
|
if (action || close) {
|
|
340
378
|
if (action && close)
|
|
341
379
|
usage();
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]
|
|
35
35
|
* --status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]
|
|
36
36
|
* bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]
|
|
37
|
+
* bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--spur-bin <path>]
|
|
37
38
|
*
|
|
38
39
|
* The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
|
|
39
40
|
* creates nothing (task 0862 R5).
|
|
@@ -51,6 +52,14 @@
|
|
|
51
52
|
* decision is still `ok: true` — the run continues — so exit 0 covers every model-level
|
|
52
53
|
* outcome; only an invalid options schema exits 1 (fail closed).
|
|
53
54
|
*
|
|
55
|
+
* The `--persist-out --from <worktree-path>` mode (task 0975 R1) is the WT-4a step a
|
|
56
|
+
* `--worktree` run executes BEFORE teardown: it copies the worktree's run provenance — the
|
|
57
|
+
* `runs`/`action_runs`/`phase_runs`/`transition_runs`/`workflow_states` rows and the two-file
|
|
58
|
+
* run records — into THIS tree's project DB and `.spur/run/` (cwd = the invoking tree).
|
|
59
|
+
* Idempotent; collisions are reported as `skipped[{id,reason}]`, never overwrites. Exit 0 =
|
|
60
|
+
* persisted (possibly with skips); exit 1 = fail closed — the driver routes to WT-5 and
|
|
61
|
+
* retains the worktree; usage errors exit 2.
|
|
62
|
+
*
|
|
54
63
|
* Exit 2 is reserved for usage errors.
|
|
55
64
|
*
|
|
56
65
|
* Env: SPUR_BIN
|
|
@@ -102,6 +111,13 @@ function usage(): never {
|
|
|
102
111
|
console.error(
|
|
103
112
|
' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
|
|
104
113
|
);
|
|
114
|
+
console.error(
|
|
115
|
+
' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--spur-bin <path>]',
|
|
116
|
+
);
|
|
117
|
+
console.error(
|
|
118
|
+
' terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, ' +
|
|
119
|
+
'failed-timeout, failed-guard, cancelled, interrupted, retry-exhausted',
|
|
120
|
+
);
|
|
105
121
|
console.error(
|
|
106
122
|
' bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]',
|
|
107
123
|
);
|
|
@@ -371,7 +387,7 @@ function runRecordLogPath(runDir: string, runId: string): string {
|
|
|
371
387
|
* exit immediately after), and never throws: an unwritable log must not wedge the run
|
|
372
388
|
* (ADR-117 R3).
|
|
373
389
|
*/
|
|
374
|
-
function
|
|
390
|
+
function appendRunLogLine(runId: string, detail: string): void {
|
|
375
391
|
try {
|
|
376
392
|
const runDir = join(process.cwd(), '.spur', 'run');
|
|
377
393
|
if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
|
|
@@ -393,7 +409,7 @@ function appendTraceFailureLine(runId: string, detail: string): void {
|
|
|
393
409
|
async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
394
410
|
const operation = input.close ? 'run.close' : 'action.finish';
|
|
395
411
|
const fail = (error: string): number => {
|
|
396
|
-
|
|
412
|
+
appendRunLogLine(
|
|
397
413
|
input.runId,
|
|
398
414
|
`trace-emission-failed operation=${operation} run=${input.runId}` +
|
|
399
415
|
`${input.node === '' ? '' : ` node=${input.node}`}${input.kind === '' ? '' : ` kind=${input.kind}`}: ${error}`,
|
|
@@ -420,7 +436,7 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
420
436
|
projectDb = await app.openInlineRunProjectDb(process.cwd());
|
|
421
437
|
const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure: unknown) => {
|
|
422
438
|
const detail = failure as { operation?: string; error?: string };
|
|
423
|
-
|
|
439
|
+
appendRunLogLine(
|
|
424
440
|
input.runId,
|
|
425
441
|
`trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? 'unknown error'}`,
|
|
426
442
|
);
|
|
@@ -443,6 +459,18 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
443
459
|
const failure = result.failure as { error?: string };
|
|
444
460
|
return fail(failure.error ?? 'unknown trace emission failure');
|
|
445
461
|
}
|
|
462
|
+
if (input.close && input.status === 'done' && result.actionRows === 0) {
|
|
463
|
+
// A run finalized `done` with ZERO recorded action rows is a bookkeeping defect
|
|
464
|
+
// (task 0975 R2): the row is already terminal — closeRun ran above — but the
|
|
465
|
+
// driver must surface this instead of reporting a clean close, and must never
|
|
466
|
+
// backfill rows. Exit 1 with the named code; the run record carries the finding.
|
|
467
|
+
const error = `run ${input.runId} closed done with zero action_runs rows`;
|
|
468
|
+
appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${error}`);
|
|
469
|
+
process.stdout.write(
|
|
470
|
+
`${JSON.stringify({ ok: false, runId: input.runId, error, code: 'NO_ACTION_ROWS', actionRows: 0 })}\n`,
|
|
471
|
+
);
|
|
472
|
+
return 1;
|
|
473
|
+
}
|
|
446
474
|
process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}\n`);
|
|
447
475
|
return 0;
|
|
448
476
|
} catch (error) {
|
|
@@ -450,7 +478,7 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
450
478
|
// The run row must exist before --close can mark it terminal (R6); a missing row
|
|
451
479
|
// is a loud correctness failure, not a best-effort emission failure (finding #4).
|
|
452
480
|
const message = error instanceof Error ? error.message : String(error);
|
|
453
|
-
|
|
481
|
+
appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
|
|
454
482
|
process.stdout.write(
|
|
455
483
|
`${JSON.stringify({ ok: false, runId: input.runId, error: message, code: 'RUN_NOT_FOUND' })}\n`,
|
|
456
484
|
);
|
|
@@ -485,6 +513,7 @@ async function runDecideMode(input: {
|
|
|
485
513
|
value?: string;
|
|
486
514
|
degraded?: boolean;
|
|
487
515
|
reason?: string;
|
|
516
|
+
source?: 'model' | 'default';
|
|
488
517
|
backend?: string | null;
|
|
489
518
|
confidence?: number | null;
|
|
490
519
|
resultFile?: string;
|
|
@@ -525,6 +554,12 @@ async function runDecideMode(input: {
|
|
|
525
554
|
}
|
|
526
555
|
if (!outcome.ok) return decideFailed(outcome.error ?? 'decide failed without an error message');
|
|
527
556
|
process.stdout.write(`${JSON.stringify({ ok: true, runId: input.runId, node: input.node, ...outcome })}\n`);
|
|
557
|
+
// 0976 R2: the run log names the decision's provenance, so a declared-default fallback is
|
|
558
|
+
// never read as a model decision. Best-effort through the same run-log appender.
|
|
559
|
+
appendRunLogLine(
|
|
560
|
+
input.runId,
|
|
561
|
+
`decide node=${input.node} value=${outcome.value ?? ''} source=${outcome.source ?? 'default'} reason=${outcome.reason ?? ''}`,
|
|
562
|
+
);
|
|
528
563
|
// Trace row is best-effort, exactly like --action: an emission failure never wedges the run.
|
|
529
564
|
return await runTraceMode({
|
|
530
565
|
runId: input.runId,
|
|
@@ -538,6 +573,33 @@ async function runDecideMode(input: {
|
|
|
538
573
|
});
|
|
539
574
|
}
|
|
540
575
|
|
|
576
|
+
/**
|
|
577
|
+
* `--persist-out --from <worktree-path>` (task 0975 R1): resolve the app through the shared
|
|
578
|
+
* chain and copy the worktree's inline-run provenance into THIS tree. No persistence policy
|
|
579
|
+
* lives here — the app's `persistWorktreeRuns` (→ domain `transferRunTables`) owns the DB
|
|
580
|
+
* transfer, the idempotence and the conflict skips. Success prints
|
|
581
|
+
* `{ok:true,persisted:<n>,skipped:[…]}` and exits 0; any failure prints `{ok:false,error}`
|
|
582
|
+
* and exits 1 (the driver routes to WT-5 and retains the worktree).
|
|
583
|
+
*/
|
|
584
|
+
async function runPersistOutMode(input: { from: string; spurBin: string }): Promise<number> {
|
|
585
|
+
try {
|
|
586
|
+
const { entry } = resolveAppEntry(input.spurBin);
|
|
587
|
+
const app = (await import(entry)) as {
|
|
588
|
+
persistWorktreeRuns: (i: {
|
|
589
|
+
fromWorkdir: string;
|
|
590
|
+
toWorkdir: string;
|
|
591
|
+
}) => Promise<{ ok: true; persisted: number; skipped: ReadonlyArray<{ id: string; reason: string }> }>;
|
|
592
|
+
};
|
|
593
|
+
const result = await app.persistWorktreeRuns({ fromWorkdir: input.from, toWorkdir: process.cwd() });
|
|
594
|
+
process.stdout.write(`${JSON.stringify({ ok: true, persisted: result.persisted, skipped: result.skipped })}\n`);
|
|
595
|
+
return 0;
|
|
596
|
+
} catch (error) {
|
|
597
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
598
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: message })}\n`);
|
|
599
|
+
return 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
541
603
|
async function main(): Promise<void> {
|
|
542
604
|
// The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
|
|
543
605
|
if (!process.versions.bun) {
|
|
@@ -555,6 +617,8 @@ async function main(): Promise<void> {
|
|
|
555
617
|
let action = false;
|
|
556
618
|
let close = false;
|
|
557
619
|
let decide = false;
|
|
620
|
+
let persistOut = false;
|
|
621
|
+
let from = '';
|
|
558
622
|
let optionsJson = '';
|
|
559
623
|
let node = '';
|
|
560
624
|
let kind = '';
|
|
@@ -573,6 +637,8 @@ async function main(): Promise<void> {
|
|
|
573
637
|
else if (argv[i] === '--action') action = true;
|
|
574
638
|
else if (argv[i] === '--close') close = true;
|
|
575
639
|
else if (argv[i] === '--decide') decide = true;
|
|
640
|
+
else if (argv[i] === '--persist-out') persistOut = true;
|
|
641
|
+
else if (argv[i] === '--from') from = argv[++i] ?? '';
|
|
576
642
|
else if (argv[i] === '--options-json') optionsJson = argv[++i] ?? '';
|
|
577
643
|
else if (argv[i] === '--node') node = argv[++i] ?? '';
|
|
578
644
|
else if (argv[i] === '--kind') kind = argv[++i] ?? '';
|
|
@@ -603,6 +669,14 @@ async function main(): Promise<void> {
|
|
|
603
669
|
process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
|
|
604
670
|
}
|
|
605
671
|
|
|
672
|
+
// Worktree provenance persist-out (task 0975 R1): mutually exclusive with every other
|
|
673
|
+
// mode; requires the source worktree path. Runs with cwd = the invoking tree.
|
|
674
|
+
if (persistOut) {
|
|
675
|
+
if (fingerprint || decide || action || close || runId !== '' || file !== '' || taskFile !== '') usage();
|
|
676
|
+
if (from.trim() === '') usage();
|
|
677
|
+
process.exit(await runPersistOutMode({ from, spurBin }));
|
|
678
|
+
}
|
|
679
|
+
|
|
606
680
|
if (action || close) {
|
|
607
681
|
if (action && close) usage();
|
|
608
682
|
if (runId.trim() === '' || status.trim() === '') usage();
|
|
@@ -154,6 +154,9 @@ function lightScope(changedFiles, exists = existsSync) {
|
|
|
154
154
|
}
|
|
155
155
|
return { files, workspaces: [...workspaces].sort(), tests: [...tests].sort() };
|
|
156
156
|
}
|
|
157
|
+
function shQuote(arg) {
|
|
158
|
+
return /^[\w./@+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
159
|
+
}
|
|
157
160
|
function workspaceHasTypecheck(workspace) {
|
|
158
161
|
try {
|
|
159
162
|
const pkg = JSON.parse(readFileSync(join(workspace, "package.json"), "utf8"));
|
|
@@ -165,11 +168,11 @@ function workspaceHasTypecheck(workspace) {
|
|
|
165
168
|
function planLightChecks(scope, hasTypecheck = workspaceHasTypecheck) {
|
|
166
169
|
const plans = [];
|
|
167
170
|
if (scope.files.length > 0) {
|
|
168
|
-
plans.push({ id: "format-lint:changed", cmd: `bunx biome check ${scope.files.join(" ")}` });
|
|
171
|
+
plans.push({ id: "format-lint:changed", cmd: `bunx biome check ${scope.files.map(shQuote).join(" ")}` });
|
|
169
172
|
}
|
|
170
173
|
for (const workspace of scope.workspaces) {
|
|
171
174
|
if (hasTypecheck(workspace)) {
|
|
172
|
-
plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${workspace} && bun run typecheck` });
|
|
175
|
+
plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${shQuote(workspace)} && bun run typecheck` });
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
const testsByWorkspace = new Map;
|
|
@@ -180,7 +183,10 @@ function planLightChecks(scope, hasTypecheck = workspaceHasTypecheck) {
|
|
|
180
183
|
testsByWorkspace.set(workspace, paths);
|
|
181
184
|
}
|
|
182
185
|
for (const [workspace, paths] of testsByWorkspace) {
|
|
183
|
-
plans.push({
|
|
186
|
+
plans.push({
|
|
187
|
+
id: `test:${workspace}`,
|
|
188
|
+
cmd: `cd ${shQuote(workspace)} && bun test ${paths.map(shQuote).join(" ")}`
|
|
189
|
+
});
|
|
184
190
|
}
|
|
185
191
|
return plans;
|
|
186
192
|
}
|
|
@@ -376,7 +382,7 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
376
382
|
appendFileSync(abs(logFile), `proof-digest: ${env.proofDigest ?? ""}
|
|
377
383
|
`);
|
|
378
384
|
let receiptFile;
|
|
379
|
-
if (
|
|
385
|
+
if (!noProgressSkip) {
|
|
380
386
|
if ((env.proofDigest ?? "").length > 0) {
|
|
381
387
|
receiptFile = join(runDir, `${env.wbs}-check-receipt.json`);
|
|
382
388
|
const receipt = buildReceipt({
|
|
@@ -439,6 +445,7 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
439
445
|
export {
|
|
440
446
|
workspaceHasTypecheck,
|
|
441
447
|
tailLines,
|
|
448
|
+
shQuote,
|
|
442
449
|
scanCoverageShortfalls,
|
|
443
450
|
runShellCommand,
|
|
444
451
|
runQualityGate,
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
* output matches a SQLite busy/locked error; the delay defaults to 10 seconds and is
|
|
27
27
|
* overridable via `SPUR_QUALITY_GATE_RETRY_DELAY_MS` (tests).
|
|
28
28
|
*
|
|
29
|
-
* Check receipts (task 0939, ADR-124): `run`
|
|
30
|
-
* (`check-receipt/v1`) when `proofDigest` is set; without a digest no receipt is written and the
|
|
29
|
+
* Check receipts (task 0939, ADR-124): `run` and `recheck` write `.spur/run/<wbs>-check-receipt.json`
|
|
30
|
+
* (`check-receipt/v1`) when `proofDigest` is set (a no-progress skip keeps the receipt it matched); without a digest no receipt is written and the
|
|
31
31
|
* log says why. The gate executes `qualityGateCmd` as one unit, so the full-tier receipt carries
|
|
32
32
|
* a single `test` row for the whole `bun run spur-check` chain (lint | typecheck | test-pre-check
|
|
33
33
|
* | test | test-post-check). `light` accumulates: a sub-check already PASS in a light receipt at
|
|
@@ -335,6 +335,15 @@ export function lightScope(changedFiles: string[], exists: (p: string) => boolea
|
|
|
335
335
|
return { files, workspaces: [...workspaces].sort(), tests: [...tests].sort() };
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Quote one argument for `sh -c`. Changed-file names come from the working tree (untracked files
|
|
340
|
+
* included), so an unquoted `x$(cmd).ts` would run `cmd` and a space would split the argument.
|
|
341
|
+
* Plain paths pass through unchanged to keep receipts readable.
|
|
342
|
+
*/
|
|
343
|
+
export function shQuote(arg: string): string {
|
|
344
|
+
return /^[\w./@+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
345
|
+
}
|
|
346
|
+
|
|
338
347
|
export interface LightCheckPlan {
|
|
339
348
|
id: string;
|
|
340
349
|
cmd: string;
|
|
@@ -364,11 +373,11 @@ export function planLightChecks(
|
|
|
364
373
|
): LightCheckPlan[] {
|
|
365
374
|
const plans: LightCheckPlan[] = [];
|
|
366
375
|
if (scope.files.length > 0) {
|
|
367
|
-
plans.push({ id: 'format-lint:changed', cmd: `bunx biome check ${scope.files.join(' ')}` });
|
|
376
|
+
plans.push({ id: 'format-lint:changed', cmd: `bunx biome check ${scope.files.map(shQuote).join(' ')}` });
|
|
368
377
|
}
|
|
369
378
|
for (const workspace of scope.workspaces) {
|
|
370
379
|
if (hasTypecheck(workspace)) {
|
|
371
|
-
plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${workspace} && bun run typecheck` });
|
|
380
|
+
plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${shQuote(workspace)} && bun run typecheck` });
|
|
372
381
|
}
|
|
373
382
|
}
|
|
374
383
|
const testsByWorkspace = new Map<string, string[]>();
|
|
@@ -379,7 +388,10 @@ export function planLightChecks(
|
|
|
379
388
|
testsByWorkspace.set(workspace, paths);
|
|
380
389
|
}
|
|
381
390
|
for (const [workspace, paths] of testsByWorkspace) {
|
|
382
|
-
plans.push({
|
|
391
|
+
plans.push({
|
|
392
|
+
id: `test:${workspace}`,
|
|
393
|
+
cmd: `cd ${shQuote(workspace)} && bun test ${paths.map(shQuote).join(' ')}`,
|
|
394
|
+
});
|
|
383
395
|
}
|
|
384
396
|
return plans;
|
|
385
397
|
}
|
|
@@ -622,9 +634,12 @@ export function runQualityGate(
|
|
|
622
634
|
writeFileSync(abs(statusFile), `${status}\n`);
|
|
623
635
|
appendFileSync(abs(logFile), `proof-digest: ${env.proofDigest ?? ''}\n`);
|
|
624
636
|
|
|
625
|
-
// 0939 R2:
|
|
637
|
+
// 0939 R2: the gate writes the full-tier receipt only with a digest to bind it to. 0976 R1:
|
|
638
|
+
// `recheck` persists the receipt it evaluated too, so a second recheck at the same digest can
|
|
639
|
+
// take the no-progress skip. A skip leaves the FAIL receipt it matched untouched — a skip never
|
|
640
|
+
// rewrites a receipt, so it can never launder FAIL into PASS.
|
|
626
641
|
let receiptFile: string | undefined;
|
|
627
|
-
if (
|
|
642
|
+
if (!noProgressSkip) {
|
|
628
643
|
if ((env.proofDigest ?? '').length > 0) {
|
|
629
644
|
receiptFile = join(runDir, `${env.wbs}-check-receipt.json`);
|
|
630
645
|
const receipt = buildReceipt({
|
|
@@ -17,7 +17,7 @@ function getEnvVars() {
|
|
|
17
17
|
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
18
18
|
var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
19
19
|
var PRIORITY_PATTERN = /^P[1-4]/;
|
|
20
|
-
var NONE_FINDING = /^(none
|
|
20
|
+
var NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|\u2014)\s*(\(.*\))?\.?$/i;
|
|
21
21
|
var DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
|
|
22
22
|
var RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
|
|
23
23
|
var DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
|
|
@@ -64,7 +64,8 @@ export interface ScanOptions {
|
|
|
64
64
|
|
|
65
65
|
const MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
66
66
|
const PRIORITY_PATTERN = /^P[1-4]/;
|
|
67
|
-
|
|
67
|
+
// Placeholder "no finding" cells, optionally with a trailing "(…)" note; "None of X…" is a real finding.
|
|
68
|
+
const NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|—)\s*(\(.*\))?\.?$/i;
|
|
68
69
|
const DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
|
|
69
70
|
const RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
|
|
70
71
|
const DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: session-review
|
|
3
|
-
description: "Review the active coding-agent session: separate resolved from open issues with evidence, propose bounded improvements. With --triage, apply pure-doc / 1–2-line fixes inline and file the rest as one
|
|
3
|
+
description: "Review the active coding-agent session: separate resolved from open issues with evidence, propose bounded improvements. With --triage, apply pure-doc / 1–2-line fixes inline and file the rest as one or more tasks. Triggers: review this session, wrap-up, triage findings."
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
version: 1.1.0
|
|
6
6
|
metadata:
|
|
@@ -31,7 +31,7 @@ cross-agent windows, recurrence, trends, or quantitative performance forensics.
|
|
|
31
31
|
| Argument | Description | Default |
|
|
32
32
|
| --- | --- | --- |
|
|
33
33
|
| `[focus]` | Question or operation to emphasize. It changes ordering, not evidence collection. | full active session |
|
|
34
|
-
| `--triage` | Opt into bounded remediation after the report: apply direct fixes (pure docs / one-to-two-line fixes) inline, then file all remaining actionable findings as
|
|
34
|
+
| `--triage` | Opt into bounded remediation after the report: apply direct fixes (pure docs / one-to-two-line fixes) inline, then file all remaining actionable findings as one or more implement-ready tasks via the CLI-gated corpus surface. | off (report-only) |
|
|
35
35
|
|
|
36
36
|
## Evidence boundary
|
|
37
37
|
|
|
@@ -64,13 +64,15 @@ three buckets — never skip triage and start fixing from the raw findings list.
|
|
|
64
64
|
- **Note** — pre-existing, environmental, or ownerless observations; report only.
|
|
65
65
|
2. **Apply direct fixes inline** — smallest surgical diff, project style, and re-verify each
|
|
66
66
|
with the targeted check (lint / test / the exact command that exhibited the issue).
|
|
67
|
-
3. **
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
3. **File the Task bucket as one or more tasks** by the shared filing rule in
|
|
68
|
+
[dev-operations.md § 2. review](../spur-dev/references/dev-operations.md#2-review) (Triage
|
|
69
|
+
step 3): one task per cohesive unit one agent can implement and verify in one run, under the
|
|
70
|
+
existing feature that owns the surface (never a new root feature), implement-ready, written
|
|
71
|
+
only through `spur task create --skip-ready` + `spur task update <wbs> --section <s> --from-file`.
|
|
72
|
+
Not one task per finding: each finding keeps its evidence, fix direction, and an AC where
|
|
73
|
+
verifiable. Exclude what direct fixes already resolved — say so in the task Background.
|
|
72
74
|
4. **Report** — add a Triage section: applied fixes (path + one-line what + verification) and
|
|
73
|
-
|
|
75
|
+
each created task WBS. The Resolved/Open tables keep their evidence rules unchanged.
|
|
74
76
|
|
|
75
77
|
## Protocol
|
|
76
78
|
|
|
@@ -156,7 +158,7 @@ improvement. Use `None` when the session is complete and no follow-up is justifi
|
|
|
156
158
|
to imported-history analysis.
|
|
157
159
|
- Report-only by default: do not create or update corpus items or edit files. The single exception
|
|
158
160
|
is `--triage` mode, which permits exactly two mutation classes — direct fixes from the triage
|
|
159
|
-
bucket, and the
|
|
161
|
+
bucket, and the triage tasks. Anything beyond that stays a proposal.
|
|
160
162
|
- Do not turn a single low-impact observation into a new policy. Report it as a candidate until it
|
|
161
163
|
recurs or demonstrates a high-impact contract violation.
|
|
162
164
|
|
|
@@ -68,7 +68,7 @@ each would be scope creep for one-liner procedures.
|
|
|
68
68
|
| # | Operation | Command | Backing | Skill / Verb | Arg-hint |
|
|
69
69
|
| --- | ---------- | ------------------- | ----------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
70
70
|
| 1 | unit | `dev-unit` | `Skill()` | `sp:code-testing` | `<target> [--coverage <pct>] [--agent <inline\|auto\|name>] [--auto]` |
|
|
71
|
-
| 2 | review | `dev-review` | `Skill()` | `sp:code-verification` (`review`) + `sp:functional-review` + `sp:code-improvement` | `[<wbs\|path>] [--agent <inline\|auto\|name>] [--focus <dims>] [--
|
|
71
|
+
| 2 | review | `dev-review` | `Skill()` | `sp:code-verification` (`review`) + `sp:functional-review` + `sp:code-improvement` | `[<wbs\|path>] [--agent <inline\|auto\|name>] [--focus <dims>] [--triage] [--worktree [<name>]]` |
|
|
72
72
|
| 3 | verify | `dev-verify` | `Skill()` | `sp:code-verification` (`verify`) | `<wbs> [--agent <inline\|auto\|name>] [--fix <none\|blockers-first\|all>] [--focus <lens>] [--bdd] [--auto] [--force] [--next] [--skip-shippable]` |
|
|
73
73
|
| 3a | verifyall | `dev-verifyall` | `Skill()` → agent | `sp:spur-dev` (`verifyall`) | `--tasks <selector> [--feature <id>] [--agent <inline\|auto\|name>] [--fix <none\|blockers-first\|all>] [--focus <lens>] [--bdd] [--auto] [--force] [--next] [--json] [--skip-shippable] [--worktree [<name>]]` |
|
|
74
74
|
| 4 | run | `dev-run` | `Skill()` | `sp:spur-dev` (`run` / `implement`) | `<wbs> [--mode <full\|implement>] [--agent <inline\|auto\|name>] [--auto] [--next] [--wrap] [--continue] [--worktree [<name>]]` |
|
|
@@ -116,7 +116,13 @@ must not be changed without updating the backing skill.
|
|
|
116
116
|
- **Modes:**
|
|
117
117
|
- **WBS mode (`<wbs>`)**: Runs functional requirements traceability (`sp:functional-review`), SECUA framework (`sp:code-verification`), and architectural depth (`sp:code-improvement`). The three skills return review fragments; the coordinator writes the combined `## Review` (F92 0593 R1).
|
|
118
118
|
- **Path mode (`<path>`)**: Runs advisory SECUA framework (`sp:code-verification`) and architectural depth (`sp:code-improvement`). Performs no task mutation.
|
|
119
|
-
- **Inputs:** `<wbs|path>` (required). Review executes inline (in-session) by default. `--agent <inline|auto|name>` selector accepted (see [SSOT](cross-cutting.md#inline-default-execution-surface)). `--focus <lens>` narrows to one SECUA dimension. Note: `--fix` and `--next` are **deprecated** (no-op with warning; route remediation to `/sp:dev-verify --fix` and progression to `/sp:dev-next`).
|
|
119
|
+
- **Inputs:** `<wbs|path>` (required). Review executes inline (in-session) by default. `--agent <inline|auto|name>` selector accepted (see [SSOT](cross-cutting.md#inline-default-execution-surface)). `--focus <lens>` narrows to one SECUA dimension. `--triage` opts into bounded remediation after the report (below). `--worktree [<name>]` isolates the triage writes (below; requires `--triage`). Note: `--fix` and `--next` are **deprecated** (no-op with warning; route remediation to `--triage` or `/sp:dev-verify --fix` and progression to `/sp:dev-next`). The task pipeline's review stage never passes `--triage` or `--worktree`.
|
|
120
|
+
- **Triage (`--triage`):** runs inline in the invoking session after the merged report — never inside `sp:super-reviewer` or the review skills, which stay report-only. With `--agent <name>`, only the review is delegated; triage still runs inline on the returned findings. Never fix straight from the raw findings list:
|
|
121
|
+
1. **Bucket every finding exactly once.** **Direct fix** — local, low-risk, obvious root cause: no design choice, no public-surface / schema / dependency change, not a shared write path, verifiable by a targeted check (docs, a guard, a missing throw, a wrong path helper, a stale comment). **Task** — real and actionable but needs design, crosses modules, touches a public surface, or is not verifiable locally; skip it when an existing task already owns it (cite that WBS). **Note** — pre-existing, environmental, or ownerless; report only.
|
|
122
|
+
2. **Apply direct fixes** — smallest surgical diff in project style, then re-verify each with its targeted check (focused test, lint, or the exact command that exhibited the defect). A fix that grows beyond the bar or fails its check moves to the Task bucket with what was learned; do not leave it half-applied.
|
|
123
|
+
3. **File the Task bucket as one or more tasks**, one per cohesive unit of work that one agent can implement and verify in one run — split unrelated fixes, merge findings that share a root cause or file set. File under the existing feature that owns the affected surface; if none fits, create a child feature under the closest existing parent — never a new root feature. Write through the CLI-gated corpus surface only (`spur task create --feature <id> --skip-ready`, then `spur task update <wbs> --section <s> --from-file`). Each task must meet the implement-ready bar ([§ 5. refine](#5-refine), `--depth ready`) so it cannot drift: per finding the evidence (`file:line`), the defect, the chosen fix direction with rejected alternatives, file targets, AC, and out-of-scope; promote `backlog → todo` only when `spur task check <wbs> --json` is clean. Name what the direct fixes already resolved in each task's Background.
|
|
124
|
+
4. **Gate and report.** Run the project gate once after the direct fixes. Report a Triage section: each applied fix (path + one-line what + verification), each filed task (WBS + feature + scope), and the Notes. Never commit or create a branch outside `--worktree`.
|
|
125
|
+
- **Worktree (`--worktree [<name>]`):** omitted → all writes land in the current working tree on the current branch; no branch creation, checkout, or commit (the operator commits). Given → the [execution-batch.md § Worktree isolation](execution-batch.md#worktree-isolation---worktree-name) lifecycle as a run of one: marker `command` = `dev-review`, `selector` = the review target, derived branch/directory slug = the WBS or the path's basename (`sp/review-<slug>-<short-id>`). Admission is "the target resolves" (a known WBS or an existing path) — no `quickReadiness` task-set check. Review and triage both run in the tree; WT-3b commits the fixes and filed tasks. The WT-4 success condition reads as "every direct fix passed its check and the project gate is green"; a failed check or gate takes the WT-5 retention path. Rejected without `--triage` (WT-7) — to review another worktree read-only, pass a path inside it.
|
|
120
126
|
- **Backing:** `sp:functional-review`, `sp:code-verification` (review mode), `sp:code-improvement`.
|
|
121
127
|
- **Behavior:** WBS mode runs functional traceability + SECUA + architecture depth, ranking findings P1–P4, and hands the merged report to the review coordinator, which writes `## Review`. Component skills never write `## Review` in coordinated mode. Path mode runs advisory SECUA + architecture depth with no task mutation.
|
|
122
128
|
- **Delegation:** WBS mode: `sp:functional-review` + `sp:code-verification` (review) + `sp:code-improvement`; Path mode: `sp:code-verification` (review) + `sp:code-improvement`.
|
|
@@ -490,16 +490,25 @@ Evidence persistence precedes destructive cleanup: a persistence failure (unread
|
|
|
490
490
|
disk-full, missing directory) routes to **WT-5** — the worktree and branch are retained so a green
|
|
491
491
|
batch can never destroy its own evidence. Reuse mode retains its operator-owned tree but still
|
|
492
492
|
persists the Step 5 report under the invoking tree; the reused tree's `.spur/run/` remains the live
|
|
493
|
-
copy while that tree lives on.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
493
|
+
copy while that tree lives on. The per-run provenance — the worktree DB's run/action rows and the
|
|
494
|
+
`.spur/run/<runId>.md` + `.state.json` records — is persisted mechanically by WT-4a's
|
|
495
|
+
`inline-run-setup.ts --persist-out --from <worktree>` call, not by hand.
|
|
496
|
+
|
|
497
|
+
**Stage records are worktree-local too (0948 R9, E7 Finding 5; persisted by 0975 R1).** Each task's
|
|
498
|
+
own per-stage run record (`.spur/run/<runId>.md` + `.state.json`) and the worktree DB's run rows are
|
|
499
|
+
written inside the worktree and **are removed with it** in create mode — the E7 batch lost exactly
|
|
500
|
+
this evidence. Copying them out is no longer a manual audit-time duty: WT-4a (create-mode block
|
|
501
|
+
below) runs `inline-run-setup.ts --persist-out --from "$WT_PATH"` **before** WT-4b holder cleanup,
|
|
502
|
+
which copies the run/action/phase/transition/workflow-state rows and both record files into the
|
|
503
|
+
invoking tree. The shapes are pinned (task 0975 R1): idempotent on re-persist; success exits 0
|
|
504
|
+
printing `{"ok":true,"persisted":<n>,"skipped":[{"id":<run-id>,"reason":"id-exists"|"external-key-conflict"|"record-conflict:<file>"}]}`
|
|
505
|
+
— an `id-exists` / `external-key-conflict` skip never modifies the pre-existing target rows, a
|
|
506
|
+
`record-conflict:<file>` skip never overwrites a divergent invoking-tree record — and any failure
|
|
507
|
+
exits 1 printing `{"ok":false,"error":<message>}` (a worktree DB run id that is not a single safe
|
|
508
|
+
filename component is rejected before any target write). Any persist-out failure
|
|
509
|
+
routes to **WT-5** — worktree and branch retained — the same copy-out-first contract as the
|
|
510
|
+
verdict persistence above. After a green persist-out, `spur workflow progress --json` in the
|
|
511
|
+
invoking tree shows the merged run `done` with its per-action rows.
|
|
503
512
|
|
|
504
513
|
## Step 6 — Batch wrap (`--wrap` / `--next`) (F96 task 0952 R1)
|
|
505
514
|
|
|
@@ -538,7 +547,7 @@ all subsequent tools, agents, task/feature writes, and run artifacts use the con
|
|
|
538
547
|
tree's cwd. A stale or empty selector, an unsupported mode, or an invalid target creates no tree and
|
|
539
548
|
no marker (WT-2/WT-7), and the required Git safety checks (WT-1) still precede creation.
|
|
540
549
|
|
|
541
|
-
> **Command wiring (task 0814 R3).** The four
|
|
550
|
+
> **Command wiring (task 0814 R3).** The four task-set worktree commands (`dev-run`, `dev-runall`,
|
|
542
551
|
> `dev-refineall`, `dev-verifyall`) each call `quickReadiness` with their operation (`run`/`refine`/
|
|
543
552
|
> `verify`), the resolved selector/status, and the filtered-set size **before** WT-1/WT-2. The
|
|
544
553
|
> admission outcome gates the tree: an invalid/empty selector, unsupported mode, or a target that
|
|
@@ -557,6 +566,13 @@ non-PASS verify verdict, or a HITL pause that ends the run take the WT-5 retenti
|
|
|
557
566
|
full pipeline is eligible — `--worktree --mode implement` is rejected (WT-7), because that mode is
|
|
558
567
|
the pipeline's implement stage and already runs in the driver's tree.
|
|
559
568
|
|
|
569
|
+
**Review triage `dev-review` (run of one).** `/sp:dev-review <target> --triage --worktree [<name>]`
|
|
570
|
+
runs this lifecycle around one review-plus-triage pass: WT-1…WT-6 apply unchanged, the marker's
|
|
571
|
+
`command` is `dev-review` and its `selector` is the review target, and the slug is the WBS or the
|
|
572
|
+
path's basename (`sp/review-<slug>-<short-id>`). It skips `quickReadiness` (there is no task set;
|
|
573
|
+
admission is "the target resolves"). WT-4 success reads as "every direct fix passed its check and the
|
|
574
|
+
project gate is green"; anything else takes WT-5. Contract: [dev-operations.md § 2. review](dev-operations.md#2-review).
|
|
575
|
+
|
|
560
576
|
One flag, two modes (see the glossary entry for the ownership rule). Bare `--worktree` is **create
|
|
561
577
|
mode** (cut a fresh branch + sibling tree). `--worktree <name>` is **reuse mode** (attach to a tree
|
|
562
578
|
that already exists); name resolution (§ WT-2 below) runs before WT-1. The deltas each mode applies
|
|
@@ -805,13 +821,23 @@ git checkout "$BASE_REF"
|
|
|
805
821
|
[ "$(git rev-list --count "$BASE_SHA..$BRANCH")" -gt 0 ] \
|
|
806
822
|
|| { echo "halt: branch carries no commits - nothing to merge" >&2; false; } # -> WT-5
|
|
807
823
|
git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
|
|
808
|
-
# if FF succeeded — WT-4a evidence persistence (Step 5, task 0720 R3)
|
|
809
|
-
# persist the batch report + verdict artifacts
|
|
810
|
-
# before anything below touches the worktree.
|
|
824
|
+
# if FF succeeded — WT-4a evidence persistence runs FIRST (Step 5, task 0720 R3):
|
|
825
|
+
# persist the batch report + verdict artifacts AND the per-run provenance (task
|
|
826
|
+
# 0975 R1) into the invoking tree before anything below touches the worktree.
|
|
827
|
+
# Any persistence failure routes to WT-5 — the worktree and branch are retained.
|
|
828
|
+
WT_PATH="$(cd "../<worktree-dir>" && pwd)" # hoisted: needed by WT-4a AND WT-4b below
|
|
829
|
+
# WT-4a provenance persist-out (task 0975 R1): copy the worktree DB's run rows plus
|
|
830
|
+
# the .spur/run/<runId>.md + .state.json records into THIS tree. Run from the main
|
|
831
|
+
# tree (cwd = the invoking tree). Idempotent; conflicts are reported, never
|
|
832
|
+
# overwritten. A non-zero exit — including a half-readable worktree — must NOT
|
|
833
|
+
# proceed to WT-4b removal:
|
|
834
|
+
SETUP_SCRIPT="plugins/sp/scripts/inline-run-setup.ts"
|
|
835
|
+
[ -f "$SETUP_SCRIPT" ] || SETUP_SCRIPT="$(superskill script path sp inline-run-setup.mjs 2>/dev/null)"
|
|
836
|
+
bun "$SETUP_SCRIPT" --persist-out --from "$WT_PATH" \
|
|
837
|
+
|| { echo "halt: worktree run-record persist-out failed - worktree retained (WT-5)" >&2; exit 1; }
|
|
811
838
|
#
|
|
812
|
-
# WT-4b — bounded CWD-holder cleanup (task 0720 R1).
|
|
813
|
-
# worktree path; a relative path or a stale entry matches the wrong processes.
|
|
814
|
-
WT_PATH="$(cd "../<worktree-dir>" && pwd)"
|
|
839
|
+
# WT-4b — bounded CWD-holder cleanup (task 0720 R1). $WT_PATH above is the EXACT
|
|
840
|
+
# absolute worktree path; a relative path or a stale entry matches the wrong processes.
|
|
815
841
|
# Holders = processes with any open fd under the worktree tree (lsof +D walks the
|
|
816
842
|
# tree; CWD holders are the common case but +D also catches open-file holders —
|
|
817
843
|
# over-match errs toward removal success; a plain -t <dir> matches only the
|
|
@@ -976,6 +1002,8 @@ fallback, because `<name>` was explicit and unambiguous intent.
|
|
|
976
1002
|
- **`--mode implement`** is rejected when combined with `--worktree` on `dev-run` — that mode *is*
|
|
977
1003
|
the pipeline's implement stage (bug-742) and runs in whatever tree the driver set up; a second
|
|
978
1004
|
worktree would split one task's evidence across two trees.
|
|
1005
|
+
- **`dev-review` without `--triage`** is rejected when combined with `--worktree` — a read-only
|
|
1006
|
+
review writes nothing worth isolating; review another worktree by passing a path inside it.
|
|
979
1007
|
- **No** create-with-name (`--worktree <name>` never creates; an unresolvable name is an error),
|
|
980
1008
|
no `--worktree-keep` variant, no auto-cleanup of stale worktrees or markers from prior runs.
|
|
981
1009
|
|