Verse le lexique local à la session et à un sous-agent à son lancement, sans le plafond de 10 000 caractères d'un hook.

hooks/register.ts 66 lines1// Mod lexique : verse le lexique local à la session principale et à un sous-agent à son lancement.
2//
3// LE TEXTE VIENT DE LA COMMANDE : `lexique session` et `lexique sous-agent <type>` décident de ce
4// qui est versé ; le mod ne porte aucune règle d'injection, il transporte leur sortie. Il les lance
5// par `$.commande.lancer`, que le mod commun fournit.
6//
7// PAS DE HOOK DE `settings.json` : un hook `SessionStart` ou `SubagentStart` plafonne son contexte
8// à 10 000 caractères et n'en garde qu'un aperçu de 2 Ko.
9//
10// ÉCHEC OUVERT : une commande qui échoue se dit sur la ligne d'état, par le mod commun, et laisse la
11// session et le sous-agent démarrer sans lexique. Une erreur du hook lui-même (lecture du projet) se
12// dit par le `.catch` de son enregistrement, qui laisse passer l'événement intact.
13import type { EngineInterface, HookFailure, Register } from 'claude-code'
14
15const ENTETE = 'Lexique versé au démarrage de ce sous-agent par le mod lexique :'
16
17// Le projet dont la commande lit le lexique local : celui de la session, que le processus lancé
18// n'hérite pas toujours.
19const projet = async ($: EngineInterface) =>
20 (await $.env.get('CLAUDE_PROJECT_DIR')) ?? (await $.session.cwd())
21
22// Le moteur saute un hook qui lève et laisse passer l'événement, mais sans rien dire : la ligne
23// d'état le dit. `next.error` est un constat du moteur, non l'erreur levée : son motif est dans
24// `message`, absent d'un dépassement de délai sans rejet.
25const signaler = ($: EngineInterface, echec: HookFailure) =>
26 $.ui.status(`lexique : ${(echec.message ?? echec.kind).slice(0, 60)}`)
27
28/** Pose les deux hooks du mod : la section de session, et le préfixe du prompt d'un sous-agent. */
29export const register: Register = on => {
30 // Une section du prompt système n'a pas de plafond, survit à `/clear` et à la compaction sans se
31 // dupliquer, et ne va pas aux sous-agents. Une conversation reprise ne la recompose pas : elle
32 // garde celle de son début jusqu'à sa compaction. Ni `prompt.context` (tous les sous-agents le
33 // reçoivent, isolés compris) ni `$.session.append` à `session.start` (perdu à `/clear`, dupliqué
34 // à chaque reprise) n'y suffisent. La commande est relancée tant qu'elle ne rend rien.
35 let local: string | undefined
36 on('prompt.compose', async ($, e, next) => {
37 const composition = await next(e)
38 local ??= await $.commande.lancer({
39 argv: ['lexique', 'session'],
40 env: { CLAUDE_PROJECT_DIR: await projet($) },
41 source: 'lexique',
42 })
43 if (!local) return composition
44 return {
45 sections: [...composition.sections, { id: 'lexique:local', text: local, scope: 'session' }],
46 }
47 }).catch(($, e, next) => {
48 signaler($, next.error)
49 return next(e)
50 })
51
52 // Le texte passe par le `prompt` du sous-agent : `$.session.append` ne peut pas le viser avant
53 // `next`, son `agentId` n'existe qu'après.
54 on('agent.spawn', async ($, e, next) => {
55 const texte = await $.commande.lancer({
56 argv: ['lexique', 'sous-agent', e.subagentType],
57 env: { CLAUDE_PROJECT_DIR: await projet($) },
58 source: 'lexique',
59 })
60 return next(texte ? { ...e, prompt: `${ENTETE}\n\n${texte}\n\n---\n\n${e.prompt}` } : e)
61 }).catch(($, e, next) => {
62 signaler($, next.error)
63 return next(e)
64 })
65}
66