cclaw-cli 0.48.15 → 0.48.16

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.
@@ -118,6 +118,11 @@ Hook-driven guards respect the \`strictness\` field in
118
118
 
119
119
  Override per-session with \`CCLAW_STRICTNESS=advisory|strict\`.
120
120
 
121
+ Hook wiring itself comes from a **single manifest** (\`src/content/hook-manifest.ts\`):
122
+ the per-harness documents at \`.claude/hooks/hooks.json\`, \`.cursor/hooks.json\`,
123
+ \`.codex/hooks.json\` are all derived from it. Inspect the live bindings with
124
+ \`cclaw internal hook-manifest\` (add \`--json\` for machine-readable output).
125
+
121
126
  ## When in doubt
122
127
 
123
128
  1. Read \`${RUNTIME_ROOT}/state/flow-state.json\` to know where you are.
@@ -1,4 +1,9 @@
1
1
  import type { HarnessId } from "../types.js";
2
- export declare const HOOK_SEMANTIC_EVENTS: readonly ["session_rehydrate", "pre_tool_prompt_guard", "pre_tool_workflow_guard", "post_tool_context_monitor", "stop_checkpoint", "precompact_digest"];
3
- export type HookSemanticEvent = (typeof HOOK_SEMANTIC_EVENTS)[number];
2
+ import { type HookSemanticEvent } from "./hook-manifest.js";
3
+ export { HOOK_SEMANTIC_EVENTS, type HookSemanticEvent } from "./hook-manifest.js";
4
+ /**
5
+ * Public semantic coverage map derived from `HOOK_MANIFEST` for
6
+ * claude/cursor/codex, plus the static OpenCode descriptor. Consumers
7
+ * should treat this as read-only.
8
+ */
4
9
  export declare const HOOK_EVENTS_BY_HARNESS: Record<HarnessId, Partial<Record<HookSemanticEvent, string>>>;
@@ -1,47 +1,31 @@
1
- export const HOOK_SEMANTIC_EVENTS = [
2
- "session_rehydrate",
3
- "pre_tool_prompt_guard",
4
- "pre_tool_workflow_guard",
5
- "post_tool_context_monitor",
6
- "stop_checkpoint",
7
- "precompact_digest"
8
- ];
9
- export const HOOK_EVENTS_BY_HARNESS = {
10
- claude: {
11
- session_rehydrate: "SessionStart matcher startup|resume|clear|compact",
12
- pre_tool_prompt_guard: "PreToolUse -> prompt-guard",
13
- pre_tool_workflow_guard: "PreToolUse -> workflow-guard",
14
- post_tool_context_monitor: "PostToolUse -> context-monitor",
15
- stop_checkpoint: "Stop -> stop-checkpoint",
16
- precompact_digest: "PreCompact -> pre-compact"
17
- },
18
- cursor: {
19
- session_rehydrate: "sessionStart/sessionResume/sessionClear/sessionCompact",
20
- pre_tool_prompt_guard: "preToolUse -> prompt-guard",
21
- pre_tool_workflow_guard: "preToolUse -> workflow-guard",
22
- post_tool_context_monitor: "postToolUse -> context-monitor",
23
- stop_checkpoint: "stop -> stop-checkpoint",
24
- precompact_digest: "sessionCompact -> pre-compact"
25
- },
26
- opencode: {
27
- session_rehydrate: "plugin event handlers + transform rehydration",
28
- pre_tool_prompt_guard: "plugin tool.execute.before -> prompt-guard",
29
- pre_tool_workflow_guard: "plugin tool.execute.before -> workflow-guard",
30
- post_tool_context_monitor: "plugin tool.execute.after -> context-monitor",
31
- stop_checkpoint: "plugin session.idle -> stop-checkpoint",
32
- precompact_digest: "plugin session.compacted -> pre-compact"
33
- },
34
- codex: {
35
- // Codex CLI v0.114+ exposes lifecycle hooks via `.codex/hooks.json`,
36
- // gated by `[features] codex_hooks = true` in `~/.codex/config.toml`.
37
- // SessionStart, Stop, and UserPromptSubmit fire for every turn;
38
- // PreToolUse/PostToolUse are **Bash-only** (Write/Edit/WebSearch/MCP
39
- // calls do not trigger them). `precompact_digest` is unmapped —
40
- // Codex has no PreCompact event; cclaw covers it via `/cc-ops retro`.
41
- session_rehydrate: "SessionStart matcher startup|resume",
42
- pre_tool_prompt_guard: "PreToolUse matcher Bash -> prompt-guard (plus UserPromptSubmit for non-Bash prompts)",
43
- pre_tool_workflow_guard: "PreToolUse matcher Bash -> workflow-guard (Bash-only)",
44
- post_tool_context_monitor: "PostToolUse matcher Bash -> context-monitor (Bash-only)",
45
- stop_checkpoint: "Stop -> stop-checkpoint"
46
- }
1
+ import { HOOK_MANIFEST_HARNESSES, semanticEventCoverage } from "./hook-manifest.js";
2
+ export { HOOK_SEMANTIC_EVENTS } from "./hook-manifest.js";
3
+ function isManifestHarness(value) {
4
+ return HOOK_MANIFEST_HARNESSES.includes(value);
5
+ }
6
+ /**
7
+ * OpenCode is covered by the inline plugin (`opencode-plugin.ts`), not
8
+ * by the generated `run-hook.mjs` dispatcher. We keep its semantic
9
+ * coverage table here as an explicit descriptor, since it does not
10
+ * flow through the hook manifest.
11
+ */
12
+ const OPENCODE_SEMANTIC_COVERAGE = {
13
+ session_rehydrate: "plugin event handlers + transform rehydration",
14
+ pre_tool_prompt_guard: "plugin tool.execute.before -> prompt-guard",
15
+ pre_tool_workflow_guard: "plugin tool.execute.before -> workflow-guard",
16
+ post_tool_context_monitor: "plugin tool.execute.after -> context-monitor",
17
+ stop_checkpoint: "plugin session.idle -> stop-checkpoint",
18
+ precompact_digest: "plugin session.compacted -> pre-compact"
47
19
  };
20
+ /**
21
+ * Public semantic coverage map derived from `HOOK_MANIFEST` for
22
+ * claude/cursor/codex, plus the static OpenCode descriptor. Consumers
23
+ * should treat this as read-only.
24
+ */
25
+ export const HOOK_EVENTS_BY_HARNESS = Object.freeze({
26
+ claude: semanticEventCoverage("claude"),
27
+ cursor: semanticEventCoverage("cursor"),
28
+ codex: semanticEventCoverage("codex"),
29
+ opencode: OPENCODE_SEMANTIC_COVERAGE
30
+ });
31
+ void isManifestHarness;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Canonical operational manifest for cclaw hooks.
3
+ *
4
+ * This is the **single source of truth** for:
5
+ *
6
+ * - the per-harness JSON generators in `./observe.ts`
7
+ * (claude/cursor/codex hook documents),
8
+ * - the semantic coverage map in `./hook-events.ts` (docs + doctor),
9
+ * - the `requiredEvents` list embedded in `src/hook-schemas/*.v1.json`
10
+ * (enforced by a parity test).
11
+ *
12
+ * When adding a new hook handler or rerouting an existing one, edit
13
+ * this file and let the downstream modules rebuild from it. Never
14
+ * hard-code a `SessionStart`, `PreToolUse`, etc. mapping outside of
15
+ * this manifest.
16
+ *
17
+ * OpenCode is deliberately out of scope here: its plugin
18
+ * (`opencode-plugin.ts`) implements the handlers inline rather than
19
+ * dispatching through `run-hook.mjs`, so its coverage is tracked
20
+ * separately in `HOOK_EVENTS_BY_HARNESS`.
21
+ */
22
+ export declare const HOOK_MANIFEST_HARNESSES: readonly ["claude", "cursor", "codex"];
23
+ export type HookManifestHarness = (typeof HOOK_MANIFEST_HARNESSES)[number];
24
+ export declare const HOOK_HANDLERS: readonly ["session-start", "prompt-guard", "workflow-guard", "context-monitor", "stop-checkpoint", "pre-compact", "verify-current-state"];
25
+ export type HookHandlerId = (typeof HOOK_HANDLERS)[number];
26
+ export interface HookBinding {
27
+ /**
28
+ * Harness-native event name (exact string; PascalCase for
29
+ * claude/codex, camelCase for cursor). Do not normalize casing.
30
+ */
31
+ event: string;
32
+ matcher?: string;
33
+ timeout?: number;
34
+ /**
35
+ * Within a single (harness, event) group, entries are sorted by
36
+ * `priority` ASC, ties broken by manifest-declaration order. Use
37
+ * this to express "this handler must run BEFORE/AFTER that handler
38
+ * on the same event" (e.g. pre-compact must run before session-start
39
+ * on cursor `sessionCompact`). Default `0`.
40
+ */
41
+ priority?: number;
42
+ }
43
+ export interface HookHandlerSpec {
44
+ handler: HookHandlerId;
45
+ description: string;
46
+ /**
47
+ * Semantic event id used by `HOOK_EVENTS_BY_HARNESS` / docs.
48
+ * `null` means this handler contributes no semantic coverage row
49
+ * (e.g. `verify-current-state` on codex is a supplementary guard,
50
+ * not a top-level semantic event).
51
+ */
52
+ semantic: HookSemanticEvent | null;
53
+ bindings: Partial<Record<HookManifestHarness, HookBinding[]>>;
54
+ }
55
+ export declare const HOOK_SEMANTIC_EVENTS: readonly ["session_rehydrate", "pre_tool_prompt_guard", "pre_tool_workflow_guard", "post_tool_context_monitor", "stop_checkpoint", "precompact_digest"];
56
+ export type HookSemanticEvent = (typeof HOOK_SEMANTIC_EVENTS)[number];
57
+ export declare const HOOK_MANIFEST: readonly HookHandlerSpec[];
58
+ export interface EventGroup {
59
+ event: string;
60
+ /**
61
+ * Entries sorted by (priority ASC, declaration order). Default
62
+ * priority is 0. Stable — ties preserve manifest order.
63
+ */
64
+ entries: Array<{
65
+ handler: HookHandlerId;
66
+ matcher?: string;
67
+ timeout?: number;
68
+ }>;
69
+ }
70
+ /**
71
+ * Group manifest bindings by harness-native event name. This is the
72
+ * core projection that observe.ts generators consume to emit the
73
+ * harness-specific JSON document.
74
+ */
75
+ export declare function groupBindingsByEvent(harness: HookManifestHarness): EventGroup[];
76
+ /** Distinct harness-native event names covered by the manifest. */
77
+ export declare function requiredEventsFor(harness: HookManifestHarness): string[];
78
+ /**
79
+ * Human-readable per-harness semantic coverage used by docs and by
80
+ * the doctor's `harness-gaps.json` synthesis.
81
+ */
82
+ export declare function semanticEventCoverage(harness: HookManifestHarness): Partial<Record<HookSemanticEvent, string>>;
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Canonical operational manifest for cclaw hooks.
3
+ *
4
+ * This is the **single source of truth** for:
5
+ *
6
+ * - the per-harness JSON generators in `./observe.ts`
7
+ * (claude/cursor/codex hook documents),
8
+ * - the semantic coverage map in `./hook-events.ts` (docs + doctor),
9
+ * - the `requiredEvents` list embedded in `src/hook-schemas/*.v1.json`
10
+ * (enforced by a parity test).
11
+ *
12
+ * When adding a new hook handler or rerouting an existing one, edit
13
+ * this file and let the downstream modules rebuild from it. Never
14
+ * hard-code a `SessionStart`, `PreToolUse`, etc. mapping outside of
15
+ * this manifest.
16
+ *
17
+ * OpenCode is deliberately out of scope here: its plugin
18
+ * (`opencode-plugin.ts`) implements the handlers inline rather than
19
+ * dispatching through `run-hook.mjs`, so its coverage is tracked
20
+ * separately in `HOOK_EVENTS_BY_HARNESS`.
21
+ */
22
+ export const HOOK_MANIFEST_HARNESSES = ["claude", "cursor", "codex"];
23
+ export const HOOK_HANDLERS = [
24
+ "session-start",
25
+ "prompt-guard",
26
+ "workflow-guard",
27
+ "context-monitor",
28
+ "stop-checkpoint",
29
+ "pre-compact",
30
+ "verify-current-state"
31
+ ];
32
+ export const HOOK_SEMANTIC_EVENTS = [
33
+ "session_rehydrate",
34
+ "pre_tool_prompt_guard",
35
+ "pre_tool_workflow_guard",
36
+ "post_tool_context_monitor",
37
+ "stop_checkpoint",
38
+ "precompact_digest"
39
+ ];
40
+ export const HOOK_MANIFEST = [
41
+ {
42
+ handler: "session-start",
43
+ description: "Rehydrate flow state, refresh Ralph Loop + compound readiness, emit bootstrap digest.",
44
+ semantic: "session_rehydrate",
45
+ bindings: {
46
+ claude: [{ event: "SessionStart", matcher: "startup|resume|clear|compact" }],
47
+ cursor: [
48
+ { event: "sessionStart" },
49
+ { event: "sessionResume" },
50
+ { event: "sessionClear" },
51
+ { event: "sessionCompact" }
52
+ ],
53
+ codex: [{ event: "SessionStart", matcher: "startup|resume" }]
54
+ }
55
+ },
56
+ {
57
+ handler: "prompt-guard",
58
+ description: "Stage-aware prompt gate (iron-laws + strictness).",
59
+ semantic: "pre_tool_prompt_guard",
60
+ bindings: {
61
+ claude: [{ event: "PreToolUse", matcher: "*" }],
62
+ cursor: [{ event: "preToolUse", matcher: "*" }],
63
+ codex: [
64
+ { event: "UserPromptSubmit" },
65
+ { event: "PreToolUse", matcher: "Bash|bash" }
66
+ ]
67
+ }
68
+ },
69
+ {
70
+ handler: "workflow-guard",
71
+ description: "TDD and workflow gate on Write/Edit/Bash style tool invocations.",
72
+ semantic: "pre_tool_workflow_guard",
73
+ bindings: {
74
+ claude: [{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash" }],
75
+ cursor: [{ event: "preToolUse", matcher: "*" }],
76
+ codex: [
77
+ { event: "UserPromptSubmit" },
78
+ { event: "PreToolUse", matcher: "Bash|bash" }
79
+ ]
80
+ }
81
+ },
82
+ {
83
+ handler: "context-monitor",
84
+ description: "Post-tool context usage + stage signal monitor.",
85
+ semantic: "post_tool_context_monitor",
86
+ bindings: {
87
+ claude: [{ event: "PostToolUse", matcher: "*" }],
88
+ cursor: [{ event: "postToolUse", matcher: "*" }],
89
+ codex: [{ event: "PostToolUse", matcher: "Bash|bash" }]
90
+ }
91
+ },
92
+ {
93
+ handler: "stop-checkpoint",
94
+ description: "Persist checkpoint with stage + run context on session stop.",
95
+ semantic: "stop_checkpoint",
96
+ bindings: {
97
+ claude: [{ event: "Stop", timeout: 10 }],
98
+ cursor: [{ event: "stop", timeout: 10 }],
99
+ codex: [{ event: "Stop", timeout: 10 }]
100
+ }
101
+ },
102
+ {
103
+ handler: "pre-compact",
104
+ description: "Write pre-compact digest (Claude+Cursor have a native event; Codex has no PreCompact — covered by `/cc-ops retro`).",
105
+ semantic: "precompact_digest",
106
+ bindings: {
107
+ claude: [{ event: "PreCompact", matcher: "manual|auto", timeout: 10 }],
108
+ // pre-compact must capture the digest BEFORE session-start
109
+ // rehydrates on cursor `sessionCompact`.
110
+ cursor: [{ event: "sessionCompact", priority: -10 }]
111
+ }
112
+ },
113
+ {
114
+ handler: "verify-current-state",
115
+ description: "Supplementary codex guard that runs on UserPromptSubmit to assert the live state matches the flow.",
116
+ semantic: null,
117
+ bindings: {
118
+ codex: [{ event: "UserPromptSubmit" }]
119
+ }
120
+ }
121
+ ];
122
+ /** Sanity: every harness in HOOK_MANIFEST_HARNESSES must be a HarnessId. */
123
+ const _harnessIdCheck = HOOK_MANIFEST_HARNESSES;
124
+ void _harnessIdCheck;
125
+ /**
126
+ * Group manifest bindings by harness-native event name. This is the
127
+ * core projection that observe.ts generators consume to emit the
128
+ * harness-specific JSON document.
129
+ */
130
+ export function groupBindingsByEvent(harness) {
131
+ const order = [];
132
+ const byEvent = new Map();
133
+ let seq = 0;
134
+ for (const spec of HOOK_MANIFEST) {
135
+ const bindings = spec.bindings[harness];
136
+ if (!bindings)
137
+ continue;
138
+ for (const binding of bindings) {
139
+ let group = byEvent.get(binding.event);
140
+ if (!group) {
141
+ group = { event: binding.event, entries: [] };
142
+ byEvent.set(binding.event, group);
143
+ order.push(binding.event);
144
+ }
145
+ group.entries.push({
146
+ handler: spec.handler,
147
+ ...(binding.matcher !== undefined ? { matcher: binding.matcher } : {}),
148
+ ...(binding.timeout !== undefined ? { timeout: binding.timeout } : {}),
149
+ priority: binding.priority ?? 0,
150
+ seq: seq++
151
+ });
152
+ }
153
+ }
154
+ return order.map((event) => {
155
+ const group = byEvent.get(event);
156
+ const sorted = [...group.entries].sort((a, b) => {
157
+ if (a.priority !== b.priority)
158
+ return a.priority - b.priority;
159
+ return a.seq - b.seq;
160
+ });
161
+ return {
162
+ event: group.event,
163
+ entries: sorted.map(({ priority: _p, seq: _s, ...entry }) => entry)
164
+ };
165
+ });
166
+ }
167
+ /** Distinct harness-native event names covered by the manifest. */
168
+ export function requiredEventsFor(harness) {
169
+ const seen = new Set();
170
+ const ordered = [];
171
+ for (const spec of HOOK_MANIFEST) {
172
+ for (const binding of spec.bindings[harness] ?? []) {
173
+ if (seen.has(binding.event))
174
+ continue;
175
+ seen.add(binding.event);
176
+ ordered.push(binding.event);
177
+ }
178
+ }
179
+ return ordered;
180
+ }
181
+ /**
182
+ * Human-readable per-harness semantic coverage used by docs and by
183
+ * the doctor's `harness-gaps.json` synthesis.
184
+ */
185
+ export function semanticEventCoverage(harness) {
186
+ const out = {};
187
+ for (const spec of HOOK_MANIFEST) {
188
+ if (spec.semantic === null)
189
+ continue;
190
+ const bindings = spec.bindings[harness];
191
+ if (!bindings || bindings.length === 0)
192
+ continue;
193
+ out[spec.semantic] = describeBindings(bindings);
194
+ }
195
+ return out;
196
+ }
197
+ function describeBindings(bindings) {
198
+ return bindings
199
+ .map((binding) => {
200
+ const pieces = [binding.event];
201
+ if (binding.matcher)
202
+ pieces.push(`matcher=${binding.matcher}`);
203
+ if (binding.timeout)
204
+ pieces.push(`timeout=${binding.timeout}s`);
205
+ return pieces.join(" ");
206
+ })
207
+ .join(" | ");
208
+ }
@@ -1,3 +1,19 @@
1
+ import { type HookHandlerId, type HookManifestHarness } from "./hook-manifest.js";
1
2
  export declare function claudeHooksJsonWithObservation(): string;
2
3
  export declare function cursorHooksJsonWithObservation(): string;
3
4
  export declare function codexHooksJsonWithObservation(): string;
5
+ /**
6
+ * Public accessor so diagnostic CLIs and tests can inspect the
7
+ * manifest without importing the private generator helpers.
8
+ */
9
+ export declare function hookManifestSnapshot(): Array<{
10
+ harness: HookManifestHarness;
11
+ events: Array<{
12
+ event: string;
13
+ entries: Array<{
14
+ handler: HookHandlerId;
15
+ matcher?: string;
16
+ timeout?: number;
17
+ }>;
18
+ }>;
19
+ }>;
@@ -1,4 +1,5 @@
1
1
  import { RUNTIME_ROOT } from "../constants.js";
2
+ import { HOOK_MANIFEST, groupBindingsByEvent } from "./hook-manifest.js";
2
3
  function hookDispatcherCommand(hookName) {
3
4
  // RUNTIME_ROOT is a relative path (".cclaw") that currently contains no
4
5
  // whitespace, so quoting is unnecessary inside the JSON-encoded command
@@ -6,139 +7,83 @@ function hookDispatcherCommand(hookName) {
6
7
  // JSON.stringify to survive spaces.
7
8
  return `node ${RUNTIME_ROOT}/hooks/run-hook.mjs ${hookName}`;
8
9
  }
10
+ /**
11
+ * Claude / Codex share the same outer envelope: each event is an
12
+ * array of `{matcher?, hooks: [{type: "command", command, timeout?}]}`
13
+ * objects. Entries with the same `matcher` are merged into a single
14
+ * outer entry so we emit one `{matcher: "..."}` block with multiple
15
+ * inner hook commands.
16
+ */
17
+ function buildClaudeLikeEvents(harness) {
18
+ const out = {};
19
+ for (const group of groupBindingsByEvent(harness)) {
20
+ const mergedByMatcher = new Map();
21
+ const order = [];
22
+ for (const entry of group.entries) {
23
+ const matcherKey = entry.matcher ?? "__no_matcher__";
24
+ let bucket = mergedByMatcher.get(matcherKey);
25
+ if (!bucket) {
26
+ bucket = {
27
+ ...(entry.matcher !== undefined ? { matcher: entry.matcher } : {}),
28
+ hooks: []
29
+ };
30
+ mergedByMatcher.set(matcherKey, bucket);
31
+ order.push(matcherKey);
32
+ }
33
+ const hookEntry = {
34
+ type: "command",
35
+ command: hookDispatcherCommand(entry.handler),
36
+ ...(entry.timeout !== undefined ? { timeout: entry.timeout } : {})
37
+ };
38
+ bucket.hooks.push(hookEntry);
39
+ }
40
+ out[group.event] = order.map((key) => mergedByMatcher.get(key));
41
+ }
42
+ return out;
43
+ }
44
+ /**
45
+ * Cursor uses a flat shape: each event maps directly to an array of
46
+ * `{command, matcher?, timeout?}` entries — no inner `hooks` array.
47
+ */
48
+ function buildCursorEvents() {
49
+ const out = {};
50
+ for (const group of groupBindingsByEvent("cursor")) {
51
+ out[group.event] = group.entries.map((entry) => ({
52
+ command: hookDispatcherCommand(entry.handler),
53
+ ...(entry.matcher !== undefined ? { matcher: entry.matcher } : {}),
54
+ ...(entry.timeout !== undefined ? { timeout: entry.timeout } : {})
55
+ }));
56
+ }
57
+ return out;
58
+ }
9
59
  export function claudeHooksJsonWithObservation() {
10
60
  return JSON.stringify({
11
61
  cclawHookSchemaVersion: 1,
12
- hooks: {
13
- SessionStart: [{
14
- matcher: "startup|resume|clear|compact",
15
- hooks: [{
16
- type: "command",
17
- command: hookDispatcherCommand("session-start")
18
- }]
19
- }],
20
- PreToolUse: [{
21
- matcher: "*",
22
- hooks: [{
23
- type: "command",
24
- command: hookDispatcherCommand("prompt-guard")
25
- }]
26
- }, {
27
- matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash",
28
- hooks: [{
29
- type: "command",
30
- command: hookDispatcherCommand("workflow-guard")
31
- }]
32
- }],
33
- PostToolUse: [{
34
- matcher: "*",
35
- hooks: [{
36
- type: "command",
37
- command: hookDispatcherCommand("context-monitor")
38
- }]
39
- }],
40
- Stop: [{
41
- hooks: [{
42
- type: "command",
43
- command: hookDispatcherCommand("stop-checkpoint"),
44
- timeout: 10
45
- }]
46
- }],
47
- PreCompact: [{
48
- matcher: "manual|auto",
49
- hooks: [{
50
- type: "command",
51
- command: hookDispatcherCommand("pre-compact"),
52
- timeout: 10
53
- }]
54
- }]
55
- }
62
+ hooks: buildClaudeLikeEvents("claude")
56
63
  }, null, 2);
57
64
  }
58
65
  export function cursorHooksJsonWithObservation() {
59
66
  return JSON.stringify({
60
67
  cclawHookSchemaVersion: 1,
61
68
  version: 1,
62
- hooks: {
63
- sessionStart: [{
64
- command: hookDispatcherCommand("session-start")
65
- }],
66
- sessionResume: [{
67
- command: hookDispatcherCommand("session-start")
68
- }],
69
- sessionClear: [{
70
- command: hookDispatcherCommand("session-start")
71
- }],
72
- sessionCompact: [{
73
- command: hookDispatcherCommand("pre-compact")
74
- }, {
75
- command: hookDispatcherCommand("session-start")
76
- }],
77
- preToolUse: [{
78
- matcher: "*",
79
- command: hookDispatcherCommand("prompt-guard")
80
- }, {
81
- matcher: "*",
82
- command: hookDispatcherCommand("workflow-guard")
83
- }],
84
- postToolUse: [{
85
- matcher: "*",
86
- command: hookDispatcherCommand("context-monitor")
87
- }],
88
- stop: [{
89
- command: hookDispatcherCommand("stop-checkpoint"),
90
- timeout: 10
91
- }]
92
- }
69
+ hooks: buildCursorEvents()
93
70
  }, null, 2);
94
71
  }
95
72
  export function codexHooksJsonWithObservation() {
96
73
  return JSON.stringify({
97
74
  cclawHookSchemaVersion: 1,
98
- hooks: {
99
- SessionStart: [{
100
- matcher: "startup|resume",
101
- hooks: [{
102
- type: "command",
103
- command: hookDispatcherCommand("session-start")
104
- }]
105
- }],
106
- UserPromptSubmit: [{
107
- hooks: [{
108
- type: "command",
109
- command: hookDispatcherCommand("prompt-guard")
110
- }, {
111
- type: "command",
112
- command: hookDispatcherCommand("workflow-guard")
113
- }, {
114
- type: "command",
115
- command: hookDispatcherCommand("verify-current-state")
116
- }]
117
- }],
118
- PreToolUse: [{
119
- matcher: "Bash|bash",
120
- hooks: [{
121
- type: "command",
122
- command: hookDispatcherCommand("prompt-guard")
123
- }, {
124
- type: "command",
125
- command: hookDispatcherCommand("workflow-guard")
126
- }]
127
- }],
128
- PostToolUse: [{
129
- matcher: "Bash|bash",
130
- hooks: [{
131
- type: "command",
132
- command: hookDispatcherCommand("context-monitor")
133
- }]
134
- }],
135
- Stop: [{
136
- hooks: [{
137
- type: "command",
138
- command: hookDispatcherCommand("stop-checkpoint"),
139
- timeout: 10
140
- }]
141
- }]
142
- }
75
+ hooks: buildClaudeLikeEvents("codex")
143
76
  }, null, 2);
144
77
  }
78
+ /**
79
+ * Public accessor so diagnostic CLIs and tests can inspect the
80
+ * manifest without importing the private generator helpers.
81
+ */
82
+ export function hookManifestSnapshot() {
83
+ return (HOOK_MANIFEST.length === 0
84
+ ? ["claude", "cursor", "codex"]
85
+ : ["claude", "cursor", "codex"]).map((harness) => ({
86
+ harness,
87
+ events: groupBindingsByEvent(harness)
88
+ }));
89
+ }
@@ -13,6 +13,7 @@ import { appendKnowledge } from "../knowledge-store.js";
13
13
  import { readFlowState, writeFlowState } from "../runs.js";
14
14
  import { FLOW_STAGES } from "../types.js";
15
15
  import { runCompoundReadinessCommand } from "./compound-readiness.js";
16
+ import { runHookManifestCommand } from "./hook-manifest.js";
16
17
  import { runEnvelopeValidateCommand } from "./envelope-validate.js";
17
18
  import { runKnowledgeDigestCommand } from "./knowledge-digest.js";
18
19
  import { runTddLoopStatusCommand } from "./tdd-loop-status.js";
@@ -674,7 +675,7 @@ async function runHookCommand(projectRoot, args, io) {
674
675
  export async function runInternalCommand(projectRoot, argv, io) {
675
676
  const [subcommand, ...tokens] = argv;
676
677
  if (!subcommand) {
677
- io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook\n");
678
+ io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n");
678
679
  return 1;
679
680
  }
680
681
  try {
@@ -702,10 +703,13 @@ export async function runInternalCommand(projectRoot, argv, io) {
702
703
  if (subcommand === "compound-readiness") {
703
704
  return await runCompoundReadinessCommand(projectRoot, tokens, io);
704
705
  }
706
+ if (subcommand === "hook-manifest") {
707
+ return await runHookManifestCommand(projectRoot, tokens, io);
708
+ }
705
709
  if (subcommand === "hook") {
706
710
  return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
707
711
  }
708
- io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook\n`);
712
+ io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n`);
709
713
  return 1;
710
714
  }
711
715
  catch (err) {
@@ -0,0 +1,16 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ /**
7
+ * `cclaw internal hook-manifest` — diagnostic command that prints
8
+ * the resolved manifest. Primary use cases:
9
+ *
10
+ * - debugging "which handler fires for event X on harness Y",
11
+ * - migration tooling that needs a machine-readable view,
12
+ * - parity verification between the source-of-truth manifest and
13
+ * per-harness generated documents.
14
+ */
15
+ export declare function runHookManifestCommand(_projectRoot: string, argv: string[], io: InternalIo): Promise<number>;
16
+ export {};
@@ -0,0 +1,77 @@
1
+ import { HOOK_MANIFEST, HOOK_MANIFEST_HARNESSES, groupBindingsByEvent, requiredEventsFor } from "../content/hook-manifest.js";
2
+ function parseArgs(tokens) {
3
+ const args = { json: false };
4
+ for (let i = 0; i < tokens.length; i += 1) {
5
+ const token = tokens[i];
6
+ if (token === "--json")
7
+ args.json = true;
8
+ else if (token === "--harness") {
9
+ const value = tokens[i + 1];
10
+ if (value !== "claude" && value !== "cursor" && value !== "codex") {
11
+ throw new Error(`--harness must be one of claude|cursor|codex, got ${String(value)}`);
12
+ }
13
+ args.harness = value;
14
+ i += 1;
15
+ }
16
+ else {
17
+ throw new Error(`Unknown hook-manifest flag: ${token}`);
18
+ }
19
+ }
20
+ return args;
21
+ }
22
+ /**
23
+ * `cclaw internal hook-manifest` — diagnostic command that prints
24
+ * the resolved manifest. Primary use cases:
25
+ *
26
+ * - debugging "which handler fires for event X on harness Y",
27
+ * - migration tooling that needs a machine-readable view,
28
+ * - parity verification between the source-of-truth manifest and
29
+ * per-harness generated documents.
30
+ */
31
+ export async function runHookManifestCommand(_projectRoot, argv, io) {
32
+ const args = parseArgs(argv);
33
+ const harnesses = args.harness ? [args.harness] : [...HOOK_MANIFEST_HARNESSES];
34
+ if (args.json) {
35
+ const payload = {
36
+ handlers: HOOK_MANIFEST.map((spec) => ({
37
+ handler: spec.handler,
38
+ description: spec.description,
39
+ semantic: spec.semantic,
40
+ bindings: spec.bindings
41
+ })),
42
+ byHarness: Object.fromEntries(harnesses.map((harness) => [
43
+ harness,
44
+ {
45
+ requiredEvents: requiredEventsFor(harness),
46
+ events: groupBindingsByEvent(harness)
47
+ }
48
+ ]))
49
+ };
50
+ io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
51
+ return 0;
52
+ }
53
+ const lines = [];
54
+ lines.push("cclaw hook manifest");
55
+ for (const harness of harnesses) {
56
+ lines.push("");
57
+ lines.push(`## ${harness}`);
58
+ const groups = groupBindingsByEvent(harness);
59
+ if (groups.length === 0) {
60
+ lines.push(" (no bindings)");
61
+ continue;
62
+ }
63
+ for (const group of groups) {
64
+ lines.push(` ${group.event}:`);
65
+ for (const entry of group.entries) {
66
+ const parts = [entry.handler];
67
+ if (entry.matcher)
68
+ parts.push(`matcher=${entry.matcher}`);
69
+ if (entry.timeout)
70
+ parts.push(`timeout=${entry.timeout}s`);
71
+ lines.push(` - ${parts.join(" ")}`);
72
+ }
73
+ }
74
+ }
75
+ io.stdout.write(`${lines.join("\n")}\n`);
76
+ return 0;
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.15",
3
+ "version": "0.48.16",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {