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
@@ -83,6 +83,10 @@ 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;
86
90
 
87
91
  ${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
88
92
  ${SHARED_STAGE_SUPPORT_SNIPPETS}
@@ -508,9 +512,10 @@ function hookEventNameForOutput(hookName) {
508
512
  if (hookName === "session-start") return "SessionStart";
509
513
  if (hookName === "prompt-guard") return "PreToolUse";
510
514
  if (hookName === "workflow-guard") return "PreToolUse";
515
+ if (hookName === "pre-tool-pipeline") return "PreToolUse";
516
+ if (hookName === "prompt-pipeline") return "UserPromptSubmit";
511
517
  if (hookName === "context-monitor") return "PostToolUse";
512
518
  if (hookName === "stop-handoff") return "Stop";
513
- if (hookName === "pre-compact") return "PreCompact";
514
519
  if (hookName === "verify-current-state") return "UserPromptSubmit";
515
520
  return "SessionStart";
516
521
  }
@@ -919,78 +924,55 @@ async function readFlowState(root) {
919
924
  };
920
925
  }
921
926
 
922
-
923
- async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
924
- const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
925
- // Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
926
- // Falls back to a local read if nothing is passed in.
927
- const raw = typeof prereadRaw === "string"
928
- ? prereadRaw
929
- : await readTextFile(knowledgeFile, "");
930
- const digest = parseKnowledgeDigest(raw, currentStage, 6);
931
- return {
932
- digestLines: digest.lines,
933
- learningsCount: digest.learningsCount
934
- };
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
935
  }
936
936
 
937
- async function readStageSupportContext(root, currentStage) {
938
- if (!isKnownStageId(currentStage)) return [];
939
- const stage = currentStage;
940
-
941
- const parts = [];
942
- const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
943
- const contract = (await readTextFile(contractPath, "")).trim();
944
- if (contract.length > 0) {
945
- parts.push(
946
- "Current stage state contract (read before drafting or editing the stage artifact):\\n" +
947
- contract
948
- );
949
- }
937
+ function parseNumericMs(value) {
938
+ return typeof value === "number" && Number.isFinite(value)
939
+ ? Math.trunc(value)
940
+ : -1;
941
+ }
950
942
 
951
- const promptName = reviewPromptFileName(stage);
952
- if (typeof promptName === "string") {
953
- const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
954
- const prompt = (await readTextFile(promptPath, "")).trim();
955
- if (prompt.length > 0) {
956
- parts.push(
957
- "Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
958
- prompt
959
- );
960
- }
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
+ };
961
957
  }
962
-
963
- return parts;
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
964
  }
965
965
 
