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.
@@ -1,3 +1,4 @@
1
+ export declare function startFlowScript(): string;
1
2
  export declare function stageCompleteScript(): string;
2
3
  export declare function runHookCmdScript(): string;
3
4
  export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
@@ -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" || (isWindows && code === "EINVAL")
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" || (isWindows && code === "EINVAL")
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
- emitAdvisoryContext(
1566
- runtime,
1567
- "verify-current-state",
1568
- "Cclaw verify-current-state requires cclaw binary on PATH."
1569
- );
1570
- process.stderr.write("[cclaw] hook: cclaw binary is required for verify-current-state\\n");
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
- // session.compacted must run pre-compact BEFORE refreshing the bootstrap
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
- // Avoid writing directly to stdout in lifecycle hooks because it can
581
- // interfere with OpenCode TUI rendering. Bootstrap is injected via
582
- // the system transform hook instead.
583
- // session.updated covers config reloads and artifact/rules edits
584
- // that happen mid-session; without it the cache would stay stale
585
- // until the next compaction or restart.
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. Persist the chosen track to \`${flowPath}\` (\`track\` field). Compute \`skippedStages\` from the track and write that too. Use the **first stage of the chosen track** as \`currentStage\` (quick → \`spec\`, medium/standard → \`brainstorm\`, trivial fast-path → \`design\` or \`spec\` per Phase 0).
89
- 11. Write the prompt to \`.cclaw/artifacts/00-idea.md\` with the following header lines: \`Class:\` (from Phase 0), \`Track:\` (chosen track + matched heuristic), \`Stack:\` (from Phase 2 detection, or \`unknown\`), and a \`Discovered context\` section if Phase 1/seed recall found references.
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: update \`00-idea.md\` with a \`Reclassification:\` entry (old new, reason, ISO timestamp) and update \`flow-state.json\` accordingly do NOT rewrite prior artifacts, they stay as history.
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. Persist the chosen track in \`${flowPath}\` (\`track\` + \`skippedStages\`). Set \`currentStage\` to the first stage of the chosen track (\`quick\` \`spec\`, \`medium\`/ \`standard\` \`brainstorm\`, trivial fast-path \`design\` or \`spec\`). Reset gate catalog.
183
- 9. Write \`${RUNTIME_ROOT}/artifacts/00-idea.md\` with the user's prompt plus header lines: \`Class:\`, \`Track:\`, \`Stack:\`, and a \`Discovered context\` section from Phase 0.5/1.
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. Record \`Reclassification:\` in \`00-idea.md\` with old/new class and a one-line reason. Do NOT rewrite prior artifacts they stay as history.
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.4",
3
+ "version": "0.51.6",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {