baldart 4.86.0 → 4.88.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/CHANGELOG.md +39 -0
- package/VERSION +1 -1
- package/framework/.claude/hooks/agent-discovery-info.sh +30 -3
- package/framework/.claude/hooks/framework-edit-gate.js +147 -14
- package/framework/agents/skills-mapping.md +30 -1
- package/framework/docs/CODEX-AGENTS.md +4 -0
- package/framework/docs/CODEX-HOOKS.md +142 -0
- package/framework/templates/primitives/AGENTS.CHANGELOG.md +9 -0
- package/framework/templates/primitives/AGENTS.md +5 -1
- package/package.json +1 -1
- package/src/commands/add.js +45 -0
- package/src/commands/doctor.js +104 -0
- package/src/commands/update.js +31 -0
- package/src/utils/hooks/claude.js +402 -0
- package/src/utils/hooks/codex.js +360 -0
- package/src/utils/hooks/registry.js +176 -0
- package/src/utils/hooks.js +17 -467
- package/src/utils/tool-adapters/claude.js +3 -0
- package/src/utils/tool-adapters/codex.js +12 -1
package/src/utils/hooks.js
CHANGED
|
@@ -1,473 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Consumer-side hook registration
|
|
2
|
+
* Consumer-side hook registration — compatibility shim.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
4
|
+
* The hook machinery is split for cross-tool parity (BALDART Codex-parity
|
|
5
|
+
* foundation, S1):
|
|
6
|
+
* - `hooks/registry.js` — the runtime-neutral hook set (one entry per logical
|
|
7
|
+
* hook, with per-runtime bindings).
|
|
8
|
+
* - `hooks/claude.js` — the Claude Code renderer (`.claude/settings.json`).
|
|
9
|
+
* - `hooks/codex.js` — the Codex renderer (`.codex/hooks.json`, S2 slice).
|
|
7
10
|
*
|
|
8
|
-
*
|
|
11
|
+
* This file preserves the historical import path `require('../utils/hooks')`
|
|
12
|
+
* and its exact public surface (`registerAll` / `verifyAll` / `getStatus` /
|
|
13
|
+
* `unregisterAll` / `createDriftPrompt` / `HOOK_REGISTRY` / `SETTINGS_FILE`),
|
|
14
|
+
* which every call site in `add` / `update` / `doctor` uses to manage the
|
|
15
|
+
* **Claude** hooks. Codex hook management is reached explicitly via
|
|
16
|
+
* `require('../utils/hooks/codex')`, never through this shim, so existing
|
|
17
|
+
* Claude behavior is byte-for-byte unchanged.
|
|
9
18
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the registry, so call sites don't change when we ship a new hook.
|
|
13
|
-
*
|
|
14
|
-
* Each hook entry carries:
|
|
15
|
-
* - `id` stable marker stored in settings.json under `hooks[].id`
|
|
16
|
-
* - `event` Claude Code event (`PreToolUse`, `Stop`, `SessionStart`, …)
|
|
17
|
-
* - `matcher` tool-name regex (use '*' for events that don't take one)
|
|
18
|
-
* - `command` shell command Claude Code runs
|
|
19
|
-
* - `order` sort key when this hook coexists with other BALDART hooks
|
|
20
|
-
* on the same event (lower runs first)
|
|
21
|
-
* - `legacyMatch` (optional) substring that identifies pre-v3.18 entries
|
|
22
|
-
* when migrating consumers from the single-hook layout
|
|
23
|
-
*
|
|
24
|
-
* **Drift policy**
|
|
25
|
-
*
|
|
26
|
-
* When an existing entry matches our `id` but its `command` differs, we don't
|
|
27
|
-
* silently rewrite it — that would obliterate user customizations. Instead we
|
|
28
|
-
* call back to `onDrift({ id, event, matcher, expected, actual })` and expect
|
|
29
|
-
* `'keep'` or `'replace'`. Callers wire `onDrift` to an inquirer prompt
|
|
30
|
-
* (with a `'diff'` option that shows the diff and re-prompts). The default
|
|
31
|
-
* when `onDrift` is not provided is `'keep'` — safe for non-interactive runs.
|
|
32
|
-
*
|
|
33
|
-
* **Failure isolation**
|
|
34
|
-
*
|
|
35
|
-
* Claude Code's runtime executes hooks on the same event in array order and
|
|
36
|
-
* isolates failures itself (a non-zero exit from a Stop/SessionStart hook
|
|
37
|
-
* does not block the others). Our only responsibility is to never corrupt
|
|
38
|
-
* the array of hooks the user has set up themselves — entries we don't own
|
|
39
|
-
* (no `id` of ours, no `legacyMatch` of ours) are preserved verbatim and
|
|
40
|
-
* stay in their original position.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
const fs = require('fs');
|
|
44
|
-
const path = require('path');
|
|
45
|
-
|
|
46
|
-
const SETTINGS_FILE = path.join('.claude', 'settings.json');
|
|
47
|
-
|
|
48
|
-
/** @typedef {Object} HookDef
|
|
49
|
-
* @property {string} id
|
|
50
|
-
* @property {string} event Claude Code event name
|
|
51
|
-
* @property {string} matcher tool-name regex, or '*' for matcherless events
|
|
52
|
-
* @property {string} command shell command
|
|
53
|
-
* @property {number} order ascending sort key among BALDART hooks on same event
|
|
54
|
-
* @property {string} [legacyMatch] substring marker for pre-v3.18 entries to migrate
|
|
55
|
-
*/
|
|
56
|
-
|
|
57
|
-
/** @type {HookDef[]} */
|
|
58
|
-
const HOOK_REGISTRY = [
|
|
59
|
-
{
|
|
60
|
-
id: 'baldart-framework-edit-gate',
|
|
61
|
-
event: 'PreToolUse',
|
|
62
|
-
matcher: 'Edit|Write|MultiEdit|NotebookEdit',
|
|
63
|
-
command: 'node .framework/framework/.claude/hooks/framework-edit-gate.js',
|
|
64
|
-
order: 100,
|
|
65
|
-
legacyMatch: 'framework-edit-gate.js',
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
// Denies `Agent` tool calls whose subagent_type matches a BALDART agent
|
|
69
|
-
// name but the consumer's .claude/agents/<name>.md is missing or its
|
|
70
|
-
// symlink is broken. Mitigates anthropics/claude-code#56869 (silent
|
|
71
|
-
// sub-agent fallback) and #20931 (broken file-based discovery) for the
|
|
72
|
-
// filesystem-detectable subset.
|
|
73
|
-
id: 'baldart-agent-discovery-gate',
|
|
74
|
-
event: 'PreToolUse',
|
|
75
|
-
matcher: 'Agent',
|
|
76
|
-
command: 'node .framework/framework/.claude/hooks/agent-discovery-gate.js',
|
|
77
|
-
order: 200,
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
// Injects an additionalContext warning at session start listing BALDART
|
|
81
|
-
// agents whose .claude/agents/<name>.md is missing. Best-effort early
|
|
82
|
-
// signal (Claude Code #10373 may suppress injection on new
|
|
83
|
-
// conversations) — the blocking counterpart is agent-discovery-gate.
|
|
84
|
-
id: 'baldart-agent-discovery-info',
|
|
85
|
-
event: 'SessionStart',
|
|
86
|
-
matcher: '*',
|
|
87
|
-
command: 'bash .framework/framework/.claude/hooks/agent-discovery-info.sh',
|
|
88
|
-
order: 100,
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
// Observation-only telemetry: records every Read of a
|
|
92
|
-
// `.baldart/overlays/*.md` file to `.baldart/telemetry/overlay-loads.jsonl`.
|
|
93
|
-
// Measures, objectively, whether skill overlays are actually loaded at
|
|
94
|
-
// runtime (skill overlays use runtime-concat, which is NOT structurally
|
|
95
|
-
// enforced — unlike agent/command overlays, which are compile-time merged).
|
|
96
|
-
// Numerator-only by design: skill invocation is prompt-expansion, not a
|
|
97
|
-
// tool call, so the denominator is not hook-observable (claude-code #43630,
|
|
98
|
-
// #22655). bash, not node — it fires on EVERY Read (hottest tool) in the
|
|
99
|
-
// critical path, so a per-read node cold-start would tax every file read.
|
|
100
|
-
// Fail-safe — never blocks. See framework/docs/OVERLAY-TELEMETRY.md.
|
|
101
|
-
id: 'baldart-overlay-telemetry',
|
|
102
|
-
event: 'PostToolUse',
|
|
103
|
-
matcher: 'Read',
|
|
104
|
-
command: 'bash .framework/framework/.claude/hooks/overlay-telemetry.sh',
|
|
105
|
-
order: 100,
|
|
106
|
-
},
|
|
107
|
-
// Future hooks (T1.3, T1.4, T2.3) will be appended here, e.g.:
|
|
108
|
-
// {
|
|
109
|
-
// id: 'baldart-capture-detect',
|
|
110
|
-
// event: 'Stop',
|
|
111
|
-
// matcher: '*',
|
|
112
|
-
// command: 'bash .framework/framework/.claude/hooks/capture-detect.sh',
|
|
113
|
-
// order: 100,
|
|
114
|
-
// },
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// settings.json I/O
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
function readSettings(cwd = process.cwd()) {
|
|
122
|
-
const full = path.join(cwd, SETTINGS_FILE);
|
|
123
|
-
if (!fs.existsSync(full)) return { path: full, settings: null, malformed: false };
|
|
124
|
-
try {
|
|
125
|
-
const raw = fs.readFileSync(full, 'utf8');
|
|
126
|
-
if (!raw.trim()) return { path: full, settings: {}, malformed: false };
|
|
127
|
-
return { path: full, settings: JSON.parse(raw), malformed: false };
|
|
128
|
-
} catch (_) {
|
|
129
|
-
return { path: full, settings: null, malformed: true };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function writeSettings(settingsPath, settings) {
|
|
134
|
-
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
135
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// Internal: locate an entry that owns a given hook definition
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Walks `settings.hooks[event]` and returns the first entry/hook pair whose
|
|
144
|
-
* `id` matches, or whose `command` contains `legacyMatch` (pre-v3.18 layout).
|
|
145
|
-
* Returns `null` when the hook is not yet registered.
|
|
19
|
+
* NOT to be confused with `src/utils/githooks.js`, which manages the repo's
|
|
20
|
+
* versioned git hooks (`core.hooksPath`).
|
|
146
21
|
*/
|
|
147
|
-
function findOwnedEntry(eventList, def) {
|
|
148
|
-
if (!Array.isArray(eventList)) return null;
|
|
149
|
-
for (let i = 0; i < eventList.length; i++) {
|
|
150
|
-
const entry = eventList[i];
|
|
151
|
-
if (!entry || !Array.isArray(entry.hooks)) continue;
|
|
152
|
-
for (let j = 0; j < entry.hooks.length; j++) {
|
|
153
|
-
const h = entry.hooks[j];
|
|
154
|
-
if (!h) continue;
|
|
155
|
-
if (h.id === def.id) return { entryIdx: i, hookIdx: j, entry, hook: h, byLegacy: false };
|
|
156
|
-
if (def.legacyMatch && h.type === 'command'
|
|
157
|
-
&& typeof h.command === 'string' && h.command.includes(def.legacyMatch)) {
|
|
158
|
-
return { entryIdx: i, hookIdx: j, entry, hook: h, byLegacy: true };
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// Status — read-only inspection of every hook in the registry
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* @typedef {Object} HookStatus
|
|
171
|
-
* @property {string} id
|
|
172
|
-
* @property {string} event
|
|
173
|
-
* @property {boolean} registered hook with this id exists in settings.json
|
|
174
|
-
* @property {boolean} drift registered but command differs from expected
|
|
175
|
-
* @property {boolean} legacy matched only by legacyMatch (id is missing)
|
|
176
|
-
* @property {string} expected command BALDART wants
|
|
177
|
-
* @property {string} [actual] command currently in settings.json (when registered)
|
|
178
|
-
*/
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Returns the status of every hook in the registry. Pure read; never writes.
|
|
182
|
-
*
|
|
183
|
-
* @param {string} cwd
|
|
184
|
-
* @returns {{ malformed: boolean, path: string, hooks: HookStatus[] }}
|
|
185
|
-
*/
|
|
186
|
-
function getStatus(cwd = process.cwd()) {
|
|
187
|
-
const { path: settingsPath, settings, malformed } = readSettings(cwd);
|
|
188
|
-
if (malformed) {
|
|
189
|
-
return { malformed: true, path: settingsPath, hooks: [] };
|
|
190
|
-
}
|
|
191
|
-
const allHooks = (settings && settings.hooks) || {};
|
|
192
|
-
const hooks = HOOK_REGISTRY.map((def) => {
|
|
193
|
-
const found = findOwnedEntry(allHooks[def.event], def);
|
|
194
|
-
if (!found) {
|
|
195
|
-
return {
|
|
196
|
-
id: def.id, event: def.event,
|
|
197
|
-
registered: false, drift: false, legacy: false,
|
|
198
|
-
expected: def.command,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
const actual = found.hook.command;
|
|
202
|
-
return {
|
|
203
|
-
id: def.id, event: def.event,
|
|
204
|
-
registered: true,
|
|
205
|
-
drift: actual !== def.command,
|
|
206
|
-
legacy: found.byLegacy,
|
|
207
|
-
expected: def.command,
|
|
208
|
-
actual,
|
|
209
|
-
};
|
|
210
|
-
});
|
|
211
|
-
return { malformed: false, path: settingsPath, hooks };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// Register — write path with drift callback
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Idempotently register every hook in the registry.
|
|
220
|
-
*
|
|
221
|
-
* @param {string} [cwd]
|
|
222
|
-
* @param {Object} [options]
|
|
223
|
-
* @param {(info: { id: string, event: string, matcher: string,
|
|
224
|
-
* expected: string, actual: string })
|
|
225
|
-
* => Promise<'keep'|'replace'>|('keep'|'replace')} [options.onDrift]
|
|
226
|
-
* Called when a hook with our `id` exists but command differs.
|
|
227
|
-
* Default: `'keep'` (safe for non-interactive / CI).
|
|
228
|
-
* @returns {Promise<{ malformed: boolean, path: string,
|
|
229
|
-
* created: boolean, updated: boolean,
|
|
230
|
-
* results: Array<{ id: string, status:
|
|
231
|
-
* 'created'|'already'|'drift-replaced'|'drift-kept'|'legacy-migrated',
|
|
232
|
-
* previousCommand?: string }> }>}
|
|
233
|
-
*/
|
|
234
|
-
async function registerAll(cwd = process.cwd(), options = {}) {
|
|
235
|
-
const onDrift = typeof options.onDrift === 'function'
|
|
236
|
-
? options.onDrift
|
|
237
|
-
: () => 'keep';
|
|
238
|
-
|
|
239
|
-
const { path: settingsPath, settings: existing, malformed } = readSettings(cwd);
|
|
240
|
-
if (malformed) {
|
|
241
|
-
return { malformed: true, path: settingsPath, created: false, updated: false, results: [] };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const wasNew = !existing;
|
|
245
|
-
const settings = existing && typeof existing === 'object' ? structuredClone(existing) : {};
|
|
246
|
-
settings.hooks = settings.hooks && typeof settings.hooks === 'object'
|
|
247
|
-
? settings.hooks
|
|
248
|
-
: {};
|
|
249
|
-
|
|
250
|
-
const results = [];
|
|
251
|
-
let modified = false;
|
|
252
|
-
|
|
253
|
-
for (const def of HOOK_REGISTRY) {
|
|
254
|
-
const list = Array.isArray(settings.hooks[def.event])
|
|
255
|
-
? settings.hooks[def.event]
|
|
256
|
-
: [];
|
|
257
|
-
|
|
258
|
-
const found = findOwnedEntry(list, def);
|
|
259
|
-
|
|
260
|
-
if (!found) {
|
|
261
|
-
list.push({
|
|
262
|
-
matcher: def.matcher,
|
|
263
|
-
hooks: [{ id: def.id, type: 'command', command: def.command }],
|
|
264
|
-
});
|
|
265
|
-
settings.hooks[def.event] = list;
|
|
266
|
-
results.push({ id: def.id, status: 'created' });
|
|
267
|
-
modified = true;
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (found.byLegacy) {
|
|
272
|
-
// Pre-v3.18 entry: stamp our id on it and align the command.
|
|
273
|
-
const previousCommand = found.hook.command;
|
|
274
|
-
found.hook.id = def.id;
|
|
275
|
-
found.hook.command = def.command;
|
|
276
|
-
results.push({ id: def.id, status: 'legacy-migrated', previousCommand });
|
|
277
|
-
modified = true;
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (found.hook.command === def.command) {
|
|
282
|
-
results.push({ id: def.id, status: 'already' });
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Drift: same id, different command. Ask the caller.
|
|
287
|
-
const decision = await onDrift({
|
|
288
|
-
id: def.id,
|
|
289
|
-
event: def.event,
|
|
290
|
-
matcher: def.matcher,
|
|
291
|
-
expected: def.command,
|
|
292
|
-
actual: found.hook.command,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
if (decision === 'replace') {
|
|
296
|
-
const previousCommand = found.hook.command;
|
|
297
|
-
found.hook.command = def.command;
|
|
298
|
-
results.push({ id: def.id, status: 'drift-replaced', previousCommand });
|
|
299
|
-
modified = true;
|
|
300
|
-
} else {
|
|
301
|
-
results.push({
|
|
302
|
-
id: def.id, status: 'drift-kept', previousCommand: found.hook.command,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (modified) writeSettings(settingsPath, settings);
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
malformed: false,
|
|
311
|
-
path: settingsPath,
|
|
312
|
-
created: modified && wasNew,
|
|
313
|
-
updated: modified && !wasNew,
|
|
314
|
-
results,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// ---------------------------------------------------------------------------
|
|
319
|
-
// Unregister — remove every BALDART hook from settings.json
|
|
320
|
-
// ---------------------------------------------------------------------------
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Remove every hook defined in the registry from settings.json. Idempotent.
|
|
324
|
-
*
|
|
325
|
-
* @param {string} [cwd]
|
|
326
|
-
* @returns {{ malformed: boolean, path: string,
|
|
327
|
-
* results: Array<{ id: string, status: 'removed'|'not-found' }> }}
|
|
328
|
-
*/
|
|
329
|
-
function unregisterAll(cwd = process.cwd()) {
|
|
330
|
-
const { path: settingsPath, settings, malformed } = readSettings(cwd);
|
|
331
|
-
if (malformed || !settings || !settings.hooks) {
|
|
332
|
-
return {
|
|
333
|
-
malformed,
|
|
334
|
-
path: settingsPath,
|
|
335
|
-
results: HOOK_REGISTRY.map((d) => ({ id: d.id, status: 'not-found' })),
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const next = structuredClone(settings);
|
|
340
|
-
const results = [];
|
|
341
|
-
let modified = false;
|
|
342
|
-
|
|
343
|
-
for (const def of HOOK_REGISTRY) {
|
|
344
|
-
const list = Array.isArray(next.hooks[def.event]) ? next.hooks[def.event] : null;
|
|
345
|
-
if (!list) {
|
|
346
|
-
results.push({ id: def.id, status: 'not-found' });
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
let removed = false;
|
|
350
|
-
const filtered = list
|
|
351
|
-
.map((entry) => {
|
|
352
|
-
if (!entry || !Array.isArray(entry.hooks)) return entry;
|
|
353
|
-
const innerKept = entry.hooks.filter((h) => {
|
|
354
|
-
if (!h) return true;
|
|
355
|
-
if (h.id === def.id) { removed = true; return false; }
|
|
356
|
-
if (def.legacyMatch && h.type === 'command'
|
|
357
|
-
&& typeof h.command === 'string' && h.command.includes(def.legacyMatch)) {
|
|
358
|
-
removed = true;
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
return true;
|
|
362
|
-
});
|
|
363
|
-
if (innerKept.length === 0) return null;
|
|
364
|
-
return { ...entry, hooks: innerKept };
|
|
365
|
-
})
|
|
366
|
-
.filter(Boolean);
|
|
367
|
-
|
|
368
|
-
if (removed) {
|
|
369
|
-
if (filtered.length === 0) {
|
|
370
|
-
delete next.hooks[def.event];
|
|
371
|
-
} else {
|
|
372
|
-
next.hooks[def.event] = filtered;
|
|
373
|
-
}
|
|
374
|
-
modified = true;
|
|
375
|
-
results.push({ id: def.id, status: 'removed' });
|
|
376
|
-
} else {
|
|
377
|
-
results.push({ id: def.id, status: 'not-found' });
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (Object.keys(next.hooks).length === 0) delete next.hooks;
|
|
382
|
-
if (modified) writeSettings(settingsPath, next);
|
|
383
|
-
|
|
384
|
-
return { malformed: false, path: settingsPath, results };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// ---------------------------------------------------------------------------
|
|
388
|
-
// Verify — post-flight assertion that every active hook is registered
|
|
389
|
-
// ---------------------------------------------------------------------------
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Confirms that every active hook in HOOK_REGISTRY is present in
|
|
393
|
-
* `.claude/settings.json`. Pure read (delegates to `getStatus`). Designed as
|
|
394
|
-
* the post-flight gate for `add` / `update --reset`: the install must never
|
|
395
|
-
* declare success while a hook is missing (the silent partial-install class,
|
|
396
|
-
* v3.27.1 / mayo 3.28→3.31). Commented-out registry entries (e.g.
|
|
397
|
-
* capture-detect) are excluded for free — `getStatus` only iterates the array.
|
|
398
|
-
*
|
|
399
|
-
* A malformed settings.json counts as a failure: registration was skipped, so
|
|
400
|
-
* the hooks are not verifiably present.
|
|
401
|
-
*
|
|
402
|
-
* @param {string} [cwd]
|
|
403
|
-
* @returns {{ ok: boolean, malformed: boolean, missing: string[], path: string }}
|
|
404
|
-
*/
|
|
405
|
-
function verifyAll(cwd = process.cwd()) {
|
|
406
|
-
const status = getStatus(cwd);
|
|
407
|
-
if (status.malformed) {
|
|
408
|
-
return {
|
|
409
|
-
ok: false,
|
|
410
|
-
malformed: true,
|
|
411
|
-
missing: HOOK_REGISTRY.map((d) => d.id),
|
|
412
|
-
path: status.path,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
const missing = status.hooks.filter((h) => !h.registered).map((h) => h.id);
|
|
416
|
-
return { ok: missing.length === 0, malformed: false, missing, path: status.path };
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// ---------------------------------------------------------------------------
|
|
420
|
-
// Drift prompt helper (interactive Keep / Replace / Show diff)
|
|
421
|
-
// ---------------------------------------------------------------------------
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Build an `onDrift` callback suitable for `registerAll`. Wraps inquirer via
|
|
425
|
-
* the project's UI helper so the CLI output stays consistent.
|
|
426
|
-
*
|
|
427
|
-
* @param {Object} UI the `src/utils/ui.js` module (avoids a
|
|
428
|
-
* circular require by accepting it from
|
|
429
|
-
* the caller)
|
|
430
|
-
* @param {Object} [options]
|
|
431
|
-
* @param {boolean} [options.autoYes] non-interactive mode: skip prompt and
|
|
432
|
-
* always return 'keep' (safe default)
|
|
433
|
-
* @returns {(info: {id:string, event:string, expected:string, actual:string})
|
|
434
|
-
* => Promise<'keep'|'replace'>}
|
|
435
|
-
*/
|
|
436
|
-
function createDriftPrompt(UI, { autoYes = false } = {}) {
|
|
437
|
-
if (autoYes) {
|
|
438
|
-
return async ({ id }) => {
|
|
439
|
-
UI.warning(`Hook drift detected for \`${id}\` — keeping current command (auto mode). Run \`baldart doctor\` interactively to resolve.`);
|
|
440
|
-
return 'keep';
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const truncate = (s, n = 70) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
|
|
445
|
-
|
|
446
|
-
return async function onDrift({ id, event, expected, actual }) {
|
|
447
|
-
UI.warning(`Hook \`${id}\` (${event}) is registered with a non-default command.`);
|
|
448
|
-
// Loop while the user keeps choosing 'diff'
|
|
449
|
-
for (;;) {
|
|
450
|
-
const choice = await UI.select(
|
|
451
|
-
`What do you want to do with \`${id}\`?`,
|
|
452
|
-
[
|
|
453
|
-
{ name: `Keep current — leave: ${truncate(actual)}`, value: 'keep' },
|
|
454
|
-
{ name: `Replace with BALDART — set: ${truncate(expected)}`, value: 'replace' },
|
|
455
|
-
{ name: `Show full diff and decide`, value: 'diff' },
|
|
456
|
-
],
|
|
457
|
-
);
|
|
458
|
-
if (choice !== 'diff') return choice;
|
|
459
|
-
console.log(` expected: ${expected}`);
|
|
460
|
-
console.log(` actual: ${actual}`);
|
|
461
|
-
}
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
22
|
|
|
465
|
-
module.exports =
|
|
466
|
-
HOOK_REGISTRY,
|
|
467
|
-
SETTINGS_FILE,
|
|
468
|
-
getStatus,
|
|
469
|
-
verifyAll,
|
|
470
|
-
registerAll,
|
|
471
|
-
unregisterAll,
|
|
472
|
-
createDriftPrompt,
|
|
473
|
-
};
|
|
23
|
+
module.exports = require('./hooks/claude');
|
|
@@ -51,6 +51,9 @@ class ClaudeAdapter {
|
|
|
51
51
|
/** Whether this tool consumes the Claude PreToolUse hook system. */
|
|
52
52
|
supportsHooks() { return true; }
|
|
53
53
|
|
|
54
|
+
/** Where this tool reads its hook configuration from. */
|
|
55
|
+
hookTargetPath() { return '.claude/settings.json'; }
|
|
56
|
+
|
|
54
57
|
/**
|
|
55
58
|
* Autodetection signal. We assume Claude is always intended (it's the
|
|
56
59
|
* framework's primary target). The opt-out is via `tools.enabled` in
|
|
@@ -74,7 +74,18 @@ class CodexAdapter {
|
|
|
74
74
|
|
|
75
75
|
supportsSubagents() { return true; }
|
|
76
76
|
supportsSlashCommands() { return false; }
|
|
77
|
-
|
|
77
|
+
|
|
78
|
+
// Codex has a native hook surface using the SAME schema as Claude Code, in
|
|
79
|
+
// `.codex/hooks.json` (project-level). BALDART manages it via a parallel
|
|
80
|
+
// renderer (`src/utils/hooks/codex.js`) that registers the framework-edit-gate
|
|
81
|
+
// (PreToolUse/apply_patch) and agent-discovery-info (SessionStart) hooks
|
|
82
|
+
// additively — preserving foreign entries (e.g. Graphify) and never faking
|
|
83
|
+
// hook trust. (Enabled once the Codex renderer + add/update/doctor wiring
|
|
84
|
+
// landed in the Codex-parity foundation, S2.)
|
|
85
|
+
supportsHooks() { return true; }
|
|
86
|
+
|
|
87
|
+
/** Where this tool reads its hook configuration from. */
|
|
88
|
+
hookTargetPath() { return '.codex/hooks.json'; }
|
|
78
89
|
|
|
79
90
|
/**
|
|
80
91
|
* Heuristic: Codex is present on the user's machine if `~/.codex/` exists
|