cclaw-cli 0.51.4 → 0.51.5
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/dist/content/node-hooks.js +51 -20
- package/dist/content/opencode-plugin.js +15 -15
- package/dist/content/start-command.js +8 -7
- package/dist/doctor.js +62 -1
- package/dist/install.js +0 -1
- package/dist/internal/advance-stage.js +143 -4
- package/package.json +1 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
1
4
|
import { DEFAULT_COMPOUND_RECURRENCE_THRESHOLD } from "../config.js";
|
|
2
5
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
3
6
|
import { SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD, SMALL_PROJECT_RECURRENCE_THRESHOLD } from "../knowledge-store.js";
|
|
@@ -7,6 +10,19 @@ function normalizePatterns(patterns, fallback) {
|
|
|
7
10
|
return [...fallback];
|
|
8
11
|
return patterns.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
9
12
|
}
|
|
13
|
+
function resolveCliEntrypointForGeneratedHook() {
|
|
14
|
+
const here = fileURLToPath(import.meta.url);
|
|
15
|
+
const candidates = [
|
|
16
|
+
path.resolve(path.dirname(here), "..", "cli.js"),
|
|
17
|
+
path.resolve(path.dirname(here), "..", "..", "dist", "cli.js")
|
|
18
|
+
];
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
// Synchronous probe runs only during cclaw-cli init/sync generation.
|
|
21
|
+
if (existsSync(candidate))
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
10
26
|
/**
|
|
11
27
|
* Node-only hook runtime (single entrypoint).
|
|
12
28
|
*
|
|
@@ -26,6 +42,7 @@ export function nodeHookRuntimeScript(options = {}) {
|
|
|
26
42
|
options.compoundRecurrenceThreshold >= 1
|
|
27
43
|
? options.compoundRecurrenceThreshold
|
|
28
44
|
: DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
45
|
+
const cliEntrypoint = resolveCliEntrypointForGeneratedHook();
|
|
29
46
|
return `#!/usr/bin/env node
|
|
30
47
|
import fs from "node:fs/promises";
|
|
31
48
|
import path from "node:path";
|
|
@@ -47,6 +64,7 @@ const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = ${JSON.stringify(tddProductionPathP
|
|
|
47
64
|
const COMPOUND_RECURRENCE_THRESHOLD = ${JSON.stringify(compoundRecurrenceThreshold)};
|
|
48
65
|
const SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD = ${JSON.stringify(SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD)};
|
|
49
66
|
const SMALL_PROJECT_RECURRENCE_THRESHOLD = ${JSON.stringify(SMALL_PROJECT_RECURRENCE_THRESHOLD)};
|
|
67
|
+
const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliEntrypoint)};
|
|
50
68
|
|
|
51
69
|
function resolveStrictness() {
|
|
52
70
|
return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
|
|
@@ -303,8 +321,28 @@ async function readStdin() {
|
|
|
303
321
|
}
|
|
304
322
|
|
|
305
323
|
async function runCclawInternal(root, args, options = {}) {
|
|
324
|
+
const cliEntrypoint = process.env.CCLAW_CLI_JS || CCLAW_CLI_ENTRYPOINT;
|
|
325
|
+
if (!cliEntrypoint || String(cliEntrypoint).trim().length === 0) {
|
|
326
|
+
return {
|
|
327
|
+
code: 1,
|
|
328
|
+
stdout: "",
|
|
329
|
+
stderr: "[cclaw] hook: local Node runtime entrypoint is missing. Re-run npx cclaw-cli sync or npx cclaw-cli upgrade.\\n",
|
|
330
|
+
missingBinary: true
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const stat = await fs.stat(cliEntrypoint);
|
|
335
|
+
if (!stat.isFile()) throw new Error("not-file");
|
|
336
|
+
} catch {
|
|
337
|
+
return {
|
|
338
|
+
code: 1,
|
|
339
|
+
stdout: "",
|
|
340
|
+
stderr: "[cclaw] hook: local Node runtime entrypoint not found at " + cliEntrypoint + ". Re-run npx cclaw-cli sync or npx cclaw-cli upgrade.\\n",
|
|
341
|
+
missingBinary: true
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
306
345
|
return await new Promise((resolve) => {
|
|
307
|
-
const isWindows = process.platform === "win32";
|
|
308
346
|
const captureStdout = options && options.captureStdout === true;
|
|
309
347
|
let settled = false;
|
|
310
348
|
let stdout = "";
|
|
@@ -316,22 +354,18 @@ async function runCclawInternal(root, args, options = {}) {
|
|
|
316
354
|
};
|
|
317
355
|
let child;
|
|
318
356
|
try {
|
|
319
|
-
child = spawn(
|
|
320
|
-
isWindows ? "cmd.exe" : "cclaw",
|
|
321
|
-
isWindows ? ["/d", "/s", "/c", "cclaw", "internal", ...args] : ["internal", ...args],
|
|
322
|
-
{
|
|
357
|
+
child = spawn(process.execPath, [cliEntrypoint, "internal", ...args], {
|
|
323
358
|
cwd: root,
|
|
324
359
|
env: process.env,
|
|
325
360
|
stdio: ["ignore", captureStdout ? "pipe" : "ignore", "pipe"]
|
|
326
|
-
}
|
|
327
|
-
);
|
|
361
|
+
});
|
|
328
362
|
} catch (error) {
|
|
329
363
|
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
330
364
|
finalize({
|
|
331
365
|
code: 1,
|
|
332
366
|
stdout,
|
|
333
367
|
stderr,
|
|
334
|
-
missingBinary: code === "ENOENT"
|
|
368
|
+
missingBinary: code === "ENOENT"
|
|
335
369
|
});
|
|
336
370
|
return;
|
|
337
371
|
}
|
|
@@ -355,7 +389,7 @@ async function runCclawInternal(root, args, options = {}) {
|
|
|
355
389
|
code: 1,
|
|
356
390
|
stdout,
|
|
357
391
|
stderr,
|
|
358
|
-
missingBinary: code === "ENOENT"
|
|
392
|
+
missingBinary: code === "ENOENT"
|
|
359
393
|
});
|
|
360
394
|
});
|
|
361
395
|
child.on("close", (code, signal) => {
|
|
@@ -368,15 +402,11 @@ async function runCclawInternal(root, args, options = {}) {
|
|
|
368
402
|
});
|
|
369
403
|
return;
|
|
370
404
|
}
|
|
371
|
-
const stderrLower = stderr.toLowerCase();
|
|
372
|
-
const missingBinary = isWindows
|
|
373
|
-
? stderrLower.includes("is not recognized as an internal or external command")
|
|
374
|
-
: false;
|
|
375
405
|
finalize({
|
|
376
406
|
code: typeof code === "number" ? code : 1,
|
|
377
407
|
stdout,
|
|
378
408
|
stderr,
|
|
379
|
-
missingBinary
|
|
409
|
+
missingBinary: false
|
|
380
410
|
});
|
|
381
411
|
});
|
|
382
412
|
});
|
|
@@ -1562,12 +1592,13 @@ async function handleVerifyCurrentState(runtime) {
|
|
|
1562
1592
|
const mode = resolveStrictness();
|
|
1563
1593
|
const result = await runCclawInternal(runtime.root, ["verify-current-state", "--quiet"]);
|
|
1564
1594
|
if (result.missingBinary) {
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
"verify-current-state"
|
|
1568
|
-
|
|
1569
|
-
)
|
|
1570
|
-
|
|
1595
|
+
const message = result.stderr.trim().length > 0
|
|
1596
|
+
? result.stderr.trim()
|
|
1597
|
+
: "Cclaw verify-current-state requires a local Node runtime entrypoint.";
|
|
1598
|
+
emitAdvisoryContext(runtime, "verify-current-state", message);
|
|
1599
|
+
process.stderr.write(result.stderr.trim().length > 0
|
|
1600
|
+
? result.stderr
|
|
1601
|
+
: "[cclaw] hook: local Node runtime entrypoint is required for verify-current-state\\n");
|
|
1571
1602
|
return 1;
|
|
1572
1603
|
}
|
|
1573
1604
|
if (mode === "strict") {
|
|
@@ -564,25 +564,25 @@ export default function cclawPlugin(ctx) {
|
|
|
564
564
|
: typeof payload;
|
|
565
565
|
logToFile("unknown event payload keys: " + keys);
|
|
566
566
|
}
|
|
567
|
-
|
|
568
|
-
// cache, otherwise the injected system prompt still shows the pre-compact
|
|
569
|
-
// digest/state until the next lifecycle event.
|
|
570
|
-
if (eventType === "session.compacted") {
|
|
571
|
-
await runHookScript("pre-compact", eventData ?? {});
|
|
572
|
-
}
|
|
573
|
-
if (
|
|
567
|
+
const isSessionLifecycle =
|
|
574
568
|
eventType === "session.created" ||
|
|
575
569
|
eventType === "session.resumed" ||
|
|
576
570
|
eventType === "session.compacted" ||
|
|
577
571
|
eventType === "session.cleared" ||
|
|
578
|
-
eventType === "session.updated"
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
572
|
+
eventType === "session.updated";
|
|
573
|
+
// session.compacted must run pre-compact BEFORE canonical rehydration,
|
|
574
|
+
// otherwise the injected system prompt can show the pre-compact
|
|
575
|
+
// digest/state until the next lifecycle event.
|
|
576
|
+
if (eventType === "session.compacted") {
|
|
577
|
+
await runHookScript("pre-compact", eventData ?? {});
|
|
578
|
+
}
|
|
579
|
+
if (isSessionLifecycle) {
|
|
580
|
+
// Keep OpenCode aligned with Claude/Cursor/Codex: session-start is
|
|
581
|
+
// the canonical rehydrate path that refreshes derived state such as
|
|
582
|
+
// Ralph Loop, compound readiness, and hook-error breadcrumbs. The
|
|
583
|
+
// plugin refreshes its local bootstrap cache afterwards so the system
|
|
584
|
+
// transform sees the side effects from the hook runtime.
|
|
585
|
+
await runHookScript("session-start", eventData ?? {});
|
|
586
586
|
await refreshBootstrapCache(true);
|
|
587
587
|
}
|
|
588
588
|
if (eventType === "session.idle") {
|
|
@@ -85,8 +85,10 @@ ${conversationLanguagePolicyMarkdown()}
|
|
|
85
85
|
> \`Recommended track: <quick|medium|standard>\` because \`<one-line reason citing matched triggers>\`.
|
|
86
86
|
> Override? (A) keep \`<recommended>\` (B) switch track (C) cancel.
|
|
87
87
|
If the harness's native ask tool is available (\`AskUserQuestion\` / \`AskQuestion\` / \`question\` / \`request_user_input\`), send exactly ONE question; on schema error, fall back to a plain-text lettered list.
|
|
88
|
-
10.
|
|
89
|
-
|
|
88
|
+
10. Start the tracked flow only through the managed helper:
|
|
89
|
+
\`cclaw internal start-flow --track=<quick|medium|standard> --class=<class> --prompt=<prompt> --stack=<stack> --reason=<matched heuristic>\`
|
|
90
|
+
If this helper fails, STOP and report the exact command/output. Do **not** manually edit \`${flowPath}\`.
|
|
91
|
+
11. The helper persists \`${flowPath}\`, computes \`skippedStages\`, sets the first stage for the track, resets the gate catalog, and writes \`.cclaw/artifacts/00-idea.md\`.
|
|
90
92
|
12. Load the **first-stage skill for the chosen track** and its command file:
|
|
91
93
|
- quick → \`.cclaw/skills/specification-authoring/SKILL.md\`
|
|
92
94
|
- medium/standard → \`.cclaw/skills/brainstorming/SKILL.md\`
|
|
@@ -100,7 +102,7 @@ If during any stage the agent discovers evidence that contradicts the initial Ph
|
|
|
100
102
|
1. Surface the new evidence in plain text.
|
|
101
103
|
2. Propose the updated \`Class\` + \`Track\` with a one-line reason.
|
|
102
104
|
3. Use the Decision Protocol to let the user accept, override, or cancel.
|
|
103
|
-
4. On acceptance:
|
|
105
|
+
4. On acceptance: run \`cclaw internal start-flow --reclassify --track=<new-track> --class=<new-class> --reason=<why>\`. The helper appends a \`Reclassification:\` entry to \`00-idea.md\` and updates flow state atomically. If it fails, STOP and report the exact output; do NOT manually edit \`flow-state.json\`.
|
|
104
106
|
|
|
105
107
|
### Without prompt (\`/cc\`)
|
|
106
108
|
|
|
@@ -179,13 +181,12 @@ ${conversationLanguagePolicyMarkdown()}
|
|
|
179
181
|
|
|
180
182
|
- On conflict, prefer \`standard\` over \`medium\`, and \`medium\` over \`quick\`.
|
|
181
183
|
- Always state the recommendation as a one-line reason citing matched triggers.
|
|
182
|
-
8.
|
|
183
|
-
9.
|
|
184
|
-
10. Load and execute the **first stage skill of the chosen track** (\`brainstorming\` for medium/standard, \`specification-authoring\` for quick) plus its matching command file.
|
|
184
|
+
8. Run the managed start helper: \`cclaw internal start-flow --track=<quick|medium|standard> --class=<class> --prompt=<prompt> --stack=<stack> --reason=<matched heuristic>\`. The helper writes \`${flowPath}\`, computes \`skippedStages\`, resets the gate catalog, and writes \`${RUNTIME_ROOT}/artifacts/00-idea.md\`. If it fails, STOP and report the exact command/output; do not manually edit flow state.
|
|
185
|
+
9. Load and execute the **first stage skill of the chosen track** (\`brainstorming\` for medium/standard, \`specification-authoring\` for quick) plus its matching command file.
|
|
185
186
|
|
|
186
187
|
### Reclassification on discovery
|
|
187
188
|
|
|
188
|
-
If mid-stage evidence contradicts the initial Class/Track decision (the "trivial" change needs a migration, the "quick" bug fix needs architecture work, an origin doc multiplies scope), STOP and re-classify using the Decision Protocol.
|
|
189
|
+
If mid-stage evidence contradicts the initial Class/Track decision (the "trivial" change needs a migration, the "quick" bug fix needs architecture work, an origin doc multiplies scope), STOP and re-classify using the Decision Protocol. On acceptance, run \`cclaw internal start-flow --reclassify --track=<new-track> --class=<new-class> --reason=<why>\`; the helper records \`Reclassification:\` in \`00-idea.md\` and updates state atomically. Do NOT rewrite prior artifacts or manually edit flow-state.
|
|
189
190
|
|
|
190
191
|
### Path B: \`/cc\` (no arguments)
|
|
191
192
|
|
package/dist/doctor.js
CHANGED
|
@@ -73,6 +73,61 @@ function collectHookCommands(value) {
|
|
|
73
73
|
const nested = collectHookCommands(obj.hooks);
|
|
74
74
|
return [...direct, ...nested];
|
|
75
75
|
}
|
|
76
|
+
function extractGeneratedCliEntrypoints(scriptContent) {
|
|
77
|
+
const paths = [];
|
|
78
|
+
for (const match of scriptContent.matchAll(/const\s+CCLAW_CLI_ENTRYPOINT\s*=\s*("(?:\\.|[^"\\])*"|null);/gu)) {
|
|
79
|
+
const raw = match[1];
|
|
80
|
+
if (!raw || raw === "null")
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
if (typeof parsed === "string" && parsed.trim().length > 0) {
|
|
85
|
+
paths.push(parsed);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// malformed generated constant; treat below as missing/unusable
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return paths;
|
|
93
|
+
}
|
|
94
|
+
async function generatedCliEntrypointsOk(projectRoot) {
|
|
95
|
+
const hookScripts = ["stage-complete.mjs", "run-hook.mjs"];
|
|
96
|
+
const problems = [];
|
|
97
|
+
const checked = [];
|
|
98
|
+
for (const script of hookScripts) {
|
|
99
|
+
const scriptPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", script);
|
|
100
|
+
if (!(await exists(scriptPath)))
|
|
101
|
+
continue;
|
|
102
|
+
const content = await fs.readFile(scriptPath, "utf8");
|
|
103
|
+
const entrypoints = extractGeneratedCliEntrypoints(content);
|
|
104
|
+
if (entrypoints.length === 0) {
|
|
105
|
+
problems.push(`${RUNTIME_ROOT}/hooks/${script} has no local CLI entrypoint`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
for (const entrypoint of entrypoints) {
|
|
109
|
+
checked.push(`${RUNTIME_ROOT}/hooks/${script} -> ${entrypoint}`);
|
|
110
|
+
try {
|
|
111
|
+
const stat = await fs.stat(entrypoint);
|
|
112
|
+
if (!stat.isFile()) {
|
|
113
|
+
problems.push(`${RUNTIME_ROOT}/hooks/${script} points to non-file ${entrypoint}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
problems.push(`${RUNTIME_ROOT}/hooks/${script} points to missing ${entrypoint}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (problems.length > 0) {
|
|
122
|
+
return { ok: false, details: problems.join("; ") };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
details: checked.length > 0
|
|
127
|
+
? `local CLI entrypoints valid: ${checked.join("; ")}`
|
|
128
|
+
: "local CLI entrypoint check skipped because generated hook scripts are absent"
|
|
129
|
+
};
|
|
130
|
+
}
|
|
76
131
|
function extractUserPromptFromIdeaArtifact(markdown) {
|
|
77
132
|
const normalized = markdown.replace(/\r\n?/gu, "\n");
|
|
78
133
|
const heading = /^##\s+User prompt\s*$/imu.exec(normalized);
|
|
@@ -617,6 +672,12 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
617
672
|
});
|
|
618
673
|
}
|
|
619
674
|
}
|
|
675
|
+
const localCliEntrypoints = await generatedCliEntrypointsOk(projectRoot);
|
|
676
|
+
checks.push({
|
|
677
|
+
name: "hook:script:local_cli_entrypoint",
|
|
678
|
+
ok: localCliEntrypoints.ok,
|
|
679
|
+
details: localCliEntrypoints.details
|
|
680
|
+
});
|
|
620
681
|
// Hook JSON files per harness. OpenCode ships hooks through its plugin
|
|
621
682
|
// system (covered below). Codex joined the managed list in v0.40.0 — Codex
|
|
622
683
|
// CLI ≥ v0.114 consumes `.codex/hooks.json` behind the `codex_hooks`
|
|
@@ -934,7 +995,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
934
995
|
for (const candidate of windowsHookConfigCandidates) {
|
|
935
996
|
if (!(await exists(candidate)))
|
|
936
997
|
continue;
|
|
937
|
-
const content = await fs.readFile(candidate, "utf8");
|
|
998
|
+
const content = (await fs.readFile(candidate, "utf8")).replace(/\\/gu, "/");
|
|
938
999
|
if (/bash\s+\.cclaw\/hooks\/|\.cclaw\/hooks\/(?:session-start|stop-handoff|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh/u.test(content)) {
|
|
939
1000
|
legacyDispatchFiles.push(path.relative(projectRoot, candidate));
|
|
940
1001
|
}
|
package/dist/install.js
CHANGED
|
@@ -8,10 +8,10 @@ import { stageSchema } from "../content/stage-schema.js";
|
|
|
8
8
|
import { appendDelegation, checkMandatoryDelegations } from "../delegation.js";
|
|
9
9
|
import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../gate-evidence.js";
|
|
10
10
|
import { extractMarkdownSectionBody, parseLearningsSection } from "../artifact-linter.js";
|
|
11
|
-
import { getAvailableTransitions, getTransitionGuards, isFlowTrack } from "../flow-state.js";
|
|
11
|
+
import { getAvailableTransitions, getTransitionGuards, isFlowTrack, createInitialFlowState } from "../flow-state.js";
|
|
12
12
|
import { appendKnowledge } from "../knowledge-store.js";
|
|
13
13
|
import { readFlowState, writeFlowState } from "../runs.js";
|
|
14
|
-
import { FLOW_STAGES } from "../types.js";
|
|
14
|
+
import { FLOW_STAGES, TRACK_STAGES } from "../types.js";
|
|
15
15
|
import { runCompoundReadinessCommand } from "./compound-readiness.js";
|
|
16
16
|
import { runHookManifestCommand } from "./hook-manifest.js";
|
|
17
17
|
import { runEnvelopeValidateCommand } from "./envelope-validate.js";
|
|
@@ -507,6 +507,70 @@ function parseHookArgs(tokens) {
|
|
|
507
507
|
}
|
|
508
508
|
return { hookName: normalizedHook };
|
|
509
509
|
}
|
|
510
|
+
function parseStartFlowArgs(tokens) {
|
|
511
|
+
let track;
|
|
512
|
+
let className;
|
|
513
|
+
let prompt;
|
|
514
|
+
let reason;
|
|
515
|
+
let stack;
|
|
516
|
+
let forceReset = false;
|
|
517
|
+
let reclassify = false;
|
|
518
|
+
let quiet = false;
|
|
519
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
520
|
+
const token = tokens[i];
|
|
521
|
+
const nextToken = tokens[i + 1];
|
|
522
|
+
const readValue = (flag) => {
|
|
523
|
+
if (token.startsWith(`${flag}=`))
|
|
524
|
+
return token.slice(flag.length + 1);
|
|
525
|
+
if (token === flag && nextToken && !nextToken.startsWith("--")) {
|
|
526
|
+
i += 1;
|
|
527
|
+
return nextToken;
|
|
528
|
+
}
|
|
529
|
+
throw new Error(`${flag} requires a value.`);
|
|
530
|
+
};
|
|
531
|
+
if (token === "--quiet") {
|
|
532
|
+
quiet = true;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (token === "--force-reset") {
|
|
536
|
+
forceReset = true;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (token === "--reclassify") {
|
|
540
|
+
reclassify = true;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (token === "--track" || token.startsWith("--track=")) {
|
|
544
|
+
const raw = readValue("--track").trim();
|
|
545
|
+
if (!isFlowTrack(raw)) {
|
|
546
|
+
throw new Error(`--track must be one of: standard, medium, quick.`);
|
|
547
|
+
}
|
|
548
|
+
track = raw;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (token === "--class" || token.startsWith("--class=")) {
|
|
552
|
+
className = readValue("--class").trim();
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (token === "--prompt" || token.startsWith("--prompt=")) {
|
|
556
|
+
prompt = readValue("--prompt").trim();
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (token === "--reason" || token.startsWith("--reason=")) {
|
|
560
|
+
reason = readValue("--reason").trim();
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (token === "--stack" || token.startsWith("--stack=")) {
|
|
564
|
+
stack = readValue("--stack").trim();
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
throw new Error(`Unknown flag for internal start-flow: ${token}`);
|
|
568
|
+
}
|
|
569
|
+
if (!track) {
|
|
570
|
+
throw new Error("internal start-flow requires --track=<standard|medium|quick>.");
|
|
571
|
+
}
|
|
572
|
+
return { track, className, prompt, reason, stack, forceReset, reclassify, quiet };
|
|
573
|
+
}
|
|
510
574
|
async function buildValidationReport(projectRoot, flowState) {
|
|
511
575
|
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
|
|
512
576
|
const gates = await verifyCurrentStageGateEvidence(projectRoot, flowState);
|
|
@@ -827,6 +891,78 @@ async function runVerifyCurrentState(projectRoot, args, io) {
|
|
|
827
891
|
}
|
|
828
892
|
return validation.ok ? 0 : 1;
|
|
829
893
|
}
|
|
894
|
+
function firstIncompleteStageForTrack(track, completedStages) {
|
|
895
|
+
const completed = new Set(completedStages);
|
|
896
|
+
const stages = TRACK_STAGES[track];
|
|
897
|
+
return stages.find((stage) => !completed.has(stage)) ?? stages[stages.length - 1] ?? "brainstorm";
|
|
898
|
+
}
|
|
899
|
+
async function appendIdeaArtifact(projectRoot, args, previous) {
|
|
900
|
+
const artifactPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "00-idea.md");
|
|
901
|
+
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
902
|
+
const now = new Date().toISOString();
|
|
903
|
+
if (args.reclassify) {
|
|
904
|
+
const entry = [
|
|
905
|
+
"",
|
|
906
|
+
`Reclassification: ${now}`,
|
|
907
|
+
`- From: ${previous?.track ?? "unknown"}`,
|
|
908
|
+
`- To: ${args.track}`,
|
|
909
|
+
`- Class: ${args.className || "unspecified"}`,
|
|
910
|
+
`- Reason: ${args.reason || "unspecified"}`
|
|
911
|
+
].join("\n") + "\n";
|
|
912
|
+
await fs.appendFile(artifactPath, entry, "utf8");
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const body = [
|
|
916
|
+
"# Idea",
|
|
917
|
+
`Class: ${args.className || "unspecified"}`,
|
|
918
|
+
`Track: ${args.track}${args.reason ? ` (${args.reason})` : ""}`,
|
|
919
|
+
`Stack: ${args.stack || "unknown"}`,
|
|
920
|
+
"",
|
|
921
|
+
"## User prompt",
|
|
922
|
+
args.prompt || "(not provided)",
|
|
923
|
+
"",
|
|
924
|
+
"## Discovered context",
|
|
925
|
+
"- None recorded by managed start-flow."
|
|
926
|
+
].join("\n") + "\n";
|
|
927
|
+
await fs.writeFile(artifactPath, body, "utf8");
|
|
928
|
+
}
|
|
929
|
+
async function runStartFlow(projectRoot, args, io) {
|
|
930
|
+
const current = await readFlowState(projectRoot);
|
|
931
|
+
const hasProgress = current.completedStages.length > 0;
|
|
932
|
+
if (!args.reclassify && hasProgress && !args.forceReset) {
|
|
933
|
+
io.stderr.write("cclaw internal start-flow: refusing to reset an active flow with completed stages without --force-reset. Ask the user before resetting.\n");
|
|
934
|
+
return 1;
|
|
935
|
+
}
|
|
936
|
+
let nextState;
|
|
937
|
+
if (args.reclassify) {
|
|
938
|
+
const completedInNewTrack = current.completedStages.filter((stage) => TRACK_STAGES[args.track].includes(stage));
|
|
939
|
+
const fresh = createInitialFlowState({ activeRunId: current.activeRunId, track: args.track });
|
|
940
|
+
nextState = {
|
|
941
|
+
...fresh,
|
|
942
|
+
completedStages: completedInNewTrack,
|
|
943
|
+
currentStage: firstIncompleteStageForTrack(args.track, completedInNewTrack),
|
|
944
|
+
rewinds: current.rewinds,
|
|
945
|
+
staleStages: current.staleStages
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
nextState = createInitialFlowState({ track: args.track });
|
|
950
|
+
}
|
|
951
|
+
await writeFlowState(projectRoot, nextState, { allowReset: true });
|
|
952
|
+
await appendIdeaArtifact(projectRoot, args, current);
|
|
953
|
+
if (!args.quiet) {
|
|
954
|
+
io.stdout.write(`${JSON.stringify({
|
|
955
|
+
ok: true,
|
|
956
|
+
command: "start-flow",
|
|
957
|
+
reclassify: args.reclassify,
|
|
958
|
+
track: nextState.track,
|
|
959
|
+
currentStage: nextState.currentStage,
|
|
960
|
+
skippedStages: nextState.skippedStages,
|
|
961
|
+
activeRunId: nextState.activeRunId
|
|
962
|
+
}, null, 2)}\n`);
|
|
963
|
+
}
|
|
964
|
+
return 0;
|
|
965
|
+
}
|
|
830
966
|
async function runHookCommand(projectRoot, args, io) {
|
|
831
967
|
const runHookPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "run-hook.mjs");
|
|
832
968
|
try {
|
|
@@ -865,13 +1001,16 @@ async function runHookCommand(projectRoot, args, io) {
|
|
|
865
1001
|
export async function runInternalCommand(projectRoot, argv, io) {
|
|
866
1002
|
const [subcommand, ...tokens] = argv;
|
|
867
1003
|
if (!subcommand) {
|
|
868
|
-
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n");
|
|
1004
|
+
io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n");
|
|
869
1005
|
return 1;
|
|
870
1006
|
}
|
|
871
1007
|
try {
|
|
872
1008
|
if (subcommand === "advance-stage") {
|
|
873
1009
|
return await runAdvanceStage(projectRoot, parseAdvanceStageArgs(tokens), io);
|
|
874
1010
|
}
|
|
1011
|
+
if (subcommand === "start-flow") {
|
|
1012
|
+
return await runStartFlow(projectRoot, parseStartFlowArgs(tokens), io);
|
|
1013
|
+
}
|
|
875
1014
|
if (subcommand === "verify-flow-state-diff") {
|
|
876
1015
|
return await runVerifyFlowStateDiff(projectRoot, parseVerifyFlowStateDiffArgs(tokens), io);
|
|
877
1016
|
}
|
|
@@ -896,7 +1035,7 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
896
1035
|
if (subcommand === "hook") {
|
|
897
1036
|
return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
|
|
898
1037
|
}
|
|
899
|
-
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n`);
|
|
1038
|
+
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n`);
|
|
900
1039
|
return 1;
|
|
901
1040
|
}
|
|
902
1041
|
catch (err) {
|