cclaw-cli 0.55.2 → 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 (92) hide show
  1. package/README.md +3 -3
  2. package/dist/artifact-linter/brainstorm.js +59 -1
  3. package/dist/artifact-linter/design.js +46 -1
  4. package/dist/artifact-linter/plan.js +22 -1
  5. package/dist/artifact-linter/review.js +35 -1
  6. package/dist/artifact-linter/scope.js +33 -9
  7. package/dist/artifact-linter/shared.d.ts +12 -10
  8. package/dist/artifact-linter/shared.js +102 -41
  9. package/dist/artifact-linter/ship.js +36 -0
  10. package/dist/artifact-linter/spec.js +23 -1
  11. package/dist/artifact-linter/tdd.js +74 -0
  12. package/dist/artifact-linter.d.ts +1 -1
  13. package/dist/artifact-linter.js +11 -1
  14. package/dist/constants.d.ts +1 -1
  15. package/dist/constants.js +1 -0
  16. package/dist/content/closeout-guidance.d.ts +1 -1
  17. package/dist/content/closeout-guidance.js +10 -11
  18. package/dist/content/core-agents.d.ts +35 -36
  19. package/dist/content/core-agents.js +189 -99
  20. package/dist/content/diff-command.js +1 -1
  21. package/dist/content/examples.d.ts +0 -3
  22. package/dist/content/examples.js +197 -752
  23. package/dist/content/hook-events.js +1 -2
  24. package/dist/content/hook-manifest.d.ts +3 -4
  25. package/dist/content/hook-manifest.js +22 -25
  26. package/dist/content/hooks.js +54 -14
  27. package/dist/content/idea.d.ts +60 -0
  28. package/dist/content/idea.js +404 -0
  29. package/dist/content/learnings.d.ts +2 -4
  30. package/dist/content/learnings.js +10 -26
  31. package/dist/content/meta-skill.js +4 -3
  32. package/dist/content/node-hooks.js +368 -164
  33. package/dist/content/observe.js +3 -3
  34. package/dist/content/opencode-plugin.js +12 -32
  35. package/dist/content/reference-patterns.js +2 -2
  36. package/dist/content/runtime-shared-snippets.d.ts +8 -0
  37. package/dist/content/runtime-shared-snippets.js +80 -0
  38. package/dist/content/session-hooks.js +1 -1
  39. package/dist/content/skills-elicitation.d.ts +1 -0
  40. package/dist/content/skills-elicitation.js +123 -0
  41. package/dist/content/skills.d.ts +1 -0
  42. package/dist/content/skills.js +54 -2
  43. package/dist/content/stage-schema.js +107 -63
  44. package/dist/content/stages/brainstorm.js +7 -3
  45. package/dist/content/stages/design.js +4 -0
  46. package/dist/content/stages/review.js +8 -8
  47. package/dist/content/stages/schema-types.d.ts +2 -2
  48. package/dist/content/stages/scope.js +7 -3
  49. package/dist/content/stages/ship.js +1 -1
  50. package/dist/content/start-command.js +4 -4
  51. package/dist/content/status-command.js +3 -3
  52. package/dist/content/subagent-context-skills.js +156 -1
  53. package/dist/content/subagents.d.ts +0 -5
  54. package/dist/content/subagents.js +12 -82
  55. package/dist/content/templates.js +108 -6
  56. package/dist/content/utility-skills.js +26 -97
  57. package/dist/flow-state.d.ts +12 -6
  58. package/dist/flow-state.js +5 -6
  59. package/dist/gate-evidence.d.ts +0 -31
  60. package/dist/gate-evidence.js +3 -181
  61. package/dist/harness-adapters.js +1 -1
  62. package/dist/hook-schemas/claude-hooks.v1.json +2 -3
  63. package/dist/hook-schemas/codex-hooks.v1.json +1 -1
  64. package/dist/hook-schemas/cursor-hooks.v1.json +1 -1
  65. package/dist/install.js +50 -7
  66. package/dist/internal/advance-stage/advance.js +22 -2
  67. package/dist/internal/advance-stage/parsers.d.ts +1 -0
  68. package/dist/internal/advance-stage/parsers.js +6 -0
  69. package/dist/internal/advance-stage/review-loop.js +1 -10
  70. package/dist/knowledge-store.d.ts +2 -20
  71. package/dist/knowledge-store.js +43 -57
  72. package/dist/policy.js +3 -3
  73. package/dist/retro-gate.js +8 -90
  74. package/dist/run-archive.js +1 -4
  75. package/dist/run-persistence.d.ts +1 -1
  76. package/dist/run-persistence.js +43 -111
  77. package/dist/runtime/run-hook.entry.d.ts +3 -0
  78. package/dist/runtime/run-hook.entry.js +5 -0
  79. package/dist/runtime/run-hook.mjs +9647 -0
  80. package/dist/track-heuristics.d.ts +7 -1
  81. package/dist/track-heuristics.js +12 -0
  82. package/package.json +4 -2
  83. package/dist/content/hook-inline-snippets.d.ts +0 -96
  84. package/dist/content/hook-inline-snippets.js +0 -515
  85. package/dist/content/idea-command.d.ts +0 -8
  86. package/dist/content/idea-command.js +0 -322
  87. package/dist/content/idea-frames.d.ts +0 -31
  88. package/dist/content/idea-frames.js +0 -140
  89. package/dist/content/idea-ranking.d.ts +0 -25
  90. package/dist/content/idea-ranking.js +0 -65
  91. package/dist/trace-matrix.d.ts +0 -27
  92. package/dist/trace-matrix.js +0 -226
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { DEFAULT_COMPOUND_RECURRENCE_THRESHOLD, DEFAULT_EARLY_LOOP_MAX_ITERATIONS } from "../config.js";
5
5
  import { RUNTIME_ROOT } from "../constants.js";
