@roomi-fields/grillhouse 0.0.0-stage → 0.23.0

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.
Files changed (166) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/METACADRE.md +41 -0
  3. package/agents/developpeur.md +10 -0
  4. package/agents/explorateur.md +9 -0
  5. package/agents/questionneur.md +11 -0
  6. package/agents/referent-technique.md +9 -0
  7. package/agents/regulateur.md +11 -0
  8. package/agents/relecteur.md +9 -0
  9. package/agents/testwriter.md +10 -0
  10. package/bin/grillhouse.mjs +125 -0
  11. package/cadre/templates/projet/.claude/settings.json +30 -0
  12. package/cadre/templates/projet/.editorconfig +47 -0
  13. package/cadre/templates/projet/.env.example +57 -0
  14. package/cadre/templates/projet/.eslintrc.cjs +56 -0
  15. package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
  16. package/cadre/templates/projet/.mcp.json +13 -0
  17. package/cadre/templates/projet/.prettierrc +11 -0
  18. package/cadre/templates/projet/CHANGELOG.md +61 -0
  19. package/cadre/templates/projet/CLAUDE.md +46 -0
  20. package/cadre/templates/projet/CONTRIBUTING.md +243 -0
  21. package/cadre/templates/projet/CREDITS.md +41 -0
  22. package/cadre/templates/projet/README.md +152 -0
  23. package/cadre/templates/projet/_gitignore +91 -0
  24. package/cadre/templates/projet/_npmignore +46 -0
  25. package/cadre/templates/projet/deployment/README.md +98 -0
  26. package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
  27. package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
  28. package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
  29. package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
  30. package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
  31. package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
  32. package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
  33. package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
  34. package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
  35. package/cadre/templates/projet/ecosystem.config.cjs +73 -0
  36. package/cadre/templates/projet/package.json +90 -0
  37. package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
  38. package/cadre/templates/projet/src/config.ts +68 -0
  39. package/cadre/templates/projet/src/errors.ts +102 -0
  40. package/cadre/templates/projet/src/index.ts +46 -0
  41. package/cadre/templates/projet/src/services/.gitkeep +8 -0
  42. package/cadre/templates/projet/src/types/index.ts +24 -0
  43. package/cadre/templates/projet/src/utils/logger.ts +106 -0
  44. package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
  45. package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
  46. package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
  47. package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
  48. package/cadre/templates/projet/tsconfig.eslint.json +9 -0
  49. package/cadre/templates/projet/tsconfig.json +41 -0
  50. package/cadre/templates/projet/tsdoc.json +6 -0
  51. package/cadre/templates/projet/vitest.config.ts +44 -0
  52. package/cadre/templates/turbo/paquets.json +29 -0
  53. package/cadre/templates/turbo/sans-paquets.json +28 -0
  54. package/charte.md +107 -0
  55. package/docs/ARCHITECTURE.md +90 -0
  56. package/hooks/crochet.mjs +33 -0
  57. package/hooks/hooks.json +58 -0
  58. package/hooks/register.tsx +558 -0
  59. package/package.json +75 -4
  60. package/scripts/avancement.mjs +244 -0
  61. package/scripts/bilan.mjs +523 -0
  62. package/scripts/branche-de-nuit.mjs +59 -0
  63. package/scripts/cadre.mjs +109 -0
  64. package/scripts/circuit.mjs +4375 -0
  65. package/scripts/code-mort.mjs +371 -0
  66. package/scripts/composants-affectes.mjs +598 -0
  67. package/scripts/consommateurs-arbitres.mjs +192 -0
  68. package/scripts/consommateurs.mjs +310 -0
  69. package/scripts/crochet.mjs +24 -0
  70. package/scripts/crochets.mjs +135 -0
  71. package/scripts/dernier-vert.mjs +208 -0
  72. package/scripts/dette.mjs +181 -0
  73. package/scripts/droits.mjs +354 -0
  74. package/scripts/enveloppe/codes.mjs +17 -0
  75. package/scripts/enveloppe/composant.mjs +11 -0
  76. package/scripts/enveloppe/enveloppe.mjs +628 -0
  77. package/scripts/enveloppe/lancer.sh +311 -0
  78. package/scripts/etapes.mjs +323 -0
  79. package/scripts/etat-du-circuit.mjs +104 -0
  80. package/scripts/frontieres.mjs +456 -0
  81. package/scripts/gardes-du-cadre.mjs +72 -0
  82. package/scripts/greffons.mjs +338 -0
  83. package/scripts/grillhouse-maj.mjs +514 -0
  84. package/scripts/index-interfaces.mjs +293 -0
  85. package/scripts/init.mjs +117 -0
  86. package/scripts/integration/file-attente.mjs +61 -0
  87. package/scripts/integration/file.mjs +494 -0
  88. package/scripts/integration/integrer.mjs +381 -0
  89. package/scripts/integration/rejeu.mjs +139 -0
  90. package/scripts/integration/verdict.mjs +13 -0
  91. package/scripts/interfaces-contre-code.mjs +547 -0
  92. package/scripts/ligne-etat/defaut.sh +110 -0
  93. package/scripts/ligne-etat/ligne-etat.mjs +93 -0
  94. package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
  95. package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
  96. package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
  97. package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
  98. package/scripts/migrations.mjs +43 -0
  99. package/scripts/nuit.sh +269 -0
  100. package/scripts/paquets.mjs +79 -0
  101. package/scripts/poussee-cles.mjs +46 -0
  102. package/scripts/preparation.mjs +84 -0
  103. package/scripts/references-par-les-types.mjs +420 -0
  104. package/scripts/registre.mjs +336 -0
  105. package/scripts/resultats.mjs +580 -0
  106. package/scripts/resumes.mjs +57 -0
  107. package/scripts/retirer-copie.sh +127 -0
  108. package/scripts/session-start.sh +203 -0
  109. package/scripts/sortie.mjs +155 -0
  110. package/scripts/sous-modules.sh +316 -0
  111. package/scripts/structure-guard.sh +27 -0
  112. package/scripts/superviseur.mjs +102 -0
  113. package/scripts/tableau.mjs +1886 -0
  114. package/scripts/tests.mjs +236 -0
  115. package/scripts/ticket.sh +23 -0
  116. package/scripts/turbo-des-paquets.mjs +899 -0
  117. package/scripts/un-commit-par-ticket.mjs +212 -0
  118. package/scripts/verifier.mjs +469 -0
  119. package/scripts/verrou-de-fichier.mjs +130 -0
  120. package/scripts/verrou-de-machine.mjs +211 -0
  121. package/scripts/verrous/commande.mjs +180 -0
  122. package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
  123. package/scripts/verrous/verrou.mjs +165 -0
  124. package/scripts/vitest-config/avancement.mjs +39 -0
  125. package/scripts/vitest-config/avancement.reporter.mjs +56 -0
  126. package/scripts/vitest-config/base-de-passe.mjs +128 -0
  127. package/scripts/vitest-config/base.d.ts +6 -0
  128. package/scripts/vitest-config/base.mjs +87 -0
  129. package/scripts/vitest-config/changements.mjs +173 -0
  130. package/scripts/vitest-config/cibles.mjs +207 -0
  131. package/scripts/vitest-config/duree.reporter.mjs +98 -0
  132. package/scripts/vitest-config/machine.setup.mjs +14 -0
  133. package/scripts/vitest-config/mesure.d.ts +6 -0
  134. package/scripts/vitest-config/mesure.mjs +13 -0
  135. package/scripts/vitest-config/mesure.setup.mjs +12 -0
  136. package/scripts/vitest-config/pression.mjs +90 -0
  137. package/scripts/vitest-config/resultats.mjs +23 -0
  138. package/scripts/vitest-config/resultats.setup.mjs +19 -0
  139. package/scripts/vitest.nuit.config.mjs +26 -0
  140. package/skills/developpeur/SKILL.md +175 -0
  141. package/skills/explorateur/SKILL.md +84 -0
  142. package/skills/grill/SKILL.md +238 -0
  143. package/skills/grill/references/releve-structurel.md +38 -0
  144. package/skills/mesure/SKILL.md +168 -0
  145. package/skills/pitmaster/SKILL.md +382 -0
  146. package/skills/pitmaster/references/cadre.md +119 -0
  147. package/skills/pitmaster/references/consigne-agent.md +308 -0
  148. package/skills/pitmaster/references/erreurs.md +25 -0
  149. package/skills/pitmaster/references/tour.md +124 -0
  150. package/skills/questionneur/SKILL.md +64 -0
  151. package/skills/redacteur/SKILL.md +84 -0
  152. package/skills/redacteur/references/documents-d-un-composant.md +105 -0
  153. package/skills/referent-technique/SKILL.md +145 -0
  154. package/skills/regulateur/SKILL.md +220 -0
  155. package/skills/release/SKILL.md +134 -0
  156. package/skills/release/scripts/update-version.cjs +92 -0
  157. package/skills/relecteur/SKILL.md +152 -0
  158. package/skills/relecteur/references/LICENSE-MIT +30 -0
  159. package/skills/relecteur/references/SOURCE.md +8 -0
  160. package/skills/relecteur/references/claims-check.md +14 -0
  161. package/skills/relecteur/references/deletion-check.md +14 -0
  162. package/skills/relecteur/references/edge-case-hunter.md +80 -0
  163. package/skills/relecteur/references/verification-gap.md +113 -0
  164. package/skills/testwriter/SKILL.md +187 -0
  165. package/types/index.d.ts +56 -0
  166. package/README.md +0 -3
@@ -0,0 +1,558 @@
1
+ import { atom, read, update } from 'claude-code'
2
+ import type { EngineInterface, Register } from 'claude-code'
3
+
4
+ import type { Etat, Fiche, Ligne, Tableau } from '../types'
5
+
6
+ // The board of a Grillhouse project, in the pane (/grillhouse) on its three levels. The frame's
7
+ // command of the project (`node_modules/.bin/grillhouse tableau`, the package
8
+ // @roomi-fields/grillhouse) measures and lays out the lines, and writes the status line for the
9
+ // person's status line command; this mod shows the lines, every 30 s (every 10 s while a line of
10
+ // progress shows) and after each `bd` or `git` command. A Grillhouse project is one whose
11
+ // package.json declares the package (`dependencies` or `devDependencies`); the mod stays silent
12
+ // outside one and inside an agent's copy (`.claude/worktrees/`).
13
+ const PANE = 'grillhouse'
14
+ // The frame's command, which the project's `npm install` links, and its package.
15
+ const GRILLHOUSE = 'node_modules/.bin/grillhouse'
16
+ const PAQUET = '@roomi-fields/grillhouse'
17
+
18
+ // Whether a package.json's text declares the frame's package.
19
+ export function declareLePaquet(texte: string | null): boolean {
20
+ try {
21
+ const p = JSON.parse(texte ?? '') as { dependencies?: object; devDependencies?: object }
22
+ return Object.hasOwn(p.dependencies ?? {}, PAQUET) || Object.hasOwn(p.devDependencies ?? {}, PAQUET)
23
+ } catch {
24
+ return false
25
+ }
26
+ }
27
+ const PERIODE = 30_000
28
+ // The width of the lines `grillhouse tableau` lays out (its LARGEUR).
29
+ const COLONNES = 52
30
+ const DOCK = 110
31
+
32
+ // Where the pane sits, and what it takes to have it docked beside the transcript.
33
+ export function placement(p: { isFullscreen: boolean; columns: number }): string {
34
+ if (p.isFullscreen && p.columns >= DOCK) return 'Tableau Grillhouse ouvert sur le côté.'
35
+ const manque = [
36
+ ...(p.isFullscreen ? [] : ["l'affichage plein écran de Claude Code (/tui fullscreen)"]),
37
+ ...(p.columns >= DOCK ? [] : [`un terminal d'au moins ${DOCK} colonnes (il en a ${p.columns})`]),
38
+ ]
39
+ return `Tableau Grillhouse ouvert au-dessus de la saisie. Pour l'avoir sur le côté : ${manque.join(' et ')}.`
40
+ }
41
+ const etat = atom({ plugin: 'grillhouse', key: 'etat' } as const, {
42
+ tableau: null,
43
+ erreur: null,
44
+ lu: 0,
45
+ choisi: null,
46
+ element: null,
47
+ grand: false,
48
+ vue: 'tableau',
49
+ groupe: null,
50
+ texte: null,
51
+ } as Etat)
52
+
53
+ export const k = (n: number) =>
54
+ n >= 1e6 ? `${(n / 1e6).toFixed(1).replace('.', ',')} M` : n >= 1e3 ? `${Math.round(n / 1e3)} k` : `${n}`
55
+ export const duree = (ms: number) => {
56
+ const min = Math.round(ms / 60_000)
57
+ return min < 60 ? `${min} min` : `${Math.floor(min / 60)} h ${String(min % 60).padStart(2, '0')}`
58
+ }
59
+ const STATUTS: Record<string, string> = {
60
+ open: 'ouvert',
61
+ in_progress: 'en cours',
62
+ blocked: 'bloqué',
63
+ deferred: 'reporté',
64
+ closed: 'fait',
65
+ }
66
+ // The card's lines, below its rule: number and whole title, then component, state, duration and
67
+ // tokens, then the start of the description. The pane gives it its last rows.
68
+ export const FICHE = 6
69
+ export function ficheLignes(f: Fiche | undefined): string[] {
70
+ if (!f) return ['alt+g (ou ctrl+x tab), puis tab : la fiche du ticket choisi ; Échap : le prompt.']
71
+ return [
72
+ `${f.numero} — ${f.titre}`,
73
+ [f.composant, STATUTS[f.statut] ?? f.statut, duree(f.travail), `${k(f.jetons)} jetons`].filter(Boolean).join(' · '),
74
+ f.resume,
75
+ ].filter(Boolean)
76
+ }
77
+
78
+ // The rows a text takes in `largeur` columns: each of its lines wrapped at its spaces, its
79
+ // indentation kept (up to half the width), a word longer than a row cut across rows; an empty line
80
+ // stays an empty row.
81
+ export function enLignes(texte: string, largeur: number): string[] {
82
+ const out: string[] = []
83
+ for (const para of texte.replace(/\t/g, ' ').split('\n')) {
84
+ const retrait = (/^ */.exec(para)?.[0] ?? '').slice(0, Math.floor(largeur / 2))
85
+ let ligne = retrait
86
+ let debut = true
87
+ for (let mot of para.trimStart().split(/ +/)) {
88
+ while (mot) {
89
+ const sep = debut ? '' : ' '
90
+ if ((ligne + sep + mot).length <= largeur) {
91
+ ligne += sep + mot
92
+ debut = false
93
+ mot = ''
94
+ } else if (debut) {
95
+ const place = largeur - ligne.length
96
+ out.push(ligne + mot.slice(0, place))
97
+ mot = mot.slice(place)
98
+ ligne = ''
99
+ } else {
100
+ out.push(ligne)
101
+ ligne = ''
102
+ debut = true
103
+ }
104
+ }
105
+ }
106
+ out.push(ligne)
107
+ }
108
+ return out
109
+ }
110
+
111
+ // The first row of the whole card's text to show: `avant` moved by `by` rows, within the text's
112
+ // `n` rows shown `rangs` at a time.
113
+ export const defiler = (avant: number, by: number, n: number, rangs: number) =>
114
+ Math.max(0, Math.min(avant + by, n - rangs))
115
+
116
+ // A line the pane lists, the key of its Button and `i`, its index in the lines shown.
117
+ export type Visible = { l: Ligne; cle: string | null; i: number }
118
+
119
+ // The lines the pane lists, each with the key of its Button: `<ticket>:<i>` for a ticket's line,
120
+ // `>:<i>` for a folded group's title, none for a plain line.
121
+ export function visibles(lignes: Ligne[]): Visible[] {
122
+ return lignes.map((l, i) => ({ l, cle: l.ticket ? `${l.ticket}:${i}` : l.groupe ? `>:${i}` : null, i }))
123
+ }
124
+
125
+ // The first of `n` lines to show in `rangs` rows: the window stays where it was (`avant`) while the
126
+ // line at `idx` shows with the lines `haut` above and `bas` below it — the ring's next stops, one
127
+ // line away by default — and moves just enough otherwise, `idx` itself always in sight. Only the
128
+ // window's lines are drawn, so the engine has nothing to scroll and an arrow moves the ring: the
129
+ // next stop must be drawn, or the arrow skips past the lines to the rule.
130
+ export function debut(n: number, idx: number, rangs: number, avant: number, haut = idx - 1, bas = idx + 1): number {
131
+ let p = avant
132
+ if (haut < p) p = haut
133
+ if (bas > p + rangs - 1) p = bas + 1 - rangs
134
+ p = Math.min(p, idx)
135
+ p = Math.max(p, idx + 1 - rangs)
136
+ return Math.max(0, Math.min(p, n - rangs))
137
+ }
138
+
139
+ // What the keyboard reaching a ticket's line changes: the line chooses its ticket, and the card
140
+ // goes back to its size.
141
+ export function viser(s: Etat, element: string): Etat {
142
+ if (element === 'g' || element === 'h' || element === 'e' || element === 'o') return s
143
+ const [tete] = element.split(':')
144
+ if (tete === '>') return { ...s, element, grand: false }
145
+ return { ...s, element, grand: false, choisi: tete ?? null }
146
+ }
147
+
148
+ // The lines the pane shows: the board's, the states' view, or one folded group's, whole.
149
+ export const lignesDe = (s: Etat): Ligne[] =>
150
+ (s.vue === 'etats'
151
+ ? s.tableau?.vueEtats
152
+ : s.vue === 'groupe'
153
+ ? s.tableau?.groupes?.[s.groupe ?? '']
154
+ : s.tableau?.lignes) ?? []
155
+
156
+ // `e` switches between the board and the states' view: the keyboard starts over in the other
157
+ // view, the card stays.
158
+ export const basculerVue = (s: Etat): Etat => ({
159
+ ...s,
160
+ vue: s.vue === 'tableau' ? 'etats' : 'tableau',
161
+ element: null,
162
+ grand: false,
163
+ })
164
+
165
+ // `o` on a folded group's title opens that group alone, whole; `o` again comes back to the board.
166
+ export const ouvrirGroupe = (s: Etat, groupe: string | undefined): Etat =>
167
+ s.vue === 'groupe'
168
+ ? { ...s, vue: 'tableau', groupe: null, element: null, grand: false }
169
+ : groupe
170
+ ? { ...s, vue: 'groupe', groupe, element: null, grand: false }
171
+ : s
172
+
173
+ // A line's text and its other text (`alterne`) take turns, ALTERNANCE ms each: the text shown at
174
+ // the turn `tour` of the pane's clock (its other text at an odd turn), its text when it has no other.
175
+ export const ALTERNANCE = 5_000
176
+ export const texteDe = (l: Ligne, tour: number) =>
177
+ l.alterne !== undefined && tour % 2 === 1 ? l.alterne : l.texte
178
+
179
+ // While the board shows a line of progress (`avancement`: an automatic pass, « il y a 8 s »), it is
180
+ // measured again every AVANCEMENT ms instead of every PERIODE: `aMesurer` says when, at the pane's
181
+ // clock.
182
+ export const AVANCEMENT = 10_000
183
+ export const aMesurer = (s: Etat, maintenant: number) =>
184
+ maintenant - s.lu >= AVANCEMENT && lignesDe(s).some(l => l.avancement === true)
185
+
186
+ // The colour of a line's tone.
187
+ export const couleur = (ton: Ligne['ton']) =>
188
+ ton === 'alerte' ? 'red' : ton === 'attention' ? 'yellow' : ton === 'actif' ? 'cyan' : undefined
189
+
190
+ // The session's project, and whether the board applies to it.
191
+ let cwd = ''
192
+ let actif = false
193
+ // The turns of the alternation of the lines' texts, one per ALTERNANCE.
194
+ let tour = 0
195
+ // The pane's window: its first line, the element it last followed, and the rows and focus it was
196
+ // last drawn with. The wheel moves it; the ring moving to another line makes it follow.
197
+ let fenetre = 0
198
+ let suivi: string | null = null
199
+ let cadre = { rangs: 1, focus: false }
200
+ // The whole card's window: the first row of its text shown, and, while it is drawn, the text's
201
+ // rows and the rows it shows them in.
202
+ let ligneTexte = 0
203
+ let texteDessine: { n: number; rangs: number } | null = null
204
+
205
+ // The lines the window runs over, every line of the board, the window placed on them: when the
206
+ // ring has moved to another line since the window last followed it, the window follows it
207
+ // (`debut`); it stays within the lines.
208
+ function placer(s: Etat, rangs: number, focus: boolean): Visible[] {
209
+ const liste = visibles(lignesDe(s))
210
+ const ici = liste.findIndex(x => x.cle === s.element)
211
+ if (focus && ici >= 0 && s.element !== suivi) {
212
+ // The ring's next stops: the nearest lines with a button, above and below.
213
+ const avant = liste.slice(0, ici).findLastIndex(x => x.cle)
214
+ const apres = liste.findIndex((x, j) => j > ici && x.cle)
215
+ fenetre = debut(liste.length, ici, rangs, fenetre, avant < 0 ? ici - 1 : avant, apres < 0 ? ici + 1 : apres)
216
+ suivi = s.element
217
+ }
218
+ fenetre = Math.max(0, Math.min(fenetre, liste.length - rangs))
219
+ return liste
220
+ }
221
+
222
+ // Measures the board again and redraws the pane.
223
+ // One board at a time: a refresh asked while one runs is folded into a single next one, so a
224
+ // loop of commands never stacks boards, each reading every ticket.
225
+ let enCours: Promise<void> | null = null
226
+ let encore = false
227
+ function rafraichir($: EngineInterface): Promise<void> {
228
+ if (enCours) {
229
+ encore = true
230
+ return enCours
231
+ }
232
+ enCours = (async () => {
233
+ try {
234
+ do {
235
+ encore = false
236
+ await mesurer($)
237
+ } while (encore)
238
+ } finally {
239
+ enCours = null
240
+ }
241
+ })()
242
+ return enCours
243
+ }
244
+
245
+ // After a `bd` or `git` command, one refresh REBOND ms after the first of a burst.
246
+ const REBOND = 3_000
247
+ let attend = false
248
+ async function bientot($: EngineInterface) {
249
+ if (attend) return
250
+ attend = true
251
+ try {
252
+ await $.clock.sleep(REBOND)
253
+ } finally {
254
+ attend = false
255
+ }
256
+ await rafraichir($)
257
+ }
258
+
259
+ async function mesurer($: EngineInterface) {
260
+ if (!actif) return
261
+ const r = await $.process.run([GRILLHOUSE, 'tableau'], { cwd, timeoutMs: 20_000 })
262
+ if (r.exitCode !== 0) {
263
+ await update($, etat, s => ({ ...s, erreur: r.stderr.slice(0, 300) }))
264
+ return
265
+ }
266
+ // A board past what a process returns (4 MiB) or that does not parse is said, never thrown.
267
+ let tableau: Tableau
268
+ try {
269
+ if (r.isStdoutTruncated) throw new Error('la sortie de grillhouse tableau dépasse 4 Mio')
270
+ tableau = JSON.parse(r.stdout) as Tableau
271
+ } catch (err) {
272
+ await update($, etat, s => ({ ...s, erreur: `Tableau illisible : ${(err as Error).message}`.slice(0, 300) }))
273
+ return
274
+ }
275
+ await update($, etat, s => ({ ...s, tableau, erreur: null, lu: Date.now() }))
276
+ }
277
+
278
+ // The whole text of a ticket, read from Beads when the card fills the pane: the board carries
279
+ // only each card's start.
280
+ async function lireTexte($: EngineInterface, id: string) {
281
+ const r = await $.process.run(['bd', 'show', id, '--json'], { cwd, timeoutMs: 20_000 })
282
+ let contenu = ''
283
+ try {
284
+ const lu = JSON.parse(r.stdout) as { description?: string }[] | { description?: string }
285
+ contenu = ((Array.isArray(lu) ? lu[0] : lu)?.description ?? '').trim()
286
+ } catch {
287
+ contenu = ''
288
+ }
289
+ await update($, etat, s => ({ ...s, texte: { id, contenu } }))
290
+ }
291
+
292
+ // The status a chantier's switch gives it: in progress → open, anything else → in progress.
293
+ export const bascule = (statut: string | undefined) => (statut === 'in_progress' ? 'open' : 'in_progress')
294
+
295
+ // Switches a chantier on or off in Beads, then measures the board again.
296
+ async function basculer($: EngineInterface, id: string, statut: string | undefined) {
297
+ const r = await $.process.run(['bd', 'update', id, '--status', bascule(statut)], { cwd, timeoutMs: 20_000 })
298
+ if (r.exitCode !== 0) await $.ui.toast(`Le chantier n'a pas basculé : ${r.stderr.slice(0, 200)}`)
299
+ await rafraichir($)
300
+ }
301
+
302
+ export const register: Register = on => {
303
+
304
+ on('session.start', async ($, e, next) => {
305
+ const started = await next(e)
306
+ cwd = e.cwd
307
+ const pkg = `${cwd}/package.json`
308
+ const texte = (await $.fs.exists(pkg)) ? String(await $.fs.read(pkg)) : null
309
+ actif = e.isInteractive && declareLePaquet(texte) && !cwd.includes('/.claude/worktrees/')
310
+ if (!actif) return started
311
+ await $.command.register({
312
+ name: 'grillhouse',
313
+ description: 'Ouvre le tableau Grillhouse : le projet, les agents, le chantier en cours',
314
+ })
315
+ void rafraichir($)
316
+ $.clock.every(PERIODE, () => void rafraichir($))
317
+ // A line with another text turns at each ALTERNANCE: the pane is drawn again, while one shows.
318
+ $.clock.every(ALTERNANCE, () => {
319
+ tour++
320
+ void read($, etat).then(s => {
321
+ if (lignesDe(s).some(l => l.alterne !== undefined)) $.ui.invalidate('ui.render')
322
+ if (aMesurer(s, Date.now())) void rafraichir($)
323
+ })
324
+ })
325
+ return started
326
+ })
327
+
328
+ // The command answers at once and the board is measured behind it: a measure takes seconds on a
329
+ // large project, and a hook past its budget (10 s) is skipped, its command answered by the engine.
330
+ on('command.run', { command: 'grillhouse' }, async ($, e) => {
331
+ void rafraichir($)
332
+ // A narrow column docked beside the transcript; a short block when seated above the prompt.
333
+ await $.ui.open({ id: PANE, title: 'Grillhouse', columns: COLONNES, rows: 12 })
334
+ return { text: placement(e.presentation) }
335
+ })
336
+
337
+ on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
338
+ const ran = await next(e)
339
+ if (actif && /(^|[\s;&|(])(bd|git)\s/.test(e.command)) void bientot($)
340
+ return ran
341
+ }).catch(($, e, next) => next(e))
342
+
343
+ // The ring that moves onto a line acts on it (`viser`).
344
+ on('ui.focus', async ($, e, next) => {
345
+ const r = await next(e)
346
+ if (e.requestId === PANE && e.element) {
347
+ const element = e.element
348
+ await update($, etat, s => viser(s, element))
349
+ }
350
+ return r
351
+ })
352
+
353
+ // The pane draws its own rows: the wheel moves its window by `e.by` rows, within its lines, and
354
+ // draws it again. On the whole card, the wheel, the arrows and the page keys move its text by
355
+ // `e.by` rows, and the engine's window is held on the row under the card's spare top row, so an
356
+ // arrow up scrolls as an arrow down does, wherever the text stands.
357
+ on('ui.scroll', async ($, e, next) => {
358
+ if (e.requestId !== PANE) return next(e)
359
+ if (texteDessine) {
360
+ ligneTexte = defiler(ligneTexte, e.by, texteDessine.n, texteDessine.rangs)
361
+ $.ui.invalidate('ui.render')
362
+ return next({ ...e, offset: 1 })
363
+ }
364
+ const liste = placer(await read($, etat), cadre.rangs, cadre.focus)
365
+ fenetre = Math.max(0, Math.min(fenetre + e.by, liste.length - cadre.rangs))
366
+ $.ui.invalidate('ui.render')
367
+ return {}
368
+ })
369
+
370
+ on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
371
+ const { Box, Text, Button } = $.ui.resolve(e)
372
+ const s = await read($, etat)
373
+ const { tableau: t, erreur, choisi, element, grand } = s
374
+ if (!t) return <Text dimColor>{erreur ?? 'Lecture du tableau…'}</Text>
375
+ const colonnes = e.props.bodyColumns
376
+ // Focused, the card shows the chosen ticket; otherwise the one that moved last.
377
+ const montre = e.props.isFocused ? choisi : (t.dernier ?? choisi)
378
+ const fiche = montre ? t.fiches[montre] : undefined
379
+ const rangs = Math.max(1, e.props.scroll.bodyRows - FICHE - 1)
380
+ cadre = { rangs, focus: e.props.isFocused }
381
+ const liste = placer(s, rangs, e.props.isFocused)
382
+ const ici = liste.findIndex(x => x.cle === element)
383
+ // The chantier the keyboard is on, which `h` switches on or off.
384
+ const chantier = ici >= 0 && liste[ici]?.l.chantier ? liste[ici]?.l.ticket : undefined
385
+ // The folded group the keyboard is on, which `o` opens.
386
+ const groupe = ici >= 0 ? liste[ici]?.l.groupe : undefined
387
+ // `g` on the rule makes the card fill the pane, and again brings it back; `h` switches the
388
+ // chantier chosen; `e` switches between the board and the states' view.
389
+ const regle = (libelle: string) =>
390
+ e.props.isFocused ? (
391
+ <Box flexDirection="row" width={colonnes} overflow="hidden">
392
+ <Text dimColor>{'── '}</Text>
393
+ <Button
394
+ key="e"
395
+ plain
396
+ hotkey="e"
397
+ onPress={() => {
398
+ fenetre = 0
399
+ suivi = null
400
+ void update($, etat, basculerVue)
401
+ }}
402
+ >
403
+ <Text dimColor>{`e : ${s.vue === 'tableau' ? 'états' : 'tableau'} `}</Text>
404
+ </Button>
405
+ {groupe || s.vue === 'groupe' ? (
406
+ <Button
407
+ key="o"
408
+ plain
409
+ hotkey="o"
410
+ onPress={() => {
411
+ fenetre = 0
412
+ suivi = null
413
+ void update($, etat, x => ouvrirGroupe(x, groupe))
414
+ }}
415
+ >
416
+ <Text dimColor>{`· o : ${s.vue === 'groupe' ? 'retour' : 'ouvrir'} `}</Text>
417
+ </Button>
418
+ ) : null}
419
+ {fiche ? (
420
+ <Button
421
+ key="g"
422
+ plain
423
+ hotkey="g"
424
+ onPress={() => {
425
+ if (!grand && montre) void lireTexte($, montre)
426
+ ligneTexte = 0
427
+ void update($, etat, s => ({ ...s, grand: !s.grand }))
428
+ }}
429
+ >
430
+ <Text dimColor>{`· g : ${libelle} `}</Text>
431
+ </Button>
432
+ ) : null}
433
+ {chantier && fiche ? (
434
+ <Button key="h" plain hotkey="h" onPress={() => void basculer($, chantier, t.fiches[chantier]?.statut)}>
435
+ <Text dimColor>{`· h : ${fiche.statut === 'in_progress' ? 'désactiver' : 'activer'} `}</Text>
436
+ </Button>
437
+ ) : null}
438
+ <Text dimColor wrap="truncate-end">
439
+ {'─'.repeat(colonnes)}
440
+ </Text>
441
+ </Box>
442
+ ) : (
443
+ <Text dimColor>{'─'.repeat(colonnes)}</Text>
444
+ )
445
+ texteDessine = null
446
+ if (grand && e.props.isFocused && fiche) {
447
+ const [titre = '', etatDuTicket] = ficheLignes(fiche)
448
+ const lignesDuTitre = enLignes(titre, colonnes)
449
+ const corps = enLignes((s.texte?.id === montre && s.texte.contenu) || fiche.resume, colonnes)
450
+ // A spare row, the rule, the title and the state above the text; the text in the rows left,
451
+ // less one, so the window shows it whole on either of its rows.
452
+ const rangsTexte = Math.max(1, e.props.scroll.bodyRows - lignesDuTitre.length - 3)
453
+ ligneTexte = defiler(ligneTexte, 0, corps.length, rangsTexte)
454
+ texteDessine = { n: corps.length, rangs: rangsTexte }
455
+ const fin = Math.min(corps.length, ligneTexte + rangsTexte)
456
+ const position = corps.length > rangsTexte ? ` · ${ligneTexte + 1}-${fin}/${corps.length}` : ''
457
+ // Two spare rows, one above and one below the card, the engine's window held on the second
458
+ // row: an arrow up and an arrow down both have a row to scroll, and raise `ui.scroll`. The
459
+ // spare row above is always drawn, so no row moves when the window does.
460
+ return (
461
+ <Box flexDirection="column" height={e.props.scroll.bodyRows + 2}>
462
+ <Text> </Text>
463
+ {regle(`retour${position}`)}
464
+ {lignesDuTitre.map(l => (
465
+ <Text bold wrap="truncate-end">
466
+ {l || ' '}
467
+ </Text>
468
+ ))}
469
+ <Text dimColor wrap="truncate-end">
470
+ {etatDuTicket}
471
+ </Text>
472
+ {corps.slice(ligneTexte, fin).map(l => (
473
+ <Text wrap="truncate-end">{l || ' '}</Text>
474
+ ))}
475
+ </Box>
476
+ )
477
+ }
478
+ const premier = fenetre
479
+ // A card fills the pane's last rows: the whole title on as many lines as it takes, then the
480
+ // component, state, duration and tokens, then the description in the rows left.
481
+ const carte = (f: Fiche | undefined) => {
482
+ const [titre, etatDuTicket, resume] = ficheLignes(f)
483
+ return (
484
+ <Box flexDirection="column" width={colonnes} height={FICHE}>
485
+ <Box flexShrink={0}>
486
+ <Text bold={!!f} dimColor={!f} wrap="wrap">
487
+ {titre}
488
+ </Text>
489
+ </Box>
490
+ {etatDuTicket ? (
491
+ <Box flexShrink={0}>
492
+ <Text dimColor wrap="truncate-end">
493
+ {etatDuTicket}
494
+ </Text>
495
+ </Box>
496
+ ) : null}
497
+ {resume ? (
498
+ <Box flexGrow={1} overflow="hidden">
499
+ <Text wrap="wrap">{resume}</Text>
500
+ </Box>
501
+ ) : null}
502
+ </Box>
503
+ )
504
+ }
505
+ return (
506
+ <Box flexDirection="column" height={e.props.scroll.bodyRows}>
507
+ <Box flexDirection="column" flexGrow={1} overflow="hidden">
508
+ {liste.slice(premier, premier + rangs).map(({ l, cle }) => {
509
+ const texte = (
510
+ <Text bold={l.ton === 'titre'} dimColor={l.ton === 'discret'} color={couleur(l.ton)} wrap="truncate-end">
511
+ {texteDe(l, tour) || ' '}
512
+ </Text>
513
+ )
514
+ if (cle && l.groupe) {
515
+ return (
516
+ <Button key={cle} plain onPress={() => void update($, etat, s => viser(s, cle))}>
517
+ {texte}
518
+ </Button>
519
+ )
520
+ }
521
+ if (!cle || !l.ticket) return texte
522
+ // A ticket's line is a plain button, in the hover group named by its ticket: the ring
523
+ // chooses it, the pointer over it shows its card.
524
+ return (
525
+ <Box hover={{ scope: l.ticket }}>
526
+ <Button
527
+ key={cle}
528
+ plain
529
+ onPress={() => void update($, etat, s => viser(s, cle))}
530
+ >
531
+ {texte}
532
+ </Button>
533
+ </Box>
534
+ )
535
+ })}
536
+ </Box>
537
+ {regle('tout le texte')}
538
+ <Box flexDirection="column" height={FICHE} flexShrink={0}>
539
+ {carte(fiche)}
540
+ {Object.entries(t.fiches).map(([id, f]) => (
541
+ // The card of the ticket under the pointer, drawn over the chosen one on a blank of its
542
+ // size, so no character of the chosen card shows through.
543
+ <Box position="absolute" top={0} left={0} display="none" hover={{ scope: id, display: 'flex' }}>
544
+ <Box flexDirection="column">
545
+ {Array.from({ length: FICHE }, () => (
546
+ <Text>{' '.repeat(colonnes)}</Text>
547
+ ))}
548
+ </Box>
549
+ <Box position="absolute" top={0} left={0}>
550
+ {carte(f)}
551
+ </Box>
552
+ </Box>
553
+ ))}
554
+ </Box>
555
+ </Box>
556
+ )
557
+ })
558
+ }
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@roomi-fields/grillhouse",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.23.0",
4
+ "description": "Roomi's Grillhouse: the frame's scripts, called by one command, grillhouse <sous-commande>",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/roomi-fields/roomis-grillhouse.git",
10
+ "directory": "packages/grillhouse"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ ".claude-plugin",
17
+ "METACADRE.md",
18
+ "agents",
19
+ "bin",
20
+ "cadre",
21
+ "charte.md",
22
+ "docs",
23
+ "hooks",
24
+ "scripts",
25
+ "skills",
26
+ "types"
27
+ ],
28
+ "bin": {
29
+ "grillhouse": "bin/grillhouse.mjs"
30
+ },
31
+ "exports": {
32
+ "./package.json": "./package.json",
33
+ "./vitest-config": {
34
+ "types": "./scripts/vitest-config/base.d.ts",
35
+ "default": "./scripts/vitest-config/base.mjs"
36
+ },
37
+ "./vitest-config/mesure": {
38
+ "types": "./scripts/vitest-config/mesure.d.ts",
39
+ "default": "./scripts/vitest-config/mesure.mjs"
40
+ }
41
+ },
42
+ "scripts": {
43
+ "prepack": "node scripts/cadre.mjs --empaqueter",
44
+ "postpack": "node scripts/cadre.mjs --nettoyer",
45
+ "test:vitest": "vitest run"
46
+ },
47
+ "dependencies": {
48
+ "@microsoft/api-extractor": "7.59.2",
49
+ "@microsoft/api-extractor-model": "7.33.13",
50
+ "@microsoft/tsdoc-config": "0.18.3",
51
+ "jsonc-parser": "3.3.1",
52
+ "semver": "^7.8.5",
53
+ "yaml": "2.9.1"
54
+ },
55
+ "peerDependencies": {
56
+ "typescript": "^5.3.3",
57
+ "vitest": "^2.0.0"
58
+ },
59
+ "grillhouse": {
60
+ "lit": [
61
+ ".beads",
62
+ ".claude",
63
+ ".dependency-cruiser.cjs",
64
+ ".eslintrc.cjs",
65
+ ".github",
66
+ ".mcp.json",
67
+ "CHANGELOG.md",
68
+ "knip.json",
69
+ "mods",
70
+ "package.json",
71
+ "packages/grillhouse/bin",
72
+ "packages/grillhouse/scripts",
73
+ "templates",
74
+ "tsdoc.json"
75
+ ]
76
+ }
77
+ }