@gobing-ai/spur 0.3.92 → 0.3.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/pipeline-budgets.json +0 -7
- package/config/plugin-scripts.json +10 -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 +180 -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 +7 -2
- package/plugins/sp/commands/dev-run.md +2 -2
- package/plugins/sp/commands/dev-runall.md +2 -2
- package/plugins/sp/lib/idea-handoff.generated.mjs +8 -4
- package/plugins/sp/lib/inline-run.generated.d.mts +1 -0
- package/plugins/sp/lib/inline-run.generated.mjs +24 -16
- 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 +75 -3
- package/plugins/sp/scripts/inline-run-setup.ts +133 -3
- package/plugins/sp/scripts/quality-gate.mjs +248 -6
- package/plugins/sp/scripts/quality-gate.ts +410 -9
- package/plugins/sp/scripts/residual-scan.mjs +12 -4
- package/plugins/sp/scripts/residual-scan.ts +32 -6
- 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/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/document-authoring.md +85 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +8 -0
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +19 -1
- 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 +1117 -472
- package/config/workflows/decision-routing-example.yaml +0 -134
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.93",
|
|
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,8 @@ 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 --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]");
|
|
33
34
|
process.exit(2);
|
|
34
35
|
}
|
|
35
36
|
var SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
@@ -151,6 +152,17 @@ async function printFingerprint(taskFile, featureFile, spurBin) {
|
|
|
151
152
|
return 0;
|
|
152
153
|
}
|
|
153
154
|
var CLOSE_STATUSES = new Set(["done", "failed", "paused"]);
|
|
155
|
+
var TERMINAL_REASONS = new Set([
|
|
156
|
+
"done",
|
|
157
|
+
"paused-operator",
|
|
158
|
+
"failed-check",
|
|
159
|
+
"failed-agent",
|
|
160
|
+
"failed-timeout",
|
|
161
|
+
"failed-guard",
|
|
162
|
+
"cancelled",
|
|
163
|
+
"interrupted",
|
|
164
|
+
"retry-exhausted"
|
|
165
|
+
]);
|
|
154
166
|
var ACTION_STATUSES = new Set(["done", "failed"]);
|
|
155
167
|
function runRecordLogPath(runDir, runId) {
|
|
156
168
|
const markdownPath = join(runDir, `${runId}.md`);
|
|
@@ -187,7 +199,7 @@ async function runTraceMode(input) {
|
|
|
187
199
|
const detail = failure;
|
|
188
200
|
appendTraceFailureLine(input.runId, `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? "unknown error"}`);
|
|
189
201
|
});
|
|
190
|
-
const result = input.close ? await writer.closeRun(input.runId, input.status) : await writer.recordAction({
|
|
202
|
+
const result = input.close ? await writer.closeRun(input.runId, input.status, undefined, input.reason) : await writer.recordAction({
|
|
191
203
|
runId: input.runId,
|
|
192
204
|
node: input.node,
|
|
193
205
|
kind: input.kind,
|
|
@@ -215,6 +227,41 @@ async function runTraceMode(input) {
|
|
|
215
227
|
projectDb?.close();
|
|
216
228
|
}
|
|
217
229
|
}
|
|
230
|
+
async function runDecideMode(input) {
|
|
231
|
+
const decideFailed = (error) => {
|
|
232
|
+
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}
|
|
233
|
+
`);
|
|
234
|
+
return 1;
|
|
235
|
+
};
|
|
236
|
+
let outcome;
|
|
237
|
+
try {
|
|
238
|
+
const { entry, portable } = resolveAppEntry(input.spurBin);
|
|
239
|
+
const app = await import(entry);
|
|
240
|
+
const lib = await import(fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url)));
|
|
241
|
+
const enabled = await lib.resolveDecideDecisionMakerEnabled(process.cwd(), portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined);
|
|
242
|
+
outcome = await app.runDecideForInlineRun({
|
|
243
|
+
workdir: process.cwd(),
|
|
244
|
+
optionsFile: input.optionsFile,
|
|
245
|
+
enabled
|
|
246
|
+
});
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return decideFailed(error instanceof Error ? error.message : String(error));
|
|
249
|
+
}
|
|
250
|
+
if (!outcome.ok)
|
|
251
|
+
return decideFailed(outcome.error ?? "decide failed without an error message");
|
|
252
|
+
process.stdout.write(`${JSON.stringify({ ok: true, runId: input.runId, node: input.node, ...outcome })}
|
|
253
|
+
`);
|
|
254
|
+
return await runTraceMode({
|
|
255
|
+
runId: input.runId,
|
|
256
|
+
close: false,
|
|
257
|
+
node: input.node,
|
|
258
|
+
kind: "decide",
|
|
259
|
+
status: "done",
|
|
260
|
+
ok: true,
|
|
261
|
+
durationMs: outcome.durationMs ?? 0,
|
|
262
|
+
spurBin: input.spurBin
|
|
263
|
+
});
|
|
264
|
+
}
|
|
218
265
|
async function main() {
|
|
219
266
|
if (!process.versions.bun) {
|
|
220
267
|
const child = spawnSync("bun", [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
@@ -231,9 +278,12 @@ async function main() {
|
|
|
231
278
|
let featureFile = "";
|
|
232
279
|
let action = false;
|
|
233
280
|
let close = false;
|
|
281
|
+
let decide = false;
|
|
282
|
+
let optionsJson = "";
|
|
234
283
|
let node = "";
|
|
235
284
|
let kind = "";
|
|
236
285
|
let status = "";
|
|
286
|
+
let reason = "";
|
|
237
287
|
let okRaw = "";
|
|
238
288
|
let durationRaw = "";
|
|
239
289
|
let spurBin = getEnvVar("SPUR_BIN") ?? "";
|
|
@@ -253,12 +303,18 @@ async function main() {
|
|
|
253
303
|
action = true;
|
|
254
304
|
else if (argv[i] === "--close")
|
|
255
305
|
close = true;
|
|
306
|
+
else if (argv[i] === "--decide")
|
|
307
|
+
decide = true;
|
|
308
|
+
else if (argv[i] === "--options-json")
|
|
309
|
+
optionsJson = argv[++i] ?? "";
|
|
256
310
|
else if (argv[i] === "--node")
|
|
257
311
|
node = argv[++i] ?? "";
|
|
258
312
|
else if (argv[i] === "--kind")
|
|
259
313
|
kind = argv[++i] ?? "";
|
|
260
314
|
else if (argv[i] === "--status")
|
|
261
315
|
status = argv[++i] ?? "";
|
|
316
|
+
else if (argv[i] === "--reason")
|
|
317
|
+
reason = argv[++i] ?? "";
|
|
262
318
|
else if (argv[i] === "--ok")
|
|
263
319
|
okRaw = argv[++i] ?? "";
|
|
264
320
|
else if (argv[i] === "--duration-ms")
|
|
@@ -271,6 +327,15 @@ async function main() {
|
|
|
271
327
|
usage();
|
|
272
328
|
process.exit(await printFingerprint(taskFile, featureFile, spurBin));
|
|
273
329
|
}
|
|
330
|
+
if (decide) {
|
|
331
|
+
if (action || close || fingerprint || file !== "" || taskFile !== "")
|
|
332
|
+
usage();
|
|
333
|
+
if (runId.trim() === "" || node.trim() === "" || optionsJson.trim() === "")
|
|
334
|
+
usage();
|
|
335
|
+
if (!SAFE_RUN_ID_RE.test(runId))
|
|
336
|
+
refuseUnsafeRunId(runId);
|
|
337
|
+
process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
|
|
338
|
+
}
|
|
274
339
|
if (action || close) {
|
|
275
340
|
if (action && close)
|
|
276
341
|
usage();
|
|
@@ -281,6 +346,12 @@ async function main() {
|
|
|
281
346
|
if (close) {
|
|
282
347
|
if (!CLOSE_STATUSES.has(status))
|
|
283
348
|
usage();
|
|
349
|
+
if (reason.trim() === "") {
|
|
350
|
+
if (status === "failed")
|
|
351
|
+
usage();
|
|
352
|
+
} else if (!TERMINAL_REASONS.has(reason)) {
|
|
353
|
+
usage();
|
|
354
|
+
}
|
|
284
355
|
process.exit(await runTraceMode({
|
|
285
356
|
runId,
|
|
286
357
|
close: true,
|
|
@@ -289,7 +360,8 @@ async function main() {
|
|
|
289
360
|
status,
|
|
290
361
|
ok: true,
|
|
291
362
|
durationMs: 0,
|
|
292
|
-
spurBin
|
|
363
|
+
spurBin,
|
|
364
|
+
...reason.trim() === "" ? {} : { reason }
|
|
293
365
|
}));
|
|
294
366
|
}
|
|
295
367
|
if (node.trim() === "" || kind.trim() === "")
|
|
@@ -31,8 +31,9 @@
|
|
|
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>]
|
|
36
37
|
*
|
|
37
38
|
* The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
|
|
38
39
|
* creates nothing (task 0862 R5).
|
|
@@ -44,6 +45,12 @@
|
|
|
44
45
|
* `.spur/run/<run-id>.md` and the script still exits 0 with `{"ok":false}` on stdout, so
|
|
45
46
|
* observation never wedges the run. `--close` is NOT best-effort: the run-row closure is
|
|
46
47
|
* bookkeeping, so a missing run row or a persistence failure exits 1 with a named error.
|
|
48
|
+
* The `--decide` mode (task 0941 R5) executes the non-pausing decide action through the SAME
|
|
49
|
+
* app runner the engine registers (`runDecideForInlineRun`), writing the same resultFile row
|
|
50
|
+
* and recording the same `action_runs` trace row (`kind=decide`, best-effort). A degraded
|
|
51
|
+
* decision is still `ok: true` — the run continues — so exit 0 covers every model-level
|
|
52
|
+
* outcome; only an invalid options schema exits 1 (fail closed).
|
|
53
|
+
*
|
|
47
54
|
* Exit 2 is reserved for usage errors.
|
|
48
55
|
*
|
|
49
56
|
* Env: SPUR_BIN
|
|
@@ -93,7 +100,10 @@ function usage(): never {
|
|
|
93
100
|
'--status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]',
|
|
94
101
|
);
|
|
95
102
|
console.error(
|
|
96
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--spur-bin <path>]',
|
|
103
|
+
' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
|
|
104
|
+
);
|
|
105
|
+
console.error(
|
|
106
|
+
' bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]',
|
|
97
107
|
);
|
|
98
108
|
process.exit(2);
|
|
99
109
|
}
|
|
@@ -305,6 +315,24 @@ async function printFingerprint(taskFile: string, featureFile: string, spurBin:
|
|
|
305
315
|
/** Terminal statuses the inline driver may declare when closing its run row. */
|
|
306
316
|
const CLOSE_STATUSES = new Set(['done', 'failed', 'paused']);
|
|
307
317
|
|
|
318
|
+
/**
|
|
319
|
+
* 0937 R2: closed terminal-reason vocabulary the driver may declare on `--close`.
|
|
320
|
+
* COPIED from packages/app/src/workflow/terminal-reason.ts — the plugin standalone
|
|
321
|
+
* contract forbids a value import of app code; the parity test asserts the copy
|
|
322
|
+
* equals the app export.
|
|
323
|
+
*/
|
|
324
|
+
const TERMINAL_REASONS = new Set([
|
|
325
|
+
'done',
|
|
326
|
+
'paused-operator',
|
|
327
|
+
'failed-check',
|
|
328
|
+
'failed-agent',
|
|
329
|
+
'failed-timeout',
|
|
330
|
+
'failed-guard',
|
|
331
|
+
'cancelled',
|
|
332
|
+
'interrupted',
|
|
333
|
+
'retry-exhausted',
|
|
334
|
+
]);
|
|
335
|
+
|
|
308
336
|
/** Finalize statuses — a finish emission is terminal, so only done|failed are valid (0868 #4). */
|
|
309
337
|
const ACTION_STATUSES = new Set(['done', 'failed']);
|
|
310
338
|
|
|
@@ -316,6 +344,8 @@ interface TraceModeInput {
|
|
|
316
344
|
readonly close: boolean;
|
|
317
345
|
readonly node: string;
|
|
318
346
|
readonly kind: string;
|
|
347
|
+
/** Declared terminal reason (0937 R2) — validated against TERMINAL_REASONS before this point. */
|
|
348
|
+
readonly reason?: string;
|
|
319
349
|
readonly status: string;
|
|
320
350
|
readonly ok: boolean;
|
|
321
351
|
readonly durationMs: number;
|
|
@@ -397,7 +427,7 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
397
427
|
});
|
|
398
428
|
const result = (
|
|
399
429
|
input.close
|
|
400
|
-
? await writer.closeRun(input.runId, input.status)
|
|
430
|
+
? await writer.closeRun(input.runId, input.status, undefined, input.reason)
|
|
401
431
|
: await writer.recordAction({
|
|
402
432
|
runId: input.runId,
|
|
403
433
|
node: input.node,
|
|
@@ -432,6 +462,82 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
432
462
|
}
|
|
433
463
|
}
|
|
434
464
|
|
|
465
|
+
/**
|
|
466
|
+
* `--decide` mode (task 0941 R5): execute the non-pausing decide action through the same app
|
|
467
|
+
* function the engine registers (`runDecideForInlineRun` → `DecideActionRunner`), then record
|
|
468
|
+
* the `action_runs` trace row through the shared writer. The decide-enabled switch
|
|
469
|
+
* (`workflow.decideDecisionMaker`) is resolved HERE at the driver boundary (ADR-082) through
|
|
470
|
+
* the bridged facade derivation and passed into the app runner as an explicit parameter —
|
|
471
|
+
* app services never load Spur config. Degraded outcomes (`disabled`,
|
|
472
|
+
* `no-backend`, `error`, `timeout`, `low-confidence`) are normal — the value is the declared
|
|
473
|
+
* default and the run continues — so they print `ok: true` and exit 0. Only an invalid options
|
|
474
|
+
* schema, an unreadable options file, or a failed config load fails closed with exit 1.
|
|
475
|
+
*/
|
|
476
|
+
async function runDecideMode(input: {
|
|
477
|
+
runId: string;
|
|
478
|
+
node: string;
|
|
479
|
+
optionsFile: string;
|
|
480
|
+
spurBin: string;
|
|
481
|
+
}): Promise<number> {
|
|
482
|
+
type DecideOutcome = {
|
|
483
|
+
ok: boolean;
|
|
484
|
+
error?: string;
|
|
485
|
+
value?: string;
|
|
486
|
+
degraded?: boolean;
|
|
487
|
+
reason?: string;
|
|
488
|
+
backend?: string | null;
|
|
489
|
+
confidence?: number | null;
|
|
490
|
+
resultFile?: string;
|
|
491
|
+
durationMs?: number;
|
|
492
|
+
};
|
|
493
|
+
const decideFailed = (error: string): number => {
|
|
494
|
+
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}\n`);
|
|
495
|
+
return 1;
|
|
496
|
+
};
|
|
497
|
+
let outcome: DecideOutcome;
|
|
498
|
+
try {
|
|
499
|
+
const { entry, portable } = resolveAppEntry(input.spurBin);
|
|
500
|
+
const app = (await import(entry)) as {
|
|
501
|
+
runDecideForInlineRun: (input2: {
|
|
502
|
+
workdir: string;
|
|
503
|
+
optionsFile: string;
|
|
504
|
+
enabled: boolean;
|
|
505
|
+
}) => Promise<DecideOutcome>;
|
|
506
|
+
};
|
|
507
|
+
// Config switch at the driver boundary (ADR-082 / task 0941 gate fix): the flag comes
|
|
508
|
+
// from the bridged facade derivation in the generated lib — the same committed bundle
|
|
509
|
+
// the inventory flow imports — never a @gobing-ai/* value import. Embedded schemas
|
|
510
|
+
// ride along only in the portable layout, mirroring the setup mode's load posture.
|
|
511
|
+
const lib = (await import(
|
|
512
|
+
fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url))
|
|
513
|
+
)) as typeof import('../lib/inline-run.generated.mjs');
|
|
514
|
+
const enabled = await lib.resolveDecideDecisionMakerEnabled(
|
|
515
|
+
process.cwd(),
|
|
516
|
+
portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined,
|
|
517
|
+
);
|
|
518
|
+
outcome = await app.runDecideForInlineRun({
|
|
519
|
+
workdir: process.cwd(),
|
|
520
|
+
optionsFile: input.optionsFile,
|
|
521
|
+
enabled,
|
|
522
|
+
});
|
|
523
|
+
} catch (error) {
|
|
524
|
+
return decideFailed(error instanceof Error ? error.message : String(error));
|
|
525
|
+
}
|
|
526
|
+
if (!outcome.ok) return decideFailed(outcome.error ?? 'decide failed without an error message');
|
|
527
|
+
process.stdout.write(`${JSON.stringify({ ok: true, runId: input.runId, node: input.node, ...outcome })}\n`);
|
|
528
|
+
// Trace row is best-effort, exactly like --action: an emission failure never wedges the run.
|
|
529
|
+
return await runTraceMode({
|
|
530
|
+
runId: input.runId,
|
|
531
|
+
close: false,
|
|
532
|
+
node: input.node,
|
|
533
|
+
kind: 'decide',
|
|
534
|
+
status: 'done',
|
|
535
|
+
ok: true,
|
|
536
|
+
durationMs: outcome.durationMs ?? 0,
|
|
537
|
+
spurBin: input.spurBin,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
435
541
|
async function main(): Promise<void> {
|
|
436
542
|
// The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
|
|
437
543
|
if (!process.versions.bun) {
|
|
@@ -448,9 +554,12 @@ async function main(): Promise<void> {
|
|
|
448
554
|
let featureFile = '';
|
|
449
555
|
let action = false;
|
|
450
556
|
let close = false;
|
|
557
|
+
let decide = false;
|
|
558
|
+
let optionsJson = '';
|
|
451
559
|
let node = '';
|
|
452
560
|
let kind = '';
|
|
453
561
|
let status = '';
|
|
562
|
+
let reason = '';
|
|
454
563
|
let okRaw = '';
|
|
455
564
|
let durationRaw = '';
|
|
456
565
|
let spurBin = getEnvVar('SPUR_BIN') ?? '';
|
|
@@ -463,9 +572,12 @@ async function main(): Promise<void> {
|
|
|
463
572
|
else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
|
|
464
573
|
else if (argv[i] === '--action') action = true;
|
|
465
574
|
else if (argv[i] === '--close') close = true;
|
|
575
|
+
else if (argv[i] === '--decide') decide = true;
|
|
576
|
+
else if (argv[i] === '--options-json') optionsJson = argv[++i] ?? '';
|
|
466
577
|
else if (argv[i] === '--node') node = argv[++i] ?? '';
|
|
467
578
|
else if (argv[i] === '--kind') kind = argv[++i] ?? '';
|
|
468
579
|
else if (argv[i] === '--status') status = argv[++i] ?? '';
|
|
580
|
+
else if (argv[i] === '--reason') reason = argv[++i] ?? '';
|
|
469
581
|
else if (argv[i] === '--ok') okRaw = argv[++i] ?? '';
|
|
470
582
|
else if (argv[i] === '--duration-ms') durationRaw = argv[++i] ?? '';
|
|
471
583
|
else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
|
|
@@ -481,12 +593,29 @@ async function main(): Promise<void> {
|
|
|
481
593
|
// ADR-117 emission modes (task 0868): the inline driver reports one completed action
|
|
482
594
|
// boundary, or closes its run row at the declared terminal state. Both share the run-id
|
|
483
595
|
// filename guard, the app-entry resolution chain and the best-effort failure contract.
|
|
596
|
+
// Non-pausing decide action (0941 R5): --decide is mutually exclusive with the run-setup,
|
|
597
|
+
// fingerprint, and trace modes; it requires the run id (filename-guarded), the state id
|
|
598
|
+
// for the trace row, and the options JSON file.
|
|
599
|
+
if (decide) {
|
|
600
|
+
if (action || close || fingerprint || file !== '' || taskFile !== '') usage();
|
|
601
|
+
if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
|
|
602
|
+
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
603
|
+
process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
|
|
604
|
+
}
|
|
605
|
+
|
|
484
606
|
if (action || close) {
|
|
485
607
|
if (action && close) usage();
|
|
486
608
|
if (runId.trim() === '' || status.trim() === '') usage();
|
|
487
609
|
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
488
610
|
if (close) {
|
|
489
611
|
if (!CLOSE_STATUSES.has(status)) usage();
|
|
612
|
+
// 0937 R2: a failed close requires a declared reason, and any declared reason
|
|
613
|
+
// must be a closed-enum value — both fail loudly BEFORE any write happens.
|
|
614
|
+
if (reason.trim() === '') {
|
|
615
|
+
if (status === 'failed') usage();
|
|
616
|
+
} else if (!TERMINAL_REASONS.has(reason)) {
|
|
617
|
+
usage();
|
|
618
|
+
}
|
|
490
619
|
process.exit(
|
|
491
620
|
await runTraceMode({
|
|
492
621
|
runId,
|
|
@@ -497,6 +626,7 @@ async function main(): Promise<void> {
|
|
|
497
626
|
ok: true,
|
|
498
627
|
durationMs: 0,
|
|
499
628
|
spurBin,
|
|
629
|
+
...(reason.trim() === '' ? {} : { reason }),
|
|
500
630
|
}),
|
|
501
631
|
);
|
|
502
632
|
}
|