cclaw-cli 2.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.js +2 -4
- 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 +0 -3
- package/dist/content/hook-manifest.d.ts +5 -2
- package/dist/content/hook-manifest.js +18 -64
- package/dist/content/node-hooks.d.ts +0 -26
- package/dist/content/node-hooks.js +237 -105
- package/dist/content/observe.js +2 -1
- package/dist/content/opencode-plugin.js +1 -72
- package/dist/content/stages/design.js +2 -2
- package/dist/content/stages/plan.js +2 -2
- package/dist/content/stages/scope.js +3 -3
- package/dist/content/stages/tdd.js +11 -11
- package/dist/gate-evidence.js +1 -5
- package/dist/hook-schema.js +3 -0
- package/dist/hook-schemas/claude-hooks.v1.json +0 -2
- package/dist/hook-schemas/codex-hooks.v1.json +0 -3
- package/dist/hook-schemas/cursor-hooks.v1.json +0 -2
- package/dist/install.d.ts +2 -7
- package/dist/install.js +20 -120
- 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/runtime/run-hook.mjs +237 -213
- package/dist/tdd-verification-evidence.js +6 -18
- package/dist/types.d.ts +0 -56
- package/package.json +1 -1
|
@@ -8,9 +8,6 @@ export { HOOK_SEMANTIC_EVENTS } from "./hook-manifest.js";
|
|
|
8
8
|
*/
|
|
9
9
|
const OPENCODE_SEMANTIC_COVERAGE = {
|
|
10
10
|
session_rehydrate: "plugin event handlers + transform rehydration",
|
|
11
|
-
pre_tool_prompt_guard: "plugin tool.execute.before -> prompt-guard",
|
|
12
|
-
pre_tool_workflow_guard: "plugin tool.execute.before -> workflow-guard",
|
|
13
|
-
post_tool_context_monitor: "plugin tool.execute.after -> context-monitor",
|
|
14
11
|
stop_handoff: "plugin session.idle -> stop-handoff"
|
|
15
12
|
};
|
|
16
13
|
/**
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export declare const HOOK_MANIFEST_HARNESSES: readonly ["claude", "cursor", "codex"];
|
|
23
23
|
export type HookManifestHarness = (typeof HOOK_MANIFEST_HARNESSES)[number];
|
|
24
|
-
export declare const HOOK_HANDLERS: readonly ["session-start", "
|
|
24
|
+
export declare const HOOK_HANDLERS: readonly ["session-start", "stop-handoff"];
|
|
25
25
|
export type HookHandlerId = (typeof HOOK_HANDLERS)[number];
|
|
26
26
|
export interface HookBinding {
|
|
27
27
|
/**
|
|
@@ -31,6 +31,8 @@ export interface HookBinding {
|
|
|
31
31
|
event: string;
|
|
32
32
|
matcher?: string;
|
|
33
33
|
timeout?: number;
|
|
34
|
+
/** Optional harness UI status line while this hook runs. */
|
|
35
|
+
statusMessage?: string;
|
|
34
36
|
/**
|
|
35
37
|
* Within a single (harness, event) group, entries are sorted by
|
|
36
38
|
* `priority` ASC, ties broken by manifest-declaration order. Use
|
|
@@ -49,7 +51,7 @@ export interface HookHandlerSpec {
|
|
|
49
51
|
semantic: HookSemanticEvent | null;
|
|
50
52
|
bindings: Partial<Record<HookManifestHarness, HookBinding[]>>;
|
|
51
53
|
}
|
|
52
|
-
export declare const HOOK_SEMANTIC_EVENTS: readonly ["session_rehydrate", "
|
|
54
|
+
export declare const HOOK_SEMANTIC_EVENTS: readonly ["session_rehydrate", "stop_handoff"];
|
|
53
55
|
export type HookSemanticEvent = (typeof HOOK_SEMANTIC_EVENTS)[number];
|
|
54
56
|
export declare const HOOK_MANIFEST: readonly HookHandlerSpec[];
|
|
55
57
|
export interface EventGroup {
|
|
@@ -62,6 +64,7 @@ export interface EventGroup {
|
|
|
62
64
|
handler: HookHandlerId;
|
|
63
65
|
matcher?: string;
|
|
64
66
|
timeout?: number;
|
|
67
|
+
statusMessage?: string;
|
|
65
68
|
}>;
|
|
66
69
|
}
|
|
67
70
|
/**
|
|
@@ -22,26 +22,16 @@
|
|
|
22
22
|
export const HOOK_MANIFEST_HARNESSES = ["claude", "cursor", "codex"];
|
|
23
23
|
export const HOOK_HANDLERS = [
|
|
24
24
|
"session-start",
|
|
25
|
-
"
|
|
26
|
-
"workflow-guard",
|
|
27
|
-
"pre-tool-pipeline",
|
|
28
|
-
"prompt-pipeline",
|
|
29
|
-
"context-monitor",
|
|
30
|
-
"stop-handoff",
|
|
31
|
-
"verify-current-state"
|
|
25
|
+
"stop-handoff"
|
|
32
26
|
];
|
|
33
27
|
export const HOOK_SEMANTIC_EVENTS = [
|
|
34
28
|
"session_rehydrate",
|
|
35
|
-
"
|
|
36
|
-
"pre_tool_workflow_guard",
|
|
37
|
-
"post_tool_context_monitor",
|
|
38
|
-
"stop_handoff",
|
|
39
|
-
"strict_state_verify"
|
|
29
|
+
"stop_handoff"
|
|
40
30
|
];
|
|
41
31
|
export const HOOK_MANIFEST = [
|
|
42
32
|
{
|
|
43
33
|
handler: "session-start",
|
|
44
|
-
description: "Rehydrate flow state
|
|
34
|
+
description: "Rehydrate flow state and emit bootstrap digest.",
|
|
45
35
|
semantic: "session_rehydrate",
|
|
46
36
|
bindings: {
|
|
47
37
|
claude: [{ event: "SessionStart", matcher: "startup|resume|clear|compact" }],
|
|
@@ -51,50 +41,13 @@ export const HOOK_MANIFEST = [
|
|
|
51
41
|
{ event: "sessionClear" },
|
|
52
42
|
{ event: "sessionCompact" }
|
|
53
43
|
],
|
|
54
|
-
codex: [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
bindings: {
|
|
62
|
-
claude: [{ event: "PreToolUse", matcher: "*" }]
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
handler: "workflow-guard",
|
|
67
|
-
description: "TDD and workflow gate on Write/Edit/Bash style tool invocations.",
|
|
68
|
-
semantic: "pre_tool_workflow_guard",
|
|
69
|
-
bindings: {
|
|
70
|
-
claude: [{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash" }]
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
handler: "pre-tool-pipeline",
|
|
75
|
-
description: "In-process pre-tool pipeline for harnesses that would otherwise spawn prompt/workflow guards separately.",
|
|
76
|
-
semantic: null,
|
|
77
|
-
bindings: {
|
|
78
|
-
cursor: [{ event: "preToolUse", matcher: "*" }],
|
|
79
|
-
codex: [{ event: "PreToolUse", matcher: "Bash|bash" }]
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
handler: "prompt-pipeline",
|
|
84
|
-
description: "In-process prompt pipeline for Codex UserPromptSubmit (prompt-guard + verify-current-state).",
|
|
85
|
-
semantic: "strict_state_verify",
|
|
86
|
-
bindings: {
|
|
87
|
-
codex: [{ event: "UserPromptSubmit" }]
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
handler: "context-monitor",
|
|
92
|
-
description: "Post-tool context usage + stage signal monitor.",
|
|
93
|
-
semantic: "post_tool_context_monitor",
|
|
94
|
-
bindings: {
|
|
95
|
-
claude: [{ event: "PostToolUse", matcher: "*" }],
|
|
96
|
-
cursor: [{ event: "postToolUse", matcher: "*" }],
|
|
97
|
-
codex: [{ event: "PostToolUse", matcher: "Bash|bash" }]
|
|
44
|
+
codex: [
|
|
45
|
+
{
|
|
46
|
+
event: "SessionStart",
|
|
47
|
+
matcher: "startup|resume",
|
|
48
|
+
statusMessage: "Running cclaw session startup checks"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
98
51
|
}
|
|
99
52
|
},
|
|
100
53
|
{
|
|
@@ -104,14 +57,14 @@ export const HOOK_MANIFEST = [
|
|
|
104
57
|
bindings: {
|
|
105
58
|
claude: [{ event: "Stop", timeout: 10 }],
|
|
106
59
|
cursor: [{ event: "stop", timeout: 10 }],
|
|
107
|
-
codex: [
|
|
60
|
+
codex: [
|
|
61
|
+
{
|
|
62
|
+
event: "Stop",
|
|
63
|
+
timeout: 10,
|
|
64
|
+
statusMessage: "Preparing cclaw handoff checklist"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
108
67
|
}
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
handler: "verify-current-state",
|
|
112
|
-
description: "Supplementary strict-mode guard callable from in-process pipelines to assert live state matches flow.",
|
|
113
|
-
semantic: null,
|
|
114
|
-
bindings: {}
|
|
115
68
|
}
|
|
116
69
|
];
|
|
117
70
|
/** Sanity: every harness in HOOK_MANIFEST_HARNESSES must be a HarnessId. */
|
|
@@ -141,6 +94,7 @@ export function groupBindingsByEvent(harness) {
|
|
|
141
94
|
handler: spec.handler,
|
|
142
95
|
...(binding.matcher !== undefined ? { matcher: binding.matcher } : {}),
|
|
143
96
|
...(binding.timeout !== undefined ? { timeout: binding.timeout } : {}),
|
|
97
|
+
...(binding.statusMessage !== undefined ? { statusMessage: binding.statusMessage } : {}),
|
|
144
98
|
priority: binding.priority ?? 0,
|
|
145
99
|
seq: seq++
|
|
146
100
|
});
|
|
@@ -1,30 +1,4 @@
|
|
|
1
1
|
export interface NodeHookRuntimeOptions {
|
|
2
|
-
/**
|
|
3
|
-
* Single enforcement knob derived from `config.strictness`. Generated hooks
|
|
4
|
-
* embed this value as the default for every guard (prompt, workflow, TDD,
|
|
5
|
-
* iron-laws-coupled blocks). `CCLAW_STRICTNESS` env var overrides at run
|
|
6
|
-
* time; per-law strictness still flows through `iron-laws.json`.
|
|
7
|
-
*/
|
|
8
|
-
strictness?: "advisory" | "strict";
|
|
9
|
-
tddTestPathPatterns?: string[];
|
|
10
|
-
tddProductionPathPatterns?: string[];
|
|
11
|
-
/**
|
|
12
|
-
* Baked-in default recurrence threshold for compound-readiness computed
|
|
13
|
-
* by the session-start hook. Derived from
|
|
14
|
-
* `config.compound.recurrenceThreshold` at install time; re-run
|
|
15
|
-
* `cclaw sync` after changing the config value so hook and CLI agree.
|
|
16
|
-
*/
|
|
17
|
-
compoundRecurrenceThreshold?: number;
|
|
18
|
-
/**
|
|
19
|
-
* Enables early-stage producer/critic loop diagnostics in session-start.
|
|
20
|
-
* Defaults to true.
|
|
21
|
-
*/
|
|
22
|
-
earlyLoopEnabled?: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Baked-in max iterations for brainstorm/scope/design early-loop status.
|
|
25
|
-
* Derived from `config.earlyLoop.maxIterations`.
|
|
26
|
-
*/
|
|
27
|
-
earlyLoopMaxIterations?: number;
|
|
28
2
|
}
|
|
29
3
|
/**
|
|
30
4
|
* Node-only hook runtime (single entrypoint).
|
|
@@ -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,6 +73,14 @@ 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
|
+
]);
|
|
86
84
|
const SESSION_DIGEST_SCHEMA_VERSION = 1;
|
|
87
85
|
const SESSION_DIGEST_CACHE_FILE = "session-digest.json";
|
|
88
86
|
const SESSION_DIGEST_REFRESH_MARKER_FILE = "session-digest.refresh.json";
|
|
@@ -91,7 +89,119 @@ const SESSION_DIGEST_REFRESH_STALE_MS = 30000;
|
|
|
91
89
|
${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
|
|
92
90
|
${SHARED_STAGE_SUPPORT_SNIPPETS}
|
|
93
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
|
+
|
|
94
203
|
function resolveStrictness() {
|
|
204
|
+
if (ACTIVE_HOOK_PROFILE === "strict") return "strict";
|
|
95
205
|
return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
|
|
96
206
|
}
|
|
97
207
|
|
|
@@ -1169,8 +1279,6 @@ async function readStageSupportContext(root, currentStage) {
|
|
|
1169
1279
|
|
|
1170
1280
|
async function handleSessionStart(runtime) {
|
|
1171
1281
|
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
1282
|
const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
|
|
1175
1283
|
|
|
1176
1284
|
|
|
@@ -1186,35 +1294,11 @@ async function handleSessionStart(runtime) {
|
|
|
1186
1294
|
);
|
|
1187
1295
|
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
|
|
1188
1296
|
|
|
1189
|
-
//
|
|
1190
|
-
//
|
|
1191
|
-
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
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
|
-
|
|
1207
|
-
const ironLawsObj = toObject(await readJsonFile(ironLawsFile, {})) || {};
|
|
1208
|
-
const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
|
|
1209
|
-
const ironLawLines = laws
|
|
1210
|
-
.filter((row) => row && typeof row === "object")
|
|
1211
|
-
.slice(0, 6)
|
|
1212
|
-
.map((row) => {
|
|
1213
|
-
const strict = row.strict === true ? "strict" : "advisory";
|
|
1214
|
-
const id = typeof row.id === "string" && row.id.length > 0 ? row.id : "law";
|
|
1215
|
-
const rule = typeof row.rule === "string" ? row.rule : "";
|
|
1216
|
-
return "- [" + strict + "] " + id + " -> " + rule;
|
|
1217
|
-
});
|
|
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 = "";
|
|
1218
1302
|
const staleStages = toObject(state.raw.staleStages) || {};
|
|
1219
1303
|
const staleStageNames = Object.keys(staleStages);
|
|
1220
1304
|
const interactionHints = toObject(state.raw.interactionHints) || {};
|
|
@@ -1275,9 +1359,6 @@ async function handleSessionStart(runtime) {
|
|
|
1275
1359
|
if (stageSupportContext.length > 0) {
|
|
1276
1360
|
parts.push(...stageSupportContext);
|
|
1277
1361
|
}
|
|
1278
|
-
if (ironLawLines.length > 0) {
|
|
1279
|
-
parts.push("Iron laws (enforced policy highlights):\\n" + ironLawLines.join("\\n"));
|
|
1280
|
-
}
|
|
1281
1362
|
if (metaContent.length > 0) {
|
|
1282
1363
|
parts.push(metaContent);
|
|
1283
1364
|
}
|
|
@@ -1316,22 +1397,80 @@ async function isGitDirty(root) {
|
|
|
1316
1397
|
});
|
|
1317
1398
|
}
|
|
1318
1399
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
return
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
+
};
|
|
1329
1469
|
}
|
|
1330
1470
|
|
|
1331
1471
|
async function handleStopHandoff(runtime) {
|
|
1332
1472
|
const state = await readFlowState(runtime.root);
|
|
1333
1473
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1334
|
-
const ironLawsFile = path.join(stateDir, "iron-laws.json");
|
|
1335
1474
|
const input = toObject(runtime.inputData) || {};
|
|
1336
1475
|
const loopCount =
|
|
1337
1476
|
typeof input.loop_count === "number" && Number.isFinite(input.loop_count)
|
|
@@ -1339,12 +1478,40 @@ async function handleStopHandoff(runtime) {
|
|
|
1339
1478
|
: 0;
|
|
1340
1479
|
|
|
1341
1480
|
const dirtyState = await isGitDirty(runtime.root);
|
|
1342
|
-
const
|
|
1343
|
-
|
|
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
|
+
}
|
|
1344
1503
|
process.stderr.write(
|
|
1345
|
-
'[cclaw] Stop
|
|
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";
|
|
1512
|
+
process.stderr.write(
|
|
1513
|
+
"[cclaw] Stop advisory: bypassing strict stop block due to safety rule (" + reason + ").\\n"
|
|
1346
1514
|
);
|
|
1347
|
-
return 1;
|
|
1348
1515
|
}
|
|
1349
1516
|
|
|
1350
1517
|
const closeoutObj = toObject(state.raw.closeout) || {};
|
|
@@ -1903,16 +2070,9 @@ async function handlePromptPipeline(runtime) {
|
|
|
1903
2070
|
function normalizeHookName(rawName) {
|
|
1904
2071
|
const value = normalizeText(rawName).toLowerCase();
|
|
1905
2072
|
if (value === "session-start") return "session-start";
|
|
1906
|
-
if (value === "session-start-refresh") return "session-start-refresh";
|
|
1907
2073
|
if (value === "stop-handoff" || value === "stop") return "stop-handoff";
|
|
1908
2074
|
if (value === "stop-checkpoint") return "stop-handoff";
|
|
1909
2075
|
if (value === "session-rehydrate") return "session-start";
|
|
1910
|
-
if (value === "prompt-guard") return "prompt-guard";
|
|
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";
|
|
1914
|
-
if (value === "context-monitor") return "context-monitor";
|
|
1915
|
-
if (value === "verify-current-state") return "verify-current-state";
|
|
1916
2076
|
return "";
|
|
1917
2077
|
}
|
|
1918
2078
|
|
|
@@ -1922,7 +2082,7 @@ async function main() {
|
|
|
1922
2082
|
process.stderr.write(
|
|
1923
2083
|
"[cclaw] run-hook: usage: node " +
|
|
1924
2084
|
RUNTIME_ROOT +
|
|
1925
|
-
"/hooks/run-hook.mjs <session-start|
|
|
2085
|
+
"/hooks/run-hook.mjs <session-start|stop-handoff>\\n"
|
|
1926
2086
|
);
|
|
1927
2087
|
process.exitCode = 1;
|
|
1928
2088
|
return;
|
|
@@ -1954,38 +2114,10 @@ async function main() {
|
|
|
1954
2114
|
process.exitCode = await handleSessionStart(runtime);
|
|
1955
2115
|
return;
|
|
1956
2116
|
}
|
|
1957
|
-
if (hookName === "session-start-refresh") {
|
|
1958
|
-
process.exitCode = await handleSessionStartRefresh(runtime);
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
2117
|
if (hookName === "stop-handoff") {
|
|
1962
2118
|
process.exitCode = await handleStopHandoff(runtime);
|
|
1963
2119
|
return;
|
|
1964
2120
|
}
|
|
1965
|
-
if (hookName === "prompt-guard") {
|
|
1966
|
-
process.exitCode = await handlePromptGuard(runtime);
|
|
1967
|
-
return;
|
|
1968
|
-
}
|
|
1969
|
-
if (hookName === "workflow-guard") {
|
|
1970
|
-
process.exitCode = await handleWorkflowGuard(runtime);
|
|
1971
|
-
return;
|
|
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
|
-
}
|
|
1981
|
-
if (hookName === "context-monitor") {
|
|
1982
|
-
process.exitCode = await handleContextMonitor(runtime);
|
|
1983
|
-
return;
|
|
1984
|
-
}
|
|
1985
|
-
if (hookName === "verify-current-state") {
|
|
1986
|
-
process.exitCode = await handleVerifyCurrentState(runtime);
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
2121
|
process.stderr.write("[cclaw] run-hook: unsupported hook " + hookName + "\\n");
|
|
1990
2122
|
process.exitCode = 1;
|
|
1991
2123
|
} catch (error) {
|
package/dist/content/observe.js
CHANGED
|
@@ -31,7 +31,8 @@ function buildClaudeLikeEvents(harness) {
|
|
|
31
31
|
const hookEntry = {
|
|
32
32
|
type: "command",
|
|
33
33
|
command: hookDispatcherCommand(entry.handler),
|
|
34
|
-
...(entry.timeout !== undefined ? { timeout: entry.timeout } : {})
|
|
34
|
+
...(entry.timeout !== undefined ? { timeout: entry.timeout } : {}),
|
|
35
|
+
...(entry.statusMessage !== undefined ? { statusMessage: entry.statusMessage } : {})
|
|
35
36
|
};
|
|
36
37
|
bucket.hooks.push(hookEntry);
|
|
37
38
|
}
|