@gobing-ai/spur 0.3.92 → 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/pipeline-budgets.json +0 -7
- package/config/plugin-scripts.json +10 -0
- 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/templates/AGENTS.md +4 -0
- package/config/templates/docs/02_ROADMAP.md +2 -0
- package/config/templates/docs/03_ARCHITECTURE.md +3 -1
- package/config/templates/docs/04_DESIGN.md +2 -0
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +10 -2
- package/config/workflow-candidates.json +72 -1
- package/config/workflows/feature-verification.yaml +1 -0
- package/config/workflows/history-anatomy.yaml +2 -0
- package/config/workflows/idea-pipeline.yaml +67 -18
- package/config/workflows/pr-review.yaml +8 -0
- package/config/workflows/task-pipeline.yaml +171 -39
- package/config/workflows/wayfinder-resolution.yaml +5 -0
- package/config/workflows/wrapup-pipeline.yaml +55 -14
- package/package.json +9 -9
- package/plugins/sp/README.md +8 -3
- package/plugins/sp/commands/dev-review-session.md +4 -4
- package/plugins/sp/commands/dev-review.md +16 -5
- package/plugins/sp/commands/dev-run.md +2 -2
- package/plugins/sp/commands/dev-runall.md +2 -2
- package/plugins/sp/hooks/pi/guard-extension.ts +17 -36
- package/plugins/sp/lib/idea-handoff.generated.mjs +8 -4
- package/plugins/sp/lib/inline-run.generated.d.mts +3 -0
- package/plugins/sp/lib/inline-run.generated.mjs +48 -19
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +1 -1
- package/plugins/sp/scripts/inline-run-setup.mjs +117 -7
- package/plugins/sp/scripts/inline-run-setup.ts +211 -7
- package/plugins/sp/scripts/quality-gate.mjs +255 -6
- package/plugins/sp/scripts/quality-gate.ts +425 -9
- package/plugins/sp/scripts/residual-scan.mjs +13 -5
- package/plugins/sp/scripts/residual-scan.ts +34 -7
- package/plugins/sp/scripts/task-diffstat.mjs +156 -0
- package/plugins/sp/scripts/task-diffstat.ts +229 -0
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +181 -0
- package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +60 -1
- package/plugins/sp/scripts/wrapup-steps.ts +89 -4
- package/plugins/sp/skills/brainstorm/SKILL.md +2 -0
- package/plugins/sp/skills/brainstorm/references/workflows.md +17 -2
- package/plugins/sp/skills/code-verification/SKILL.md +2 -2
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -2
- package/plugins/sp/skills/session-review/SKILL.md +11 -9
- package/plugins/sp/skills/spur-check/SKILL.md +112 -0
- package/plugins/sp/skills/spur-dev/SKILL.md +2 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +14 -2
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +8 -2
- package/plugins/sp/skills/spur-dev/references/document-authoring.md +85 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +46 -18
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +24 -3
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +34 -2
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +5 -5
- package/plugins/sp/skills/spur-dev/templates/design.md +31 -0
- package/plugins/sp/skills/spur-dev/templates/plan.md +32 -0
- package/plugins/sp/skills/spur-doctor/SKILL.md +60 -14
- package/schemas/state-machine-workflow.schema.json +4 -0
- package/spur.js +19577 -19822
- 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/config/workflows/decision-routing-example.yaml +0 -134
- 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"]
|
|
@@ -45,11 +45,11 @@ const DOCUMENTED = {
|
|
|
45
45
|
'file.read.into-var',
|
|
46
46
|
'hitl.confirm',
|
|
47
47
|
'hitl.input',
|
|
48
|
-
'hitl.select',
|
|
49
48
|
'agent.run',
|
|
50
49
|
'proof.fingerprint',
|
|
51
50
|
'run.artifact',
|
|
52
51
|
'command.gate',
|
|
52
|
+
'decide',
|
|
53
53
|
]),
|
|
54
54
|
guards: new Set(['always', 'shell', 'action-ok', 'contract-violation']),
|
|
55
55
|
} as const;
|
|
@@ -29,7 +29,10 @@ function usage() {
|
|
|
29
29
|
console.error("Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]");
|
|
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
|
-
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--spur-bin <path>]");
|
|
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");
|
|
35
|
+
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]");
|
|
33
36
|
process.exit(2);
|
|
34
37
|
}
|
|
35
38
|
var SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
@@ -151,6 +154,17 @@ async function printFingerprint(taskFile, featureFile, spurBin) {
|
|
|
151
154
|
return 0;
|
|
152
155
|
}
|
|
153
156
|
var CLOSE_STATUSES = new Set(["done", "failed", "paused"]);
|
|
157
|
+
var TERMINAL_REASONS = new Set([
|
|
158
|
+
"done",
|
|
159
|
+
"paused-operator",
|
|
160
|
+
"failed-check",
|
|
161
|
+
"failed-agent",
|
|
162
|
+
"failed-timeout",
|
|
163
|
+
"failed-guard",
|
|
164
|
+
"cancelled",
|
|
165
|
+
"interrupted",
|
|
166
|
+
"retry-exhausted"
|
|
167
|
+
]);
|
|
154
168
|
var ACTION_STATUSES = new Set(["done", "failed"]);
|
|
155
169
|
function runRecordLogPath(runDir, runId) {
|
|
156
170
|
const markdownPath = join(runDir, `${runId}.md`);
|
|
@@ -159,7 +173,7 @@ function runRecordLogPath(runDir, runId) {
|
|
|
159
173
|
return legacyLogPath;
|
|
160
174
|
return markdownPath;
|
|
161
175
|
}
|
|
162
|
-
function
|
|
176
|
+
function appendRunLogLine(runId, detail) {
|
|
163
177
|
try {
|
|
164
178
|
const runDir = join(process.cwd(), ".spur", "run");
|
|
165
179
|
if (!existsSync(runDir))
|
|
@@ -173,7 +187,7 @@ function appendTraceFailureLine(runId, detail) {
|
|
|
173
187
|
async function runTraceMode(input) {
|
|
174
188
|
const operation = input.close ? "run.close" : "action.finish";
|
|
175
189
|
const fail = (error) => {
|
|
176
|
-
|
|
190
|
+
appendRunLogLine(input.runId, `trace-emission-failed operation=${operation} run=${input.runId}${input.node === "" ? "" : ` node=${input.node}`}${input.kind === "" ? "" : ` kind=${input.kind}`}: ${error}`);
|
|
177
191
|
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}
|
|
178
192
|
`);
|
|
179
193
|
return input.close ? 1 : 0;
|
|
@@ -185,9 +199,9 @@ async function runTraceMode(input) {
|
|
|
185
199
|
projectDb = await app.openInlineRunProjectDb(process.cwd());
|
|
186
200
|
const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure) => {
|
|
187
201
|
const detail = failure;
|
|
188
|
-
|
|
202
|
+
appendRunLogLine(input.runId, `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? "unknown error"}`);
|
|
189
203
|
});
|
|
190
|
-
const result = input.close ? await writer.closeRun(input.runId, input.status) : await writer.recordAction({
|
|
204
|
+
const result = input.close ? await writer.closeRun(input.runId, input.status, undefined, input.reason) : await writer.recordAction({
|
|
191
205
|
runId: input.runId,
|
|
192
206
|
node: input.node,
|
|
193
207
|
kind: input.kind,
|
|
@@ -199,13 +213,20 @@ async function runTraceMode(input) {
|
|
|
199
213
|
const failure = result.failure;
|
|
200
214
|
return fail(failure.error ?? "unknown trace emission failure");
|
|
201
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
|
+
}
|
|
202
223
|
process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}
|
|
203
224
|
`);
|
|
204
225
|
return 0;
|
|
205
226
|
} catch (error) {
|
|
206
227
|
if (input.close && error.name === "RunRowNotFoundError") {
|
|
207
228
|
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
-
|
|
229
|
+
appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
|
|
209
230
|
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: message, code: "RUN_NOT_FOUND" })}
|
|
210
231
|
`);
|
|
211
232
|
return 1;
|
|
@@ -215,6 +236,57 @@ async function runTraceMode(input) {
|
|
|
215
236
|
projectDb?.close();
|
|
216
237
|
}
|
|
217
238
|
}
|
|
239
|
+
async function runDecideMode(input) {
|
|
240
|
+
const decideFailed = (error) => {
|
|
241
|
+
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}
|
|
242
|
+
`);
|
|
243
|
+
return 1;
|
|
244
|
+
};
|
|
245
|
+
let outcome;
|
|
246
|
+
try {
|
|
247
|
+
const { entry, portable } = resolveAppEntry(input.spurBin);
|
|
248
|
+
const app = await import(entry);
|
|
249
|
+
const lib = await import(fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url)));
|
|
250
|
+
const enabled = await lib.resolveDecideDecisionMakerEnabled(process.cwd(), portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined);
|
|
251
|
+
outcome = await app.runDecideForInlineRun({
|
|
252
|
+
workdir: process.cwd(),
|
|
253
|
+
optionsFile: input.optionsFile,
|
|
254
|
+
enabled
|
|
255
|
+
});
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return decideFailed(error instanceof Error ? error.message : String(error));
|
|
258
|
+
}
|
|
259
|
+
if (!outcome.ok)
|
|
260
|
+
return decideFailed(outcome.error ?? "decide failed without an error message");
|
|
261
|
+
process.stdout.write(`${JSON.stringify({ ok: true, runId: input.runId, node: input.node, ...outcome })}
|
|
262
|
+
`);
|
|
263
|
+
appendRunLogLine(input.runId, `decide node=${input.node} value=${outcome.value ?? ""} source=${outcome.source ?? "default"} reason=${outcome.reason ?? ""}`);
|
|
264
|
+
return await runTraceMode({
|
|
265
|
+
runId: input.runId,
|
|
266
|
+
close: false,
|
|
267
|
+
node: input.node,
|
|
268
|
+
kind: "decide",
|
|
269
|
+
status: "done",
|
|
270
|
+
ok: true,
|
|
271
|
+
durationMs: outcome.durationMs ?? 0,
|
|
272
|
+
spurBin: input.spurBin
|
|
273
|
+
});
|
|
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
|
+
}
|
|
218
290
|
async function main() {
|
|
219
291
|
if (!process.versions.bun) {
|
|
220
292
|
const child = spawnSync("bun", [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
@@ -231,9 +303,14 @@ async function main() {
|
|
|
231
303
|
let featureFile = "";
|
|
232
304
|
let action = false;
|
|
233
305
|
let close = false;
|
|
306
|
+
let decide = false;
|
|
307
|
+
let persistOut = false;
|
|
308
|
+
let from = "";
|
|
309
|
+
let optionsJson = "";
|
|
234
310
|
let node = "";
|
|
235
311
|
let kind = "";
|
|
236
312
|
let status = "";
|
|
313
|
+
let reason = "";
|
|
237
314
|
let okRaw = "";
|
|
238
315
|
let durationRaw = "";
|
|
239
316
|
let spurBin = getEnvVar("SPUR_BIN") ?? "";
|
|
@@ -253,12 +330,22 @@ async function main() {
|
|
|
253
330
|
action = true;
|
|
254
331
|
else if (argv[i] === "--close")
|
|
255
332
|
close = true;
|
|
333
|
+
else if (argv[i] === "--decide")
|
|
334
|
+
decide = true;
|
|
335
|
+
else if (argv[i] === "--persist-out")
|
|
336
|
+
persistOut = true;
|
|
337
|
+
else if (argv[i] === "--from")
|
|
338
|
+
from = argv[++i] ?? "";
|
|
339
|
+
else if (argv[i] === "--options-json")
|
|
340
|
+
optionsJson = argv[++i] ?? "";
|
|
256
341
|
else if (argv[i] === "--node")
|
|
257
342
|
node = argv[++i] ?? "";
|
|
258
343
|
else if (argv[i] === "--kind")
|
|
259
344
|
kind = argv[++i] ?? "";
|
|
260
345
|
else if (argv[i] === "--status")
|
|
261
346
|
status = argv[++i] ?? "";
|
|
347
|
+
else if (argv[i] === "--reason")
|
|
348
|
+
reason = argv[++i] ?? "";
|
|
262
349
|
else if (argv[i] === "--ok")
|
|
263
350
|
okRaw = argv[++i] ?? "";
|
|
264
351
|
else if (argv[i] === "--duration-ms")
|
|
@@ -271,6 +358,22 @@ async function main() {
|
|
|
271
358
|
usage();
|
|
272
359
|
process.exit(await printFingerprint(taskFile, featureFile, spurBin));
|
|
273
360
|
}
|
|
361
|
+
if (decide) {
|
|
362
|
+
if (action || close || fingerprint || file !== "" || taskFile !== "")
|
|
363
|
+
usage();
|
|
364
|
+
if (runId.trim() === "" || node.trim() === "" || optionsJson.trim() === "")
|
|
365
|
+
usage();
|
|
366
|
+
if (!SAFE_RUN_ID_RE.test(runId))
|
|
367
|
+
refuseUnsafeRunId(runId);
|
|
368
|
+
process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
|
|
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
|
+
}
|
|
274
377
|
if (action || close) {
|
|
275
378
|
if (action && close)
|
|
276
379
|
usage();
|
|
@@ -281,6 +384,12 @@ async function main() {
|
|
|
281
384
|
if (close) {
|
|
282
385
|
if (!CLOSE_STATUSES.has(status))
|
|
283
386
|
usage();
|
|
387
|
+
if (reason.trim() === "") {
|
|
388
|
+
if (status === "failed")
|
|
389
|
+
usage();
|
|
390
|
+
} else if (!TERMINAL_REASONS.has(reason)) {
|
|
391
|
+
usage();
|
|
392
|
+
}
|
|
284
393
|
process.exit(await runTraceMode({
|
|
285
394
|
runId,
|
|
286
395
|
close: true,
|
|
@@ -289,7 +398,8 @@ async function main() {
|
|
|
289
398
|
status,
|
|
290
399
|
ok: true,
|
|
291
400
|
durationMs: 0,
|
|
292
|
-
spurBin
|
|
401
|
+
spurBin,
|
|
402
|
+
...reason.trim() === "" ? {} : { reason }
|
|
293
403
|
}));
|
|
294
404
|
}
|
|
295
405
|
if (node.trim() === "" || kind.trim() === "")
|
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
* bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
|
|
32
32
|
* bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]
|
|
33
33
|
* bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> \
|
|
34
|
+
* bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]
|
|
34
35
|
* --status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]
|
|
35
|
-
* bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--spur-bin <path>]
|
|
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>]
|
|
36
38
|
*
|
|
37
39
|
* The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
|
|
38
40
|
* creates nothing (task 0862 R5).
|
|
@@ -44,6 +46,20 @@
|
|
|
44
46
|
* `.spur/run/<run-id>.md` and the script still exits 0 with `{"ok":false}` on stdout, so
|
|
45
47
|
* observation never wedges the run. `--close` is NOT best-effort: the run-row closure is
|
|
46
48
|
* bookkeeping, so a missing run row or a persistence failure exits 1 with a named error.
|
|
49
|
+
* The `--decide` mode (task 0941 R5) executes the non-pausing decide action through the SAME
|
|
50
|
+
* app runner the engine registers (`runDecideForInlineRun`), writing the same resultFile row
|
|
51
|
+
* and recording the same `action_runs` trace row (`kind=decide`, best-effort). A degraded
|
|
52
|
+
* decision is still `ok: true` — the run continues — so exit 0 covers every model-level
|
|
53
|
+
* outcome; only an invalid options schema exits 1 (fail closed).
|
|
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
|
+
*
|
|
47
63
|
* Exit 2 is reserved for usage errors.
|
|
48
64
|
*
|
|
49
65
|
* Env: SPUR_BIN
|
|
@@ -93,7 +109,17 @@ function usage(): never {
|
|
|
93
109
|
'--status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]',
|
|
94
110
|
);
|
|
95
111
|
console.error(
|
|
96
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--spur-bin <path>]',
|
|
112
|
+
' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
|
|
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
|
+
);
|
|
121
|
+
console.error(
|
|
122
|
+
' bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]',
|
|
97
123
|
);
|
|
98
124
|
process.exit(2);
|
|
99
125
|
}
|
|
@@ -305,6 +331,24 @@ async function printFingerprint(taskFile: string, featureFile: string, spurBin:
|
|
|
305
331
|
/** Terminal statuses the inline driver may declare when closing its run row. */
|
|
306
332
|
const CLOSE_STATUSES = new Set(['done', 'failed', 'paused']);
|
|
307
333
|
|
|
334
|
+
/**
|
|
335
|
+
* 0937 R2: closed terminal-reason vocabulary the driver may declare on `--close`.
|
|
336
|
+
* COPIED from packages/app/src/workflow/terminal-reason.ts — the plugin standalone
|
|
337
|
+
* contract forbids a value import of app code; the parity test asserts the copy
|
|
338
|
+
* equals the app export.
|
|
339
|
+
*/
|
|
340
|
+
const TERMINAL_REASONS = new Set([
|
|
341
|
+
'done',
|
|
342
|
+
'paused-operator',
|
|
343
|
+
'failed-check',
|
|
344
|
+
'failed-agent',
|
|
345
|
+
'failed-timeout',
|
|
346
|
+
'failed-guard',
|
|
347
|
+
'cancelled',
|
|
348
|
+
'interrupted',
|
|
349
|
+
'retry-exhausted',
|
|
350
|
+
]);
|
|
351
|
+
|
|
308
352
|
/** Finalize statuses — a finish emission is terminal, so only done|failed are valid (0868 #4). */
|
|
309
353
|
const ACTION_STATUSES = new Set(['done', 'failed']);
|
|
310
354
|
|
|
@@ -316,6 +360,8 @@ interface TraceModeInput {
|
|
|
316
360
|
readonly close: boolean;
|
|
317
361
|
readonly node: string;
|
|
318
362
|
readonly kind: string;
|
|
363
|
+
/** Declared terminal reason (0937 R2) — validated against TERMINAL_REASONS before this point. */
|
|
364
|
+
readonly reason?: string;
|
|
319
365
|
readonly status: string;
|
|
320
366
|
readonly ok: boolean;
|
|
321
367
|
readonly durationMs: number;
|
|
@@ -341,7 +387,7 @@ function runRecordLogPath(runDir: string, runId: string): string {
|
|
|
341
387
|
* exit immediately after), and never throws: an unwritable log must not wedge the run
|
|
342
388
|
* (ADR-117 R3).
|
|
343
389
|
*/
|
|
344
|
-
function
|
|
390
|
+
function appendRunLogLine(runId: string, detail: string): void {
|
|
345
391
|
try {
|
|
346
392
|
const runDir = join(process.cwd(), '.spur', 'run');
|
|
347
393
|
if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
|
|
@@ -363,7 +409,7 @@ function appendTraceFailureLine(runId: string, detail: string): void {
|
|
|
363
409
|
async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
364
410
|
const operation = input.close ? 'run.close' : 'action.finish';
|
|
365
411
|
const fail = (error: string): number => {
|
|
366
|
-
|
|
412
|
+
appendRunLogLine(
|
|
367
413
|
input.runId,
|
|
368
414
|
`trace-emission-failed operation=${operation} run=${input.runId}` +
|
|
369
415
|
`${input.node === '' ? '' : ` node=${input.node}`}${input.kind === '' ? '' : ` kind=${input.kind}`}: ${error}`,
|
|
@@ -390,14 +436,14 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
390
436
|
projectDb = await app.openInlineRunProjectDb(process.cwd());
|
|
391
437
|
const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure: unknown) => {
|
|
392
438
|
const detail = failure as { operation?: string; error?: string };
|
|
393
|
-
|
|
439
|
+
appendRunLogLine(
|
|
394
440
|
input.runId,
|
|
395
441
|
`trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? 'unknown error'}`,
|
|
396
442
|
);
|
|
397
443
|
});
|
|
398
444
|
const result = (
|
|
399
445
|
input.close
|
|
400
|
-
? await writer.closeRun(input.runId, input.status)
|
|
446
|
+
? await writer.closeRun(input.runId, input.status, undefined, input.reason)
|
|
401
447
|
: await writer.recordAction({
|
|
402
448
|
runId: input.runId,
|
|
403
449
|
node: input.node,
|
|
@@ -413,6 +459,18 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
413
459
|
const failure = result.failure as { error?: string };
|
|
414
460
|
return fail(failure.error ?? 'unknown trace emission failure');
|
|
415
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
|
+
}
|
|
416
474
|
process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}\n`);
|
|
417
475
|
return 0;
|
|
418
476
|
} catch (error) {
|
|
@@ -420,7 +478,7 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
420
478
|
// The run row must exist before --close can mark it terminal (R6); a missing row
|
|
421
479
|
// is a loud correctness failure, not a best-effort emission failure (finding #4).
|
|
422
480
|
const message = error instanceof Error ? error.message : String(error);
|
|
423
|
-
|
|
481
|
+
appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
|
|
424
482
|
process.stdout.write(
|
|
425
483
|
`${JSON.stringify({ ok: false, runId: input.runId, error: message, code: 'RUN_NOT_FOUND' })}\n`,
|
|
426
484
|
);
|
|
@@ -432,6 +490,116 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
432
490
|
}
|
|
433
491
|
}
|
|
434
492
|
|
|
493
|
+
/**
|
|
494
|
+
* `--decide` mode (task 0941 R5): execute the non-pausing decide action through the same app
|
|
495
|
+
* function the engine registers (`runDecideForInlineRun` → `DecideActionRunner`), then record
|
|
496
|
+
* the `action_runs` trace row through the shared writer. The decide-enabled switch
|
|
497
|
+
* (`workflow.decideDecisionMaker`) is resolved HERE at the driver boundary (ADR-082) through
|
|
498
|
+
* the bridged facade derivation and passed into the app runner as an explicit parameter —
|
|
499
|
+
* app services never load Spur config. Degraded outcomes (`disabled`,
|
|
500
|
+
* `no-backend`, `error`, `timeout`, `low-confidence`) are normal — the value is the declared
|
|
501
|
+
* default and the run continues — so they print `ok: true` and exit 0. Only an invalid options
|
|
502
|
+
* schema, an unreadable options file, or a failed config load fails closed with exit 1.
|
|
503
|
+
*/
|
|
504
|
+
async function runDecideMode(input: {
|
|
505
|
+
runId: string;
|
|
506
|
+
node: string;
|
|
507
|
+
optionsFile: string;
|
|
508
|
+
spurBin: string;
|
|
509
|
+
}): Promise<number> {
|
|
510
|
+
type DecideOutcome = {
|
|
511
|
+
ok: boolean;
|
|
512
|
+
error?: string;
|
|
513
|
+
value?: string;
|
|
514
|
+
degraded?: boolean;
|
|
515
|
+
reason?: string;
|
|
516
|
+
source?: 'model' | 'default';
|
|
517
|
+
backend?: string | null;
|
|
518
|
+
confidence?: number | null;
|
|
519
|
+
resultFile?: string;
|
|
520
|
+
durationMs?: number;
|
|
521
|
+
};
|
|
522
|
+
const decideFailed = (error: string): number => {
|
|
523
|
+
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}\n`);
|
|
524
|
+
return 1;
|
|
525
|
+
};
|
|
526
|
+
let outcome: DecideOutcome;
|
|
527
|
+
try {
|
|
528
|
+
const { entry, portable } = resolveAppEntry(input.spurBin);
|
|
529
|
+
const app = (await import(entry)) as {
|
|
530
|
+
runDecideForInlineRun: (input2: {
|
|
531
|
+
workdir: string;
|
|
532
|
+
optionsFile: string;
|
|
533
|
+
enabled: boolean;
|
|
534
|
+
}) => Promise<DecideOutcome>;
|
|
535
|
+
};
|
|
536
|
+
// Config switch at the driver boundary (ADR-082 / task 0941 gate fix): the flag comes
|
|
537
|
+
// from the bridged facade derivation in the generated lib — the same committed bundle
|
|
538
|
+
// the inventory flow imports — never a @gobing-ai/* value import. Embedded schemas
|
|
539
|
+
// ride along only in the portable layout, mirroring the setup mode's load posture.
|
|
540
|
+
const lib = (await import(
|
|
541
|
+
fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url))
|
|
542
|
+
)) as typeof import('../lib/inline-run.generated.mjs');
|
|
543
|
+
const enabled = await lib.resolveDecideDecisionMakerEnabled(
|
|
544
|
+
process.cwd(),
|
|
545
|
+
portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined,
|
|
546
|
+
);
|
|
547
|
+
outcome = await app.runDecideForInlineRun({
|
|
548
|
+
workdir: process.cwd(),
|
|
549
|
+
optionsFile: input.optionsFile,
|
|
550
|
+
enabled,
|
|
551
|
+
});
|
|
552
|
+
} catch (error) {
|
|
553
|
+
return decideFailed(error instanceof Error ? error.message : String(error));
|
|
554
|
+
}
|
|
555
|
+
if (!outcome.ok) return decideFailed(outcome.error ?? 'decide failed without an error message');
|
|
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
|
+
);
|
|
563
|
+
// Trace row is best-effort, exactly like --action: an emission failure never wedges the run.
|
|
564
|
+
return await runTraceMode({
|
|
565
|
+
runId: input.runId,
|
|
566
|
+
close: false,
|
|
567
|
+
node: input.node,
|
|
568
|
+
kind: 'decide',
|
|
569
|
+
status: 'done',
|
|
570
|
+
ok: true,
|
|
571
|
+
durationMs: outcome.durationMs ?? 0,
|
|
572
|
+
spurBin: input.spurBin,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
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
|
+
|
|
435
603
|
async function main(): Promise<void> {
|
|
436
604
|
// The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
|
|
437
605
|
if (!process.versions.bun) {
|
|
@@ -448,9 +616,14 @@ async function main(): Promise<void> {
|
|
|
448
616
|
let featureFile = '';
|
|
449
617
|
let action = false;
|
|
450
618
|
let close = false;
|
|
619
|
+
let decide = false;
|
|
620
|
+
let persistOut = false;
|
|
621
|
+
let from = '';
|
|
622
|
+
let optionsJson = '';
|
|
451
623
|
let node = '';
|
|
452
624
|
let kind = '';
|
|
453
625
|
let status = '';
|
|
626
|
+
let reason = '';
|
|
454
627
|
let okRaw = '';
|
|
455
628
|
let durationRaw = '';
|
|
456
629
|
let spurBin = getEnvVar('SPUR_BIN') ?? '';
|
|
@@ -463,9 +636,14 @@ async function main(): Promise<void> {
|
|
|
463
636
|
else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
|
|
464
637
|
else if (argv[i] === '--action') action = true;
|
|
465
638
|
else if (argv[i] === '--close') close = true;
|
|
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] ?? '';
|
|
642
|
+
else if (argv[i] === '--options-json') optionsJson = argv[++i] ?? '';
|
|
466
643
|
else if (argv[i] === '--node') node = argv[++i] ?? '';
|
|
467
644
|
else if (argv[i] === '--kind') kind = argv[++i] ?? '';
|
|
468
645
|
else if (argv[i] === '--status') status = argv[++i] ?? '';
|
|
646
|
+
else if (argv[i] === '--reason') reason = argv[++i] ?? '';
|
|
469
647
|
else if (argv[i] === '--ok') okRaw = argv[++i] ?? '';
|
|
470
648
|
else if (argv[i] === '--duration-ms') durationRaw = argv[++i] ?? '';
|
|
471
649
|
else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
|
|
@@ -481,12 +659,37 @@ async function main(): Promise<void> {
|
|
|
481
659
|
// ADR-117 emission modes (task 0868): the inline driver reports one completed action
|
|
482
660
|
// boundary, or closes its run row at the declared terminal state. Both share the run-id
|
|
483
661
|
// filename guard, the app-entry resolution chain and the best-effort failure contract.
|
|
662
|
+
// Non-pausing decide action (0941 R5): --decide is mutually exclusive with the run-setup,
|
|
663
|
+
// fingerprint, and trace modes; it requires the run id (filename-guarded), the state id
|
|
664
|
+
// for the trace row, and the options JSON file.
|
|
665
|
+
if (decide) {
|
|
666
|
+
if (action || close || fingerprint || file !== '' || taskFile !== '') usage();
|
|
667
|
+
if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
|
|
668
|
+
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
669
|
+
process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
|
|
670
|
+
}
|
|
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
|
+
|
|
484
680
|
if (action || close) {
|
|
485
681
|
if (action && close) usage();
|
|
486
682
|
if (runId.trim() === '' || status.trim() === '') usage();
|
|
487
683
|
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
488
684
|
if (close) {
|
|
489
685
|
if (!CLOSE_STATUSES.has(status)) usage();
|
|
686
|
+
// 0937 R2: a failed close requires a declared reason, and any declared reason
|
|
687
|
+
// must be a closed-enum value — both fail loudly BEFORE any write happens.
|
|
688
|
+
if (reason.trim() === '') {
|
|
689
|
+
if (status === 'failed') usage();
|
|
690
|
+
} else if (!TERMINAL_REASONS.has(reason)) {
|
|
691
|
+
usage();
|
|
692
|
+
}
|
|
490
693
|
process.exit(
|
|
491
694
|
await runTraceMode({
|
|
492
695
|
runId,
|
|
@@ -497,6 +700,7 @@ async function main(): Promise<void> {
|
|
|
497
700
|
ok: true,
|
|
498
701
|
durationMs: 0,
|
|
499
702
|
spurBin,
|
|
703
|
+
...(reason.trim() === '' ? {} : { reason }),
|
|
500
704
|
}),
|
|
501
705
|
);
|
|
502
706
|
}
|