cclaw-cli 1.0.0 → 3.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 +13 -5
- package/dist/cli.js +2 -9
- package/dist/config.d.ts +11 -67
- package/dist/config.js +59 -649
- package/dist/content/hook-events.js +1 -5
- package/dist/content/hook-manifest.d.ts +6 -4
- package/dist/content/hook-manifest.js +16 -65
- package/dist/content/hooks.js +54 -14
- package/dist/content/meta-skill.js +4 -3
- package/dist/content/node-hooks.d.ts +0 -26
- package/dist/content/node-hooks.js +459 -157
- package/dist/content/observe.js +5 -4
- package/dist/content/opencode-plugin.js +1 -78
- 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 +6 -2
- package/dist/content/stages/plan.js +2 -2
- package/dist/content/stages/scope.js +9 -5
- package/dist/content/stages/tdd.js +11 -11
- 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/gate-evidence.js +1 -5
- package/dist/hook-schema.js +3 -0
- package/dist/hook-schemas/claude-hooks.v1.json +2 -5
- package/dist/hook-schemas/codex-hooks.v1.json +1 -4
- package/dist/hook-schemas/cursor-hooks.v1.json +1 -3
- package/dist/install.d.ts +2 -7
- package/dist/install.js +32 -123
- 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/internal/compound-readiness.js +1 -16
- package/dist/internal/early-loop-status.js +1 -3
- package/dist/internal/runtime-integrity.js +0 -20
- package/dist/policy.js +6 -9
- package/dist/run-persistence.d.ts +1 -1
- package/dist/run-persistence.js +29 -2
- package/dist/runtime/run-hook.mjs +459 -265
- package/dist/tdd-verification-evidence.js +6 -18
- package/dist/track-heuristics.d.ts +7 -1
- package/dist/track-heuristics.js +12 -0
- package/dist/types.d.ts +0 -56
- package/package.json +1 -1
|
@@ -5,11 +5,6 @@ import { DEFAULT_COMPOUND_RECURRENCE_THRESHOLD, DEFAULT_EARLY_LOOP_MAX_ITERATION
|
|
|
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
7
|
import { SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS, SHARED_STAGE_SUPPORT_SNIPPETS } from "./runtime-shared-snippets.js";
|
|
8
|
-
function normalizePatterns(patterns, fallback) {
|
|
9
|
-
if (!patterns || patterns.length === 0)
|
|
10
|
-
return [...fallback];
|
|
11
|
-
return patterns.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
12
|
-
}
|
|
13
8
|
function resolveCliRuntimeForGeneratedHook() {
|
|
14
9
|
const here = fileURLToPath(import.meta.url);
|
|
15
10
|
const candidates = [
|
|
@@ -39,24 +34,19 @@ function resolveCliRuntimeForGeneratedHook() {
|
|
|
39
34
|
* bash/python/jq runtime dependencies.
|
|
40
35
|
*/
|
|
41
36
|
export function nodeHookRuntimeScript(options = {}) {
|
|
42
|
-
|
|
43
|
-
const
|
|
37
|
+
void options;
|
|
38
|
+
const strictness = "advisory";
|
|
39
|
+
const tddTestPathPatterns = [
|
|
44
40
|
"**/*.test.*",
|
|
45
41
|
"**/tests/**",
|
|
46
42
|
"**/__tests__/**"
|
|
47
|
-
]
|
|
48
|
-
const tddProductionPathPatterns =
|
|
49
|
-
const compoundRecurrenceThreshold =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const earlyLoopEnabled = options.earlyLoopEnabled !== false;
|
|
55
|
-
const earlyLoopMaxIterations = typeof options.earlyLoopMaxIterations === "number" &&
|
|
56
|
-
Number.isInteger(options.earlyLoopMaxIterations) &&
|
|
57
|
-
options.earlyLoopMaxIterations >= 1
|
|
58
|
-
? options.earlyLoopMaxIterations
|
|
59
|
-
: DEFAULT_EARLY_LOOP_MAX_ITERATIONS;
|
|
43
|
+
];
|
|
44
|
+
const tddProductionPathPatterns = [];
|
|
45
|
+
const compoundRecurrenceThreshold = DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
46
|
+
const earlyLoopEnabled = true;
|
|
47
|
+
const earlyLoopMaxIterations = DEFAULT_EARLY_LOOP_MAX_ITERATIONS;
|
|
48
|
+
const defaultHookProfile = "standard";
|
|
49
|
+
const defaultDisabledHooks = [];
|
|
60
50
|
const cliRuntime = resolveCliRuntimeForGeneratedHook();
|
|
61
51
|
return `#!/usr/bin/env node
|
|
62
52
|
import fs from "node:fs/promises";
|
|
@@ -83,11 +73,135 @@ const EARLY_LOOP_ENABLED = ${JSON.stringify(earlyLoopEnabled)};
|
|
|
83
73
|
const EARLY_LOOP_MAX_ITERATIONS = ${JSON.stringify(earlyLoopMaxIterations)};
|
|
84
74
|
const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
|
|
85
75
|
const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
|
|
76
|
+
const DEFAULT_HOOK_PROFILE = ${JSON.stringify(defaultHookProfile)};
|
|
77
|
+
const DEFAULT_DISABLED_HOOKS = ${JSON.stringify(defaultDisabledHooks)};
|
|
78
|
+
const HOOK_PROFILE_VALUES = new Set(["minimal", "standard", "strict"]);
|
|
79
|
+
const MINIMAL_PROFILE_ALLOWED_HOOKS = new Set([
|
|
80
|
+
"session-start",
|
|
81
|
+
"session-start-refresh",
|
|
82
|
+
"stop-handoff"
|
|
83
|
+
]);
|
|
84
|
+
const SESSION_DIGEST_SCHEMA_VERSION = 1;
|
|
85
|
+
const SESSION_DIGEST_CACHE_FILE = "session-digest.json";
|
|
86
|
+
const SESSION_DIGEST_REFRESH_MARKER_FILE = "session-digest.refresh.json";
|
|
87
|
+
const SESSION_DIGEST_REFRESH_STALE_MS = 30000;
|
|
86
88
|
|
|
87
89
|
${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
|
|
88
90
|
${SHARED_STAGE_SUPPORT_SNIPPETS}
|
|
89
91
|
|
|
92
|
+
let ACTIVE_HOOK_PROFILE = DEFAULT_HOOK_PROFILE;
|
|
93
|
+
|
|
94
|
+
function normalizeHookToken(value) {
|
|
95
|
+
return String(value == null ? "" : value).trim().toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseHookProfile(rawValue, fallback = "standard") {
|
|
99
|
+
const normalized = normalizeHookToken(rawValue);
|
|
100
|
+
if (HOOK_PROFILE_VALUES.has(normalized)) return normalized;
|
|
101
|
+
return fallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseDisabledHooksCsv(rawValue) {
|
|
105
|
+
const raw = typeof rawValue === "string" ? rawValue : "";
|
|
106
|
+
if (raw.trim().length === 0) return [];
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const token of raw.split(",")) {
|
|
109
|
+
const normalized = normalizeHookToken(token);
|
|
110
|
+
if (normalized.length === 0) continue;
|
|
111
|
+
if (!out.includes(normalized)) out.push(normalized);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseInlineYamlList(rawValue) {
|
|
117
|
+
const raw = typeof rawValue === "string" ? rawValue.trim() : "";
|
|
118
|
+
if (!raw.startsWith("[") || !raw.endsWith("]")) return [];
|
|
119
|
+
const inside = raw.slice(1, -1).trim();
|
|
120
|
+
if (inside.length === 0) return [];
|
|
121
|
+
return inside.split(",").map((token) => normalizeHookToken(token.replace(/^['"]|['"]$/g, ""))).filter((token) => token.length > 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseConfigHookProfile(rawYaml) {
|
|
125
|
+
if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
const match = rawYaml.match(/^\\s*hookProfile\\s*:\\s*([A-Za-z0-9_-]+)\\s*$/m);
|
|
129
|
+
if (!match || typeof match[1] !== "string") return "";
|
|
130
|
+
return parseHookProfile(match[1], "");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseConfigDisabledHooks(rawYaml) {
|
|
134
|
+
if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const lines = rawYaml.split(/\\r?\\n/u);
|
|
138
|
+
const out = [];
|
|
139
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
140
|
+
const line = lines[i];
|
|
141
|
+
const inlineMatch = line.match(/^\\s*disabledHooks\\s*:\\s*(\\[[^\\]]*\\])\\s*$/u);
|
|
142
|
+
if (inlineMatch) {
|
|
143
|
+
for (const value of parseInlineYamlList(inlineMatch[1])) {
|
|
144
|
+
if (!out.includes(value)) out.push(value);
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const blockMatch = line.match(/^(\\s*)disabledHooks\\s*:\\s*$/u);
|
|
149
|
+
if (!blockMatch) continue;
|
|
150
|
+
const baseIndent = blockMatch[1] ? blockMatch[1].length : 0;
|
|
151
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
152
|
+
const nextLine = lines[j];
|
|
153
|
+
const indent = (nextLine.match(/^(\\s*)/u)?.[1].length ?? 0);
|
|
154
|
+
const trimmed = nextLine.trim();
|
|
155
|
+
if (trimmed.length === 0) continue;
|
|
156
|
+
if (indent <= baseIndent) break;
|
|
157
|
+
const itemMatch = nextLine.match(/^\\s*-\\s*(.+?)\\s*$/u);
|
|
158
|
+
if (!itemMatch) continue;
|
|
159
|
+
const normalized = normalizeHookToken(itemMatch[1].replace(/^['"]|['"]$/g, ""));
|
|
160
|
+
if (normalized.length === 0) continue;
|
|
161
|
+
if (!out.includes(normalized)) out.push(normalized);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function readConfigHookPolicy(root) {
|
|
168
|
+
const configPath = path.join(root, RUNTIME_ROOT, "config.yaml");
|
|
169
|
+
const raw = await readTextFile(configPath, "");
|
|
170
|
+
const profile = parseConfigHookProfile(raw);
|
|
171
|
+
const disabledHooks = parseConfigDisabledHooks(raw);
|
|
172
|
+
return { profile, disabledHooks };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function resolveHookPolicy(root) {
|
|
176
|
+
const fromConfig = await readConfigHookPolicy(root);
|
|
177
|
+
const configProfile = parseHookProfile(fromConfig.profile, DEFAULT_HOOK_PROFILE);
|
|
178
|
+
const configDisabledHooks = Array.isArray(fromConfig.disabledHooks) && fromConfig.disabledHooks.length > 0
|
|
179
|
+
? fromConfig.disabledHooks
|
|
180
|
+
: DEFAULT_DISABLED_HOOKS;
|
|
181
|
+
|
|
182
|
+
const envProfileRaw = process.env.CCLAW_HOOK_PROFILE;
|
|
183
|
+
const envProfile = parseHookProfile(envProfileRaw, "");
|
|
184
|
+
const profile = envProfile.length > 0 ? envProfile : configProfile;
|
|
185
|
+
|
|
186
|
+
const envDisabledRaw = process.env.CCLAW_DISABLED_HOOKS;
|
|
187
|
+
const envDisabledHooks = parseDisabledHooksCsv(envDisabledRaw);
|
|
188
|
+
const disabledHooks = envDisabledHooks.length > 0 ? envDisabledHooks : configDisabledHooks;
|
|
189
|
+
const disabled = new Set(disabledHooks.map((value) => normalizeHookToken(value)));
|
|
190
|
+
return { profile, disabled };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function hookDisabledByProfile(profile, hookName) {
|
|
194
|
+
if (profile !== "minimal") return false;
|
|
195
|
+
return !MINIMAL_PROFILE_ALLOWED_HOOKS.has(hookName);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isHookDisabled(policy, hookName) {
|
|
199
|
+
if (policy.disabled.has(hookName)) return true;
|
|
200
|
+
return hookDisabledByProfile(policy.profile, hookName);
|
|
201
|
+
}
|
|
202
|
+
|
|
90
203
|
function resolveStrictness() {
|
|
204
|
+
if (ACTIVE_HOOK_PROFILE === "strict") return "strict";
|
|
91
205
|
return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
|
|
92
206
|
}
|
|
93
207
|
|
|
@@ -508,9 +622,10 @@ function hookEventNameForOutput(hookName) {
|
|
|
508
622
|
if (hookName === "session-start") return "SessionStart";
|
|
509
623
|
if (hookName === "prompt-guard") return "PreToolUse";
|
|
510
624
|
if (hookName === "workflow-guard") return "PreToolUse";
|
|
625
|
+
if (hookName === "pre-tool-pipeline") return "PreToolUse";
|
|
626
|
+
if (hookName === "prompt-pipeline") return "UserPromptSubmit";
|
|
511
627
|
if (hookName === "context-monitor") return "PostToolUse";
|
|
512
628
|
if (hookName === "stop-handoff") return "Stop";
|
|
513
|
-
if (hookName === "pre-compact") return "PreCompact";
|
|
514
629
|
if (hookName === "verify-current-state") return "UserPromptSubmit";
|
|
515
630
|
return "SessionStart";
|
|
516
631
|
}
|
|
@@ -919,78 +1034,55 @@ async function readFlowState(root) {
|
|
|
919
1034
|
};
|
|
920
1035
|
}
|
|
921
1036
|
|
|
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
|
-
};
|
|
1037
|
+
async function readFileMtimeMs(filePath) {
|
|
1038
|
+
try {
|
|
1039
|
+
const stat = await fs.stat(filePath);
|
|
1040
|
+
if (!stat.isFile()) return 0;
|
|
1041
|
+
return Math.trunc(stat.mtimeMs);
|
|
1042
|
+
} catch {
|
|
1043
|
+
return 0;
|
|
1044
|
+
}
|
|
935
1045
|
}
|
|
936
1046
|
|
|
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
|
-
}
|
|
1047
|
+
function parseNumericMs(value) {
|
|
1048
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
1049
|
+
? Math.trunc(value)
|
|
1050
|
+
: -1;
|
|
1051
|
+
}
|
|
950
1052
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1053
|
+
async function readSessionDigestLines(stateDir, state, flowStateMtimeMs) {
|
|
1054
|
+
const cachePath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
1055
|
+
const cache = toObject(await readJsonFile(cachePath, {})) || {};
|
|
1056
|
+
const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
|
|
1057
|
+
const sameStage = typeof cache.currentStage === "string" ? cache.currentStage === state.currentStage : true;
|
|
1058
|
+
const sameRun = typeof cache.activeRunId === "string" ? cache.activeRunId === state.activeRunId : true;
|
|
1059
|
+
const fresh = cachedMtimeMs === flowStateMtimeMs && sameStage && sameRun;
|
|
1060
|
+
if (!fresh) {
|
|
1061
|
+
return {
|
|
1062
|
+
ralphLoopLine: "",
|
|
1063
|
+
earlyLoopLine: "",
|
|
1064
|
+
compoundReadinessLine: "",
|
|
1065
|
+
fresh: false
|
|
1066
|
+
};
|
|
961
1067
|
}
|
|
962
|
-
|
|
963
|
-
|
|
1068
|
+
return {
|
|
1069
|
+
ralphLoopLine: typeof cache.ralphLoopLine === "string" ? cache.ralphLoopLine : "",
|
|
1070
|
+
earlyLoopLine: typeof cache.earlyLoopLine === "string" ? cache.earlyLoopLine : "",
|
|
1071
|
+
compoundReadinessLine: typeof cache.compoundReadinessLine === "string" ? cache.compoundReadinessLine : "",
|
|
1072
|
+
fresh: true
|
|
1073
|
+
};
|
|
964
1074
|
}
|
|
965
1075
|
|
|
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.
|
|
1076
|
+
async function refreshSessionDigestCache(root, state, flowStateMtimeMs) {
|
|
1077
|
+
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
988
1078
|
let ralphLoopLine = "";
|
|
989
1079
|
let earlyLoopLine = "";
|
|
1080
|
+
let compoundReadinessLine = "";
|
|
1081
|
+
|
|
990
1082
|
if (state.currentStage === "tdd") {
|
|
991
1083
|
try {
|
|
992
1084
|
const internalRalph = await runCclawInternal(
|
|
993
|
-
|
|
1085
|
+
root,
|
|
994
1086
|
["tdd-loop-status", "--json", "--write"],
|
|
995
1087
|
{ captureStdout: true }
|
|
996
1088
|
);
|
|
@@ -1003,12 +1095,8 @@ async function handleSessionStart(runtime) {
|
|
|
1003
1095
|
}
|
|
1004
1096
|
ralphLoopLine = formatRalphLoopStatusLineFromJson(ralphStatus);
|
|
1005
1097
|
} 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
1098
|
await recordHookError(
|
|
1011
|
-
|
|
1099
|
+
root,
|
|
1012
1100
|
"session-start:ralph-loop",
|
|
1013
1101
|
err instanceof Error ? err.message : String(err)
|
|
1014
1102
|
);
|
|
@@ -1020,7 +1108,7 @@ async function handleSessionStart(runtime) {
|
|
|
1020
1108
|
) {
|
|
1021
1109
|
try {
|
|
1022
1110
|
const internalEarly = await runCclawInternal(
|
|
1023
|
-
|
|
1111
|
+
root,
|
|
1024
1112
|
[
|
|
1025
1113
|
"early-loop-status",
|
|
1026
1114
|
"--json",
|
|
@@ -1042,21 +1130,17 @@ async function handleSessionStart(runtime) {
|
|
|
1042
1130
|
earlyLoopLine = formatEarlyLoopStatusLineFromJson(earlyLoopStatus);
|
|
1043
1131
|
} catch (err) {
|
|
1044
1132
|
await recordHookError(
|
|
1045
|
-
|
|
1133
|
+
root,
|
|
1046
1134
|
"session-start:early-loop",
|
|
1047
1135
|
err instanceof Error ? err.message : String(err)
|
|
1048
1136
|
);
|
|
1049
1137
|
}
|
|
1050
1138
|
}
|
|
1051
1139
|
|
|
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
1140
|
try {
|
|
1057
1141
|
const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
|
|
1058
1142
|
const internalReadiness = await runCclawInternal(
|
|
1059
|
-
|
|
1143
|
+
root,
|
|
1060
1144
|
shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
|
|
1061
1145
|
{ captureStdout: true }
|
|
1062
1146
|
);
|
|
@@ -1067,30 +1151,165 @@ async function handleSessionStart(runtime) {
|
|
|
1067
1151
|
compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
|
|
1068
1152
|
}
|
|
1069
1153
|
} 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
1154
|
await recordHookError(
|
|
1075
|
-
|
|
1155
|
+
root,
|
|
1076
1156
|
"session-start:compound-readiness",
|
|
1077
1157
|
err instanceof Error ? err.message : String(err)
|
|
1078
1158
|
);
|
|
1079
1159
|
}
|
|
1080
1160
|
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
.
|
|
1085
|
-
|
|
1086
|
-
.
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1161
|
+
const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
1162
|
+
await writeJsonFile(digestPath, {
|
|
1163
|
+
schemaVersion: SESSION_DIGEST_SCHEMA_VERSION,
|
|
1164
|
+
generatedAt: new Date().toISOString(),
|
|
1165
|
+
flowStateMtimeMs,
|
|
1166
|
+
currentStage: state.currentStage,
|
|
1167
|
+
activeRunId: state.activeRunId,
|
|
1168
|
+
ralphLoopLine,
|
|
1169
|
+
earlyLoopLine,
|
|
1170
|
+
compoundReadinessLine
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async function scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs) {
|
|
1175
|
+
if (flowStateMtimeMs <= 0) return;
|
|
1176
|
+
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1177
|
+
const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
1178
|
+
const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
|
|
1179
|
+
|
|
1180
|
+
const cache = toObject(await readJsonFile(digestPath, {})) || {};
|
|
1181
|
+
const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
|
|
1182
|
+
if (cachedMtimeMs === flowStateMtimeMs) return;
|
|
1183
|
+
|
|
1184
|
+
const marker = toObject(await readJsonFile(markerPath, {})) || {};
|
|
1185
|
+
const markerMtimeMs = parseNumericMs(marker.flowStateMtimeMs);
|
|
1186
|
+
const markerStartedAtMs = parseNumericMs(marker.startedAtMs);
|
|
1187
|
+
const markerFresh =
|
|
1188
|
+
markerMtimeMs === flowStateMtimeMs &&
|
|
1189
|
+
markerStartedAtMs > 0 &&
|
|
1190
|
+
Date.now() - markerStartedAtMs < SESSION_DIGEST_REFRESH_STALE_MS;
|
|
1191
|
+
if (markerFresh) return;
|
|
1192
|
+
|
|
1193
|
+
await writeJsonFile(markerPath, {
|
|
1194
|
+
flowStateMtimeMs,
|
|
1195
|
+
startedAtMs: Date.now(),
|
|
1196
|
+
currentStage: state.currentStage,
|
|
1197
|
+
activeRunId: state.activeRunId
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
try {
|
|
1201
|
+
const child = spawn(process.execPath, [process.argv[1], "session-start-refresh"], {
|
|
1202
|
+
cwd: runtime.root,
|
|
1203
|
+
stdio: "ignore",
|
|
1204
|
+
windowsHide: true,
|
|
1205
|
+
detached: true,
|
|
1206
|
+
env: {
|
|
1207
|
+
...process.env,
|
|
1208
|
+
CCLAW_PROJECT_ROOT: runtime.root,
|
|
1209
|
+
CCLAW_BG_WORKER: "1"
|
|
1210
|
+
}
|
|
1091
1211
|
});
|
|
1212
|
+
child.unref();
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
await fs.rm(markerPath, { force: true }).catch(() => undefined);
|
|
1215
|
+
await recordHookError(
|
|
1216
|
+
runtime.root,
|
|
1217
|
+
"session-start:spawn-refresh",
|
|
1218
|
+
err instanceof Error ? err.message : String(err)
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
async function handleSessionStartRefresh(runtime) {
|
|
1224
|
+
const state = await readFlowState(runtime.root);
|
|
1225
|
+
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1226
|
+
const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
|
|
1227
|
+
try {
|
|
1228
|
+
const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
|
|
1229
|
+
await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
|
|
1230
|
+
} finally {
|
|
1231
|
+
await fs.rm(markerPath, { force: true }).catch(() => undefined);
|
|
1232
|
+
}
|
|
1233
|
+
return 0;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
|
|
1238
|
+
const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
1239
|
+
// Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
|
|
1240
|
+
// Falls back to a local read if nothing is passed in.
|
|
1241
|
+
const raw = typeof prereadRaw === "string"
|
|
1242
|
+
? prereadRaw
|
|
1243
|
+
: await readTextFile(knowledgeFile, "");
|
|
1244
|
+
const digest = parseKnowledgeDigest(raw, currentStage, 6);
|
|
1245
|
+
return {
|
|
1246
|
+
digestLines: digest.lines,
|
|
1247
|
+
learningsCount: digest.learningsCount
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
async function readStageSupportContext(root, currentStage) {
|
|
1252
|
+
if (!isKnownStageId(currentStage)) return [];
|
|
1253
|
+
const stage = currentStage;
|
|
1254
|
+
|
|
1255
|
+
const parts = [];
|
|
1256
|
+
const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
|
|
1257
|
+
const contract = (await readTextFile(contractPath, "")).trim();
|
|
1258
|
+
if (contract.length > 0) {
|
|
1259
|
+
parts.push(
|
|
1260
|
+
"Current stage state contract (read before drafting or editing the stage artifact):\\n" +
|
|
1261
|
+
contract
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const promptName = reviewPromptFileName(stage);
|
|
1266
|
+
if (typeof promptName === "string") {
|
|
1267
|
+
const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
|
|
1268
|
+
const prompt = (await readTextFile(promptPath, "")).trim();
|
|
1269
|
+
if (prompt.length > 0) {
|
|
1270
|
+
parts.push(
|
|
1271
|
+
"Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
|
|
1272
|
+
prompt
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return parts;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async function handleSessionStart(runtime) {
|
|
1281
|
+
const state = await readFlowState(runtime.root);
|
|
1282
|
+
const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
// Read knowledge.jsonl exactly once per session-start while holding the
|
|
1286
|
+
// SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
|
|
1287
|
+
// see a partial (mid-write) snapshot. Both the digest and
|
|
1288
|
+
// compound-readiness derive from this single read.
|
|
1289
|
+
const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
1290
|
+
const knowledgeRaw = await readTextFileLocked(
|
|
1291
|
+
knowledgeLockPathInline(runtime.root),
|
|
1292
|
+
knowledgeFilePath,
|
|
1293
|
+
""
|
|
1294
|
+
);
|
|
1295
|
+
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
|
|
1296
|
+
|
|
1297
|
+
// Wave 21 honest-core: session-start no longer runs background helper
|
|
1298
|
+
// pipelines or digest caches. It rehydrates flow + knowledge only.
|
|
1299
|
+
const ralphLoopLine = "";
|
|
1300
|
+
const earlyLoopLine = "";
|
|
1301
|
+
const compoundReadinessLine = "";
|
|
1092
1302
|
const staleStages = toObject(state.raw.staleStages) || {};
|
|
1093
1303
|
const staleStageNames = Object.keys(staleStages);
|
|
1304
|
+
const interactionHints = toObject(state.raw.interactionHints) || {};
|
|
1305
|
+
const stageInteractionHint = toObject(interactionHints[state.currentStage]);
|
|
1306
|
+
const skipQuestionsHintActive = stageInteractionHint?.skipQuestions === true;
|
|
1307
|
+
const skipQuestionsSource = typeof stageInteractionHint?.sourceStage === "string"
|
|
1308
|
+
? stageInteractionHint.sourceStage
|
|
1309
|
+
: "";
|
|
1310
|
+
const skipQuestionsRecordedAt = typeof stageInteractionHint?.recordedAt === "string"
|
|
1311
|
+
? stageInteractionHint.recordedAt
|
|
1312
|
+
: "";
|
|
1094
1313
|
const metaContent = (await readTextFile(metaSkillFile, "")).trim();
|
|
1095
1314
|
const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
|
|
1096
1315
|
|
|
@@ -1123,6 +1342,14 @@ async function handleSessionStart(runtime) {
|
|
|
1123
1342
|
" (use npx cclaw-cli internal rewind --ack <stage> after redo)."
|
|
1124
1343
|
);
|
|
1125
1344
|
}
|
|
1345
|
+
if (skipQuestionsHintActive) {
|
|
1346
|
+
parts.push(
|
|
1347
|
+
"Adaptive elicitation hint: this stage inherits a prior user stop signal (--skip-questions" +
|
|
1348
|
+
(skipQuestionsSource ? " from " + skipQuestionsSource : "") +
|
|
1349
|
+
(skipQuestionsRecordedAt ? " at " + skipQuestionsRecordedAt : "") +
|
|
1350
|
+
"). Draft with available context unless irreversible/security override checks still require explicit confirmation."
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1126
1353
|
if (knowledge.digestLines.length > 0) {
|
|
1127
1354
|
parts.push(
|
|
1128
1355
|
"Knowledge digest (top relevant entries):\\n" +
|
|
@@ -1132,9 +1359,6 @@ async function handleSessionStart(runtime) {
|
|
|
1132
1359
|
if (stageSupportContext.length > 0) {
|
|
1133
1360
|
parts.push(...stageSupportContext);
|
|
1134
1361
|
}
|
|
1135
|
-
if (ironLawLines.length > 0) {
|
|
1136
|
-
parts.push("Iron laws (enforced policy highlights):\\n" + ironLawLines.join("\\n"));
|
|
1137
|
-
}
|
|
1138
1362
|
if (metaContent.length > 0) {
|
|
1139
1363
|
parts.push(metaContent);
|
|
1140
1364
|
}
|
|
@@ -1173,22 +1397,80 @@ async function isGitDirty(root) {
|
|
|
1173
1397
|
});
|
|
1174
1398
|
}
|
|
1175
1399
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
return
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1400
|
+
const STOP_BLOCK_LIMIT_PER_TRANSCRIPT = 2;
|
|
1401
|
+
|
|
1402
|
+
function asBoolean(value) {
|
|
1403
|
+
if (value === true || value === false) return value;
|
|
1404
|
+
if (typeof value === "number") return Number.isFinite(value) && value !== 0;
|
|
1405
|
+
if (typeof value !== "string") return false;
|
|
1406
|
+
const normalized = value.trim().toLowerCase();
|
|
1407
|
+
if (normalized.length === 0) return false;
|
|
1408
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function stringTokenHit(value, tokens) {
|
|
1412
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
1413
|
+
if (normalized.length === 0) return false;
|
|
1414
|
+
return tokens.some((token) => normalized.includes(token));
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function sanitizeStopSessionKey(raw) {
|
|
1418
|
+
const normalized = normalizeText(raw)
|
|
1419
|
+
.toLowerCase()
|
|
1420
|
+
.replace(/[^a-z0-9._-]+/gu, "-")
|
|
1421
|
+
.replace(/^-+|-+$/gu, "");
|
|
1422
|
+
return normalized.length > 0 ? normalized.slice(0, 96) : "global";
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function extractStopSignals(input, fallbackSessionKey) {
|
|
1426
|
+
const event = toObject(input.event) || {};
|
|
1427
|
+
const session = toObject(input.session) || {};
|
|
1428
|
+
const contextLimit =
|
|
1429
|
+
asBoolean(input.context_limit) ||
|
|
1430
|
+
asBoolean(input.contextLimit) ||
|
|
1431
|
+
asBoolean(event.context_limit) ||
|
|
1432
|
+
asBoolean(event.contextLimit) ||
|
|
1433
|
+
stringTokenHit(input.reason, ["context_limit", "context limit"]) ||
|
|
1434
|
+
stringTokenHit(event.reason, ["context_limit", "context limit"]) ||
|
|
1435
|
+
stringTokenHit(input.stop_reason, ["context_limit", "context limit"]) ||
|
|
1436
|
+
stringTokenHit(event.stop_reason, ["context_limit", "context limit"]);
|
|
1437
|
+
const userAbort =
|
|
1438
|
+
asBoolean(input.user_abort) ||
|
|
1439
|
+
asBoolean(input.userAbort) ||
|
|
1440
|
+
asBoolean(input.user_cancelled) ||
|
|
1441
|
+
asBoolean(input.userCancelled) ||
|
|
1442
|
+
asBoolean(event.user_abort) ||
|
|
1443
|
+
asBoolean(event.userAbort) ||
|
|
1444
|
+
stringTokenHit(input.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
|
|
1445
|
+
stringTokenHit(event.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
|
|
1446
|
+
stringTokenHit(input.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
|
|
1447
|
+
stringTokenHit(event.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]);
|
|
1448
|
+
const stopHookActive =
|
|
1449
|
+
asBoolean(input.stop_hook_active) ||
|
|
1450
|
+
asBoolean(input.stopHookActive) ||
|
|
1451
|
+
asBoolean(event.stop_hook_active) ||
|
|
1452
|
+
asBoolean(event.stopHookActive);
|
|
1453
|
+
|
|
1454
|
+
const sessionKeyCandidate =
|
|
1455
|
+
(typeof input.transcript_id === "string" && input.transcript_id) ||
|
|
1456
|
+
(typeof input.transcriptId === "string" && input.transcriptId) ||
|
|
1457
|
+
(typeof input.session_id === "string" && input.session_id) ||
|
|
1458
|
+
(typeof input.sessionId === "string" && input.sessionId) ||
|
|
1459
|
+
(typeof session.id === "string" && session.id) ||
|
|
1460
|
+
fallbackSessionKey;
|
|
1461
|
+
const sessionKey = sanitizeStopSessionKey(sessionKeyCandidate);
|
|
1462
|
+
|
|
1463
|
+
return {
|
|
1464
|
+
contextLimit,
|
|
1465
|
+
userAbort,
|
|
1466
|
+
stopHookActive,
|
|
1467
|
+
sessionKey
|
|
1468
|
+
};
|
|
1186
1469
|
}
|
|
1187
1470
|
|
|
1188
1471
|
async function handleStopHandoff(runtime) {
|
|
1189
1472
|
const state = await readFlowState(runtime.root);
|
|
1190
1473
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1191
|
-
const ironLawsFile = path.join(stateDir, "iron-laws.json");
|
|
1192
1474
|
const input = toObject(runtime.inputData) || {};
|
|
1193
1475
|
const loopCount =
|
|
1194
1476
|
typeof input.loop_count === "number" && Number.isFinite(input.loop_count)
|
|
@@ -1196,12 +1478,40 @@ async function handleStopHandoff(runtime) {
|
|
|
1196
1478
|
: 0;
|
|
1197
1479
|
|
|
1198
1480
|
const dirtyState = await isGitDirty(runtime.root);
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1481
|
+
const stopSignals = extractStopSignals(input, "run-" + state.activeRunId);
|
|
1482
|
+
const safetyBypassActive = stopSignals.stopHookActive || stopSignals.userAbort || stopSignals.contextLimit;
|
|
1483
|
+
if (dirtyState === "dirty" && !safetyBypassActive) {
|
|
1484
|
+
const stopBlocksPath = path.join(stateDir, "stop-blocks-" + stopSignals.sessionKey + ".json");
|
|
1485
|
+
const prior = toObject(await readJsonFile(stopBlocksPath, {})) || {};
|
|
1486
|
+
const priorCount =
|
|
1487
|
+
typeof prior.blockCount === "number" && Number.isFinite(prior.blockCount)
|
|
1488
|
+
? Math.max(0, Math.trunc(prior.blockCount))
|
|
1489
|
+
: 0;
|
|
1490
|
+
if (priorCount < STOP_BLOCK_LIMIT_PER_TRANSCRIPT) {
|
|
1491
|
+
const nextCount = priorCount + 1;
|
|
1492
|
+
await writeJsonFile(stopBlocksPath, {
|
|
1493
|
+
schemaVersion: 1,
|
|
1494
|
+
sessionKey: stopSignals.sessionKey,
|
|
1495
|
+
blockCount: nextCount,
|
|
1496
|
+
updatedAt: new Date().toISOString()
|
|
1497
|
+
});
|
|
1498
|
+
process.stderr.write(
|
|
1499
|
+
'[cclaw] Stop blocked by iron law "stop-clean-or-handoff": working tree is dirty. Commit/revert changes or record blockers in the current artifact before ending the session.\\n'
|
|
1500
|
+
);
|
|
1501
|
+
return 1;
|
|
1502
|
+
}
|
|
1503
|
+
process.stderr.write(
|
|
1504
|
+
'[cclaw] Stop advisory: dirty working tree detected, but block limit reached for this transcript (max 2). Continuing with handoff reminder only.\\n'
|
|
1505
|
+
);
|
|
1506
|
+
} else if (dirtyState === "dirty" && safetyBypassActive) {
|
|
1507
|
+
const reason = stopSignals.stopHookActive
|
|
1508
|
+
? "stop_hook_active"
|
|
1509
|
+
: stopSignals.userAbort
|
|
1510
|
+
? "user_abort"
|
|
1511
|
+
: "context_limit";
|
|
1201
1512
|
process.stderr.write(
|
|
1202
|
-
|
|
1513
|
+
"[cclaw] Stop advisory: bypassing strict stop block due to safety rule (" + reason + ").\\n"
|
|
1203
1514
|
);
|
|
1204
|
-
return 1;
|
|
1205
1515
|
}
|
|
1206
1516
|
|
|
1207
1517
|
const closeoutObj = toObject(state.raw.closeout) || {};
|
|
@@ -1235,10 +1545,6 @@ async function handleStopHandoff(runtime) {
|
|
|
1235
1545
|
return 0;
|
|
1236
1546
|
}
|
|
1237
1547
|
|
|
1238
|
-
async function handlePreCompact(_runtime) {
|
|
1239
|
-
return 0;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
1548
|
async function handlePromptGuard(runtime) {
|
|
1243
1549
|
const mode = resolveStrictness();
|
|
1244
1550
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
@@ -1740,17 +2046,33 @@ async function handleVerifyCurrentState(runtime) {
|
|
|
1740
2046
|
return 0;
|
|
1741
2047
|
}
|
|
1742
2048
|
|
|
2049
|
+
async function handlePreToolPipeline(runtime) {
|
|
2050
|
+
const promptExitCode = await handlePromptGuard(runtime);
|
|
2051
|
+
if (promptExitCode !== 0) {
|
|
2052
|
+
return promptExitCode;
|
|
2053
|
+
}
|
|
2054
|
+
return await handleWorkflowGuard(runtime);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
async function handlePromptPipeline(runtime) {
|
|
2058
|
+
const promptExitCode = await handlePromptGuard(runtime);
|
|
2059
|
+
if (promptExitCode !== 0) {
|
|
2060
|
+
return promptExitCode;
|
|
2061
|
+
}
|
|
2062
|
+
const verifyExitCode = await handleVerifyCurrentState(runtime);
|
|
2063
|
+
if (verifyExitCode !== 0) {
|
|
2064
|
+
return verifyExitCode;
|
|
2065
|
+
}
|
|
2066
|
+
runtime.writeJson({ ok: true });
|
|
2067
|
+
return 0;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1743
2070
|
function normalizeHookName(rawName) {
|
|
1744
2071
|
const value = normalizeText(rawName).toLowerCase();
|
|
1745
2072
|
if (value === "session-start") return "session-start";
|
|
1746
2073
|
if (value === "stop-handoff" || value === "stop") return "stop-handoff";
|
|
1747
2074
|
if (value === "stop-checkpoint") return "stop-handoff";
|
|
1748
|
-
if (value === "pre-compact" || value === "precompact") return "pre-compact";
|
|
1749
2075
|
if (value === "session-rehydrate") return "session-start";
|
|
1750
|
-
if (value === "prompt-guard") return "prompt-guard";
|
|
1751
|
-
if (value === "workflow-guard") return "workflow-guard";
|
|
1752
|
-
if (value === "context-monitor") return "context-monitor";
|
|
1753
|
-
if (value === "verify-current-state") return "verify-current-state";
|
|
1754
2076
|
return "";
|
|
1755
2077
|
}
|
|
1756
2078
|
|
|
@@ -1760,7 +2082,7 @@ async function main() {
|
|
|
1760
2082
|
process.stderr.write(
|
|
1761
2083
|
"[cclaw] run-hook: usage: node " +
|
|
1762
2084
|
RUNTIME_ROOT +
|
|
1763
|
-
"/hooks/run-hook.mjs <session-start|stop-handoff
|
|
2085
|
+
"/hooks/run-hook.mjs <session-start|stop-handoff>\\n"
|
|
1764
2086
|
);
|
|
1765
2087
|
process.exitCode = 1;
|
|
1766
2088
|
return;
|
|
@@ -1796,26 +2118,6 @@ async function main() {
|
|
|
1796
2118
|
process.exitCode = await handleStopHandoff(runtime);
|
|
1797
2119
|
return;
|
|
1798
2120
|
}
|
|
1799
|
-
if (hookName === "pre-compact") {
|
|
1800
|
-
process.exitCode = await handlePreCompact(runtime);
|
|
1801
|
-
return;
|
|
1802
|
-
}
|
|
1803
|
-
if (hookName === "prompt-guard") {
|
|
1804
|
-
process.exitCode = await handlePromptGuard(runtime);
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
if (hookName === "workflow-guard") {
|
|
1808
|
-
process.exitCode = await handleWorkflowGuard(runtime);
|
|
1809
|
-
return;
|
|
1810
|
-
}
|
|
1811
|
-
if (hookName === "context-monitor") {
|
|
1812
|
-
process.exitCode = await handleContextMonitor(runtime);
|
|
1813
|
-
return;
|
|
1814
|
-
}
|
|
1815
|
-
if (hookName === "verify-current-state") {
|
|
1816
|
-
process.exitCode = await handleVerifyCurrentState(runtime);
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
2121
|
process.stderr.write("[cclaw] run-hook: unsupported hook " + hookName + "\\n");
|
|
1820
2122
|
process.exitCode = 1;
|
|
1821
2123
|
} catch (error) {
|