SLOPSHOPPER

pv2-veille

Prototype de pipeline-v2 : surveille les fichiers touchés, les Signaux de dérive et les sous-agents d'un Chantier.

newbandguardcommandtoastprocess
v0.1.0no licenseupdated 2026-10-13NeOzay/.claude/pipeline-v2/proto-veille
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · pv2-veille
› fix the failing auth test and add an audit log call ╭───────────────────────────────╮ │ pv2-veille │ ⏺ Read(src/auth.ts) │ Signal de dérive : S1, S2, S6 │ ⎿ Read 6 lines ╰───────────────────────────────╯ ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /lever ⎿ pv2-veille: La bande va lever à son prochain dessin. Étape · prévus : aucun · hors prévus : src/auth.ts, src/audit.ts, src/cache.ts Signaux : tombés S1, S2, S6 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Étape · prévus : aucun · hors prévus : src/auth.ts, src/audit.ts, src/cache.ts Signaux : tombés S1, S2, S6
Source 1 files
hooks/register.tsx 160 lines
1// Veille du Chantier pipeline-v2 : ce que des hooks voient pendant que le modèle travaille.
2//
3// Prototype jetable, sans gestion d'erreurs (Signal de dérive S3). Il répond aux éléments 5, 7 et
4// 12 des constats : fichiers touchés face aux fichiers de l'Étape en cours, état et verdict des
5// sous-agents, Signaux de dérive vérifiables par commande.
6//
7// Les fichiers prévus sont ceux de la ligne de l'Étape `[>]` du Suivi, entre accents graves, avant
8// « vérif ». Les Signaux se relancent après chaque `Edit` ou `Write`, chacun par sa commande du
9// Brief ; un toast dit celui qui tombe. Un sous-agent se suit par l'appel de l'outil `Agent` :
10// lancé avant `next`, terminé quand `next` rend, son verdict lu dans sa réponse.
11//
12// `/lever` fait lever la bande, `/lever-commande` fait lever la commande elle-même : ce que le
13// moteur montre d'un hook qui échoue est la matière de la politique d'échec.
14
15import type { EngineInterface, Register } from 'claude-code'
16
17const SUIVI = '.claude/implementation/pipeline-v2.md'
18
19// Les Signaux de dérive du Brief qu'une commande vérifie : elle sort 0 quand le Signal tombe.
20const SIGNAUX = {
21  S1: 'test -n "$(git diff master --stat -- skills/implementation-tracker skills/intent-brief skills/git-smart-commit)"',
22  S2: 'test -n "$(git diff master --name-only -- skills .claude/skills settings.json)" || grep -q CLAUDE_CODE_PLUGIN_DIRS settings.json',
23  S6: 'test -n "$(git diff master -- LEXIQUE.md)"',
24} as const
25
26type Agent = { id: string; type: string; etat: string; verdict: string }
27
28let prevus: string[] = []
29let etape = ''
30const horsPrevus = new Set<string>()
31let tombes: string[] = []
32const agents: Agent[] = []
33let leve = false
34
35const noter = async ($: EngineInterface, texte: string) => {
36  const chemin = `${$.plugin.root}/journal.log`
37  const avant = await $.fs.read(chemin).catch(() => '')
38  await $.fs.write(chemin, `${avant}${new Date(await $.clock.now()).toISOString()} ${texte}\n`)
39}
40
41const lirePrevus = async ($: EngineInterface) => {
42  const suivi = await $.fs.read(`${await $.session.root()}/${SUIVI}`)
43  const ligne = suivi.match(/^- \[>\] (\d+)\. ([\s\S]*?)— vérif/m)
44  etape = ligne?.[1] ?? '?'
45  prevus = [...(ligne?.[2] ?? '').matchAll(/`([^`]+)`/g)].map(m => m[1] ?? '')
46}
47
48const verifier = async ($: EngineInterface) => {
49  const racine = await $.session.root()
50  const avant = tombes
51  tombes = []
52  for (const [nom, commande] of Object.entries(SIGNAUX)) {
53    const r = await $.process.run(['bash', '-c', commande], { cwd: racine })
54    if (r.exitCode === 0) {
55      tombes.push(nom)
56    }
57  }
58  const nouveaux = tombes.filter(nom => !avant.includes(nom))
59  if (nouveaux.length > 0) {
60    $.ui.toast(`Signal de dérive : ${nouveaux.join(', ')}`, { timeoutMs: 15000 })
61  }
62  await noter($, `signaux tombés : ${tombes.join(', ') || 'aucun'}`)
63  $.ui.invalidate('ui.render')
64}
65
66export const register: Register = on => {
67  on('session.start', async ($, e, next) => {
68    await $.command.register({ name: 'lever', description: 'pv2-veille : fait lever la bande' })
69    await $.command.register({
70      name: 'lever-commande',
71      description: 'pv2-veille : fait lever cette commande',
72    })
73    await lirePrevus($)
74    await noter($, `session.start : Étape ${etape}, prévus ${JSON.stringify(prevus)}`)
75    await verifier($)
76
77    return next(e)
78  })
79
80  on('tool.call', async ($, e, next) => {
81    const ran = await next(e)
82    if (e.tool !== 'Edit' && e.tool !== 'Write') {
83      return ran
84    }
85    const racine = await $.session.root()
86    const relatif = e.file_path.startsWith(`${racine}/`)
87      ? e.file_path.slice(racine.length + 1)
88      : e.file_path
89    const estPrevu = prevus.some(prevu => relatif === prevu || relatif.startsWith(prevu))
90    if (!estPrevu) {
91      horsPrevus.add(relatif)
92    }
93    await noter($, `${e.tool} ${relatif} : ${estPrevu ? 'prévu' : 'hors prévus'}`)
94    await verifier($)
95
96    return ran
97  })
98
99  on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
100    const agent: Agent = {
101      id: e.tool_use_id,
102      type: e.subagent_type ?? 'general-purpose',
103      etat: 'en cours',
104      verdict: '',
105    }
106    agents.push(agent)
107    await noter($, `Agent lancé : ${agent.type} (${e.description})`)
108    $.ui.invalidate('ui.render')
109    const ran = await next(e)
110    const texte = ran.text ?? JSON.stringify(ran.result ?? '')
111    agent.etat = ran.deny !== undefined ? 'refusé' : ran.isError === true ? 'en erreur' : 'terminé'
112    agent.verdict = texte.match(/VERDICT\s*:\s*([A-ZÉ ]+)/)?.[1]?.trim() ?? ''
113    await noter(
114      $,
115      `Agent ${agent.etat} : ${agent.type}, verdict « ${agent.verdict || 'aucun'} », ${texte.length} caractères`,
116    )
117    $.ui.invalidate('ui.render')
118
119    return ran
120  })
121
122  on('command.run', { command: 'lever' }, async () => {
123    leve = true
124
125    return { text: 'La bande va lever à son prochain dessin.' }
126  })
127
128  on('command.run', { command: 'lever-commande' }, async () => {
129    throw new Error('pv2-veille : erreur voulue dans command.run')
130  })
131
132  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
133    if (leve) {
134      throw new Error('pv2-veille : erreur voulue dans ui.render')
135    }
136    if (e.props.hasSurvey) {
137      return next(e)
138    }
139    const { Box, Text } = $.ui.resolve(e)
140    const dernier = agents.at(-1)
141
142    return (
143      <Box flexDirection="column">
144        <Text>
145          Étape {etape} · prévus : {prevus.join(', ') || 'aucun'}
146          {horsPrevus.size > 0 ? ` · hors prévus : ${[...horsPrevus].join(', ')}` : ''}
147        </Text>
148        <Text>Signaux : {tombes.length === 0 ? 'aucun tombé' : `tombés ${tombes.join(', ')}`}</Text>
149        {dernier && (
150          <Text>
151            Agents : {agents.filter(agent => agent.etat === 'en cours').length} en cours · dernier{' '}
152            {dernier.type} {dernier.etat}
153            {dernier.verdict ? ` — ${dernier.verdict}` : ''}
154          </Text>
155        )}
156      </Box>
157    )
158  })
159}
160