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.
- package/dist/artifact-linter/brainstorm.js +15 -1
- package/dist/artifact-linter/design.js +14 -0
- package/dist/artifact-linter/scope.js +14 -0
- package/dist/artifact-linter/shared.d.ts +1 -0
- package/dist/artifact-linter/shared.js +32 -0
- package/dist/artifact-linter.js +11 -1
- package/dist/content/hook-events.js +1 -2
- package/dist/content/hook-manifest.d.ts +3 -4
- package/dist/content/hook-manifest.js +22 -25
- package/dist/content/hooks.js +54 -14
- package/dist/content/meta-skill.js +4 -3
- package/dist/content/node-hooks.js +259 -89
- package/dist/content/observe.js +3 -3
- package/dist/content/opencode-plugin.js +0 -6
- package/dist/content/skills-elicitation.d.ts +1 -0
- package/dist/content/skills-elicitation.js +123 -0
- package/dist/content/skills.js +6 -4
- package/dist/content/stages/brainstorm.js +7 -3
- package/dist/content/stages/design.js +4 -0
- package/dist/content/stages/scope.js +6 -2
- package/dist/content/start-command.js +4 -4
- package/dist/content/templates.js +21 -0
- package/dist/flow-state.d.ts +7 -0
- package/dist/flow-state.js +1 -0
- package/dist/hook-schemas/claude-hooks.v1.json +2 -3
- package/dist/hook-schemas/codex-hooks.v1.json +1 -1
- package/dist/hook-schemas/cursor-hooks.v1.json +1 -1
- package/dist/install.js +12 -3
- package/dist/internal/advance-stage/advance.js +22 -1
- package/dist/internal/advance-stage/parsers.d.ts +1 -0
- package/dist/internal/advance-stage/parsers.js +6 -0
- package/dist/run-persistence.d.ts +1 -1
- package/dist/run-persistence.js +29 -2
- package/dist/runtime/run-hook.mjs +259 -89
- package/dist/track-heuristics.d.ts +7 -1
- package/dist/track-heuristics.js +12 -0
- 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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
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
|
|
967
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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|
|
|
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 === "
|
|
1796
|
-
process.exitCode = await
|
|
1957
|
+
if (hookName === "session-start-refresh") {
|
|
1958
|
+
process.exitCode = await handleSessionStartRefresh(runtime);
|
|
1797
1959
|
return;
|
|
1798
1960
|
}
|
|
1799
|
-
if (hookName === "
|
|
1800
|
-
process.exitCode = await
|
|
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;
|
package/dist/content/observe.js
CHANGED
|
@@ -56,20 +56,20 @@ function buildCursorEvents() {
|
|
|
56
56
|
}
|
|
57
57
|
export function claudeHooksJsonWithObservation() {
|
|
58
58
|
return JSON.stringify({
|
|
59
|
-
cclawHookSchemaVersion:
|
|
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:
|
|
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:
|
|
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
|
+
}
|