966
- async function handleSessionStart(runtime) {
967
- const state = await readFlowState(runtime.root);
968
- const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
969
- const ironLawsFile = path.join(stateDir, "iron-laws.json");
970
- const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
971
-
972
-
973
- // Read knowledge.jsonl exactly once per session-start while holding the
974
- // SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
975
- // see a partial (mid-write) snapshot. Both the digest and
976
- // compound-readiness derive from this single read.
977
- const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
978
- const knowledgeRaw = await readTextFileLocked(
979
- knowledgeLockPathInline(runtime.root),
980
- knowledgeFilePath,
981
- ""
982
- );
983
- const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
984
-
985
- // Refresh Ralph Loop status each session-start so /cc and the model
986
- // both read a consistent "iter=N, acClosed=[...]" snapshot. Runs only when
987
- // we are in tdd — other stages skip the write to keep the file stable.
966
+ async function refreshSessionDigestCache(root, state, flowStateMtimeMs) {
967
+ const stateDir = path.join(root, RUNTIME_ROOT, "state");
988
968
  let ralphLoopLine = "";
989
969
  let earlyLoopLine = "";
970
+ let compoundReadinessLine = "";
971
+
990
972
  if (state.currentStage === "tdd") {
991
973
  try {
992
974
  const internalRalph = await runCclawInternal(
993
- runtime.root,
975
+ root,
994
976
  ["tdd-loop-status", "--json", "--write"],
995
977
  { captureStdout: true }
996
978
  );
@@ -1003,12 +985,8 @@ async function handleSessionStart(runtime) {
1003
985
  }
1004
986
  ralphLoopLine = formatRalphLoopStatusLineFromJson(ralphStatus);
1005
987
  } catch (err) {
1006
- // Best-effort — a malformed cycle log should never break
1007
- // session-start. But we DO leave a breadcrumb in
1008
- // hook-errors.jsonl so \`npx cclaw-cli sync\` can surface chronic
1009
- // failures (previously this was a silent swallow).
1010
988
  await recordHookError(
1011
- runtime.root,
989
+ root,
1012
990
  "session-start:ralph-loop",
1013
991
  err instanceof Error ? err.message : String(err)
1014
992
  );
@@ -1020,7 +998,7 @@ async function handleSessionStart(runtime) {
1020
998
  ) {
1021
999
  try {
1022
1000
  const internalEarly = await runCclawInternal(
1023
- runtime.root,
1001
+ root,
1024
1002
  [
1025
1003
  "early-loop-status",
1026
1004
  "--json",
@@ -1042,21 +1020,17 @@ async function handleSessionStart(runtime) {
1042
1020
  earlyLoopLine = formatEarlyLoopStatusLineFromJson(earlyLoopStatus);
1043
1021
  } catch (err) {
1044
1022
  await recordHookError(
1045
- runtime.root,
1023
+ root,
1046
1024
  "session-start:early-loop",
1047
1025
  err instanceof Error ? err.message : String(err)
1048
1026
  );
1049
1027
  }
1050
1028
  }
1051
1029
 
1052
- // Keep compound-readiness.json fresh on every session-start (cheap derived
1053
- // summary). Surface a one-line nudge only from review and ship stages
1054
- // where lifting becomes relevant; earlier stages update the file silently.
1055
- let compoundReadinessLine = "";
1056
1030
  try {
1057
1031
  const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
1058
1032
  const internalReadiness = await runCclawInternal(
1059
- runtime.root,
1033
+ root,
1060
1034
  shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
1061
1035
  { captureStdout: true }
1062
1036
  );
@@ -1067,17 +1041,169 @@ async function handleSessionStart(runtime) {
1067
1041
  compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
1068
1042
  }
1069
1043
  } catch (err) {
1070
- // Best-effort — a malformed knowledge.jsonl must never break
1071
- // session-start. But we DO leave a breadcrumb in
1072
- // hook-errors.jsonl so config/IO problems become visible in
1073
- // \`npx cclaw-cli sync\` instead of silently degrading readiness output.
1074
1044
  await recordHookError(
1075
- runtime.root,
1045
+ root,
1076
1046
  "session-start:compound-readiness",
1077
1047
  err instanceof Error ? err.message : String(err)
1078
1048
  );
1079
1049
  }
1080
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
+
1126
+
1127
+ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
1128
+ const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
1129
+ // Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
1130
+ // Falls back to a local read if nothing is passed in.
1131
+ const raw = typeof prereadRaw === "string"
1132
+ ? prereadRaw
1133
+ : await readTextFile(knowledgeFile, "");
1134
+ const digest = parseKnowledgeDigest(raw, currentStage, 6);
1135
+ return {
1136
+ digestLines: digest.lines,
1137
+ learningsCount: digest.learningsCount
1138
+ };
1139
+ }
1140
+
1141
+ async function readStageSupportContext(root, currentStage) {
1142
+ if (!isKnownStageId(currentStage)) return [];
1143
+ const stage = currentStage;
1144
+
1145
+ const parts = [];
1146
+ const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
1147
+ const contract = (await readTextFile(contractPath, "")).trim();
1148
+ if (contract.length > 0) {
1149
+ parts.push(
1150
+ "Current stage state contract (read before drafting or editing the stage artifact):\\n" +
1151
+ contract
1152
+ );
1153
+ }
1154
+
1155
+ const promptName = reviewPromptFileName(stage);
1156
+ if (typeof promptName === "string") {
1157
+ const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
1158
+ const prompt = (await readTextFile(promptPath, "")).trim();
1159
+ if (prompt.length > 0) {
1160
+ parts.push(
1161
+ "Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
1162
+ prompt
1163
+ );
1164
+ }
1165
+ }
1166
+
1167
+ return parts;
1168
+ }
1169
+
1170
+ async function handleSessionStart(runtime) {
1171
+ const state = await readFlowState(runtime.root);
1172
+ const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
1173
+ const ironLawsFile = path.join(stateDir, "iron-laws.json");
1174
+ const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
1175
+
1176
+
1177
+ // Read knowledge.jsonl exactly once per session-start while holding the
1178
+ // SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
1179
+ // see a partial (mid-write) snapshot. Both the digest and
1180
+ // compound-readiness derive from this single read.
1181
+ const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
1182
+ const knowledgeRaw = await readTextFileLocked(
1183
+ knowledgeLockPathInline(runtime.root),
1184
+ knowledgeFilePath,
1185
+ ""
1186
+ );
1187
+ const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
1188
+
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;
1206
+
1081
1207
  const ironLawsObj = toObject(await readJsonFile(ironLawsFile, {})) || {};
1082
1208
  const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
1083
1209
  const ironLawLines = laws
@@ -1091,6 +1217,15 @@ async function handleSessionStart(runtime) {
1091
1217
  });
1092
1218
  const staleStages = toObject(state.raw.staleStages) || {};
1093
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
+ : "";
1094
1229
  const metaContent = (await readTextFile(metaSkillFile, "")).trim();
1095
1230
  const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
1096
1231
 
@@ -1123,6 +1258,14 @@ async function handleSessionStart(runtime) {
1123
1258
  " (use npx cclaw-cli internal rewind --ack <stage> after redo)."
1124
1259
  );
1125
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
+ }
1126
1269
  if (knowledge.digestLines.length > 0) {
1127
1270
  parts.push(
1128
1271
  "Knowledge digest (top relevant entries):\\n" +
@@ -1235,10 +1378,6 @@ async function handleStopHandoff(runtime) {
1235
1378
  return 0;
1236
1379
  }
1237
1380
 
1238
- async function handlePreCompact(_runtime) {
1239
- return 0;
1240
- }
1241
-
1242
1381
  async function handlePromptGuard(runtime) {
1243
1382
  const mode = resolveStrictness();
1244
1383
  const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
@@ -1740,15 +1879,38 @@ async function handleVerifyCurrentState(runtime) {
1740
1879
  return 0;
1741
1880
  }
1742
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
+
1743
1903
  function normalizeHookName(rawName) {
1744
1904
  const value = normalizeText(rawName).toLowerCase();
1745
1905
  if (value === "session-start") return "session-start";
1906
+ if (value === "session-start-refresh") return "session-start-refresh";
1746
1907
  if (value === "stop-handoff" || value === "stop") return "stop-handoff";
1747
1908
  if (value === "stop-checkpoint") return "stop-handoff";
1748
- if (value === "pre-compact" || value === "precompact") return "pre-compact";
1749
1909
  if (value === "session-rehydrate") return "session-start";
1750
1910
  if (value === "prompt-guard") return "prompt-guard";
1751
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";
1752
1914
  if (value === "context-monitor") return "context-monitor";
1753
1915
  if (value === "verify-current-state") return "verify-current-state";
1754
1916
  return "";
@@ -1760,7 +1922,7 @@ async function main() {
1760
1922
  process.stderr.write(
1761
1923
  "[cclaw] run-hook: usage: node " +
1762
1924
  RUNTIME_ROOT +
1763
- "/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"
1764
1926
  );
1765
1927
  process.exitCode = 1;
1766
1928
  return;
@@ -1792,12 +1954,12 @@ async function main() {
1792
1954
  process.exitCode = await handleSessionStart(runtime);
1793
1955
  return;
1794
1956
  }
1795
- if (hookName === "stop-handoff") {
1796
- process.exitCode = await handleStopHandoff(runtime);
1957
+ if (hookName === "session-start-refresh") {
1958
+ process.exitCode = await handleSessionStartRefresh(runtime);
1797
1959
  return;
1798
1960
  }
1799
- if (hookName === "pre-compact") {
1800
- process.exitCode = await handlePreCompact(runtime);
1961
+ if (hookName === "stop-handoff") {
1962
+ process.exitCode = await handleStopHandoff(runtime);
1801
1963
  return;
1802
1964
  }
1803
1965
  if (hookName === "prompt-guard") {
@@ -1808,6 +1970,14 @@ async function main() {
1808
1970
  process.exitCode = await handleWorkflowGuard(runtime);
1809
1971
  return;
1810
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
+ }
1811
1981
  if (hookName === "context-monitor") {
1812
1982
  process.exitCode = await handleContextMonitor(runtime);
1813
1983
  return;
@@ -56,20 +56,20 @@ function buildCursorEvents() {
56
56
  }
57
57
  export function claudeHooksJsonWithObservation() {
58
58
  return JSON.stringify({
59
- cclawHookSchemaVersion: 1,
59
+ cclawHookSchemaVersion: 2,
60
60
  hooks: buildClaudeLikeEvents("claude")
61
61
  }, null, 2);
62
62
  }
63
63
  export function cursorHooksJsonWithObservation() {
64
64
  return JSON.stringify({
65
- cclawHookSchemaVersion: 1,
65
+ cclawHookSchemaVersion: 2,
66
66
  version: 1,
67
67
  hooks: buildCursorEvents()
68
68
  }, null, 2);
69
69
  }
70
70
  export function codexHooksJsonWithObservation() {
71
71
  return JSON.stringify({
72
- cclawHookSchemaVersion: 1,
72
+ cclawHookSchemaVersion: 2,
73
73
  hooks: buildClaudeLikeEvents("codex")
74
74
  }, null, 2);
75
75
  }
@@ -607,12 +607,6 @@ export default function cclawPlugin(ctx) {
607
607
  eventType === "session.compacted" ||
608
608
  eventType === "session.cleared" ||
609
609
  eventType === "session.updated";
610
- // session.compacted must run pre-compact BEFORE canonical rehydration,
611
- // otherwise the injected system prompt can show the pre-compact
612
- // digest/state until the next lifecycle event.
613
- if (eventType === "session.compacted") {
614
- await runHookScript("pre-compact", eventData ?? {});
615
- }
616
610
  if (isSessionLifecycle) {
617
611
  // Keep OpenCode aligned with Claude/Cursor/Codex: session-start is
618
612
  // the canonical rehydrate path that refreshes derived state such as
@@ -0,0 +1 @@
1
+ export declare function adaptiveElicitationSkillMarkdown(): string;
@@ -0,0 +1,123 @@
1
+ import { RUNTIME_ROOT } from "../constants.js";
2
+ import { questionBudgetHint } from "../track-heuristics.js";
3
+ import { FLOW_TRACKS } from "../types.js";
4
+ const ELICITATION_STAGES = ["brainstorm", "scope", "design"];
5
+ function renderQuestionBudgetHintTable() {
6
+ const rows = [];
7
+ for (const track of FLOW_TRACKS) {
8
+ for (const stage of ELICITATION_STAGES) {
9
+ const hint = questionBudgetHint(track, stage);
10
+ rows.push(`| \`${track}\` | \`${stage}\` | ${hint.min} | ${hint.recommended} | ${hint.hardCapWarning} |`);
11
+ }
12
+ }
13
+ return `| Track | Stage | Min | Recommended | Hard cap warning |
14
+ |---|---|---|---|---|
15
+ ${rows.join("\n")}`;
16
+ }
17
+ export function adaptiveElicitationSkillMarkdown() {
18
+ const budgetTable = renderQuestionBudgetHintTable();
19
+ return `---
20
+ name: adaptive-elicitation
21
+ description: "Harness-native one-question-at-a-time dialogue for brainstorm/scope/design with stop signals, smart-skip, and append-only Q&A logging."
22
+ ---
23
+
24
+ # Adaptive Elicitation
25
+
26
+ Pinned anchor: "Don't tell it what to do, give it success criteria and watch it go."
27
+
28
+ ## HARD-GATE
29
+ - User does not run cclaw manually. Do not tell the user to run CLI commands for answers.
30
+ - Ask exactly one question per turn and wait for the answer before asking the next one.
31
+ - Use harness-native question tools first; prose fallback is allowed only when the tool is unavailable.
32
+ - Keep a running Q&A trace in the active artifact under \`## Q&A Log\` in \`${RUNTIME_ROOT}/artifacts/\` as append-only rows.
33
+
34
+ ## Harness Question Surface
35
+
36
+ Preferred native tool names:
37
+ - Claude Code: \`AskUserQuestion\`
38
+ - Codex: \`request_user_input\`
39
+ - Gemini: \`ask_user\`
40
+ - Cursor: \`AskQuestion\`
41
+
42
+ If unavailable, ask one concise prose question and explicitly wait for chat answer.
43
+
44
+ ## Core Protocol
45
+
46
+ 1. Ask one decision-changing question.
47
+ 2. Wait for the answer.
48
+ 3. Append one row to \`## Q&A Log\`: \`Turn | Question | User answer (1-line) | Decision impact\`.
49
+ 4. Self-evaluate:
50
+ - What did I learn?
51
+ - Is context enough to draft now? (yes/no + reason)
52
+ - If no, what is the next most decision-changing question?
53
+ 5. Repeat until context is clear OR user asks to proceed.
54
+
55
+ ## Question Shape Rules
56
+
57
+ - Prefer single-select multiple choice when one direction/priority/next step must be chosen.
58
+ - Use multi-select only for compatible sets (goals, constraints, non-goals).
59
+ - Smart-skip questions already answered earlier (directly or implicitly) and log "skipped (already covered)" when relevant.
60
+
61
+ ## Stop Signals (Natural Language)
62
+
63
+ Treat these as stop-and-draft signals:
64
+ - RU: "достаточно", "хватит", "давай драфт"
65
+ - EN: "enough", "skip", "just draft it", "stop asking", "move on"
66
+ - UA: "досить", "вистачить", "давай драфт", "рухаємось далі"
67
+
68
+ When detected:
69
+ - Do not ask another question in this stage loop.
70
+ - Move to drafting with available context.
71
+ - For internal agent calls only, pass \`--skip-questions\` on the next advance helper call.
72
+
73
+ ## Conditional Grilling (Only On Risk Triggers)
74
+
75
+ Ask an extra 3-5 sharp questions only when one of these triggers appears:
76
+ - Irreversibility (data deletion, schema migration, breaking API/contract)
77
+ - Security/auth boundary changes
78
+ - Domain-model ambiguity with multiple plausible invariants
79
+
80
+ Do not ask extra questions "for theater" on simple low-risk work.
81
+
82
+ ## Question Budget Hint (Soft Guidance)
83
+
84
+ Use as orientation, never as a hard stop. Source of truth is \`questionBudgetHint(track, stage)\`:
85
+
86
+ ${budgetTable}
87
+
88
+ Track mapping note: \`quick\` ~= lightweight, \`medium\` ~= standard, \`standard\` ~= deep.
89
+ Stop based on clarity/user signal, not raw count.
90
+
91
+ ## Stage Forcing Questions
92
+
93
+ Always keep at least one unresolved forcing question in play until answered or explicitly waived:
94
+
95
+ - Brainstorm:
96
+ - What pain are we solving?
97
+ - What is the most direct path?
98
+ - What happens if we do nothing?
99
+ - Who is the operator/user impacted first?
100
+ - What are non-negotiable no-go boundaries?
101
+ - Scope:
102
+ - What is definitely in and definitely out?
103
+ - Which decisions are already locked upstream?
104
+ - What is the rollback path if this fails?
105
+ - What are the top failure modes we must design for?
106
+ - Design:
107
+ - What is the data flow end-to-end?
108
+ - Where are the seams/interfaces and ownership boundaries?
109
+ - Which invariants must always hold?
110
+ - What will we explicitly NOT refactor now?
111
+
112
+ ## One-Way Override (Irreversible Decisions)
113
+
114
+ For irreversible moves (deletion, schema migration, breaking API):
115
+ - Ask for explicit confirmation even if user asked to stop questions.
116
+ - Proceed only after explicit override ("I understand the irreversible risk; proceed").
117
+ - Record the override in \`## Q&A Log\` and in the stage artifact decision section.
118
+
119
+ ## Completion Rule
120
+
121
+ "Continue until clear OR user wants to proceed."
122
+ Never force a fixed N-question script.`;
123
+ }