dev-loops 0.3.0 → 0.5.0
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/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +1 -1
- package/.claude/agents/refiner.md +1 -0
- package/.claude/hooks/_run-context.mjs +9 -16
- package/.claude/skills/copilot-pr-followup/SKILL.md +9 -7
- package/.claude/skills/dev-loop/SKILL.md +17 -7
- package/.claude/skills/dev-loop/templates/slides-story-review.md +54 -0
- package/.claude/skills/docs/anti-patterns.md +1 -1
- package/.claude/skills/docs/artifact-authority-contract.md +86 -31
- package/.claude/skills/docs/copilot-loop-operations.md +1 -1
- package/.claude/skills/docs/issue-intake-procedure.md +4 -0
- package/.claude/skills/docs/local-planning-flow.md +63 -0
- package/.claude/skills/docs/local-planning-worked-example.md +139 -0
- package/.claude/skills/docs/merge-preconditions.md +100 -1
- package/.claude/skills/docs/plan-file-contract.md +37 -0
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +55 -7
- package/.claude/skills/docs/spike-mode-contract.md +237 -0
- package/.claude/skills/docs/stop-conditions.md +1 -0
- package/.claude/skills/docs/tracker-first-loop-state.md +6 -6
- package/.claude/skills/local-implementation/SKILL.md +8 -2
- package/CHANGELOG.md +69 -0
- package/README.md +9 -2
- package/agents/refiner.agent.md +1 -0
- package/cli/index.mjs +21 -2
- package/extension/README.md +1 -1
- package/package.json +6 -4
- package/scripts/README.md +2 -2
- package/scripts/github/capture-review-threads.mjs +20 -2
- package/scripts/github/offer-human-handoff.mjs +147 -0
- package/scripts/github/probe-ci-status.mjs +468 -0
- package/scripts/github/probe-copilot-review.mjs +69 -3
- package/scripts/github/request-copilot-review.mjs +3 -3
- package/scripts/github/resolve-handoff-candidates.mjs +412 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +20 -5
- package/scripts/lib/jq-output.mjs +297 -0
- package/scripts/loop/_stale-runner-detection.mjs +1 -1
- package/scripts/loop/_worktree-path.mjs +27 -0
- package/scripts/loop/check-retro-tooling.mjs +246 -0
- package/scripts/loop/cleanup-worktree.mjs +175 -0
- package/scripts/loop/copilot-pr-handoff.mjs +22 -4
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +35 -2
- package/scripts/loop/detect-stale-runner.mjs +3 -4
- package/scripts/loop/docs-grill-contract.mjs +70 -0
- package/scripts/loop/ensure-worktree.mjs +219 -0
- package/scripts/loop/info.mjs +21 -2
- package/scripts/loop/outer-loop.mjs +1 -1
- package/scripts/loop/pr-runner-coordination.mjs +21 -5
- package/scripts/loop/pre-flight-gate.mjs +10 -7
- package/scripts/loop/pre-push-main-guard.mjs +4 -4
- package/scripts/loop/provision-worktree.mjs +243 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +181 -10
- package/scripts/loop/resolve-pr-conflicts.mjs +357 -0
- package/scripts/loop/run-queue.mjs +25 -1
- package/scripts/loop/run-watch-cycle.mjs +152 -25
- package/scripts/loop/slides-story-review-contract.mjs +123 -0
- package/scripts/pages/build-site.mjs +136 -0
- package/scripts/projects/add-queue-item.mjs +32 -7
- package/scripts/projects/archive-done-items.mjs +2 -1
- package/scripts/projects/ensure-queue-board.mjs +2 -1
- package/scripts/projects/list-queue-items.mjs +14 -3
- package/scripts/projects/move-queue-item.mjs +14 -3
- package/scripts/projects/reorder-queue-item.mjs +5 -4
- package/scripts/projects/sync-item-status.mjs +2 -1
- package/scripts/refine/_refine-helpers.mjs +20 -0
- package/scripts/refine/exit-spike.mjs +186 -0
- package/scripts/refine/promote-plan.mjs +387 -0
- package/scripts/refine/refine-plan-file.mjs +165 -0
- package/scripts/refine/validate-plan-file.mjs +64 -0
- package/scripts/refine/validate-spike-file.mjs +87 -0
- package/skills/copilot-pr-followup/SKILL.md +9 -7
- package/skills/dev-loop/SKILL.md +13 -3
- package/skills/dev-loop/templates/slides-story-review.md +54 -0
- package/skills/docs/anti-patterns.md +1 -1
- package/skills/docs/artifact-authority-contract.md +86 -31
- package/skills/docs/copilot-loop-operations.md +1 -1
- package/skills/docs/issue-intake-procedure.md +4 -0
- package/skills/docs/local-planning-flow.md +63 -0
- package/skills/docs/local-planning-worked-example.md +139 -0
- package/skills/docs/merge-preconditions.md +100 -1
- package/skills/docs/plan-file-contract.md +37 -0
- package/skills/docs/retrospective-checkpoint-contract.md +55 -7
- package/skills/docs/spike-mode-contract.md +237 -0
- package/skills/docs/stop-conditions.md +1 -0
- package/skills/docs/tracker-first-loop-state.md +6 -6
- package/skills/local-implementation/SKILL.md +8 -2
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
5
6
|
|
|
6
|
-
const USAGE = `Usage: dev-loops
|
|
7
|
+
const USAGE = `Usage: dev-loops queue list --repo <owner/name> --project <number|id> [--column <name>] [--limit <n>]
|
|
8
|
+
(dev-loops project list … is a back-compat alias)
|
|
7
9
|
|
|
8
10
|
List GitHub Projects V2 items filtered by Status column, ordered by position
|
|
9
11
|
ascending. Returns machine-readable JSON.
|
|
@@ -18,10 +20,12 @@ Options:
|
|
|
18
20
|
Output (stdout):
|
|
19
21
|
JSON: { ok: true, items: [{ issueNumber, prNumber, title, url, itemId, contentId, status }, ...] }
|
|
20
22
|
|
|
23
|
+
${JQ_OUTPUT_USAGE}
|
|
24
|
+
|
|
21
25
|
Exit codes:
|
|
22
26
|
0 — success
|
|
23
27
|
1 — usage or argument error
|
|
24
|
-
2 — GitHub API error
|
|
28
|
+
2 — GitHub API error / invalid --jq filter
|
|
25
29
|
3 — project, field, or column not found
|
|
26
30
|
`.trim();
|
|
27
31
|
|
|
@@ -44,6 +48,7 @@ function parseCliArgs(argv) {
|
|
|
44
48
|
column: { type: "string" },
|
|
45
49
|
limit: { type: "string" },
|
|
46
50
|
help: { type: "boolean", short: "h" },
|
|
51
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
47
52
|
},
|
|
48
53
|
allowPositionals: true,
|
|
49
54
|
strict: false,
|
|
@@ -82,6 +87,12 @@ function parseCliArgs(argv) {
|
|
|
82
87
|
args.limit = val;
|
|
83
88
|
break;
|
|
84
89
|
}
|
|
90
|
+
case "jq":
|
|
91
|
+
args.jq = requireValue(token, "--jq requires a filter");
|
|
92
|
+
break;
|
|
93
|
+
case "silent":
|
|
94
|
+
args.silent = true;
|
|
95
|
+
break;
|
|
85
96
|
default:
|
|
86
97
|
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
87
98
|
}
|
|
@@ -473,7 +484,7 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
473
484
|
}
|
|
474
485
|
try {
|
|
475
486
|
const result = await main(args, { env });
|
|
476
|
-
|
|
487
|
+
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
477
488
|
} catch (err) {
|
|
478
489
|
stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
|
|
479
490
|
process.exitCode = classifyExitCode(err);
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
5
6
|
|
|
6
|
-
const USAGE = `Usage: dev-loops
|
|
7
|
+
const USAGE = `Usage: dev-loops queue move --repo <owner/name> --project <number|id> --item <number|node-id> --to-column <name>
|
|
8
|
+
(dev-loops project move … is a back-compat alias)
|
|
7
9
|
|
|
8
10
|
Move a GitHub Projects V2 item between Status columns.
|
|
9
11
|
|
|
@@ -17,10 +19,12 @@ Options:
|
|
|
17
19
|
Output (stdout):
|
|
18
20
|
JSON: { ok: true, item: { itemId, issueNumber, prNumber, previousColumn, newColumn } }
|
|
19
21
|
|
|
22
|
+
${JQ_OUTPUT_USAGE}
|
|
23
|
+
|
|
20
24
|
Exit codes:
|
|
21
25
|
0 — success
|
|
22
26
|
1 — usage or argument error
|
|
23
|
-
2 — GitHub API error
|
|
27
|
+
2 — GitHub API error / invalid --jq filter
|
|
24
28
|
3 — project, field, column, or item not found
|
|
25
29
|
`.trim();
|
|
26
30
|
|
|
@@ -43,6 +47,7 @@ function parseCliArgs(argv) {
|
|
|
43
47
|
item: { type: "string" },
|
|
44
48
|
"to-column": { type: "string" },
|
|
45
49
|
help: { type: "boolean", short: "h" },
|
|
50
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
46
51
|
},
|
|
47
52
|
allowPositionals: true,
|
|
48
53
|
strict: false,
|
|
@@ -75,6 +80,12 @@ function parseCliArgs(argv) {
|
|
|
75
80
|
case "to-column":
|
|
76
81
|
args.toColumn = requireValue(token, "--to-column requires a value");
|
|
77
82
|
break;
|
|
83
|
+
case "jq":
|
|
84
|
+
args.jq = requireValue(token, "--jq requires a filter");
|
|
85
|
+
break;
|
|
86
|
+
case "silent":
|
|
87
|
+
args.silent = true;
|
|
88
|
+
break;
|
|
78
89
|
default:
|
|
79
90
|
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
80
91
|
}
|
|
@@ -531,7 +542,7 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
531
542
|
}
|
|
532
543
|
try {
|
|
533
544
|
const result = await main(args, { env });
|
|
534
|
-
|
|
545
|
+
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
535
546
|
} catch (err) {
|
|
536
547
|
stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
|
|
537
548
|
process.exitCode = classifyExitCode(err);
|
|
@@ -4,10 +4,11 @@ import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
|
|
6
6
|
const USAGE = `Usage:
|
|
7
|
-
dev-loops
|
|
8
|
-
dev-loops
|
|
9
|
-
dev-loops
|
|
10
|
-
dev-loops
|
|
7
|
+
dev-loops queue reorder --repo <owner/name> --project <number|id> --item <number|node-id> [--after <number|node-id>]
|
|
8
|
+
dev-loops queue reorder move-to-top <ref> --repo <owner/name> --project <number|id>
|
|
9
|
+
dev-loops queue reorder move-after <ref> <after-ref> --repo <owner/name> --project <number|id>
|
|
10
|
+
dev-loops queue reorder order <ref1> <ref2> ... --repo <owner/name> --project <number|id>
|
|
11
|
+
(dev-loops project reorder … is a back-compat alias)
|
|
11
12
|
|
|
12
13
|
Reorder GitHub Projects V2 items by board position via updateProjectV2ItemPosition.
|
|
13
14
|
|
|
@@ -4,7 +4,8 @@ import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
|
4
4
|
import { syncBoardStatus } from "@dev-loops/core/loop/queue-board-sync";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
|
|
7
|
-
const USAGE = `Usage: dev-loops
|
|
7
|
+
const USAGE = `Usage: dev-loops queue sync-status --repo <owner/name> --item <number> --to-column <name>
|
|
8
|
+
(dev-loops project sync-status … is a back-compat alias)
|
|
8
9
|
|
|
9
10
|
Sync a queued issue/PR's board Status column on a dev-loop transition (e.g.
|
|
10
11
|
PR opened → "In Progress", merged → "Done"). Resolves the board from .devloops
|
|
@@ -233,6 +233,26 @@ export function extractSection(body, headingText) {
|
|
|
233
233
|
return body.slice(start, end).trim();
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Shared base-section checker for the phase-doc-format validators (plan + spike).
|
|
238
|
+
* For each heading, reports its distinct missing_* code when the section is
|
|
239
|
+
* absent or has an empty body. Pure; no side effects.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} markdownText
|
|
242
|
+
* @param {string} checker checker name echoed back in the result
|
|
243
|
+
* @param {Record<string,string>} sectionCodes heading → missing_* code (key order = section order)
|
|
244
|
+
* @returns {{ checker: string, ok: boolean, errors: { code: string, message: string }[] }}
|
|
245
|
+
*/
|
|
246
|
+
export function checkBaseSections(markdownText, checker, sectionCodes) {
|
|
247
|
+
const errors = [];
|
|
248
|
+
for (const [heading, code] of Object.entries(sectionCodes)) {
|
|
249
|
+
if (!extractSection(markdownText, heading)) {
|
|
250
|
+
errors.push({ code, message: `Missing or empty ## ${heading} section.` });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { checker, ok: errors.length === 0, errors };
|
|
254
|
+
}
|
|
255
|
+
|
|
236
256
|
export function normalizeScopeToken(value) {
|
|
237
257
|
return String(value ?? "")
|
|
238
258
|
.trim()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
|
|
6
|
+
import { buildParseError, formatCliError } from "../_core-helpers.mjs";
|
|
7
|
+
import { requireTokenValue } from "../_cli-primitives.mjs";
|
|
8
|
+
import { extractSection, isDirectCliRun } from "./_refine-helpers.mjs";
|
|
9
|
+
import {
|
|
10
|
+
validateSpikeExplorationSections,
|
|
11
|
+
SPIKE_FILE_EXIT_MARKER_SECTION,
|
|
12
|
+
} from "./validate-spike-file.mjs";
|
|
13
|
+
import { evaluateSpikeIntakeState } from "@dev-loops/core/loop/spike-intake-contract";
|
|
14
|
+
import {
|
|
15
|
+
evaluateSpikeExit,
|
|
16
|
+
buildGraduatedPlanBody,
|
|
17
|
+
SPIKE_EXIT_ACTION,
|
|
18
|
+
SPIKE_EXIT_DISPOSITION,
|
|
19
|
+
} from "@dev-loops/core/loop/spike-exit-contract";
|
|
20
|
+
|
|
21
|
+
const USAGE = `Usage:
|
|
22
|
+
exit-spike.mjs --spike-file <path> --disposition <discard|graduate> [--plan-file <path>] [--json]
|
|
23
|
+
Conclude a spike that has reached a Recommendation (spike_ready_for_exit).
|
|
24
|
+
discard The recommendation is "don't pursue": drop the spike with ZERO
|
|
25
|
+
tracker artifacts. The findings doc is the whole record.
|
|
26
|
+
graduate Emit a #947-consumable local-first plan file (Status/Objective/
|
|
27
|
+
In scope/Explicit non-goals) built from the spike's
|
|
28
|
+
Question/Approach/Findings/Recommendation. The plan then enters the
|
|
29
|
+
existing plan->PR promotion path (scripts/refine/promote-plan.mjs).
|
|
30
|
+
--plan-file is required and graduation is idempotent (re-running
|
|
31
|
+
reproduces the same plan file).
|
|
32
|
+
|
|
33
|
+
Fail-closed: a spike that is not ready for exit (no Recommendation) or an
|
|
34
|
+
unknown disposition makes ZERO tracker mutation and exits 1. No GitHub artifact
|
|
35
|
+
is created on any path; graduation only writes a local plan file.
|
|
36
|
+
|
|
37
|
+
Required:
|
|
38
|
+
--spike-file <path> Path to the spike findings artifact
|
|
39
|
+
--disposition <which> discard | graduate
|
|
40
|
+
Optional:
|
|
41
|
+
--plan-file <path> Output plan-file path (required for graduate)
|
|
42
|
+
--json Machine-readable JSON output
|
|
43
|
+
--help Show this help
|
|
44
|
+
Exit codes:
|
|
45
|
+
0 Exit succeeded (discard recorded; or plan file written for graduate)
|
|
46
|
+
1 Argument error, or fail-closed (not ready / unknown disposition)`.trim();
|
|
47
|
+
|
|
48
|
+
const parseError = buildParseError(USAGE);
|
|
49
|
+
|
|
50
|
+
export function parseExitSpikeCliArgs(argv) {
|
|
51
|
+
const { tokens } = parseArgs({
|
|
52
|
+
args: [...argv],
|
|
53
|
+
options: {
|
|
54
|
+
help: { type: "boolean", short: "h" },
|
|
55
|
+
"spike-file": { type: "string" },
|
|
56
|
+
disposition: { type: "string" },
|
|
57
|
+
"plan-file": { type: "string" },
|
|
58
|
+
json: { type: "boolean" },
|
|
59
|
+
},
|
|
60
|
+
allowPositionals: true,
|
|
61
|
+
strict: false,
|
|
62
|
+
tokens: true,
|
|
63
|
+
});
|
|
64
|
+
const options = { help: false, spikeFile: undefined, disposition: undefined, planFile: undefined, json: false };
|
|
65
|
+
for (const token of tokens) {
|
|
66
|
+
if (token.kind === "positional") {
|
|
67
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
68
|
+
}
|
|
69
|
+
if (token.kind !== "option") continue;
|
|
70
|
+
if (token.name === "help") {
|
|
71
|
+
options.help = true;
|
|
72
|
+
return options;
|
|
73
|
+
}
|
|
74
|
+
if (token.name === "spike-file") {
|
|
75
|
+
options.spikeFile = requireTokenValue(token, parseError, { flagPattern: /^-/u });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (token.name === "disposition") {
|
|
79
|
+
options.disposition = requireTokenValue(token, parseError, { flagPattern: /^-/u });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (token.name === "plan-file") {
|
|
83
|
+
options.planFile = requireTokenValue(token, parseError, { flagPattern: /^-/u });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (token.name === "json") {
|
|
87
|
+
options.json = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
91
|
+
}
|
|
92
|
+
if (typeof options.spikeFile !== "string" || options.spikeFile.trim().length === 0) {
|
|
93
|
+
throw parseError("exit-spike requires --spike-file <path>");
|
|
94
|
+
}
|
|
95
|
+
if (typeof options.disposition !== "string" || options.disposition.trim().length === 0) {
|
|
96
|
+
throw parseError("exit-spike requires --disposition <discard|graduate>");
|
|
97
|
+
}
|
|
98
|
+
// graduate emits a plan file; the destination must be explicit so we never
|
|
99
|
+
// guess a path. discard writes nothing, so --plan-file is irrelevant there.
|
|
100
|
+
if (options.disposition === SPIKE_EXIT_DISPOSITION.GRADUATE
|
|
101
|
+
&& (typeof options.planFile !== "string" || options.planFile.trim().length === 0)) {
|
|
102
|
+
throw parseError("exit-spike --disposition graduate requires --plan-file <path>");
|
|
103
|
+
}
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function emit(stdout, json, summary, humanLines) {
|
|
108
|
+
if (json) {
|
|
109
|
+
stdout.write(`${JSON.stringify(summary)}\n`);
|
|
110
|
+
} else {
|
|
111
|
+
stdout.write(`${humanLines.join("\n")}\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
|
|
116
|
+
const options = parseExitSpikeCliArgs(argv);
|
|
117
|
+
if (options.help) {
|
|
118
|
+
stdout.write(`${USAGE}\n`);
|
|
119
|
+
return { ok: true, help: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const spikePath = path.resolve(options.spikeFile);
|
|
123
|
+
const markdownText = await readFile(spikePath, "utf8");
|
|
124
|
+
|
|
125
|
+
// Compute the intake-state facts with the P1 surfaces, then hand them to the
|
|
126
|
+
// pure exit decision. Per spike-intake-contract, `baseSectionsValid` is the
|
|
127
|
+
// exploration scaffold (Question/Approach/Findings); the Recommendation is the
|
|
128
|
+
// separate exit-marker that flips in-progress -> ready-for-exit. So a
|
|
129
|
+
// scaffold-valid spike without a Recommendation classifies as in-progress
|
|
130
|
+
// (fail-closed reason `not_ready_for_exit`), not ambiguous.
|
|
131
|
+
const baseSectionsValid = validateSpikeExplorationSections(markdownText).ok;
|
|
132
|
+
const hasRecommendation = Boolean(extractSection(markdownText, SPIKE_FILE_EXIT_MARKER_SECTION));
|
|
133
|
+
const { state: spikeIntakeState } = evaluateSpikeIntakeState({ baseSectionsValid, hasRecommendation });
|
|
134
|
+
const decision = evaluateSpikeExit({ spikeIntakeState, disposition: options.disposition });
|
|
135
|
+
|
|
136
|
+
if (!decision.ok) {
|
|
137
|
+
// Fail closed: no tracker artifact, no plan file — surface the reason.
|
|
138
|
+
const summary = { ok: false, reason: decision.reason, spikeIntakeState: decision.spikeIntakeState ?? spikeIntakeState };
|
|
139
|
+
emit(stdout, options.json, summary, [`exit-spike: FAIL (${decision.reason})`]);
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
return summary;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// DISCARD: zero artifacts. The findings doc on disk is the whole record.
|
|
145
|
+
if (decision.action === SPIKE_EXIT_ACTION.DISCARD) {
|
|
146
|
+
const summary = { ok: true, action: SPIKE_EXIT_ACTION.DISCARD, spikeFile: spikePath };
|
|
147
|
+
emit(stdout, options.json, summary, [
|
|
148
|
+
"exit-spike: PASS (discard)",
|
|
149
|
+
` spike: ${spikePath}`,
|
|
150
|
+
" no tracker artifact created (findings doc is the record)",
|
|
151
|
+
]);
|
|
152
|
+
return summary;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// GRADUATE: build a base-valid plan body from the spike sections and write it.
|
|
156
|
+
// Pure builder owns the section mapping + fail-closed on empty sections; this
|
|
157
|
+
// script owns the file I/O only. Overwriting the same path is idempotent.
|
|
158
|
+
const planPath = path.resolve(options.planFile);
|
|
159
|
+
const planBody = buildGraduatedPlanBody({
|
|
160
|
+
question: extractSection(markdownText, "Question"),
|
|
161
|
+
approach: extractSection(markdownText, "Approach"),
|
|
162
|
+
findings: extractSection(markdownText, "Findings"),
|
|
163
|
+
recommendation: extractSection(markdownText, SPIKE_FILE_EXIT_MARKER_SECTION),
|
|
164
|
+
});
|
|
165
|
+
await writeFile(planPath, planBody, "utf8");
|
|
166
|
+
|
|
167
|
+
const summary = {
|
|
168
|
+
ok: true,
|
|
169
|
+
action: SPIKE_EXIT_ACTION.GRADUATE,
|
|
170
|
+
spikeFile: spikePath,
|
|
171
|
+
planFile: planPath,
|
|
172
|
+
};
|
|
173
|
+
emit(stdout, options.json, summary, [
|
|
174
|
+
"exit-spike: PASS (graduate)",
|
|
175
|
+
` spike: ${spikePath}`,
|
|
176
|
+
` plan: ${planPath} (local-first; promote with scripts/refine/promote-plan.mjs)`,
|
|
177
|
+
]);
|
|
178
|
+
return summary;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
182
|
+
runCli().catch((error) => {
|
|
183
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
});
|
|
186
|
+
}
|