cclaw-cli 0.51.3 → 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.
@@ -1633,25 +1633,34 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
1633
1633
  // IDs all fail the lint; absence of the section remains advisory so
1634
1634
  // scope stays optional for small/quick tracks.
1635
1635
  if (headingPresent(sections, "Locked Decisions (D-XX)")) {
1636
- const decisionIds = extractDecisionIds(lockedDecisionsBody);
1637
- const bulletLines = lockedDecisionsBody
1636
+ const listDecisionLines = lockedDecisionsBody
1638
1637
  .split(/\r?\n/u)
1639
1638
  .map((line) => line.trim())
1640
- .filter((line) => /^(?:[-*]|\|)\s+\S/u.test(line));
1641
- const orphanBullets = bulletLines.filter((line) => !/\bD-\d+\b/u.test(line));
1639
+ .filter((line) => /^[-*]\s+\S/u.test(line));
1640
+ const tableDecisionRows = getMarkdownTableRows(lockedDecisionsBody);
1641
+ const tableDecisionLines = tableDecisionRows.map((row) => row.join(" | "));
1642
+ const decisionLines = [...listDecisionLines, ...tableDecisionLines];
1643
+ const orphanDecisionLines = decisionLines.filter((line) => !/\bD-\d+\b/u.test(line));
1644
+ const rowDecisionIds = [
1645
+ ...listDecisionLines.map((line) => /\bD-\d+\b/u.exec(line)?.[0]),
1646
+ ...tableDecisionRows.map((row) => /\bD-\d+\b/u.exec(row[0] ?? "")?.[0])
1647
+ ].filter((id) => typeof id === "string");
1642
1648
  const duplicateIds = (() => {
1643
- const all = lockedDecisionsBody.match(/\bD-\d+\b/gu) ?? [];
1644
1649
  const counts = new Map();
1645
- for (const id of all)
1650
+ for (const id of rowDecisionIds)
1646
1651
  counts.set(id, (counts.get(id) ?? 0) + 1);
1647
1652
  return [...counts.entries()].filter(([, n]) => n > 1).map(([id]) => id);
1648
1653
  })();
1649
1654
  const issues = [];
1650
- if (decisionIds.length === 0 && bulletLines.length === 0) {
1655
+ if (rowDecisionIds.length === 0 && decisionLines.length === 0) {
1651
1656
  issues.push("section is empty");
1652
1657
  }
1653
- if (orphanBullets.length > 0) {
1654
- issues.push(`${orphanBullets.length} bullet(s) missing a D-XX ID`);
1658
+ if (orphanDecisionLines.length > 0) {
1659
+ const examples = orphanDecisionLines
1660
+ .slice(0, 3)
1661
+ .map((line) => `\`${line.slice(0, 120)}\``)
1662
+ .join(", ");
1663
+ issues.push(`${orphanDecisionLines.length} decision row(s) missing a D-XX ID${examples.length > 0 ? `: ${examples}` : ""}`);
1655
1664
  }
1656
1665
  if (duplicateIds.length > 0) {
1657
1666
  issues.push(`duplicate IDs: ${duplicateIds.join(", ")}`);
@@ -1662,7 +1671,7 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
1662
1671
  rule: "Locked Decisions section must list each decision with a unique stable D-XX ID.",
1663
1672
  found: issues.length === 0,
1664
1673
  details: issues.length === 0
1665
- ? `${decisionIds.length} decision ID(s) recorded with no duplicates.`
1674
+ ? `${rowDecisionIds.length} decision ID(s) recorded with no duplicates.`
1666
1675
  : issues.join("; ")
1667
1676
  });
1668
1677
  }
@@ -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
+ \`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: 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 \`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. 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: \`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. 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 \`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
@@ -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",
@@ -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.3",
3
+ "version": "0.51.5",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {