SLOPSHOPPER

pv2-actions

Prototype de pipeline-v2 : ouvrir Suivi, Brief et audit en un geste, choix de fin d'Étape en boutons, proposition de commit générée.

newpanebandcommandtoastmodel
v0.1.0no licenseupdated 2026-10-13NeOzay/.claude/pipeline-v2/proto-actions
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · pv2-actions
│ ┃ audit ✕ › fix the failing auth test and add an audit log call │ ┃ Ouvrir │ ┃ /work/app/.claude/implementation/pipeline-v2 ⏺ Read(src/auth.ts) │ ┃ .audit/aucun-audit ⎿ Read 6 lines │ ┃ file:///work/app/.claude/implementation/pipe ⏺ Update(src/auth.ts) │ ┃ line-v2.audit/aucun-audit ⎿ Added 2 lines, removed 1 line │ ┃ absent ⏺ 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 │ │ › /suivi │ ⎿ pv2-actions: suivi ouvert. │ │ Étape terminée — choix (test pv2-actions, rien n’est commité) : [ 1. Valider et passer ][ 2. Valider et attendre ][ 3. Voir le Suivi ][ 4. Autre chose ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Étape terminée — choix (test pv2-actions, rien n’est commité) : [ 1. Valider et passer ][ 2. Valider et attendre ][ 3. Voir le Suivi ][ 4. Autre chose ]
Pane · audit
Ouvrir /work/app/.claude/implementation/pipeline-v2.audit/aucun-aud it file:///work/app/.claude/implementation/pipeline-v2.audit/au cun-audit absent
Pane · Commit proposé
OK [ Valider ][ Refuser ]
Source 1 files
hooks/register.tsx 203 lines
1// Actions du Chantier pipeline-v2 : ce qu'un geste dans l'interface peut déclencher.
2//
3// Prototype jetable, sans gestion d'erreurs (Signal de dérive S3). Il répond aux éléments 2, 3 et
4// 11 des constats : proposition de commit générée, ouverture du Suivi, du Brief et de l'audit,
5// choix de fin d'Étape sans tour du modèle.
6//
7// Aucun bouton ne commit ni ne modifie le Suivi : chacun essaie une voie du moteur et l'écrit au
8// journal. Les quatre choix de fin d'Étape essaient chacun une voie différente — soumettre un
9// message, remplir l'invite, lancer une commande, poser une question dans le dialogue du moteur —
10// pour voir laquelle se passe du modèle.
11
12import type { EngineInterface, Register } from 'claude-code'
13
14const IMPL = '.claude/implementation'
15const SLUG = 'pipeline-v2'
16const FICHIER = 'fichier'
17const PROPOSITION = 'proposition'
18
19// Le diff tronqué : assez pour un message de commit, pas de quoi remplir la fenêtre du modèle.
20const DIFF_MAX = 20_000
21
22type Ouvert = { titre: string; chemin: string; texte: string }
23
24let ouvert: Ouvert | undefined
25let proposition = ''
26let choixVisibles = false
27
28const noter = async ($: EngineInterface, texte: string) => {
29  const chemin = `${$.plugin.root}/journal.log`
30  const avant = await $.fs.read(chemin).catch(() => '')
31  await $.fs.write(chemin, `${avant}${new Date(await $.clock.now()).toISOString()} ${texte}\n`)
32}
33
34const ouvrir = async ($: EngineInterface, titre: string, relatif: string) => {
35  const racine = await $.session.root()
36  const chemin = `${racine}/${relatif}`
37  const texte = await $.fs.read(chemin).catch(() => '*absent*')
38  ouvert = { titre, chemin, texte }
39  const place = await $.ui.open({ id: FICHIER, title: titre })
40  await noter($, `/${titre} ${relatif} ui.open ${JSON.stringify(place)}`)
41  $.ui.invalidate('ui.render')
42
43  return { text: `${titre} ouvert.` }
44}
45
46const dernierAudit = async ($: EngineInterface) => {
47  const dossier = `${IMPL}/${SLUG}.audit`
48  const fichiers = await $.fs.list(dossier).catch(() => [])
49  const nom = fichiers
50    .map(entree => entree.name)
51    .sort()
52    .at(-1)
53
54  return nom === undefined ? `${dossier}/aucun-audit` : `${dossier}/${nom}`
55}
56
57export const register: Register = on => {
58  on('session.start', async ($, e, next) => {
59    for (const [name, description] of [
60      ['suivi', 'pipeline-v2 : ouvre le Suivi'],
61      ['brief', 'pipeline-v2 : ouvre le Brief'],
62      ['audit', 'pipeline-v2 : ouvre le dernier audit'],
63      ['fin', 'pipeline-v2 : montre les choix de fin d’Étape'],
64      ['proposer', 'pipeline-v2 : propose un message de commit pour le diff'],
65    ] as const) {
66      await $.command.register({ name, description })
67    }
68
69    return next(e)
70  })
71
72  on('command.run', { command: 'suivi' }, $ => ouvrir($, 'suivi', `${IMPL}/${SLUG}.md`))
73  on('command.run', { command: 'brief' }, $ => ouvrir($, 'brief', `${IMPL}/${SLUG}.brief.md`))
74  on('command.run', { command: 'audit' }, async $ => ouvrir($, 'audit', await dernierAudit($)))
75
76  on('command.run', { command: 'fin' }, async $ => {
77    choixVisibles = true
78    $.ui.invalidate('ui.render')
79    await noter($, '/fin : choix montrés')
80
81    return { text: 'Choix de fin d’Étape montrés au-dessus de l’invite.' }
82  })
83
84  on('command.run', { command: 'proposer' }, async $ => {
85    const racine = await $.session.root()
86    const stat = await $.process.run(['git', 'diff', 'HEAD', '--stat'], { cwd: racine })
87    const diff = await $.process.run(['git', 'diff', 'HEAD'], { cwd: racine })
88    const debut = await $.clock.now()
89    const reponse = await $.model.complete({
90      model: 'haiku',
91      system:
92        'Tu rédiges un message de commit Conventional Commits, en français, à partir d’un diff. Rends le message seul : un titre de 72 caractères au plus, une ligne vide, puis au plus cinq puces.',
93      prompt: `${stat.stdout}\n\n${diff.stdout.slice(0, DIFF_MAX)}`,
94      maxTokens: 400,
95    })
96    const duree = (await $.clock.now()) - debut
97    proposition = reponse.isAnswered ? reponse.text.trim() : `échec : ${reponse.reason}`
98    await noter(
99      $,
100      `/proposer : ${diff.stdout.length} caractères de diff, ${duree} ms, ${reponse.isAnswered ? `${reponse.usage.output_tokens} tokens rendus` : 'pas de réponse'}`,
101    )
102    const place = await $.ui.open({ id: PROPOSITION, title: 'Commit proposé' })
103    await noter($, `ui.open ${JSON.stringify(place)}`)
104    $.ui.invalidate('ui.render')
105
106    return { text: 'Proposition de commit ouverte.' }
107  })
108
109  on('ui.render', { component: 'Pane', requestId: FICHIER }, async ($, e) => {
110    const { Box, Markdown } = $.ui.resolve(e)
111
112    return (
113      <Box flexDirection="column">
114        <Markdown text={`[Ouvrir ${ouvert?.chemin}](file://${ouvert?.chemin})`} />
115        <Markdown text={ouvert?.texte ?? ''} />
116      </Box>
117    )
118  })
119
120  on('ui.render', { component: 'Pane', requestId: PROPOSITION }, async ($, e) => {
121    const { Box, Button, Code } = $.ui.resolve(e)
122
123    return (
124      <Box flexDirection="column">
125        <Code source={proposition} />
126        <Box>
127          <Button
128            key="valider"
129            label="Valider"
130            onPress={async () => {
131              await noter($, `proposition validée (surface ${e.surface}), rien n’est commité`)
132              $.ui.toast('Proposition validée : journalisée, rien n’est commité.')
133            }}
134          />
135          <Button
136            key="refuser"
137            label="Refuser"
138            onPress={async () => {
139              await noter($, `proposition refusée (surface ${e.surface})`)
140              await $.ui.close({ id: PROPOSITION })
141            }}
142          />
143        </Box>
144      </Box>
145    )
146  })
147
148  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
149    if (e.props.hasSurvey || !choixVisibles) {
150      return next(e)
151    }
152    const { Box, Button, Text } = $.ui.resolve(e)
153
154    return (
155      <Box flexDirection="column">
156        <Text>Étape terminée — choix (test pv2-actions, rien n’est commité) :</Text>
157        <Box>
158          <Button
159            key="passer"
160            label="1. Valider et passer"
161            onPress={async () => {
162              await noter($, `choix passer (surface ${e.surface}) : $.prompt.submit`)
163              const r = await $.prompt.submit({
164                text: 'Test pv2-actions : bouton « Valider et passer » pressé. Ne fais rien ; réponds seulement « reçu ».',
165              })
166              await noter($, `$.prompt.submit → ${JSON.stringify(r)}`)
167            }}
168          />
169          <Button
170            key="attendre"
171            label="2. Valider et attendre"
172            onPress={async () => {
173              await noter($, `choix attendre (surface ${e.surface}) : $.prompt.fill`)
174              const r = await $.prompt.fill({ text: 'Valider et attendre' })
175              await noter($, `$.prompt.fill → ${JSON.stringify(r)}`)
176            }}
177          />
178          <Button
179            key="suivi"
180            label="3. Voir le Suivi"
181            onPress={async () => {
182              await noter($, `choix suivi (surface ${e.surface}) : $.command.run`)
183              const r = await $.command.run({ command: 'suivi' })
184              await noter($, `$.command.run → ${JSON.stringify(r)}`)
185            }}
186          />
187          <Button
188            key="autre"
189            label="4. Autre chose"
190            onPress={async () => {
191              await noter($, `choix autre (surface ${e.surface}) : $.ui.ask`)
192              const r = await $.ui.ask('Que faire de cette Étape ?', ['Retouche', 'Passation'])
193              await noter($, `$.ui.ask → ${r}`)
194              choixVisibles = false
195              $.ui.invalidate('ui.render')
196            }}
197          />
198        </Box>
199      </Box>
200    )
201  })
202}
203