cclaw-cli 0.51.4 → 0.51.6
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/hooks.d.ts +1 -0
- package/dist/content/hooks.js +122 -0
- 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 +63 -1
- package/dist/install.js +3 -2
- package/dist/internal/advance-stage.js +143 -4
- package/package.json +1 -1
package/dist/content/hooks.d.ts
CHANGED
package/dist/content/hooks.js
CHANGED
|
@@ -16,6 +16,128 @@ function resolveCliEntrypointForGeneratedHook() {
|
|
|
16
16
|
}
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
|
+
function internalHelperScript(helperName, internalSubcommand, usage) {
|
|
20
|
+
const cliEntrypoint = resolveCliEntrypointForGeneratedHook();
|
|
21
|
+
return `#!/usr/bin/env node
|
|
22
|
+
import fs from "node:fs/promises";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import process from "node:process";
|
|
25
|
+
import { spawn } from "node:child_process";
|
|
26
|
+
|
|
27
|
+
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
28
|
+
const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliEntrypoint)};
|
|
29
|
+
const HELPER_NAME = ${JSON.stringify(helperName)};
|
|
30
|
+
const INTERNAL_SUBCOMMAND = ${JSON.stringify(internalSubcommand)};
|
|
31
|
+
const USAGE = ${JSON.stringify(usage)};
|
|
32
|
+
|
|
33
|
+
async function detectRoot() {
|
|
34
|
+
const candidates = [
|
|
35
|
+
process.env.CCLAW_PROJECT_ROOT,
|
|
36
|
+
process.env.CLAUDE_PROJECT_DIR,
|
|
37
|
+
process.env.CURSOR_PROJECT_DIR,
|
|
38
|
+
process.env.CURSOR_PROJECT_ROOT,
|
|
39
|
+
process.env.OPENCODE_PROJECT_DIR,
|
|
40
|
+
process.env.OPENCODE_PROJECT_ROOT,
|
|
41
|
+
process.cwd()
|
|
42
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
43
|
+
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
try {
|
|
46
|
+
const runtimePath = path.join(candidate, RUNTIME_ROOT);
|
|
47
|
+
const stat = await fs.stat(runtimePath);
|
|
48
|
+
if (stat.isDirectory()) return candidate;
|
|
49
|
+
} catch {
|
|
50
|
+
// continue
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return candidates[0] || process.cwd();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printUsage() {
|
|
57
|
+
process.stderr.write(USAGE + "\\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const [, , ...flags] = process.argv;
|
|
62
|
+
if (flags.includes("--help") || flags.includes("-h")) {
|
|
63
|
+
printUsage();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const root = await detectRoot();
|
|
68
|
+
const runtimePath = path.join(root, RUNTIME_ROOT);
|
|
69
|
+
try {
|
|
70
|
+
const stat = await fs.stat(runtimePath);
|
|
71
|
+
if (!stat.isDirectory()) throw new Error("not-dir");
|
|
72
|
+
} catch {
|
|
73
|
+
process.stderr.write("[cclaw] " + HELPER_NAME + ": runtime root not found at " + runtimePath + "\\n");
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const cliEntrypoint = process.env.CCLAW_CLI_JS || CCLAW_CLI_ENTRYPOINT;
|
|
79
|
+
if (!cliEntrypoint || cliEntrypoint.trim().length === 0) {
|
|
80
|
+
process.stderr.write(
|
|
81
|
+
"[cclaw] " + HELPER_NAME + ": local Node runtime entrypoint is missing. Re-run npx cclaw-cli sync or npx cclaw-cli upgrade to regenerate hooks.\\n"
|
|
82
|
+
);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const stat = await fs.stat(cliEntrypoint);
|
|
89
|
+
if (!stat.isFile()) throw new Error("not-file");
|
|
90
|
+
} catch {
|
|
91
|
+
process.stderr.write(
|
|
92
|
+
"[cclaw] " + HELPER_NAME + ": local Node runtime entrypoint not found at " + cliEntrypoint + ". Re-run npx cclaw-cli sync or npx cclaw-cli upgrade to regenerate hooks.\\n"
|
|
93
|
+
);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const child = spawn(process.execPath, [cliEntrypoint, "internal", INTERNAL_SUBCOMMAND, ...flags], {
|
|
99
|
+
cwd: root,
|
|
100
|
+
env: process.env,
|
|
101
|
+
stdio: "inherit"
|
|
102
|
+
});
|
|
103
|
+
let spawnErrored = false;
|
|
104
|
+
|
|
105
|
+
child.on("error", (error) => {
|
|
106
|
+
spawnErrored = true;
|
|
107
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
108
|
+
if (code === "ENOENT") {
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
"[cclaw] " + HELPER_NAME + ": node executable not found while invoking local runtime. Re-run npx cclaw-cli doctor.\\n"
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
"[cclaw] " + HELPER_NAME + ": failed to invoke local Node runtime (" +
|
|
115
|
+
(error instanceof Error ? error.message : String(error)) +
|
|
116
|
+
").\\n"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
child.on("close", (code, signal) => {
|
|
123
|
+
if (spawnErrored) {
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (signal) {
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
process.exitCode = typeof code === "number" && code >= 0 ? code : 1;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void main();
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
export function startFlowScript() {
|
|
139
|
+
return internalHelperScript("start-flow", "start-flow", "Usage: node " + RUNTIME_ROOT + "/hooks/start-flow.mjs --track=<standard|medium|quick> [--class=...] [--prompt=...] [--stack=...] [--reason=...] [--reclassify] [--force-reset]");
|
|
140
|
+
}
|
|
19
141
|
export function stageCompleteScript() {
|
|
20
142
|
const cliEntrypoint = resolveCliEntrypointForGeneratedHook();
|
|
21
143
|
return `#!/usr/bin/env node
|
|
@@ -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
|
+
\`node .cclaw/hooks/start-flow.mjs --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 \`node .cclaw/hooks/start-flow.mjs --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: \`node .cclaw/hooks/start-flow.mjs --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 \`node .cclaw/hooks/start-flow.mjs --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", "start-flow.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);
|
|
@@ -589,6 +644,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
589
644
|
"run-hook.mjs",
|
|
590
645
|
"run-hook.cmd",
|
|
591
646
|
"stage-complete.mjs",
|
|
647
|
+
"start-flow.mjs",
|
|
592
648
|
"opencode-plugin.mjs"
|
|
593
649
|
]) {
|
|
594
650
|
const scriptPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", script);
|
|
@@ -617,6 +673,12 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
617
673
|
});
|
|
618
674
|
}
|
|
619
675
|
}
|
|
676
|
+
const localCliEntrypoints = await generatedCliEntrypointsOk(projectRoot);
|
|
677
|
+
checks.push({
|
|
678
|
+
name: "hook:script:local_cli_entrypoint",
|
|
679
|
+
ok: localCliEntrypoints.ok,
|
|
680
|
+
details: localCliEntrypoints.details
|
|
681
|
+
});
|
|
620
682
|
// Hook JSON files per harness. OpenCode ships hooks through its plugin
|
|
621
683
|
// system (covered below). Codex joined the managed list in v0.40.0 — Codex
|
|
622
684
|
// CLI ≥ v0.114 consumes `.codex/hooks.json` behind the `codex_hooks`
|
|
@@ -934,7 +996,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
934
996
|
for (const candidate of windowsHookConfigCandidates) {
|
|
935
997
|
if (!(await exists(candidate)))
|
|
936
998
|
continue;
|
|
937
|
-
const content = await fs.readFile(candidate, "utf8");
|
|
999
|
+
const content = (await fs.readFile(candidate, "utf8")).replace(/\\/gu, "/");
|
|
938
1000
|
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
1001
|
legacyDispatchFiles.push(path.relative(projectRoot, candidate));
|
|
940
1002
|
}
|
package/dist/install.js
CHANGED
|
@@ -12,7 +12,7 @@ import { viewCommandContract, viewCommandSkillMarkdown } from "./content/view-co
|
|
|
12
12
|
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
13
13
|
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
14
14
|
import { ironLawRuntimeDocument, ironLawsSkillMarkdown } from "./content/iron-laws.js";
|
|
15
|
-
import { stageCompleteScript, runHookCmdScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
15
|
+
import { stageCompleteScript, startFlowScript, runHookCmdScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
16
16
|
import { nodeHookRuntimeScript } from "./content/node-hooks.js";
|
|
17
17
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
18
18
|
import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
|
|
@@ -128,7 +128,6 @@ const DEPRECATED_HOOK_FILES = [
|
|
|
128
128
|
"_lib.sh",
|
|
129
129
|
"session-start.sh",
|
|
130
130
|
"stop-checkpoint.sh",
|
|
131
|
-
"run-hook.cmd",
|
|
132
131
|
"stage-complete.sh",
|
|
133
132
|
"pre-compact.sh",
|
|
134
133
|
"prompt-guard.sh",
|
|
@@ -706,6 +705,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
706
705
|
strictLaws: config.ironLaws?.strictLaws
|
|
707
706
|
}), null, 2)}\n`);
|
|
708
707
|
await writeFileSafe(path.join(hooksDir, "stage-complete.mjs"), stageCompleteScript());
|
|
708
|
+
await writeFileSafe(path.join(hooksDir, "start-flow.mjs"), startFlowScript());
|
|
709
709
|
await writeFileSafe(path.join(hooksDir, "run-hook.mjs"), nodeHookRuntimeScript({
|
|
710
710
|
strictness: effectiveStrictness,
|
|
711
711
|
tddTestPathPatterns: config.tdd?.testPathPatterns ?? config.tddTestGlobs,
|
|
@@ -718,6 +718,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
718
718
|
try {
|
|
719
719
|
for (const script of [
|
|
720
720
|
"stage-complete.mjs",
|
|
721
|
+
"start-flow.mjs",
|
|
721
722
|
"run-hook.mjs",
|
|
722
723
|
"run-hook.cmd",
|
|
723
724
|
"opencode-plugin.mjs"
|
|
@@ -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) {
|