wendkeep 0.65.0 → 0.66.1
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 +44 -0
- package/README.en.md +19 -6
- package/README.md +19 -6
- package/docs/en/commands/getting-started.md +8 -0
- package/docs/en/commands/memory-migration.md +2 -1
- package/docs/en/commands/memory.md +14 -3
- package/docs/pt-BR/commands/getting-started.md +8 -0
- package/docs/pt-BR/commands/memory-migration.md +2 -1
- package/docs/pt-BR/commands/memory.md +13 -3
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/hooks/obsidian-common.mjs +24 -124
- package/hooks/session-identity.mjs +21 -136
- package/hooks/session-stop.mjs +18 -384
- package/hooks/token-usage.mjs +12 -56
- package/package.json +13 -4
- package/packages/cli/src/index.mjs +1 -1
- package/packages/integrations/package.json +2 -1
- package/packages/integrations/src/hook-envelope.mjs +103 -0
- package/packages/integrations/src/host-hooks.mjs +80 -0
- package/packages/integrations/src/index.mjs +6 -0
- package/packages/integrations/src/prompt-content.mjs +20 -0
- package/packages/integrations/src/session-identity.mjs +171 -0
- package/packages/integrations/src/transcript-usage.mjs +53 -0
- package/packages/integrations/src/transcripts.mjs +423 -0
- package/packages/vault/src/memory-schema.mjs +25 -0
- package/packages/vault/src/memory-store.mjs +22 -2
- package/src/memory.mjs +177 -17
- package/src/taxonomy.mjs +24 -78
package/src/memory.mjs
CHANGED
|
@@ -237,37 +237,191 @@ function readCandidates(vault) {
|
|
|
237
237
|
.split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
function priorCandidateDecision(vault, candidateId) {
|
|
241
|
+
return readMemoryLedger(vault).events.find(
|
|
242
|
+
(event) => event.candidate_decision?.candidate_id === candidateId,
|
|
243
|
+
) || null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function candidateEvent(candidate, eventId) {
|
|
247
|
+
const events = Array.isArray(candidate.events) ? candidate.events : [];
|
|
248
|
+
if (candidate.reason === 'conflict' && !eventId) {
|
|
249
|
+
throw new Error('Candidate de conflito exige eventId (--event na CLI).');
|
|
250
|
+
}
|
|
251
|
+
if (!eventId) return events.length === 1 ? events[0] : null;
|
|
252
|
+
const selected = events.find((event) => event.event_id === eventId);
|
|
253
|
+
if (!selected) throw new Error(`event_id ${eventId} não pertence ao candidate ${candidate.candidate_id}.`);
|
|
254
|
+
return selected;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function assertCompatibleDecision(prior, { action, eventId }) {
|
|
258
|
+
const decision = prior.candidate_decision;
|
|
259
|
+
const sameSelection = action !== 'promote'
|
|
260
|
+
|| (decision.selected_event_id || null) === (eventId || null);
|
|
261
|
+
if (decision.action !== action || !sameSelection) {
|
|
262
|
+
throw new Error(`Candidate ${decision.candidate_id} já possui decisão incompatível (${decision.action}).`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function matchesPromotedAttempt(attempt, selected) {
|
|
267
|
+
if (attempt?.memory_mode !== 'v2' || attempt.state !== 'projected'
|
|
268
|
+
|| !Array.isArray(attempt.event_ids) || !attempt.event_ids.includes(selected.event_id)) return false;
|
|
269
|
+
if (attempt.activation_id && selected.activation_id
|
|
270
|
+
&& attempt.activation_id !== selected.activation_id) return false;
|
|
271
|
+
if (Number.isInteger(attempt.activation_epoch) && Number.isInteger(selected.activation_epoch)
|
|
272
|
+
&& attempt.activation_epoch !== selected.activation_epoch) return false;
|
|
273
|
+
if (Number.isInteger(attempt.turn_sequence) && Number.isInteger(selected.turn_sequence)
|
|
274
|
+
&& attempt.turn_sequence !== selected.turn_sequence) return false;
|
|
275
|
+
if (attempt.canonical_session_id && selected.canonical_session_id
|
|
276
|
+
&& attempt.canonical_session_id !== selected.canonical_session_id) return false;
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function snapshotPromotedAttemptCheckpoints(vault, selected) {
|
|
281
|
+
if (!selected) return new Map();
|
|
282
|
+
const registry = readSessionRegistry(vault);
|
|
283
|
+
return new Map(Object.entries(registry.sessions || {})
|
|
284
|
+
.filter(([, entry]) => matchesPromotedAttempt(entry?.last_memory_attempt, selected))
|
|
285
|
+
.map(([sessionId, entry]) => [sessionId, {
|
|
286
|
+
attempt: attemptFingerprint(entry.last_memory_attempt),
|
|
287
|
+
checkpoint: memoryCheckpointFingerprint(entry),
|
|
288
|
+
}]));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function refreshPromotedAttemptCheckpoint(vault, {
|
|
292
|
+
candidateId, decisionEventId, selected, checkpoint, decidedAt, expectedAttempts,
|
|
293
|
+
}) {
|
|
294
|
+
if (!selected || !checkpoint || !expectedAttempts?.size) return 0;
|
|
295
|
+
return mutateSessionRegistry(vault, (registry) => {
|
|
296
|
+
let refreshed = 0;
|
|
297
|
+
for (const [sessionId, entry] of Object.entries(registry.sessions || {})) {
|
|
298
|
+
const expected = expectedAttempts.get(sessionId);
|
|
299
|
+
if (!expected) continue;
|
|
300
|
+
const attempt = entry?.last_memory_attempt;
|
|
301
|
+
if (attemptFingerprint(attempt) !== expected.attempt
|
|
302
|
+
|| memoryCheckpointFingerprint(entry) !== expected.checkpoint) continue;
|
|
303
|
+
|
|
304
|
+
const alreadyAudited = (entry.memory_candidate_decisions || [])
|
|
305
|
+
.some((audit) => audit.decision_event_id === decisionEventId);
|
|
306
|
+
if (alreadyAudited && sameCheckpoint(attempt.checkpoint, checkpoint)
|
|
307
|
+
&& sameCheckpoint(entry.memory_checkpoint, checkpoint)) continue;
|
|
308
|
+
const originalCheckpoint = cloneJson(attempt.checkpoint || entry.memory_checkpoint || null);
|
|
309
|
+
attempt.checkpoint = cloneJson(checkpoint);
|
|
310
|
+
entry.memory_checkpoint = cloneJson(checkpoint);
|
|
311
|
+
entry.memory_status = 'projected';
|
|
312
|
+
if (!alreadyAudited) {
|
|
313
|
+
entry.memory_candidate_decisions = [
|
|
314
|
+
...(Array.isArray(entry.memory_candidate_decisions) ? entry.memory_candidate_decisions : []),
|
|
315
|
+
{
|
|
316
|
+
v: 1,
|
|
317
|
+
type: 'candidate_checkpoint_refreshed',
|
|
318
|
+
candidate_id: candidateId,
|
|
319
|
+
decision_event_id: decisionEventId,
|
|
320
|
+
selected_event_id: selected.event_id,
|
|
321
|
+
decided_at: decidedAt,
|
|
322
|
+
original_checkpoint: originalCheckpoint,
|
|
323
|
+
checkpoint: cloneJson(checkpoint),
|
|
324
|
+
},
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
refreshed += 1;
|
|
328
|
+
}
|
|
329
|
+
return refreshed;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function decideMemoryCandidate(vault, {
|
|
334
|
+
action, candidateId, value, eventId, beforeCheckpointRefresh,
|
|
335
|
+
} = {}) {
|
|
241
336
|
if (!['promote', 'reject'].includes(action)) throw new TypeError('action deve ser promote ou reject.');
|
|
242
337
|
if (!candidateId) throw new TypeError('candidateId é obrigatório.');
|
|
338
|
+
const preflight = projectMemoryOutbox(vault);
|
|
339
|
+
if (preflight.status === 'busy') return { status: 'busy', candidateId };
|
|
340
|
+
const prior = priorCandidateDecision(vault, candidateId);
|
|
341
|
+
if (prior) {
|
|
342
|
+
assertCompatibleDecision(prior, { action, eventId });
|
|
343
|
+
const selected = action === 'promote' && prior.candidate_decision.selected_event_id
|
|
344
|
+
? readMemoryLedger(vault).events.find(
|
|
345
|
+
(event) => event.event_id === prior.candidate_decision.selected_event_id,
|
|
346
|
+
)
|
|
347
|
+
: null;
|
|
348
|
+
const expectedAttempts = snapshotPromotedAttemptCheckpoints(vault, selected);
|
|
349
|
+
beforeCheckpointRefresh?.();
|
|
350
|
+
const checkpointRefreshed = action === 'promote'
|
|
351
|
+
? refreshPromotedAttemptCheckpoint(vault, {
|
|
352
|
+
candidateId,
|
|
353
|
+
decisionEventId: prior.event_id,
|
|
354
|
+
selected,
|
|
355
|
+
checkpoint: preflight.checkpoint,
|
|
356
|
+
decidedAt: prior.observed_at,
|
|
357
|
+
expectedAttempts,
|
|
358
|
+
})
|
|
359
|
+
: 0;
|
|
360
|
+
return {
|
|
361
|
+
status: action === 'promote' ? 'promoted' : 'rejected',
|
|
362
|
+
candidateId,
|
|
363
|
+
eventId: prior.event_id,
|
|
364
|
+
alreadyApplied: true,
|
|
365
|
+
checkpointRefreshed,
|
|
366
|
+
projection: preflight,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
243
369
|
const candidates = readCandidates(vault);
|
|
244
370
|
const candidate = candidates.find((item) => item.candidate_id === candidateId);
|
|
245
371
|
if (!candidate) throw new Error(`Candidate não encontrado: ${candidateId}`);
|
|
372
|
+
if (action === 'promote' && candidate.reason === 'blocked_by_core') {
|
|
373
|
+
throw new Error(`Candidate ${candidateId} está blocked_by_core; edite CORE ou rejeite o candidate.`);
|
|
374
|
+
}
|
|
375
|
+
const selected = action === 'promote' ? candidateEvent(candidate, eventId) : null;
|
|
376
|
+
const expectedAttempts = snapshotPromotedAttemptCheckpoints(vault, selected);
|
|
377
|
+
const selectedValue = selected?.value ?? value ?? candidate.value ?? candidate.proposed_value;
|
|
378
|
+
if (action === 'promote' && selectedValue === undefined) {
|
|
379
|
+
throw new Error(`Candidate ${candidateId} não contém valor promovível.`);
|
|
380
|
+
}
|
|
246
381
|
const now = new Date().toISOString();
|
|
382
|
+
const decision = {
|
|
383
|
+
candidate_id: candidateId,
|
|
384
|
+
action,
|
|
385
|
+
event_ids: Array.isArray(candidate.event_ids) ? [...candidate.event_ids].sort() : [],
|
|
386
|
+
...(selected ? { selected_event_id: selected.event_id } : {}),
|
|
387
|
+
};
|
|
247
388
|
const event = {
|
|
248
389
|
v: 1,
|
|
249
|
-
event_id: `cli-${action}-${hash(candidateId).slice(0, 20)}`,
|
|
390
|
+
event_id: `cli-${action}-${hash(`${candidateId}\0${selected?.event_id || ''}`).slice(0, 20)}`,
|
|
250
391
|
project_id: projectId(vault),
|
|
251
|
-
memory_key: action === 'promote' ? candidate.memory_key : `candidate.
|
|
252
|
-
operation: 'assert',
|
|
253
|
-
value: sanitizeMemoryText(action === 'promote' ?
|
|
392
|
+
memory_key: action === 'promote' ? candidate.memory_key : `candidate.decision.${candidateId}`,
|
|
393
|
+
operation: action === 'promote' && selected ? 'replace' : 'assert',
|
|
394
|
+
value: sanitizeMemoryText(action === 'promote' ? selectedValue : 'rejected'),
|
|
254
395
|
authority: 'verified',
|
|
255
|
-
activation_id: 'wendkeep-memory-cli',
|
|
256
|
-
|
|
396
|
+
activation_id: selected?.activation_id || 'wendkeep-memory-cli',
|
|
397
|
+
...(Number.isInteger(selected?.activation_epoch) ? { activation_epoch: selected.activation_epoch } : {}),
|
|
398
|
+
turn_sequence: selected?.turn_sequence ?? 0,
|
|
257
399
|
observed_at: now,
|
|
258
400
|
evidence: [`candidate:${candidateId}`],
|
|
401
|
+
candidate_decision: decision,
|
|
402
|
+
...(selected ? { supersedes: decision.event_ids } : {}),
|
|
259
403
|
};
|
|
260
404
|
enqueueMemoryEvent(vault, event);
|
|
261
405
|
const projection = projectMemoryOutbox(vault);
|
|
262
406
|
if (projection.status === 'busy') return { status: 'busy', candidateId };
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
407
|
+
beforeCheckpointRefresh?.();
|
|
408
|
+
const checkpointRefreshed = action === 'promote'
|
|
409
|
+
? refreshPromotedAttemptCheckpoint(vault, {
|
|
410
|
+
candidateId,
|
|
411
|
+
decisionEventId: event.event_id,
|
|
412
|
+
selected,
|
|
413
|
+
checkpoint: projection.checkpoint,
|
|
414
|
+
decidedAt: now,
|
|
415
|
+
expectedAttempts,
|
|
416
|
+
})
|
|
417
|
+
: 0;
|
|
418
|
+
return {
|
|
419
|
+
status: action === 'promote' ? 'promoted' : 'rejected',
|
|
420
|
+
candidateId,
|
|
421
|
+
eventId: event.event_id,
|
|
422
|
+
checkpointRefreshed,
|
|
423
|
+
projection,
|
|
424
|
+
};
|
|
271
425
|
}
|
|
272
426
|
|
|
273
427
|
function cloneJson(value) {
|
|
@@ -1038,8 +1192,14 @@ export function runMemory(argv) {
|
|
|
1038
1192
|
apply: reconcileArgs.apply,
|
|
1039
1193
|
});
|
|
1040
1194
|
}
|
|
1041
|
-
else if (sub === 'promote' || sub === 'reject')
|
|
1042
|
-
|
|
1195
|
+
else if (sub === 'promote' || sub === 'reject') {
|
|
1196
|
+
const eventId = option(argv, '--event');
|
|
1197
|
+
if (sub === 'reject' && eventId) throw memoryUsageError('--event é permitido somente em memory promote.');
|
|
1198
|
+
result = decideMemoryCandidate(vault, {
|
|
1199
|
+
action: sub, candidateId: positional, ...(eventId ? { eventId } : {}),
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
else { process.stderr.write('wendkeep memory: use status | migrate [--apply] | repair | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
|
|
1043
1203
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1044
1204
|
if (sub === 'status' && argv.includes('--gate')) process.exitCode = result.status === 'blocked' ? 1 : 0;
|
|
1045
1205
|
else if (sub === 'reconcile' && reconcileArgs.apply) process.exitCode = result.health?.status === 'blocked' ? 1 : 0;
|
package/src/taxonomy.mjs
CHANGED
|
@@ -3,8 +3,31 @@ import {
|
|
|
3
3
|
mcpServerEntry,
|
|
4
4
|
selectMcpServers,
|
|
5
5
|
} from '../packages/mcp/src/index.mjs';
|
|
6
|
+
import {
|
|
7
|
+
CHANGE_GATE_HOOKS,
|
|
8
|
+
CHANGE_NUDGE_HOOKS,
|
|
9
|
+
CODEX_MATCHER_EVENTS,
|
|
10
|
+
SESSION_HOOKS,
|
|
11
|
+
codexHookEntry,
|
|
12
|
+
codexHookSpecs,
|
|
13
|
+
hookCommand,
|
|
14
|
+
hookCommandLocal,
|
|
15
|
+
hookCommandLocalLegacy,
|
|
16
|
+
} from '../packages/integrations/src/host-hooks.mjs';
|
|
6
17
|
|
|
7
|
-
export {
|
|
18
|
+
export {
|
|
19
|
+
CHANGE_GATE_HOOKS,
|
|
20
|
+
CHANGE_NUDGE_HOOKS,
|
|
21
|
+
CODEX_MATCHER_EVENTS,
|
|
22
|
+
MCP_SERVER_KEY,
|
|
23
|
+
SESSION_HOOKS,
|
|
24
|
+
codexHookEntry,
|
|
25
|
+
codexHookSpecs,
|
|
26
|
+
hookCommand,
|
|
27
|
+
hookCommandLocal,
|
|
28
|
+
hookCommandLocalLegacy,
|
|
29
|
+
mcpServerEntry,
|
|
30
|
+
};
|
|
8
31
|
|
|
9
32
|
// Shared, data-only constants for the wendkeep installer and CLI.
|
|
10
33
|
// Kept free of side effects so both bin/ and src/ can import it cheaply.
|
|
@@ -101,83 +124,6 @@ export const RUNNABLE_HOOKS = [
|
|
|
101
124
|
'plan-capture',
|
|
102
125
|
];
|
|
103
126
|
|
|
104
|
-
// The three Claude Code session hooks, expressed as `wendkeep hook <name>` so the
|
|
105
|
-
// installed package is the single source of truth (update with `npm update wendkeep`,
|
|
106
|
-
// no re-copying). Returned as a spec the merge logic folds into settings.json.
|
|
107
|
-
export const SESSION_HOOKS = [
|
|
108
|
-
// Memory + active-change injection. Runs FIRST on SessionStart (order -10, folds before
|
|
109
|
-
// session-start) so the agent gets CORE + DIGEST + the active change + lessons as context.
|
|
110
|
-
// matcher 'startup|clear|compact' re-injects after a compaction/clear, not only cold startup.
|
|
111
|
-
// timeout 45 (was 15): measured ~4s warm via npx, but Windows startup contention (several npx
|
|
112
|
-
// cold-starts at once — a sibling MCP took 26s in a real log) blew 15s and silently dropped the
|
|
113
|
-
// memory injection for the whole session.
|
|
114
|
-
{ event: 'SessionStart', matcher: 'startup|clear|compact', name: 'brain-inject', timeout: 45, order: -10, codex: true, statusMessage: 'wendkeep: injecting memory + active change' },
|
|
115
|
-
{ event: 'SessionStart', matcher: 'startup', name: 'session-start', timeout: 30, codex: true, statusMessage: 'wendkeep: opening Obsidian session' },
|
|
116
|
-
{ event: 'Stop', matcher: null, name: 'session-stop', timeout: 60, codex: true, statusMessage: 'wendkeep: writing session checkpoint' },
|
|
117
|
-
{ event: 'UserPromptSubmit', matcher: null, name: 'session-ensure', timeout: 30, codex: true, statusMessage: 'wendkeep: ensuring active session' },
|
|
118
|
-
// Capture an interactive decision (AskUserQuestion) — options + the user's choice — into 04-Decisões.
|
|
119
|
-
// codex: AskUserQuestion is a Claude-only tool; there is nothing to match on.
|
|
120
|
-
{ event: 'PostToolUse', matcher: 'AskUserQuestion', name: 'decision-capture', timeout: 15, statusMessage: 'wendkeep: recording decision' },
|
|
121
|
-
// Refresh subagent/workflow telemetry as each subagent finishes (resilient to a missed Stop).
|
|
122
|
-
{ event: 'SubagentStop', matcher: null, name: 'subagent-stop', timeout: 20, codex: true, statusMessage: 'wendkeep: subagent telemetry' },
|
|
123
|
-
// Log plan/task progress into the active session note when a task is marked complete.
|
|
124
|
-
// codex: TaskCompleted is not in Codex's hook event enum.
|
|
125
|
-
{ event: 'TaskCompleted', matcher: null, name: 'task-log', timeout: 10, statusMessage: 'wendkeep: plan progress' },
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
export function hookCommand(name) {
|
|
129
|
-
return `npx wendkeep hook ${name}`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Forma node-direta do comando de hook: 1 processo (~100-250ms) em vez dos 3 do npx (cold-start
|
|
133
|
-
// de segundos no Windows). Usada pelos hooks de ALTA FREQUÊNCIA (por prompt / por tool-call)
|
|
134
|
-
// quando o projeto tem wendkeep instalado localmente; o init decide (hookCommandFor).
|
|
135
|
-
export function hookCommandLocal(name) {
|
|
136
|
-
return `node "${'${CLAUDE_PROJECT_DIR}'}/node_modules/wendkeep/hooks/${name}.mjs"`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function hookCommandLocalLegacy(name) {
|
|
140
|
-
return `node node_modules/wendkeep/hooks/${name}.mjs`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Hooks do lifecycle de change (0.31.0) — enforcement do loop a2. Nudges (contexto/aviso/
|
|
144
|
-
// cobrança/captura de plano) e gate (deny/ask no Bash). Separados em dois grupos para
|
|
145
|
-
// preservar a opção futura de gates opt-in; hoje o init wira TODOS por default.
|
|
146
|
-
// preferLocal: alta frequência → invocação node-direta quando houver instalação local.
|
|
147
|
-
export const CHANGE_NUDGE_HOOKS = [
|
|
148
|
-
{ event: 'UserPromptSubmit', matcher: null, name: 'change-context', timeout: 15, order: 10, preferLocal: true, codex: true, statusMessage: 'wendkeep: change ping' },
|
|
149
|
-
// codex: reads tool_input.file_path, which Codex's apply_patch envelope does not carry.
|
|
150
|
-
{ event: 'PostToolUse', matcher: 'Edit|Write|MultiEdit', name: 'change-warn', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change warn' },
|
|
151
|
-
// codex: no ExitPlanMode equivalent — update_plan is the running TODO list, not an approval.
|
|
152
|
-
{ event: 'PostToolUse', matcher: 'ExitPlanMode', name: 'plan-capture', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: capturing approved plan' },
|
|
153
|
-
{ event: 'Stop', matcher: null, name: 'change-nag', timeout: 15, order: 10, preferLocal: true, codex: true, statusMessage: 'wendkeep: open tasks check' },
|
|
154
|
-
];
|
|
155
|
-
export const CHANGE_GATE_HOOKS = [
|
|
156
|
-
// codex: reads tool_input.command; Codex's exec sends a raw string and exec_command an argv,
|
|
157
|
-
// so the guard would silently fail OPEN — worse than absent, since the docs would promise it.
|
|
158
|
-
{ event: 'PreToolUse', matcher: 'Bash', name: 'change-guard', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change gate' },
|
|
159
|
-
];
|
|
160
|
-
|
|
161
|
-
// --- Codex projection ---------------------------------------------------------
|
|
162
|
-
// Codex reads <project>/.codex/hooks.json (PascalCase event keys, same group shape as
|
|
163
|
-
// Claude's settings.json). Only specs that opt in with `codex: true` are projected — the
|
|
164
|
-
// rest carry a `// codex:` comment above them saying why. Three deltas from Claude, each
|
|
165
|
-
// verified against codex-rs and each silent when wrong: the timeout key is `timeoutSec`
|
|
166
|
-
// (`timeout` is not a field and falls through to a 600s default), there is no
|
|
167
|
-
// ${CLAUDE_PROJECT_DIR} so `preferLocal` never applies, and matcher is only honoured on
|
|
168
|
-
// SessionStart (UserPromptSubmit/Stop null it at discovery).
|
|
169
|
-
export const CODEX_MATCHER_EVENTS = new Set(['SessionStart']);
|
|
170
|
-
|
|
171
|
-
export function codexHookSpecs(specs) {
|
|
172
|
-
return specs.filter((h) => h.codex === true && !h.command);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function codexHookEntry(spec) {
|
|
176
|
-
const entry = { type: 'command', command: hookCommand(spec.name), timeoutSec: spec.timeout };
|
|
177
|
-
if (spec.statusMessage) entry.statusMessage = spec.statusMessage;
|
|
178
|
-
return entry;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
127
|
// --- companion plugins / MCP --------------------------------------------------
|
|
182
128
|
// Optional tools wendkeep init can pin alongside the vault. Each is wired through
|
|
183
129
|
// the MOST agent-agnostic mechanism it supports; the Claude Code plugin entry
|