6
6
  import { SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD, SMALL_PROJECT_RECURRENCE_THRESHOLD } from "../knowledge-store.js";
7
- import { HOOK_INLINE_SHARED_HELPERS, COMPOUND_READINESS_INLINE_SOURCE, RALPH_LOOP_INLINE_SOURCE, EARLY_LOOP_INLINE_SOURCE } from "./hook-inline-snippets.js";
7
+ import { SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS, SHARED_STAGE_SUPPORT_SNIPPETS } from "./runtime-shared-snippets.js";
8
8
  function normalizePatterns(patterns, fallback) {
9
9
  if (!patterns || patterns.length === 0)
10
10
  return [...fallback];
@@ -83,6 +83,13 @@ const EARLY_LOOP_ENABLED = ${JSON.stringify(earlyLoopEnabled)};
83
83
  const EARLY_LOOP_MAX_ITERATIONS = ${JSON.stringify(earlyLoopMaxIterations)};
84
84
  const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
85
85
  const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
86
+ const SESSION_DIGEST_SCHEMA_VERSION = 1;
87
+ const SESSION_DIGEST_CACHE_FILE = "session-digest.json";
88
+ const SESSION_DIGEST_REFRESH_MARKER_FILE = "session-digest.refresh.json";
89
+ const SESSION_DIGEST_REFRESH_STALE_MS = 30000;
90
+
91
+ ${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
92
+ ${SHARED_STAGE_SUPPORT_SNIPPETS}
86
93
 
87
94
  function resolveStrictness() {
88
95
  return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
@@ -427,6 +434,73 @@ async function runCclawInternal(root, args, options = {}) {
427
434
  });
428
435
  }
429
436
 
437
+ function compactStderr(value) {
438
+ const raw = typeof value === "string" ? value : "";
439
+ return raw.replace(/\\s+/gu, " ").trim();
440
+ }
441
+
442
+ function summarizeInternalFailure(operation, result) {
443
+ const detail = compactStderr(result && typeof result === "object" ? result.stderr : "");
444
+ return detail.length > 0 ? operation + ": " + detail : operation + " failed";
445
+ }
446
+
447
+ function parseJsonStdoutObject(result) {
448
+ const raw = typeof (result && result.stdout) === "string" ? result.stdout.trim() : "";
449
+ if (raw.length === 0) return null;
450
+ try {
451
+ return toObject(JSON.parse(raw));
452
+ } catch {
453
+ return null;
454
+ }
455
+ }
456
+
457
+ function firstStdoutLine(value) {
458
+ const raw = typeof value === "string" ? value : "";
459
+ const lines = raw
460
+ .split(/\\r?\\n/gu)
461
+ .map((line) => line.trim())
462
+ .filter((line) => line.length > 0);
463
+ return lines[0] || "";
464
+ }
465
+
466
+ function formatRalphLoopStatusLineFromJson(status) {
467
+ const redOpenSlices = Array.isArray(status.redOpenSlices)
468
+ ? status.redOpenSlices.filter((value) => typeof value === "string")
469
+ : [];
470
+ const redOpen = redOpenSlices.length > 0 ? redOpenSlices.join(",") : "none";
471
+ const loopIteration =
472
+ typeof status.loopIteration === "number" && Number.isFinite(status.loopIteration)
473
+ ? Math.trunc(status.loopIteration)
474
+ : 0;
475
+ const sliceCount =
476
+ typeof status.sliceCount === "number" && Number.isFinite(status.sliceCount)
477
+ ? Math.trunc(status.sliceCount)
478
+ : 0;
479
+ const acClosed = Array.isArray(status.acClosed) ? status.acClosed.length : 0;
480
+ return "Ralph Loop: iter=" + String(loopIteration) +
481
+ ", slices=" + String(sliceCount) +
482
+ ", acClosed=" + String(acClosed) +
483
+ ", redOpen=" + redOpen;
484
+ }
485
+
486
+ function formatEarlyLoopStatusLineFromJson(status) {
487
+ const stage = typeof status.stage === "string" ? status.stage : "unknown";
488
+ const iteration =
489
+ typeof status.iteration === "number" && Number.isFinite(status.iteration)
490
+ ? Math.trunc(status.iteration)
491
+ : 0;
492
+ const maxIterations =
493
+ typeof status.maxIterations === "number" && Number.isFinite(status.maxIterations)
494
+ ? Math.trunc(status.maxIterations)
495
+ : EARLY_LOOP_MAX_ITERATIONS;
496
+ const openConcerns = Array.isArray(status.openConcerns) ? status.openConcerns.length : 0;
497
+ const convergence = status.convergenceTripped === true ? "tripped" : "clear";
498
+ return "Early Loop: stage=" + stage +
499
+ ", iter=" + String(iteration) + "/" + String(maxIterations) +
500
+ ", open=" + String(openConcerns) +
501
+ ", convergence=" + convergence;
502
+ }
503
+
430
504
  function detectHarness(env) {
431
505
  if (env.CLAUDE_PROJECT_DIR) return "claude";
432
506
  if (env.CURSOR_PROJECT_DIR || env.CURSOR_PROJECT_ROOT) return "cursor";
@@ -438,9 +512,10 @@ function hookEventNameForOutput(hookName) {
438
512
  if (hookName === "session-start") return "SessionStart";
439
513
  if (hookName === "prompt-guard") return "PreToolUse";
440
514
  if (hookName === "workflow-guard") return "PreToolUse";
515
+ if (hookName === "pre-tool-pipeline") return "PreToolUse";
516
+ if (hookName === "prompt-pipeline") return "UserPromptSubmit";
441
517
  if (hookName === "context-monitor") return "PostToolUse";
442
518
  if (hookName === "stop-handoff") return "Stop";
443
- if (hookName === "pre-compact") return "PreCompact";
444
519
  if (hookName === "verify-current-state") return "UserPromptSubmit";
445
520
  return "SessionStart";
446
521
  }
@@ -839,16 +914,215 @@ async function readFlowState(root) {
839
914
  // empty object. Silent fallbacks used to mask stale CLI+hook drift.
840
915
  const parsed = await readJsonFile(statePath, {}, { root, stage: "read-flow-state" });
841
916
  const obj = toObject(parsed) || {};
842
- const completed = Array.isArray(obj.completedStages) ? obj.completedStages : [];
917
+ const summary = summarizeFlowState(obj);
843
918
  return {
844
919
  filePath: statePath,
845
- currentStage: typeof obj.currentStage === "string" ? obj.currentStage : "none",
846
- activeRunId: typeof obj.activeRunId === "string" ? obj.activeRunId : "active",
847
- completedCount: completed.length,
920
+ currentStage: summary.stage,
921
+ activeRunId: summary.activeRunId === "none" ? "active" : summary.activeRunId,
922
+ completedCount: summary.completed,
848
923
  raw: obj
849
924
  };
850
925
  }
851
926
 
927
+ async function readFileMtimeMs(filePath) {
928
+ try {
929
+ const stat = await fs.stat(filePath);
930
+ if (!stat.isFile()) return 0;
931
+ return Math.trunc(stat.mtimeMs);
932
+ } catch {
933
+ return 0;
934
+ }
935
+ }
936
+
937
+ function parseNumericMs(value) {
938
+ return typeof value === "number" && Number.isFinite(value)
939
+ ? Math.trunc(value)
940
+ : -1;
941
+ }
942
+
943
+ async function readSessionDigestLines(stateDir, state, flowStateMtimeMs) {
944
+ const cachePath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
945
+ const cache = toObject(await readJsonFile(cachePath, {})) || {};
946
+ const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
947
+ const sameStage = typeof cache.currentStage === "string" ? cache.currentStage === state.currentStage : true;
948
+ const sameRun = typeof cache.activeRunId === "string" ? cache.activeRunId === state.activeRunId : true;
949
+ const fresh = cachedMtimeMs === flowStateMtimeMs && sameStage && sameRun;
950
+ if (!fresh) {
951
+ return {
952
+ ralphLoopLine: "",
953
+ earlyLoopLine: "",
954
+ compoundReadinessLine: "",
955
+ fresh: false
956
+ };
957
+ }
958
+ return {
959
+ ralphLoopLine: typeof cache.ralphLoopLine === "string" ? cache.ralphLoopLine : "",
960
+ earlyLoopLine: typeof cache.earlyLoopLine === "string" ? cache.earlyLoopLine : "",
961
+ compoundReadinessLine: typeof cache.compoundReadinessLine === "string" ? cache.compoundReadinessLine : "",
962
+ fresh: true
963
+ };
964
+ }
965
+
966
+ async function refreshSessionDigestCache(root, state, flowStateMtimeMs) {
967
+ const stateDir = path.join(root, RUNTIME_ROOT, "state");
968
+ let ralphLoopLine = "";
969
+ let earlyLoopLine = "";
970
+ let compoundReadinessLine = "";
971
+
972
+ if (state.currentStage === "tdd") {
973
+ try {
974
+ const internalRalph = await runCclawInternal(
975
+ root,
976
+ ["tdd-loop-status", "--json", "--write"],
977
+ { captureStdout: true }
978
+ );
979
+ if (internalRalph.code !== 0) {
980
+ throw new Error(summarizeInternalFailure("tdd-loop-status", internalRalph));
981
+ }
982
+ const ralphStatus = parseJsonStdoutObject(internalRalph);
983
+ if (!ralphStatus) {
984
+ throw new Error("tdd-loop-status returned empty or malformed JSON");
985
+ }
986
+ ralphLoopLine = formatRalphLoopStatusLineFromJson(ralphStatus);
987
+ } catch (err) {
988
+ await recordHookError(
989
+ root,
990
+ "session-start:ralph-loop",
991
+ err instanceof Error ? err.message : String(err)
992
+ );
993
+ }
994
+ }
995
+ if (
996
+ EARLY_LOOP_ENABLED &&
997
+ (state.currentStage === "brainstorm" || state.currentStage === "scope" || state.currentStage === "design")
998
+ ) {
999
+ try {
1000
+ const internalEarly = await runCclawInternal(
1001
+ root,
1002
+ [
1003
+ "early-loop-status",
1004
+ "--json",
1005
+ "--write",
1006
+ "--stage",
1007
+ state.currentStage,
1008
+ "--run-id",
1009
+ state.activeRunId
1010
+ ],
1011
+ { captureStdout: true }
1012
+ );
1013
+ if (internalEarly.code !== 0) {
1014
+ throw new Error(summarizeInternalFailure("early-loop-status", internalEarly));
1015
+ }
1016
+ const earlyLoopStatus = parseJsonStdoutObject(internalEarly);
1017
+ if (!earlyLoopStatus) {
1018
+ throw new Error("early-loop-status returned empty or malformed JSON");
1019
+ }
1020
+ earlyLoopLine = formatEarlyLoopStatusLineFromJson(earlyLoopStatus);
1021
+ } catch (err) {
1022
+ await recordHookError(
1023
+ root,
1024
+ "session-start:early-loop",
1025
+ err instanceof Error ? err.message : String(err)
1026
+ );
1027
+ }
1028
+ }
1029
+
1030
+ try {
1031
+ const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
1032
+ const internalReadiness = await runCclawInternal(
1033
+ root,
1034
+ shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
1035
+ { captureStdout: true }
1036
+ );
1037
+ if (internalReadiness.code !== 0) {
1038
+ throw new Error(summarizeInternalFailure("compound-readiness", internalReadiness));
1039
+ }
1040
+ if (shouldShowReadiness) {
1041
+ compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
1042
+ }
1043
+ } catch (err) {
1044
+ await recordHookError(
1045
+ root,
1046
+ "session-start:compound-readiness",
1047
+ err instanceof Error ? err.message : String(err)
1048
+ );
1049
+ }
1050
+
1051
+ const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
1052
+ await writeJsonFile(digestPath, {
1053
+ schemaVersion: SESSION_DIGEST_SCHEMA_VERSION,
1054
+ generatedAt: new Date().toISOString(),
1055
+ flowStateMtimeMs,
1056
+ currentStage: state.currentStage,
1057
+ activeRunId: state.activeRunId,
1058
+ ralphLoopLine,
1059
+ earlyLoopLine,
1060
+ compoundReadinessLine
1061
+ });
1062
+ }
1063
+
1064
+ async function scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs) {
1065
+ if (flowStateMtimeMs <= 0) return;
1066
+ const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
1067
+ const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
1068
+ const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
1069
+
1070
+ const cache = toObject(await readJsonFile(digestPath, {})) || {};
1071
+ const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
1072
+ if (cachedMtimeMs === flowStateMtimeMs) return;
1073
+
1074
+ const marker = toObject(await readJsonFile(markerPath, {})) || {};
1075
+ const markerMtimeMs = parseNumericMs(marker.flowStateMtimeMs);
1076
+ const markerStartedAtMs = parseNumericMs(marker.startedAtMs);
1077
+ const markerFresh =
1078
+ markerMtimeMs === flowStateMtimeMs &&
1079
+ markerStartedAtMs > 0 &&
1080
+ Date.now() - markerStartedAtMs < SESSION_DIGEST_REFRESH_STALE_MS;
1081
+ if (markerFresh) return;
1082
+
1083
+ await writeJsonFile(markerPath, {
1084
+ flowStateMtimeMs,
1085
+ startedAtMs: Date.now(),
1086
+ currentStage: state.currentStage,
1087
+ activeRunId: state.activeRunId
1088
+ });
1089
+
1090
+ try {
1091
+ const child = spawn(process.execPath, [process.argv[1], "session-start-refresh"], {
1092
+ cwd: runtime.root,
1093
+ stdio: "ignore",
1094
+ windowsHide: true,
1095
+ detached: true,
1096
+ env: {
1097
+ ...process.env,
1098
+ CCLAW_PROJECT_ROOT: runtime.root,
1099
+ CCLAW_BG_WORKER: "1"
1100
+ }
1101
+ });
1102
+ child.unref();
1103
+ } catch (err) {
1104
+ await fs.rm(markerPath, { force: true }).catch(() => undefined);
1105
+ await recordHookError(
1106
+ runtime.root,
1107
+ "session-start:spawn-refresh",
1108
+ err instanceof Error ? err.message : String(err)
1109
+ );
1110
+ }
1111
+ }
1112
+
1113
+ async function handleSessionStartRefresh(runtime) {
1114
+ const state = await readFlowState(runtime.root);
1115
+ const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
1116
+ const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
1117
+ try {
1118
+ const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
1119
+ await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
1120
+ } finally {
1121
+ await fs.rm(markerPath, { force: true }).catch(() => undefined);
1122
+ }
1123
+ return 0;
1124
+ }
1125
+
852
1126
 
853
1127
  async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
854
1128
  const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
@@ -857,44 +1131,16 @@ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
857
1131
  const raw = typeof prereadRaw === "string"
858
1132
  ? prereadRaw
859
1133
  : await readTextFile(knowledgeFile, "");
860
- const lines = raw.split(/\\r?\\n/gu).map((line) => line.trim()).filter((line) => line.length > 0);
861
- let learningsCount = 0;
862
- const parsedRows = [];
863
- for (const line of lines) {
864
- if (line.startsWith("{")) learningsCount += 1;
865
- try {
866
- const parsed = JSON.parse(line);
867
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
868
- parsedRows.push(parsed);
869
- } catch {
870
- // ignore malformed knowledge line in digest
871
- }
872
- }
873
- const relevant = parsedRows
874
- .filter((row) => {
875
- const stage = typeof row.stage === "string" ? row.stage : null;
876
- return stage === null || stage === currentStage;
877
- })
878
- .slice(-6)
879
- .reverse()
880
- .map((row) => {
881
- const confidence = typeof row.confidence === "string" ? row.confidence : "unknown";
882
- const stage = typeof row.stage === "string" ? row.stage : "global";
883
- const domain = typeof row.domain === "string" ? row.domain : "general";
884
- const trigger = typeof row.trigger === "string" ? row.trigger : "trigger";
885
- const action = typeof row.action === "string" ? row.action : "action";
886
- return "- [" + confidence + " • " + stage + " • " + domain + "] " + trigger + " -> " + action;
887
- });
1134
+ const digest = parseKnowledgeDigest(raw, currentStage, 6);
888
1135
  return {
889
- digestLines: relevant,
890
- learningsCount
1136
+ digestLines: digest.lines,
1137
+ learningsCount: digest.learningsCount
891
1138
  };
892
1139
  }
