SLOPSHOPPER

lexique

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

newstatuspromptagents
v0.2.0no licenseupdated 2026-10-15NeOzay/.claude/skills/lexique
A shopper browsing a rack in a slop shop
Source 1 files
hooks/register.ts 66 lines
1// 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