cclaw-cli 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/artifact-linter/brainstorm.js +15 -1
  2. package/dist/artifact-linter/design.js +14 -0
  3. package/dist/artifact-linter/scope.js +14 -0
  4. package/dist/artifact-linter/shared.d.ts +1 -0
  5. package/dist/artifact-linter/shared.js +32 -0
  6. package/dist/artifact-linter.js +11 -1
  7. package/dist/content/hook-events.js +1 -2
  8. package/dist/content/hook-manifest.d.ts +3 -4
  9. package/dist/content/hook-manifest.js +22 -25
  10. package/dist/content/hooks.js +54 -14
  11. package/dist/content/meta-skill.js +4 -3
  12. package/dist/content/node-hooks.js +259 -89
  13. package/dist/content/observe.js +3 -3
  14. package/dist/content/opencode-plugin.js +0 -6
  15. package/dist/content/skills-elicitation.d.ts +1 -0
  16. package/dist/content/skills-elicitation.js +123 -0
  17. package/dist/content/skills.js +6 -4
  18. package/dist/content/stages/brainstorm.js +7 -3
  19. package/dist/content/stages/design.js +4 -0
  20. package/dist/content/stages/scope.js +6 -2
  21. package/dist/content/start-command.js +4 -4
  22. package/dist/content/templates.js +21 -0
  23. package/dist/flow-state.d.ts +7 -0
  24. package/dist/flow-state.js +1 -0
  25. package/dist/hook-schemas/claude-hooks.v1.json +2 -3
  26. package/dist/hook-schemas/codex-hooks.v1.json +1 -1
  27. package/dist/hook-schemas/cursor-hooks.v1.json +1 -1
  28. package/dist/install.js +12 -3
  29. package/dist/internal/advance-stage/advance.js +22 -1
  30. package/dist/internal/advance-stage/parsers.d.ts +1 -0
  31. package/dist/internal/advance-stage/parsers.js +6 -0
  32. package/dist/run-persistence.d.ts +1 -1
  33. package/dist/run-persistence.js +29 -2
  34. package/dist/runtime/run-hook.mjs +259 -89
  35. package/dist/track-heuristics.d.ts +7 -1
  36. package/dist/track-heuristics.js +12 -0
  37. package/package.json +1 -1
@@ -7715,6 +7715,10 @@ const EARLY_LOOP_ENABLED = ${JSON.stringify(earlyLoopEnabled)};
7715
7715
  const EARLY_LOOP_MAX_ITERATIONS = ${JSON.stringify(earlyLoopMaxIterations)};
7716
7716
  const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
7717
7717
  const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
7718
+ const SESSION_DIGEST_SCHEMA_VERSION = 1;
7719
+ const SESSION_DIGEST_CACHE_FILE = "session-digest.json";
7720
+ const SESSION_DIGEST_REFRESH_MARKER_FILE = "session-digest.refresh.json";
7721
+ const SESSION_DIGEST_REFRESH_STALE_MS = 30000;
7718
7722
 
7719
7723
  ${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
7720
7724
  ${SHARED_STAGE_SUPPORT_SNIPPETS}
@@ -8140,9 +8144,10 @@ function hookEventNameForOutput(hookName) {
8140
8144
  if (hookName === "session-start") return "SessionStart";
8141
8145
  if (hookName === "prompt-guard") return "PreToolUse";
8142
8146
  if (hookName === "workflow-guard") return "PreToolUse";
8147
+ if (hookName === "pre-tool-pipeline") return "PreToolUse";
8148
+ if (hookName === "prompt-pipeline") return "UserPromptSubmit";
8143
8149
  if (hookName === "context-monitor") return "PostToolUse";
8144
8150
  if (hookName === "stop-handoff") return "Stop";
8145
- if (hookName === "pre-compact") return "PreCompact";
8146
8151
  if (hookName === "verify-current-state") return "UserPromptSubmit";
8147
8152
  return "SessionStart";
8148
8153
  }
@@ -8551,78 +8556,55 @@ async function readFlowState(root) {
8551
8556
  };
8552
8557
  }