893
1140
 
894
1141
  async function readStageSupportContext(root, currentStage) {
895
- const stage = typeof currentStage === "string" ? currentStage : "";
896
- const validStages = new Set(["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"]);
897
- if (!validStages.has(stage)) return [];
1142
+ if (!isKnownStageId(currentStage)) return [];
1143
+ const stage = currentStage;
898
1144
 
899
1145
  const parts = [];
900
1146
  const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
@@ -906,12 +1152,7 @@ async function readStageSupportContext(root, currentStage) {
906
1152
  );
907
1153
  }
908
1154
 
909
- const reviewPromptByStage = {
910
- brainstorm: "brainstorm-self-review.md",
911
- scope: "scope-ceo-review.md",
912
- design: "design-eng-review.md"
913
- };
914
- const promptName = reviewPromptByStage[stage];
1155
+ const promptName = reviewPromptFileName(stage);
915
1156
  if (typeof promptName === "string") {
916
1157
  const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
917
1158
  const prompt = (await readTextFile(promptPath, "")).trim();
@@ -945,99 +1186,23 @@ async function handleSessionStart(runtime) {
945
1186
  );
946
1187
  const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
947
1188
 
948
- // Refresh Ralph Loop status each session-start so /cc and the model
949
- // both read a consistent "iter=N, acClosed=[...]" snapshot. Runs only when
950
- // we are in tdd — other stages skip the write to keep the file stable.
951
- let ralphLoopLine = "";
952
- let earlyLoopLine = "";
953
- if (state.currentStage === "tdd") {
954
- try {
955
- const ralphStatus = await computeRalphLoopStatusInline(stateDir, state.activeRunId);
956
- await writeJsonFile(path.join(stateDir, "ralph-loop.json"), ralphStatus);
957
- const redOpen = ralphStatus.redOpenSlices.length > 0
958
- ? ralphStatus.redOpenSlices.join(",")
959
- : "none";
960
- ralphLoopLine = "Ralph Loop: iter=" + String(ralphStatus.loopIteration) +
961
- ", slices=" + String(ralphStatus.sliceCount) +
962
- ", acClosed=" + String(ralphStatus.acClosed.length) +
963
- ", redOpen=" + redOpen;
964
- } catch (err) {
965
- // Best-effort — a malformed cycle log should never break
966
- // session-start. But we DO leave a breadcrumb in
967
- // hook-errors.jsonl so \`npx cclaw-cli sync\` can surface chronic
968
- // failures (previously this was a silent swallow).
969
- await recordHookError(
970
- runtime.root,
971
- "session-start:ralph-loop",
972
- err instanceof Error ? err.message : String(err)
973
- );
974
- }
975
- }
976
- if (
977
- EARLY_LOOP_ENABLED &&
978
- (state.currentStage === "brainstorm" || state.currentStage === "scope" || state.currentStage === "design")
979
- ) {
980
- try {
981
- const earlyLoopStatus = await computeEarlyLoopStatusInline(
982
- stateDir,
983
- state.currentStage,
984
- state.activeRunId,
985
- EARLY_LOOP_MAX_ITERATIONS
986
- );
987
- await writeJsonFile(path.join(stateDir, "early-loop.json"), earlyLoopStatus);
988
- earlyLoopLine = formatEarlyLoopStatusLineInline(earlyLoopStatus);
989
- } catch (err) {
990
- await recordHookError(
991
- runtime.root,
992
- "session-start:early-loop",
993
- err instanceof Error ? err.message : String(err)
994
- );
995
- }
996
- }
997
-
998
- // Keep compound-readiness.json fresh on every session-start (cheap derived
999
- // summary). Surface a one-line nudge only from review and ship stages
1000
- // where lifting becomes relevant; earlier stages update the file silently.
1001
- let compoundReadinessLine = "";
1002
- try {
1003
- let readiness = null;
1004
- const internalReadiness = await runCclawInternal(
1005
- runtime.root,
1006
- ["compound-readiness", "--json"],
1007
- { captureStdout: true }
1008
- );
1009
- if (internalReadiness.code === 0 && internalReadiness.stdout.trim().length > 0) {
1010
- try {
1011
- const parsed = JSON.parse(internalReadiness.stdout);
1012
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1013
- readiness = parsed;
1014
- }
1015
- } catch {
1016
- readiness = null;
1017
- }
1018
- }
1019
- if (!readiness) {
1020
- const archivedRunsCount = await countArchivedRunsInline(runtime.root);
1021
- readiness = await computeCompoundReadinessInline(runtime.root, {
1022
- prereadRaw: knowledgeRaw,
1023
- ...(typeof archivedRunsCount === "number" ? { archivedRunsCount } : {})
1024
- });
1025
- await writeJsonFile(path.join(stateDir, "compound-readiness.json"), readiness);
1026
- }
1027
- if (state.currentStage === "review" || state.currentStage === "ship") {
1028
- compoundReadinessLine = formatCompoundReadinessLineInline(toObject(readiness) || {});
1029
- }
1030
- } catch (err) {
1031
- // Best-effort — a malformed knowledge.jsonl must never break
1032
- // session-start. But we DO leave a breadcrumb in
1033
- // hook-errors.jsonl so config/IO problems become visible in
1034
- // \`npx cclaw-cli sync\` instead of silently degrading readiness output.
1035
- await recordHookError(
1036
- runtime.root,
1037
- "session-start:compound-readiness",
1038
- err instanceof Error ? err.message : String(err)
1039
- );
1040
- }
1189
+ // Fast path: read precomputed status lines from session-digest cache.
1190
+ // If cache is stale, schedule a debounced background refresh so this hook
1191
+ // returns quickly inside harness startup.
1192
+ const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
1193
+ const forceSyncRefresh =
1194
+ normalizeText(process.env.CCLAW_SESSION_START_BG_SYNC).toLowerCase() === "1" ||
1195
+ ["1", "true", "yes"].includes(normalizeText(process.env.VITEST).toLowerCase());
1196
+ let sessionDigest = await readSessionDigestLines(stateDir, state, flowStateMtimeMs);
1197
+ if (forceSyncRefresh && flowStateMtimeMs > 0) {
1198
+ await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
1199
+ sessionDigest = await readSessionDigestLines(stateDir, state, flowStateMtimeMs);
1200
+ } else if (!sessionDigest.fresh) {
1201
+ await scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs);
1202
+ }
1203
+ const ralphLoopLine = sessionDigest.ralphLoopLine;
1204
+ const earlyLoopLine = sessionDigest.earlyLoopLine;
1205
+ const compoundReadinessLine = sessionDigest.compoundReadinessLine;
1041
1206
 
1042
1207
  const ironLawsObj = toObject(await readJsonFile(ironLawsFile, {})) || {};
1043
1208
  const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
@@ -1052,6 +1217,15 @@ async function handleSessionStart(runtime) {
1052
1217
  });
1053
1218
  const staleStages = toObject(state.raw.staleStages) || {};
1054
1219
  const staleStageNames = Object.keys(staleStages);
1220
+ const interactionHints = toObject(state.raw.interactionHints) || {};
1221
+ const stageInteractionHint = toObject(interactionHints[state.currentStage]);
1222
+ const skipQuestionsHintActive = stageInteractionHint?.skipQuestions === true;
1223
+ const skipQuestionsSource = typeof stageInteractionHint?.sourceStage === "string"
1224
+ ? stageInteractionHint.sourceStage
1225
+ : "";
1226
+ const skipQuestionsRecordedAt = typeof stageInteractionHint?.recordedAt === "string"
1227
+ ? stageInteractionHint.recordedAt
1228
+ : "";
1055
1229
  const metaContent = (await readTextFile(metaSkillFile, "")).trim();
1056
1230
  const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
1057
1231
 
@@ -1063,8 +1237,8 @@ async function handleSessionStart(runtime) {
1063
1237
  "/8 completed, run=" +
1064
1238
  state.activeRunId +
1065
1239
  "). Active artifacts: " +
1066
- RUNTIME_ROOT +
1067
- "/artifacts/. Learnings: " +
1240
+ activeArtifactsPathLabel(RUNTIME_ROOT) +
1241
+ " Learnings: " +
1068
1242
  String(knowledge.learningsCount) +
1069
1243
  " entries."
1070
1244
  ];
@@ -1084,6 +1258,14 @@ async function handleSessionStart(runtime) {
1084
1258
  " (use npx cclaw-cli internal rewind --ack <stage> after redo)."
1085
1259
  );
1086
1260
  }
1261
+ if (skipQuestionsHintActive) {
1262
+ parts.push(
1263
+ "Adaptive elicitation hint: this stage inherits a prior user stop signal (--skip-questions" +
1264
+ (skipQuestionsSource ? " from " + skipQuestionsSource : "") +
1265
+ (skipQuestionsRecordedAt ? " at " + skipQuestionsRecordedAt : "") +
1266
+ "). Draft with available context unless irreversible/security override checks still require explicit confirmation."
1267
+ );
1268
+ }
1087
1269
  if (knowledge.digestLines.length > 0) {
1088
1270
  parts.push(
1089
1271
  "Knowledge digest (top relevant entries):\\n" +
@@ -1169,7 +1351,7 @@ async function handleStopHandoff(runtime) {
1169
1351
  const shipSubstate = typeof closeoutObj.shipSubstate === "string" ? closeoutObj.shipSubstate : "idle";
1170
1352
  const closeoutContext =
1171
1353
  state.currentStage === "ship" || shipSubstate !== "idle"
1172
- ? " closeout.shipSubstate=" + shipSubstate + "; closeout chain=retro -> compound -> archive; continue closeout with /cc."
1354
+ ? " closeout.shipSubstate=" + shipSubstate + "; closeout chain=post_ship_review -> archive; continue closeout with /cc."
1173
1355
  : "";
1174
1356
 
1175
1357
  const message =
@@ -1196,10 +1378,6 @@ async function handleStopHandoff(runtime) {
1196
1378
  return 0;
1197
1379
  }
1198
1380
 
1199
- async function handlePreCompact(_runtime) {
1200
- return 0;
1201
- }
1202
-
1203
1381
  async function handlePromptGuard(runtime) {
1204
1382
  const mode = resolveStrictness();
1205
1383
  const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
@@ -1246,17 +1424,6 @@ async function handlePromptGuard(runtime) {
1246
1424
  return 0;
1247
1425
  }
1248
1426
 
1249
- // Inline mirrors of canonical CLI computations (compound-readiness,
1250
- // ralph-loop, early-loop) are factored into
1251
- // src/content/hook-inline-snippets.ts so
1252
- // this 2000+-line file no longer owns their bodies. Each snippet carries
1253
- // an explicit "mirrors X, parity enforced by Y" comment in the snippets
1254
- // module. Parity is enforced by tests/unit/ralph-loop-parity.test.ts.
1255
- ${HOOK_INLINE_SHARED_HELPERS}
1256
- ${COMPOUND_READINESS_INLINE_SOURCE}
1257
- ${RALPH_LOOP_INLINE_SOURCE}
1258
- ${EARLY_LOOP_INLINE_SOURCE}
1259
-
1260
1427
  async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
1261
1428
  const cycleRaw = await readTextFile(path.join(stateDir, "tdd-cycle-log.jsonl"), "");
1262
1429
  for (const line of cycleRaw.split(/\\r?\\n/gu)) {
@@ -1468,8 +1635,14 @@ async function handleWorkflowGuard(runtime) {
1468
1635
  // writes that actually belonged to a new, not-yet-red S-2. Now
1469
1636
  // we reuse the canonical Ralph Loop status: if NO slice has an
1470
1637
  // open RED, we block.
1471
- const ralphStatus = await computeRalphLoopStatusInline(stateDir, currentRun);
1472
- if (!ralphStatus.redOpen) {
1638
+ const internalRalph = await runCclawInternal(
1639
+ runtime.root,
1640
+ ["tdd-loop-status", "--json", "--no-write"],
1641
+ { captureStdout: true }
1642
+ );
1643
+ const ralphStatus = parseJsonStdoutObject(internalRalph);
1644
+ const redOpen = internalRalph.code === 0 && ralphStatus?.redOpen === true;
1645
+ if (!redOpen) {
1473
1646
  reasons.push("tdd_write_without_open_red");
1474
1647
  }
1475
1648
  }
@@ -1706,15 +1879,38 @@ async function handleVerifyCurrentState(runtime) {
1706
1879
  return 0;
1707
1880
  }
1708
1881
 
1882
+ async function handlePreToolPipeline(runtime) {
1883
+ const promptExitCode = await handlePromptGuard(runtime);
1884
+ if (promptExitCode !== 0) {
1885
+ return promptExitCode;
1886
+ }
1887
+ return await handleWorkflowGuard(runtime);
1888
+ }
1889
+
1890
+ async function handlePromptPipeline(runtime) {
1891
+ const promptExitCode = await handlePromptGuard(runtime);
1892
+ if (promptExitCode !== 0) {
1893
+ return promptExitCode;
1894
+ }
1895
+ const verifyExitCode = await handleVerifyCurrentState(runtime);
1896
+ if (verifyExitCode !== 0) {
1897
+ return verifyExitCode;
1898
+ }
1899
+ runtime.writeJson({ ok: true });
1900
+ return 0;
1901
+ }
1902
+
1709
1903
  function normalizeHookName(rawName) {
1710
1904
  const value = normalizeText(rawName).toLowerCase();
1711
1905
  if (value === "session-start") return "session-start";
1906
+ if (value === "session-start-refresh") return "session-start-refresh";
1712
1907
  if (value === "stop-handoff" || value === "stop") return "stop-handoff";
1713
1908
  if (value === "stop-checkpoint") return "stop-handoff";
1714
- if (value === "pre-compact" || value === "precompact") return "pre-compact";
1715
1909
  if (value === "session-rehydrate") return "session-start";
1716
1910
  if (value === "prompt-guard") return "prompt-guard";
1717
1911
  if (value === "workflow-guard") return "workflow-guard";
1912
+ if (value === "pre-tool-pipeline" || value === "pretool-pipeline") return "pre-tool-pipeline";
1913
+ if (value === "prompt-pipeline" || value === "promptpipeline") return "prompt-pipeline";
1718
1914
  if (value === "context-monitor") return "context-monitor";
1719
1915
  if (value === "verify-current-state") return "verify-current-state";
1720
1916
  return "";
@@ -1726,7 +1922,7 @@ async function main() {
1726
1922
  process.stderr.write(
1727
1923
  "[cclaw] run-hook: usage: node " +
1728
1924
  RUNTIME_ROOT +
1729
- "/hooks/run-hook.mjs <session-start|stop-handoff|pre-compact|prompt-guard|workflow-guard|context-monitor|verify-current-state>\\n"
1925
+ "/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"
1730
1926
  );
1731
1927
  process.exitCode = 1;
1732
1928
  return;
@@ -1758,12 +1954,12 @@ async function main() {
1758
1954
  process.exitCode = await handleSessionStart(runtime);
1759
1955
  return;
1760
1956
  }
1761
- if (hookName === "stop-handoff") {
1762
- process.exitCode = await handleStopHandoff(runtime);
1957
+ if (hookName === "session-start-refresh") {
1958
+ process.exitCode = await handleSessionStartRefresh(runtime);
1763
1959
  return;
1764
1960
  }
1765
- if (hookName === "pre-compact") {
1766
- process.exitCode = await handlePreCompact(runtime);
1961
+ if (hookName === "stop-handoff") {
1962
+ process.exitCode = await handleStopHandoff(runtime);
1767
1963
  return;
1768
1964
  }
1769
1965
  if (hookName === "prompt-guard") {
@@ -1774,6 +1970,14 @@ async function main() {
1774
1970
  process.exitCode = await handleWorkflowGuard(runtime);
1775
1971
  return;
1776
1972
  }
1973
+ if (hookName === "pre-tool-pipeline") {
1974
+ process.exitCode = await handlePreToolPipeline(runtime);
1975
+ return;
1976
+ }
1977
+ if (hookName === "prompt-pipeline") {
1978
+ process.exitCode = await handlePromptPipeline(runtime);
1979
+ return;
1980
+ }
1777
1981
  if (hookName === "context-monitor") {
1778
1982
  process.exitCode = await handleContextMonitor(runtime);
1779
1983
  return;