8553
8558
 
8554
-
8555
- async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
8556
- const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
8557
- // Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
8558
- // Falls back to a local read if nothing is passed in.
8559
- const raw = typeof prereadRaw === "string"
8560
- ? prereadRaw
8561
- : await readTextFile(knowledgeFile, "");
8562
- const digest = parseKnowledgeDigest(raw, currentStage, 6);
8563
- return {
8564
- digestLines: digest.lines,
8565
- learningsCount: digest.learningsCount
8566
- };
8559
+ async function readFileMtimeMs(filePath) {
8560
+ try {
8561
+ const stat = await fs.stat(filePath);
8562
+ if (!stat.isFile()) return 0;
8563
+ return Math.trunc(stat.mtimeMs);
8564
+ } catch {
8565
+ return 0;
8566
+ }
8567
8567
  }
8568
8568
 
8569
- async function readStageSupportContext(root, currentStage) {
8570
- if (!isKnownStageId(currentStage)) return [];
8571
- const stage = currentStage;
8572
-
8573
- const parts = [];
8574
- const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
8575
- const contract = (await readTextFile(contractPath, "")).trim();
8576
- if (contract.length > 0) {
8577
- parts.push(
8578
- "Current stage state contract (read before drafting or editing the stage artifact):\\n" +
8579
- contract
8580
- );
8581
- }
8569
+ function parseNumericMs(value) {
8570
+ return typeof value === "number" && Number.isFinite(value)
8571
+ ? Math.trunc(value)
8572
+ : -1;
8573
+ }
8582
8574
 
8583
- const promptName = reviewPromptFileName(stage);
8584
- if (typeof promptName === "string") {
8585
- const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
8586
- const prompt = (await readTextFile(promptPath, "")).trim();
8587
- if (prompt.length > 0) {
8588
- parts.push(
8589
- "Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
8590
- prompt
8591
- );
8592
- }
8575
+ async function readSessionDigestLines(stateDir, state, flowStateMtimeMs) {
8576
+ const cachePath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
8577
+ const cache = toObject(await readJsonFile(cachePath, {})) || {};
8578
+ const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
8579
+ const sameStage = typeof cache.currentStage === "string" ? cache.currentStage === state.currentStage : true;
8580
+ const sameRun = typeof cache.activeRunId === "string" ? cache.activeRunId === state.activeRunId : true;
8581
+ const fresh = cachedMtimeMs === flowStateMtimeMs && sameStage && sameRun;
8582
+ if (!fresh) {
8583
+ return {
8584
+ ralphLoopLine: "",
8585
+ earlyLoopLine: "",
8586
+ compoundReadinessLine: "",
8587
+ fresh: false
8588
+ };
8593
8589
  }
8594
-
8595
- return parts;
8590
+ return {
8591
+ ralphLoopLine: typeof cache.ralphLoopLine === "string" ? cache.ralphLoopLine : "",
8592
+ earlyLoopLine: typeof cache.earlyLoopLine === "string" ? cache.earlyLoopLine : "",
8593
+ compoundReadinessLine: typeof cache.compoundReadinessLine === "string" ? cache.compoundReadinessLine : "",
8594
+ fresh: true
8595
+ };
8596
8596
  }
8597
8597
 
8598
- async function handleSessionStart(runtime) {
8599
- const state = await readFlowState(runtime.root);
8600
- const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
8601
- const ironLawsFile = path.join(stateDir, "iron-laws.json");
8602
- const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
8603
-
8604
-
8605
- // Read knowledge.jsonl exactly once per session-start while holding the
8606
- // SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
8607
- // see a partial (mid-write) snapshot. Both the digest and
8608
- // compound-readiness derive from this single read.
8609
- const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
8610
- const knowledgeRaw = await readTextFileLocked(
8611
- knowledgeLockPathInline(runtime.root),
8612
- knowledgeFilePath,
8613
- ""
8614
- );
8615
- const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
8616
-
8617
- // Refresh Ralph Loop status each session-start so /cc and the model
8618
- // both read a consistent "iter=N, acClosed=[...]" snapshot. Runs only when
8619
- // we are in tdd \u2014 other stages skip the write to keep the file stable.
8598
+ async function refreshSessionDigestCache(root, state, flowStateMtimeMs) {
8599
+ const stateDir = path.join(root, RUNTIME_ROOT, "state");
8620
8600
  let ralphLoopLine = "";
8621
8601
  let earlyLoopLine = "";
8602
+ let compoundReadinessLine = "";
8603
+
8622
8604
  if (state.currentStage === "tdd") {
8623
8605
  try {
8624
8606
  const internalRalph = await runCclawInternal(
8625
- runtime.root,
8607
+ root,
8626
8608
  ["tdd-loop-status", "--json", "--write"],
8627
8609
  { captureStdout: true }
8628
8610
  );
@@ -8635,12 +8617,8 @@ async function handleSessionStart(runtime) {
8635
8617
  }
8636
8618
  ralphLoopLine = formatRalphLoopStatusLineFromJson(ralphStatus);
8637
8619
  } catch (err) {
8638
- // Best-effort \u2014 a malformed cycle log should never break
8639
- // session-start. But we DO leave a breadcrumb in
8640
- // hook-errors.jsonl so \`npx cclaw-cli sync\` can surface chronic
8641
- // failures (previously this was a silent swallow).
8642
8620
  await recordHookError(
8643
- runtime.root,
8621
+ root,
8644
8622
  "session-start:ralph-loop",
8645
8623
  err instanceof Error ? err.message : String(err)
8646
8624
  );
@@ -8652,7 +8630,7 @@ async function handleSessionStart(runtime) {
8652
8630
  ) {
8653
8631
  try {
8654
8632
  const internalEarly = await runCclawInternal(
8655
- runtime.root,
8633
+ root,
8656
8634
  [
8657
8635
  "early-loop-status",
8658
8636
  "--json",
@@ -8674,21 +8652,17 @@ async function handleSessionStart(runtime) {
8674
8652
  earlyLoopLine = formatEarlyLoopStatusLineFromJson(earlyLoopStatus);
8675
8653
  } catch (err) {
8676
8654
  await recordHookError(
8677
- runtime.root,
8655
+ root,
8678
8656
  "session-start:early-loop",
8679
8657
  err instanceof Error ? err.message : String(err)
8680
8658
  );
8681
8659
  }
8682
8660
  }
8683
8661
 
8684
- // Keep compound-readiness.json fresh on every session-start (cheap derived
8685
- // summary). Surface a one-line nudge only from review and ship stages
8686
- // where lifting becomes relevant; earlier stages update the file silently.
8687
- let compoundReadinessLine = "";
8688
8662
  try {
8689
8663
  const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
8690
8664
  const internalReadiness = await runCclawInternal(
8691
- runtime.root,
8665
+ root,
8692
8666
  shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
8693
8667
  { captureStdout: true }
8694
8668
  );
@@ -8699,17 +8673,169 @@ async function handleSessionStart(runtime) {
8699
8673
  compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
8700
8674
  }
8701
8675
  } catch (err) {
8702
- // Best-effort \u2014 a malformed knowledge.jsonl must never break
8703
- // session-start. But we DO leave a breadcrumb in
8704
- // hook-errors.jsonl so config/IO problems become visible in
8705
- // \`npx cclaw-cli sync\` instead of silently degrading readiness output.
8706
8676
  await recordHookError(
8707
- runtime.root,
8677
+ root,
8708
8678
  "session-start:compound-readiness",
8709
8679
  err instanceof Error ? err.message : String(err)
8710
8680
  );
8711
8681
  }
8712
8682
 
8683
+ const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
8684
+ await writeJsonFile(digestPath, {
8685
+ schemaVersion: SESSION_DIGEST_SCHEMA_VERSION,
8686
+ generatedAt: new Date().toISOString(),
8687
+ flowStateMtimeMs,
8688
+ currentStage: state.currentStage,
8689
+ activeRunId: state.activeRunId,
8690
+ ralphLoopLine,
8691
+ earlyLoopLine,
8692
+ compoundReadinessLine
8693
+ });
8694
+ }
8695
+
8696
+ async function scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs) {
8697
+ if (flowStateMtimeMs <= 0) return;
8698
+ const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
8699
+ const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
8700
+ const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
8701
+
8702
+ const cache = toObject(await readJsonFile(digestPath, {})) || {};
8703
+ const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
8704
+ if (cachedMtimeMs === flowStateMtimeMs) return;
8705
+
8706
+ const marker = toObject(await readJsonFile(markerPath, {})) || {};
8707
+ const markerMtimeMs = parseNumericMs(marker.flowStateMtimeMs);
8708
+ const markerStartedAtMs = parseNumericMs(marker.startedAtMs);
8709
+ const markerFresh =
8710
+ markerMtimeMs === flowStateMtimeMs &&
8711
+ markerStartedAtMs > 0 &&
8712
+ Date.now() - markerStartedAtMs < SESSION_DIGEST_REFRESH_STALE_MS;
8713
+ if (markerFresh) return;
8714
+
8715
+ await writeJsonFile(markerPath, {
8716
+ flowStateMtimeMs,
8717
+ startedAtMs: Date.now(),
8718
+ currentStage: state.currentStage,
8719
+ activeRunId: state.activeRunId
8720
+ });
8721
+
8722
+ try {
8723
+ const child = spawn(process.execPath, [process.argv[1], "session-start-refresh"], {
8724
+ cwd: runtime.root,
8725
+ stdio: "ignore",
8726
+ windowsHide: true,
8727
+ detached: true,
8728
+ env: {
8729
+ ...process.env,
8730
+ CCLAW_PROJECT_ROOT: runtime.root,
8731
+ CCLAW_BG_WORKER: "1"
8732
+ }
8733
+ });
8734
+ child.unref();
8735
+ } catch (err) {
8736
+ await fs.rm(markerPath, { force: true }).catch(() => undefined);
8737
+ await recordHookError(
8738
+ runtime.root,
8739
+ "session-start:spawn-refresh",
8740
+ err instanceof Error ? err.message : String(err)
8741
+ );
8742
+ }
8743
+ }
8744
+
8745
+ async function handleSessionStartRefresh(runtime) {
8746
+ const state = await readFlowState(runtime.root);
8747
+ const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
8748
+ const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
8749
+ try {
8750
+ const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
8751
+ await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
8752
+ } finally {
8753
+ await fs.rm(markerPath, { force: true }).catch(() => undefined);
8754
+ }
8755
+ return 0;
8756
+ }
8757
+
8758
+
8759
+ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
8760
+ const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
8761
+ // Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
8762
+ // Falls back to a local read if nothing is passed in.
8763
+ const raw = typeof prereadRaw === "string"
8764
+ ? prereadRaw
8765
+ : await readTextFile(knowledgeFile, "");
8766
+ const digest = parseKnowledgeDigest(raw, currentStage, 6);
8767
+ return {
8768
+ digestLines: digest.lines,
8769
+ learningsCount: digest.learningsCount
8770
+ };
8771
+ }
8772
+
8773
+ async function readStageSupportContext(root, currentStage) {
8774
+ if (!isKnownStageId(currentStage)) return [];
8775
+ const stage = currentStage;
8776
+
8777
+ const parts = [];
8778
+ const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
8779
+ const contract = (await readTextFile(contractPath, "")).trim();
8780
+ if (contract.length > 0) {
8781
+ parts.push(
8782
+ "Current stage state contract (read before drafting or editing the stage artifact):\\n" +
8783
+ contract
8784
+ );
8785
+ }
8786
+
8787
+ const promptName = reviewPromptFileName(stage);
8788
+ if (typeof promptName === "string") {
8789
+ const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
8790
+ const prompt = (await readTextFile(promptPath, "")).trim();
8791
+ if (prompt.length > 0) {
8792
+ parts.push(
8793
+ "Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
8794
+ prompt
8795
+ );
8796
+ }
8797
+ }
8798
+
8799
+ return parts;
8800
+ }
8801
+
8802
+ async function handleSessionStart(runtime) {
8803
+ const state = await readFlowState(runtime.root);
8804
+ const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
8805
+ const ironLawsFile = path.join(stateDir, "iron-laws.json");
8806
+ const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
8807
+
8808
+
8809
+ // Read knowledge.jsonl exactly once per session-start while holding the
8810
+ // SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
8811
+ // see a partial (mid-write) snapshot. Both the digest and
8812
+ // compound-readiness derive from this single read.
8813
+ const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
8814
+ const knowledgeRaw = await readTextFileLocked(
8815
+ knowledgeLockPathInline(runtime.root),
8816
+ knowledgeFilePath,
8817
+ ""
8818
+ );
8819
+ const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
8820
+
8821
+ // Fast path: read precomputed status lines from session-digest cache.
8822
+ // If cache is stale, schedule a debounced background refresh so this hook
8823
+ // returns quickly inside harness startup.
8824
+ const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
8825
+ const forceSyncRefresh =
8826
+ normalizeText(process.env.CCLAW_SESSION_START_BG_SYNC).toLowerCase() === "1" ||
8827
+ ["1", "true", "yes"].includes(normalizeText(process.env.VITEST).toLowerCase());
8828
+ let sessionDigest = await readSessionDigestLines(stateDir, state, flowStateMtimeMs);
8829
+ if (forceSyncRefresh && flowStateMtimeMs > 0) {
8830
+ await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
8831
+ sessionDigest = await readSessionDigestLines(stateDir, state, flowStateMtimeMs);
8832
+ } else if (!sessionDigest.fresh) {
8833
+ await scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs);
8834
+ }
8835
+ const ralphLoopLine = sessionDigest.ralphLoopLine;
8836
+ const earlyLoopLine = sessionDigest.earlyLoopLine;
8837
+ const compoundReadinessLine = sessionDigest.compoundReadinessLine;
8838
+
8713
8839
  const ironLawsObj = toObject(await readJsonFile(ironLawsFile, {})) || {};
8714
8840
  const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
8715
8841
  const ironLawLines = laws
@@ -8723,6 +8849,15 @@ async function handleSessionStart(runtime) {
8723
8849
  });
8724
8850
  const staleStages = toObject(state.raw.staleStages) || {};
8725
8851
  const staleStageNames = Object.keys(staleStages);
8852
+ const interactionHints = toObject(state.raw.interactionHints) || {};
8853
+ const stageInteractionHint = toObject(interactionHints[state.currentStage]);
8854
+ const skipQuestionsHintActive = stageInteractionHint?.skipQuestions === true;
8855
+ const skipQuestionsSource = typeof stageInteractionHint?.sourceStage === "string"
8856
+ ? stageInteractionHint.sourceStage
8857
+ : "";
8858
+ const skipQuestionsRecordedAt = typeof stageInteractionHint?.recordedAt === "string"
8859
+ ? stageInteractionHint.recordedAt
8860
+ : "";
8726
8861
  const metaContent = (await readTextFile(metaSkillFile, "")).trim();
8727
8862
  const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
8728
8863
 
@@ -8755,6 +8890,14 @@ async function handleSessionStart(runtime) {
8755
8890
  " (use npx cclaw-cli internal rewind --ack <stage> after redo)."
8756
8891
  );
8757
8892
  }
8893
+ if (skipQuestionsHintActive) {
8894
+ parts.push(
8895
+ "Adaptive elicitation hint: this stage inherits a prior user stop signal (--skip-questions" +
8896
+ (skipQuestionsSource ? " from " + skipQuestionsSource : "") +
8897
+ (skipQuestionsRecordedAt ? " at " + skipQuestionsRecordedAt : "") +
8898
+ "). Draft with available context unless irreversible/security override checks still require explicit confirmation."
8899
+ );
8900
+ }
8758
8901
  if (knowledge.digestLines.length > 0) {
8759
8902
  parts.push(
8760
8903
  "Knowledge digest (top relevant entries):\\n" +
@@ -8867,10 +9010,6 @@ async function handleStopHandoff(runtime) {
8867
9010
  return 0;
8868
9011
  }
8869
9012
 
8870
- async function handlePreCompact(_runtime) {
8871
- return 0;
8872
- }
8873
-
8874
9013
  async function handlePromptGuard(runtime) {
8875
9014
  const mode = resolveStrictness();
8876
9015
  const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
@@ -9372,15 +9511,38 @@ async function handleVerifyCurrentState(runtime) {
9372
9511
  return 0;
9373
9512
  }
9374
9513
 
9514
+ async function handlePreToolPipeline(runtime) {
9515
+ const promptExitCode = await handlePromptGuard(runtime);
9516
+ if (promptExitCode !== 0) {
9517
+ return promptExitCode;
9518
+ }
9519
+ return await handleWorkflowGuard(runtime);
9520
+ }
9521
+
9522
+ async function handlePromptPipeline(runtime) {
9523
+ const promptExitCode = await handlePromptGuard(runtime);
9524
+ if (promptExitCode !== 0) {
9525
+ return promptExitCode;
9526
+ }
9527
+ const verifyExitCode = await handleVerifyCurrentState(runtime);
9528
+ if (verifyExitCode !== 0) {
9529
+ return verifyExitCode;
9530
+ }
9531
+ runtime.writeJson({ ok: true });
9532
+ return 0;
9533
+ }
9534
+
9375
9535
  function normalizeHookName(rawName) {
9376
9536
  const value = normalizeText(rawName).toLowerCase();
9377
9537
  if (value === "session-start") return "session-start";
9538
+ if (value === "session-start-refresh") return "session-start-refresh";
9378
9539
  if (value === "stop-handoff" || value === "stop") return "stop-handoff";
9379
9540
  if (value === "stop-checkpoint") return "stop-handoff";
9380
- if (value === "pre-compact" || value === "precompact") return "pre-compact";
9381
9541
  if (value === "session-rehydrate") return "session-start";
9382
9542
  if (value === "prompt-guard") return "prompt-guard";
9383
9543
  if (value === "workflow-guard") return "workflow-guard";
9544
+ if (value === "pre-tool-pipeline" || value === "pretool-pipeline") return "pre-tool-pipeline";
9545
+ if (value === "prompt-pipeline" || value === "promptpipeline") return "prompt-pipeline";
9384
9546
  if (value === "context-monitor") return "context-monitor";
9385
9547
  if (value === "verify-current-state") return "verify-current-state";
9386
9548
  return "";
@@ -9392,7 +9554,7 @@ async function main() {
9392
9554
  process.stderr.write(
9393
9555
  "[cclaw] run-hook: usage: node " +
9394
9556
  RUNTIME_ROOT +
9395
- "/hooks/run-hook.mjs <session-start|stop-handoff|pre-compact|prompt-guard|workflow-guard|context-monitor|verify-current-state>\\n"
9557
+ "/hooks/run-hook.mjs <session-start|session-start-refresh|stop-handoff|prompt-guard|workflow-guard|pre-tool-pipeline|prompt-pipeline|context-monitor|verify-current-state>\\n"
9396
9558
  );
9397
9559
  process.exitCode = 1;
9398
9560
  return;
@@ -9424,12 +9586,12 @@ async function main() {
9424
9586
  process.exitCode = await handleSessionStart(runtime);
9425
9587
  return;
9426
9588
  }
9427
- if (hookName === "stop-handoff") {
9428
- process.exitCode = await handleStopHandoff(runtime);
9589
+ if (hookName === "session-start-refresh") {
9590
+ process.exitCode = await handleSessionStartRefresh(runtime);
9429
9591
  return;
9430
9592
  }
9431
- if (hookName === "pre-compact") {
9432
- process.exitCode = await handlePreCompact(runtime);
9593
+ if (hookName === "stop-handoff") {
9594
+ process.exitCode = await handleStopHandoff(runtime);
9433
9595
  return;
9434
9596
  }
9435
9597
  if (hookName === "prompt-guard") {
@@ -9440,6 +9602,14 @@ async function main() {
9440
9602
  process.exitCode = await handleWorkflowGuard(runtime);
9441
9603
  return;
9442
9604
  }
9605
+ if (hookName === "pre-tool-pipeline") {
9606
+ process.exitCode = await handlePreToolPipeline(runtime);
9607
+ return;
9608
+ }
9609
+ if (hookName === "prompt-pipeline") {
9610
+ process.exitCode = await handlePromptPipeline(runtime);
9611
+ return;
9612
+ }
9443
9613
  if (hookName === "context-monitor") {
9444
9614
  process.exitCode = await handleContextMonitor(runtime);
9445
9615
  return;
@@ -1,4 +1,4 @@
1
- import type { FlowTrack, TrackHeuristicRule, TrackHeuristicsConfig } from "./types.js";
1
+ import type { FlowStage, FlowTrack, TrackHeuristicRule, TrackHeuristicsConfig } from "./types.js";
2
2
  export interface TrackResolution {
3
3
  track: FlowTrack;
4
4
  reason: string;
@@ -6,6 +6,11 @@ export interface TrackResolution {
6
6
  confidence: "high" | "medium" | "low";
7
7
  overrideGuidance: string;
8
8
  }
9
+ export interface QuestionBudgetHint {
10
+ min: number;
11
+ recommended: number;
12
+ hardCapWarning: number;
13
+ }
9
14
  /**
10
15
  * Reference implementation of the track classifier the /cc skill prose
11
16
  * describes. Tests pin its behavior so the built-in defaults stay honest.
@@ -14,6 +19,7 @@ export interface TrackResolution {
14
19
  * "advisory" language.
15
20
  */
16
21
  export declare function resolveTrackFromPrompt(prompt: string, config: TrackHeuristicsConfig | undefined): TrackResolution;
22
+ export declare function questionBudgetHint(track: FlowTrack, stage: FlowStage): QuestionBudgetHint;
17
23
  export declare const TRACK_HEURISTICS_DEFAULTS: {
18
24
  readonly fallback: "standard";
19
25
  readonly evaluationOrder: readonly ("quick" | "medium" | "standard")[];
@@ -54,6 +54,12 @@ const DEFAULT_RULES = {
54
54
  // into runtime, so cclaw stopped offering the knob in v0.38.0.
55
55
  const EVALUATION_ORDER = ["standard", "medium", "quick"];
56
56
  const DEFAULT_FALLBACK = "standard";
57
+ const ADAPTIVE_ELICITATION_STAGES = new Set(["brainstorm", "scope", "design"]);
58
+ const QUESTION_BUDGET_HINTS_BY_TRACK = {
59
+ quick: { min: 2, recommended: 3, hardCapWarning: 4 },
60
+ medium: { min: 5, recommended: 6, hardCapWarning: 8 },
61
+ standard: { min: 10, recommended: 12, hardCapWarning: 14 }
62
+ };
57
63
  function hasToken(promptLower, token) {
58
64
  return promptLower.includes(token.toLowerCase());
59
65
  }
@@ -130,6 +136,12 @@ export function resolveTrackFromPrompt(prompt, config) {
130
136
  overrideGuidance: "Confirm or override before state is written; choose quick only for known low-blast-radius work, medium for known architecture with product framing, standard for uncertainty."
131
137
  };
132
138
  }
139
+ export function questionBudgetHint(track, stage) {
140
+ if (!ADAPTIVE_ELICITATION_STAGES.has(stage)) {
141
+ return { min: 0, recommended: 0, hardCapWarning: 0 };
142
+ }
143
+ return QUESTION_BUDGET_HINTS_BY_TRACK[track];
144
+ }
133
145
  export const TRACK_HEURISTICS_DEFAULTS = {
134
146
  fallback: DEFAULT_FALLBACK,
135
147
  evaluationOrder: EVALUATION_ORDER,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {