@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,1886 @@
1
+ #!/usr/bin/env node
2
+ // The board of a project, on three levels, measured on Beads, the register of finished agents,
3
+ // the transcripts of running ones and the running processes — never on what an agent declares.
4
+ // The Grillhouse mod shows its lines; `npm run tableau` prints them.
5
+ //
6
+ // npx grillhouse tableau the board, as JSON (with its lines, 52 columns wide)
7
+ // npx grillhouse tableau --texte the board, as text
8
+ //
9
+ // Each measure writes the status line (the chantier in progress) to `grillhouse-etat.txt` in the
10
+ // common git directory, where the status line (`scripts/ligne-etat/`) reads it.
11
+ //
12
+ // - Global: the work tickets of the project (a ticket without children; a mother is a grouping)
13
+ // by state, the agents' working time and tokens of the day and in all, the alerts.
14
+ // - Agents: the tree of the steps of the process (scripts/etapes.mjs), one block per step and
15
+ // sub-step, with who works there, then the arbitrage of a stop, for the whole project; a step
16
+ // that names a declaration (the regeneration of the references, 3.1) has its line only when the
17
+ // project's package.json declares it (`grillhouse.references`). A ticket
18
+ // stands at the step of every agent that runs on it, or waits in one place: at
19
+ // the step of the next role, deduced from the roles of the flow that ran on it and the
20
+ // reviewer's last verdict (`ACCEPTÉ` or `RENDU` at the start of a comment). A regulator waits for
21
+ // nothing: it comes on a stop of the circuit, which its entry shows.
22
+ // - Circuits: one entry per open ticket whose circuit (scripts/circuit.mjs) runs or stopped, read
23
+ // in its state `.claude/worktrees/<ticket>.circuit.json`: it runs while the process
24
+ // `npx grillhouse circuit <ticket>` runs, and stands at the step of its state, with its
25
+ // component, its rank among the title's components and its attempt; while it runs a session of
26
+ // the regulator on a stop (its state names the regulator's session, `regulateur`), while the
27
+ // questionneur questions a decision of its regulator, or once it stopped, it stands at the
28
+ // arbitrage of the stopped step; while the questionneur questions its plan (its state names the
29
+ // questioning, `questionnement`, of the plan), it stands at the questioning of the plan; its
30
+ // card gives the stop (its name and code) and the first line of its reason, or, ended on the
31
+ // regulator's question (« question »), that question. A ticket with a circuit waits in no block.
32
+ // - Bilans: the card of a ticket whose circuit kept its bilan (scripts/bilan.mjs,
33
+ // `.claude/worktrees/<ticket>.bilan.json`) opens on its key figures: wall time, share lost,
34
+ // sessions, cost.
35
+ // - Chantier: a root epic with work in progress, its tree of mothers, each with its work tickets
36
+ // done over all; the tree shows the open work: a closed ticket, mother included, counts without
37
+ // a line, and a closed mother's open tickets take her place. The other chantiers, one line each.
38
+ // Then the tickets closed today.
39
+ // - Lines: the head, AGENTS, CHANTIER, then FAITS AUJOURD'HUI, every line of each list shown; a
40
+ // line folds only lines of another state (the tree's « + 2 prêts ») or the chantiers not in
41
+ // progress.
42
+ // - Measures: a finished agent from the register (`scripts/registre.mjs`), a running one from its
43
+ // transcript: a role agent launched in a shell (its envelope's process, which names its copy
44
+ // and its role) while that process runs, a sub-agent or a session whose transcript moved in the
45
+ // last 2 minutes.
46
+ // With an end in the register, it runs only when an incoming message (of the person, another
47
+ // agent, a notification) follows its latest end strictly (a reviewer resumed by a message); the
48
+ // writings of its last turn after that end leave it finished, measured by that latest end.
49
+ // A sub-agent, whatever its role, runs also while its last tool call awaits its answer, its
50
+ // transcript having moved in the last 30 minutes; a session keeps the 2-minute rule alone. The
51
+ // board reads no transcript older than 30 minutes: a shell agent whose transcript is older shows
52
+ // by its process alone. A sub-agent whose prompt names no ticket takes that of the regulator
53
+ // that launched it.
54
+ // - Time: a transcript's working time is the union of its turns, each bounded by the earliest and
55
+ // the latest date of its events, whatever their order in the file; a transcript is read where the
56
+ // board stopped, its reading kept in the cache file. A ticket's working time adds the steps its
57
+ // circuit leads itself, with no agent (the proof, the checks, the final pass, the integration,
58
+ // the queue): the register's lines of the role `circuit`, which no block shows and no role of the
59
+ // flow counts as a passage, and the step under way of a running circuit (`mene` in its state) up
60
+ // to now; they count out of the turns of the ticket's agents, so the ticket's time is the union
61
+ // of both, each instant once, and they spend no tokens.
62
+ // - Progress: the line of a ticket whose circuit runs an automatic step gives the progress of
63
+ // its pass, read in the progress directory its state names (scripts/avancement.mjs), in
64
+ // figures, as Bazel's counter of actions: « faits/prévus · <durée> » since the start of the
65
+ // pass, after the counters of its loops, or in their place when room lacks (the subject or the
66
+ // component is cut, never the figures); the survey of the components (1.1), the time since its
67
+ // start; the queue (5.2), the steps of its CI. Past FIGEE without a sign,
68
+ // « ⚠ figée <durée> · faits/prévus », the time since its last sign, in the tone of an alert.
69
+ // A measure out of a ticket that holds the lock of the machine (a supervisor's or an
70
+ // integration's `npm run verifier`) stands at the head of AGENTS:
71
+ // « ⏳ Mesure · <dépôt> · faits/prévus · <durée> », since its start,
72
+ // « ⚠ Mesure figée <durée> · … » past FIGEE. Such a line is marked `avancement`, which the
73
+ // pane measures again more often.
74
+ // - Alerts: one red alert per open night ticket (its subject, then its number), tickets awaiting
75
+ // the responsable, a shell agent silent for 10 minutes, a ticket in progress without anything
76
+ // running for 30 minutes.
77
+ import { execFileSync } from 'node:child_process';
78
+ import {
79
+ closeSync,
80
+ existsSync,
81
+ openSync,
82
+ readdirSync,
83
+ readFileSync,
84
+ readSync,
85
+ statSync,
86
+ writeFileSync,
87
+ } from 'node:fs';
88
+ import os from 'node:os';
89
+ import path from 'node:path';
90
+ import { pathToFileURL } from 'node:url';
91
+ import {
92
+ ARBITRAGE,
93
+ AUTO,
94
+ bouclesDe,
95
+ ETAPES,
96
+ etapeDuCircuit,
97
+ etapeDuRole,
98
+ gesteDe,
99
+ juge,
100
+ libelle,
101
+ quiDe,
102
+ REGULATEUR,
103
+ SUITE,
104
+ } from './etapes.mjs';
105
+ import { bilansGardes, enBref } from './bilan.mjs';
106
+ import { verdictDe } from './integration/verdict.mjs';
107
+ import { lireAvancement, lireLaMesure } from './avancement.mjs';
108
+
109
+ const MINUTE = 60_000;
110
+ export const SILENCE = 10 * MINUTE;
111
+ export const SANS_AGENT = 30 * MINUTE;
112
+ const RECENT = 2 * MINUTE;
113
+ export const LARGEUR = 52;
114
+ // An automatic pass without a sign for this long is frozen: its line says so, in the tone of an
115
+ // alert.
116
+ export const FIGEE = 5 * MINUTE;
117
+ // The room a circuit's line keeps for its subject or its component beside the counters of its
118
+ // loops and the progress of its pass: below it, the progress takes the counters' place.
119
+ const PLACE_DU_SUJET = 10;
120
+ // The role of a register line that a step of a circuit writes (scripts/circuit.mjs): no agent,
121
+ // no role of the flow.
122
+ export const ROLE_DU_CIRCUIT = 'circuit';
123
+ // A role by its agent's name: an agent of the frame's plugin is named bare or scoped by the
124
+ // plugin (`grillhouse:<rôle>`), as Claude Code names a plugin's agents.
125
+ export const roleDeLAgent = nom =>
126
+ typeof nom === 'string' ? nom.replace(/^grillhouse:/, '') : null;
127
+ // A sub-agent's role from its meta file: the type it was spawned as; a named agent's `agentType`
128
+ // is its name.
129
+ export const roleDuMeta = m =>
130
+ roleDeLAgent(m.spawnedAgentType ?? m.customAgentType ?? m.agentType ?? null);
131
+ // The states of a ticket, in the order the states' view lists them, with their meaning.
132
+ export const ETATS = [
133
+ { cle: 'enCours', nom: 'en cours', sens: 'un agent y travaille' },
134
+ { cle: 'enAttente', nom: 'en attente', sens: 'ton go, prêt à partir' },
135
+ { cle: 'bloques', nom: 'bloqué', sens: 'ton go, il attend un autre ticket' },
136
+ { cle: 'sansGo', nom: 'sans go', sens: 'validé, il attend ton go' },
137
+ { cle: 'aValider', nom: 'à valider', sens: 'une trouvaille attend ton accord' },
138
+ { cle: 'reportes', nom: 'reporté', sens: 'mis de côté' },
139
+ { cle: 'fermes', nom: 'fait', sens: 'fermé' },
140
+ ];
141
+ const enTirets = p => p.replace(/[^A-Za-z0-9]/g, '-');
142
+
143
+ // The origins of an incoming message: the person, another agent, a notification, the supervisor
144
+ // resuming a sub-agent.
145
+ const ORIGINES = new Set(['human', 'peer', 'task-notification', 'coordinator']);
146
+ // The mark the tool writes when the person stops a turn.
147
+ const INTERRUPTION = /^\[Request interrupted by user( for tool use)?\]$/;
148
+
149
+ // An incoming message opens a turn: a message of the person, of another agent, a notification or
150
+ // the supervisor's.
151
+ // Its origin decides; a message without origin is one unless it is a tool's result, a meta message
152
+ // (a system reminder, a loaded skill), the summary written after a compaction or the interruption
153
+ // mark.
154
+ function entrant(e) {
155
+ if (e.type !== 'user') {
156
+ return false;
157
+ }
158
+ if (e.origin) {
159
+ return ORIGINES.has(e.origin.kind);
160
+ }
161
+ const contenu = e.message?.content;
162
+ if (e.isMeta || e.isCompactSummary) {
163
+ return false;
164
+ }
165
+ if (Array.isArray(contenu)) {
166
+ if (contenu.some(c => c.type === 'tool_result')) {
167
+ return false;
168
+ }
169
+ return !INTERRUPTION.test(contenu.map(c => c.text ?? '').join(''));
170
+ }
171
+ return typeof contenu === 'string' && !INTERRUPTION.test(contenu);
172
+ }
173
+
174
+ // The reading of a transcript is a state that lines are added to, one JSON object per line: it
175
+ // holds the tokens, the turns, the start and end, the last incoming message, the ticket, the role,
176
+ // the cost and the last tool call, and it is plain JSON, so the board keeps it between two
177
+ // measures and reads only what was appended. A turn is bounded by the earliest and the latest date
178
+ // of its events, whatever their order in the file; the first one holds the events before the first
179
+ // incoming message, each next one opens at an incoming message and holds the events up to the next.
180
+ const etatNeuf = () => ({
181
+ jetons: 0,
182
+ cache: 0,
183
+ premier: null,
184
+ dernier: null,
185
+ entre: null,
186
+ ticket: null,
187
+ role: null,
188
+ cout: null,
189
+ appel: null,
190
+ reponses: [],
191
+ tours: [],
192
+ recu: false,
193
+ vus: [],
194
+ });
195
+
196
+ // The union of intervals, ordered by date: no instant is in two of them.
197
+ function reunir(intervalles) {
198
+ const out = [];
199
+ for (const [debut, fin] of [...intervalles].sort((a, b) => a[0] - b[0] || a[1] - b[1])) {
200
+ const dernier = out[out.length - 1];
201
+ if (dernier && debut <= dernier[1]) {
202
+ dernier[1] = Math.max(dernier[1], fin);
203
+ } else {
204
+ out.push([debut, fin]);
205
+ }
206
+ }
207
+ return out;
208
+ }
209
+
210
+ // Adds the lines of `texte` to the state `etat`, which it returns.
211
+ function avancer(etat, texte) {
212
+ const vus = new Set(etat.vus);
213
+ const reponses = new Set(etat.reponses);
214
+ for (const ligne of texte.split('\n')) {
215
+ if (!ligne.trim()) {
216
+ continue;
217
+ }
218
+ let e;
219
+ try {
220
+ e = JSON.parse(ligne);
221
+ } catch {
222
+ continue;
223
+ }
224
+ if (e.type === 'agent-setting') {
225
+ etat.role ??= e.agentSetting ?? null;
226
+ }
227
+ if (e.type === 'cost-state' && typeof e.totalCostUSD === 'number') {
228
+ etat.cout = e.totalCostUSD;
229
+ }
230
+ const t = e.timestamp ? Date.parse(e.timestamp) : NaN;
231
+ if (!Number.isNaN(t)) {
232
+ etat.premier = etat.premier === null ? t : Math.min(etat.premier, t);
233
+ etat.dernier = etat.dernier === null ? t : Math.max(etat.dernier, t);
234
+ // The first incoming message continues the turn its earlier events opened.
235
+ const recoit = entrant(e);
236
+ const coupe = recoit && etat.recu;
237
+ etat.recu ||= recoit;
238
+ if (recoit) {
239
+ etat.entre = etat.entre === null ? t : Math.max(etat.entre, t);
240
+ }
241
+ if (etat.tours.length === 0 || coupe) {
242
+ etat.tours.push([t, t]);
243
+ } else {
244
+ const tour = etat.tours[etat.tours.length - 1];
245
+ tour[0] = Math.min(tour[0], t);
246
+ tour[1] = Math.max(tour[1], t);
247
+ }
248
+ }
249
+ for (const c of Array.isArray(e.message?.content) ? e.message.content : []) {
250
+ if (c.type === 'tool_use') {
251
+ etat.appel = c.id;
252
+ } else if (c.type === 'tool_result') {
253
+ reponses.add(c.tool_use_id);
254
+ }
255
+ }
256
+ if (etat.ticket === null && e.type === 'user') {
257
+ const contenu = e.message?.content;
258
+ const texteUser =
259
+ typeof contenu === 'string'
260
+ ? contenu
261
+ : Array.isArray(contenu)
262
+ ? contenu.map(c => c.text ?? '').join(' ')
263
+ : '';
264
+ const m = /TON TICKET : ([A-Za-z0-9_-]+-[A-Za-z0-9]+(?:\.[0-9]+)*)/.exec(texteUser);
265
+ etat.ticket = m ? m[1] : '';
266
+ }
267
+ const u = e.message?.usage;
268
+ const id = e.message?.id ?? e.requestId;
269
+ if (!u || (id && vus.has(id))) {
270
+ continue;
271
+ }
272
+ if (id) {
273
+ vus.add(id);
274
+ }
275
+ etat.jetons +=
276
+ (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
277
+ etat.cache += u.cache_read_input_tokens ?? 0;
278
+ }
279
+ etat.vus = [...vus];
280
+ etat.reponses = [...reponses];
281
+ return etat;
282
+ }
283
+
284
+ // The measures of a state: the tokens, the turns and their working time (the union of the turns,
285
+ // the waits left out), the start (the oldest date) and the end (the latest), the ticket, the role
286
+ // (a session launched with `--agent`) and the cost (the total a session writes). `entre` is the
287
+ // time of the latest incoming message, or null; `attend` tells whether the last tool call still
288
+ // awaits its answer, the call and its answer paired by their identifier.
289
+ const mesurer = etat => ({
290
+ jetons: etat.jetons,
291
+ cache: etat.cache,
292
+ travail: reunir(etat.tours).reduce((s, [debut, fin]) => s + (fin - debut), 0),
293
+ tours: etat.tours.map(tour => [...tour]),
294
+ premier: etat.premier,
295
+ dernier: etat.dernier,
296
+ entre: etat.entre,
297
+ attend: etat.appel !== null && !etat.reponses.includes(etat.appel),
298
+ ticket: etat.ticket || null,
299
+ role: etat.role,
300
+ cout: etat.cout,
301
+ });
302
+
303
+ // The measures of the transcript text `texte` held in memory.
304
+ export const lireTranscription = texte => mesurer(avancer(etatNeuf(), texte));
305
+
306
+ // One line per agent, its latest end: the register holds a line at each stop of an agent, each
307
+ // the reading of its whole transcript at that stop, so the latest one holds all its sessions.
308
+ function parAgent(registre) {
309
+ const derniere = new Map();
310
+ for (const r of registre) {
311
+ const avant = derniere.get(r.agent);
312
+ if (!avant || Date.parse(r.fin) > Date.parse(avant.fin)) {
313
+ derniere.set(r.agent, r);
314
+ }
315
+ }
316
+ return [...derniere.values()];
317
+ }
318
+
319
+ // The intervals of <intervalles> (ordered, apart) out of those of <retires> (ordered, apart).
320
+ function sauf(intervalles, retires) {
321
+ const out = [];
322
+ for (const [debut, fin] of intervalles) {
323
+ let d = debut;
324
+ for (const [rd, rf] of retires) {
325
+ if (rf <= d || rd >= fin) {
326
+ continue;
327
+ }
328
+ if (rd > d) {
329
+ out.push([d, rd]);
330
+ }
331
+ d = Math.max(d, rf);
332
+ if (d >= fin) {
333
+ break;
334
+ }
335
+ }
336
+ if (d < fin) {
337
+ out.push([d, fin]);
338
+ }
339
+ }
340
+ return out;
341
+ }
342
+
343
+ // The register's lines of this project, one per agent: its latest end.
344
+ export function lireRegistre(texte, racine) {
345
+ const out = [];
346
+ for (const l of texte.split('\n')) {
347
+ try {
348
+ const e = JSON.parse(l);
349
+ if (e.projet === racine) {
350
+ out.push(e);
351
+ }
352
+ } catch {
353
+ // A torn line counts nothing.
354
+ }
355
+ }
356
+ return parAgent(out);
357
+ }
358
+
359
+ // The bytes of `f` from `decalage` to `taille`, cut after their last whole line: a line still being
360
+ // written waits for its end, unless it is already whole JSON.
361
+ function lireDepuis(f, decalage, taille) {
362
+ const fd = openSync(f, 'r');
363
+ try {
364
+ const tampon = Buffer.alloc(taille - decalage);
365
+ const lus = readSync(fd, tampon, 0, tampon.length, decalage);
366
+ const lu = tampon.subarray(0, lus);
367
+ const fin = lu.lastIndexOf(10) + 1;
368
+ let entier = lu.subarray(0, fin);
369
+ if (fin < lu.length) {
370
+ try {
371
+ JSON.parse(lu.subarray(fin).toString('utf8'));
372
+ entier = lu;
373
+ } catch {
374
+ // The line is not whole yet.
375
+ }
376
+ }
377
+ return { texte: entier.toString('utf8'), lus: entier.length };
378
+ } finally {
379
+ closeSync(fd);
380
+ }
381
+ }
382
+
383
+ // The read unit of a transcript file: a chunk holds whole lines, a line longer than it widens it.
384
+ const MORCEAU = 1_048_576;
385
+
386
+ // Adds the lines of `f` between the bytes `decalage` and `taille` to the state `etat`, chunk by
387
+ // chunk so the file is never held whole, and returns the bytes it consumed.
388
+ function avancerDepuis(etat, f, decalage, taille) {
389
+ let position = decalage;
390
+ let pas = MORCEAU;
391
+ while (position < taille) {
392
+ const fin = Math.min(taille, position + pas);
393
+ const { texte, lus } = lireDepuis(f, position, fin);
394
+ if (lus === 0) {
395
+ if (fin === taille) {
396
+ break;
397
+ }
398
+ pas *= 2;
399
+ continue;
400
+ }
401
+ avancer(etat, texte);
402
+ position += lus;
403
+ pas = MORCEAU;
404
+ }
405
+ return position - decalage;
406
+ }
407
+
408
+ // The measures of the transcript file `f`, read in chunks. Throws when the file cannot be read.
409
+ export function lireFichier(f) {
410
+ const etat = etatNeuf();
411
+ avancerDepuis(etat, f, 0, statSync(f).size);
412
+ return mesurer(etat);
413
+ }
414
+
415
+ // A transcript read where it stopped: `cache` maps a file to its reading state and the byte it
416
+ // reached. A transcript only grows; one that became shorter is read again from its start.
417
+ const lecteur = cache => f => {
418
+ try {
419
+ const taille = statSync(f).size;
420
+ let { etat, decalage } = cache[f] ?? {};
421
+ if (!etat || taille < decalage) {
422
+ etat = etatNeuf();
423
+ decalage = 0;
424
+ }
425
+ if (taille > decalage) {
426
+ decalage += avancerDepuis(etat, f, decalage, taille);
427
+ }
428
+ cache[f] = { etat, decalage };
429
+ return mesurer(etat);
430
+ } catch {
431
+ return null;
432
+ }
433
+ };
434
+ const fichiers = (dir, fin) =>
435
+ existsSync(dir)
436
+ ? readdirSync(dir)
437
+ .filter(n => n.endsWith(fin))
438
+ .map(n => path.join(dir, n))
439
+ : [];
440
+
441
+ // Whether the last tool call of the transcript `f` awaits its answer, read from the end of the
442
+ // file (the last call and its answer sit in its last bytes) so a transcript that waits on nothing
443
+ // is never read whole.
444
+ const FIN_LUE = 65_536;
445
+ function appelEnAttente(f) {
446
+ try {
447
+ const taille = statSync(f).size;
448
+ const debut = Math.max(0, taille - FIN_LUE);
449
+ const { texte } = lireDepuis(f, debut, taille);
450
+ return lireTranscription(
451
+ texte
452
+ .split('\n')
453
+ .slice(debut > 0 ? 1 : 0)
454
+ .join('\n')
455
+ ).attend;
456
+ } catch {
457
+ return false;
458
+ }
459
+ }
460
+
461
+ // Whether the agent whose transcript is `f` runs at `maintenant`: its transcript moved in the last
462
+ // 2 minutes; a sub-agent (`sousAgent`) runs also while its last tool call awaits its answer, its
463
+ // transcript having moved in the last 30 minutes. The board and the catch-up of the register share
464
+ // this definition.
465
+ export function tourne(f, maintenant, sousAgent) {
466
+ const age = maintenant - statSync(f).mtimeMs;
467
+ return age < RECENT || (sousAgent && age <= SANS_AGENT && appelEnAttente(f));
468
+ }
469
+
470
+ // A sub-agent's meta file (next to its transcript `f`), or {}.
471
+ const metaDe = f => {
472
+ try {
473
+ return JSON.parse(readFileSync(f.replace(/\.jsonl$/, '.meta.json'), 'utf8'));
474
+ } catch {
475
+ return {};
476
+ }
477
+ };
478
+
479
+ // The ticket a sub-agent takes from the regulator that launched it: the ticket its prompt names,
480
+ // read from the regulator's transcript in the same folder; null when its parent is not a regulator
481
+ // (by its spawned role) or names no ticket. With `depuis`, a regulator whose transcript has not
482
+ // moved since that time (ms) is not read. `lire` reads a transcript (the board passes its cached
483
+ // reader).
484
+ export function ticketDuRegulateur(f, depuis = -Infinity, lire = lecteur({})) {
485
+ const parent = metaDe(f).parentAgentId;
486
+ if (!parent) {
487
+ return null;
488
+ }
489
+ const g = path.join(path.dirname(f), `agent-${parent}.jsonl`);
490
+ if (roleDuMeta(metaDe(g)) !== REGULATEUR) {
491
+ return null;
492
+ }
493
+ try {
494
+ if (statSync(g).mtimeMs < depuis) {
495
+ return null;
496
+ }
497
+ return lire(g)?.ticket ?? null;
498
+ } catch {
499
+ return null;
500
+ }
501
+ }
502
+
503
+ // The agents running now, each with its measures so far. `ps` is the process list
504
+ // (`pid etimes args` per line); `cache` keeps the readings of unchanged transcripts. A role agent
505
+ // launched in a shell shows by the envelope lancer.sh starts for it,
506
+ // `enveloppe.mjs <racine>/.claude/worktrees/<ticket> <composant> <rôle> --`: its copy gives the
507
+ // ticket, its third argument the role (the prompt goes on the standard input, absent from the line).
508
+ export function lireVivant({ racine, home, prefix, maintenant, ps, cache = {} }) {
509
+ const lire = lecteur(cache);
510
+ const vivants = [];
511
+ const limite = maintenant - SANS_AGENT;
512
+ const projets = path.join(home, '.claude', 'projects');
513
+ const copies = path.join(racine, '.claude', 'worktrees').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
514
+ const motif = new RegExp(
515
+ `enveloppe\\.mjs ${copies}/(${prefix}-[A-Za-z0-9]+(?:\\.[0-9]+)*) \\S+ (\\S+) -- `
516
+ );
517
+ const vus = new Set();
518
+ for (const ligne of ps.split('\n')) {
519
+ const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(ligne);
520
+ const e = m && motif.exec(m[3]);
521
+ if (!e || vus.has(e[1])) {
522
+ continue;
523
+ }
524
+ const [, ticket, role] = e;
525
+ vus.add(ticket);
526
+ const copie = path.join(racine, '.claude', 'worktrees', ticket);
527
+ const dernier = fichiers(path.join(projets, enTirets(copie)), '.jsonl')
528
+ .map(f => ({ f, mtime: statSync(f).mtimeMs }))
529
+ .sort((a, b) => b.mtime - a.mtime)[0];
530
+ const lu = dernier && dernier.mtime >= limite ? lire(dernier.f) : null;
531
+ vivants.push({
532
+ agent: dernier ? path.basename(dernier.f, '.jsonl') : `pid-${m[1]}`,
533
+ ticket,
534
+ role,
535
+ ligne: true,
536
+ depuis: maintenant - Number(m[2]) * 1000,
537
+ silence: dernier ? maintenant - dernier.mtime : null,
538
+ entre: lu?.entre ?? null,
539
+ jetons: lu?.jetons ?? 0,
540
+ travail: lu?.travail ?? 0,
541
+ tours: lu?.tours ?? [],
542
+ });
543
+ }
544
+ const propre = path.join(projets, enTirets(racine));
545
+ for (const f of fichiers(propre, '.jsonl').filter(g => tourne(g, maintenant, false))) {
546
+ const lu = lire(f);
547
+ if (lu) {
548
+ vivants.push({
549
+ agent: path.basename(f, '.jsonl'),
550
+ ticket: null,
551
+ role: null,
552
+ ligne: false,
553
+ depuis: lu.premier ?? maintenant,
554
+ entre: lu.entre,
555
+ jetons: lu.jetons,
556
+ travail: lu.travail,
557
+ tours: lu.tours,
558
+ });
559
+ }
560
+ }
561
+ const sessions = existsSync(propre)
562
+ ? readdirSync(propre, { withFileTypes: true }).filter(e => e.isDirectory())
563
+ : [];
564
+ for (const s of sessions) {
565
+ const dossier = path.join(propre, s.name, 'subagents');
566
+ for (const f of fichiers(dossier, '.jsonl').filter(g => tourne(g, maintenant, true))) {
567
+ const lu = lire(f);
568
+ if (!lu) {
569
+ continue;
570
+ }
571
+ const meta = metaDe(f);
572
+ vivants.push({
573
+ agent: path.basename(f, '.jsonl').replace(/^agent-/, ''),
574
+ ticket: lu.ticket ?? ticketDuRegulateur(f, limite, lire),
575
+ role: roleDuMeta(meta),
576
+ ligne: false,
577
+ depuis: lu.premier ?? maintenant,
578
+ entre: lu.entre,
579
+ jetons: lu.jetons,
580
+ travail: lu.travail,
581
+ tours: lu.tours,
582
+ });
583
+ }
584
+ }
585
+ return vivants;
586
+ }
587
+
588
+ // The circuits of the repository at <racine>: each state `.claude/worktrees/<ticket>.circuit.json`
589
+ // scripts/circuit.mjs writes, but those at « integre », with `vivant` when `ps` (`pid etimes args`
590
+ // per line) shows its process, `grillhouse circuit <ticket>` (the command the supervisor
591
+ // launches, `npx grillhouse circuit <ticket>`), `regulateur` when the state names a
592
+ // session of the regulator, `questionnement` the object of the questioning under way (`plan` or
593
+ // `decision`), null without one, `question` the
594
+ // regulator's whole question for a circuit ended on it,
595
+ // the counters of its bounded loops (`boucles`, by their key), and the rank of its component among the title's (`rang`, from 1) and their number (`total`),
596
+ // the progress of the automatic pass under way (`avancement`, read in the directory its state
597
+ // names, scripts/avancement.mjs: `{ faits, prevus, dernier }`, null without one) and the start of
598
+ // the survey of the components under way (`releve`, ms, null without one). A state that does not
599
+ // parse is left out.
600
+ export function lireCircuits({ racine, prefix, ps }) {
601
+ const dossier = path.join(racine, '.claude', 'worktrees');
602
+ const motif = new RegExp(`grillhouse circuit (${prefix}-[A-Za-z0-9]+(?:\\.[0-9]+)*)(?:\\s|$)`);
603
+ const enVie = new Set();
604
+ for (const ligne of ps.split('\n')) {
605
+ const m = /^\s*\d+\s+\d+\s+(.*)$/.exec(ligne);
606
+ const e = m && motif.exec(m[1]);
607
+ if (e) {
608
+ enVie.add(e[1]);
609
+ }
610
+ }
611
+ const out = [];
612
+ for (const f of fichiers(dossier, '.circuit.json')) {
613
+ let e;
614
+ try {
615
+ e = JSON.parse(readFileSync(f, 'utf8'));
616
+ } catch {
617
+ continue;
618
+ }
619
+ if (!e || e.etape === 'integre') {
620
+ continue;
621
+ }
622
+ const ticket = path.basename(f, '.circuit.json');
623
+ out.push({
624
+ ticket,
625
+ etape: e.etape ?? null,
626
+ composant: e.composant ?? null,
627
+ tentative: e.tentative ?? null,
628
+ finale: e.finale === true,
629
+ code: e.code ?? null,
630
+ arret: e.raison ?? null,
631
+ raison: premiereLigne(e.rapport),
632
+ question: e.raison === 'question' ? String(e.rapport ?? '').trim() || null : null,
633
+ regulateur: Boolean(e.regulateur),
634
+ questionnement: e.questionnement?.objet ?? null,
635
+ ...rangDe(e),
636
+ boucles: e.boucles && typeof e.boucles === 'object' ? e.boucles : {},
637
+ mene:
638
+ typeof e.mene === 'string' && !Number.isNaN(Date.parse(e.mene)) ? Date.parse(e.mene) : null,
639
+ avancement:
640
+ typeof e.avancement?.dossier === 'string' ? lireAvancement(e.avancement.dossier) : null,
641
+ releve:
642
+ typeof e.releve === 'string' && !Number.isNaN(Date.parse(e.releve))
643
+ ? Date.parse(e.releve)
644
+ : null,
645
+ vivant: enVie.has(ticket),
646
+ });
647
+ }
648
+ return out;
649
+ }
650
+ // The rank of the state <e>'s component among the title's components, from 1, and their number;
651
+ // both null when the state does not name them.
652
+ const rangDe = e => {
653
+ const l = Array.isArray(e.composants) ? e.composants : [];
654
+ const i = l.indexOf(e.composant);
655
+ return i < 0 ? { rang: null, total: null } : { rang: i + 1, total: l.length };
656
+ };
657
+ // The first line of a report that is not blank, or null.
658
+ const premiereLigne = r =>
659
+ String(r ?? '')
660
+ .split('\n')
661
+ .map(x => x.trim())
662
+ .find(Boolean) ?? null;
663
+
664
+ // The reviewer's last verdict among a ticket's comments (verdictDe: a comment of the reviewer's
665
+ // author), or null.
666
+ export function verdict(commentaires) {
667
+ let v = null;
668
+ for (const c of [...commentaires].sort(
669
+ (a, b) => Date.parse(a.created_at) - Date.parse(b.created_at)
670
+ )) {
671
+ const lu = verdictDe(c);
672
+ if (lu) {
673
+ v = { verdict: lu, date: Date.parse(c.created_at) };
674
+ }
675
+ }
676
+ return v;
677
+ }
678
+
679
+ // The role a ticket waits for, from the roles of the flow that ran on it (finished, oldest first)
680
+ // and the reviewer's last verdict; null when it waits for none: an accepted lot waits for no
681
+ // agent, the circuit integrates it (scripts/circuit.mjs).
682
+ export function etape(passages, v) {
683
+ const faits = passages.filter(p => etapeDuRole(p.role) && !juge(p.role));
684
+ const dernier = faits[faits.length - 1];
685
+ if (!dernier) {
686
+ return 'testwriter';
687
+ }
688
+ const fin = Date.parse(dernier.fin);
689
+ const jugé = v && v.date >= fin ? v.verdict : null;
690
+ if (dernier.role === 'testwriter') {
691
+ return 'developpeur';
692
+ }
693
+ if (dernier.role === 'developpeur') {
694
+ return jugé === 'ACCEPTÉ' ? null : jugé === 'RENDU' ? 'developpeur' : 'relecteur';
695
+ }
696
+ if (dernier.role === 'relecteur') {
697
+ return v?.verdict === 'ACCEPTÉ' ? null : 'developpeur';
698
+ }
699
+ return null;
700
+ }
701
+
702
+ // A ticket's number, subject and component: its title « number — subject — component » when
703
+ // the number ends its id, else its id without the prefix and its whole title.
704
+ export function lecture(t, prefix) {
705
+ const brut =
706
+ prefix && t.id.startsWith(`${prefix}-`)
707
+ ? t.id.slice(prefix.length + 1)
708
+ : t.id.slice(t.id.indexOf('-') + 1);
709
+ const parts = (t.title ?? '').split(' — ');
710
+ if (parts.length >= 2 && (brut === parts[0] || brut.endsWith(`.${parts[0]}`))) {
711
+ return parts.length >= 3
712
+ ? {
713
+ numero: parts[0],
714
+ sujet: parts.slice(1, -1).join(' — '),
715
+ composant: parts[parts.length - 1],
716
+ }
717
+ : { numero: parts[0], sujet: parts[1], composant: '' };
718
+ }
719
+ return { numero: brut, sujet: t.title ?? t.id, composant: '' };
720
+ }
721
+
722
+ // The board, from what was read. Pure: every source comes in, the time included.
723
+ export function assembler({
724
+ tous,
725
+ prefix = '',
726
+ registre: lignesDuRegistre,
727
+ vivants,
728
+ circuits = [],
729
+ verdicts = {},
730
+ maintenant,
731
+ jour,
732
+ horsTicket = null,
733
+ declarations: lesDeclarations = [],
734
+ }) {
735
+ // A circuit's lines are its steps, no agent's passage.
736
+ const etapesFinies = lignesDuRegistre.filter(r => r.role === ROLE_DU_CIRCUIT);
737
+ const registre = parAgent(lignesDuRegistre.filter(r => r.role !== ROLE_DU_CIRCUIT));
738
+ // A living agent runs without an end in the register, or when an incoming message follows its
739
+ // latest end strictly; the writings of its last turn after that end leave it finished. A running
740
+ // agent's transcript holds its sessions already in the register.
741
+ const fins = new Map(registre.map(r => [r.agent, Date.parse(r.fin)]));
742
+ const enCoursMesures = vivants.filter(
743
+ v => !fins.has(v.agent) || (v.entre ?? -Infinity) > fins.get(v.agent)
744
+ );
745
+ const repris = new Set(enCoursMesures.map(v => v.agent));
746
+ const finis = registre.filter(r => !repris.has(r.agent));
747
+ const parId = new Map(tous.map(t => [t.id, t]));
748
+ const lu = new Map(tous.map(t => [t.id, lecture(t, prefix)]));
749
+ const numero = id => lu.get(id)?.numero ?? id;
750
+ const titre = t => lu.get(t.id);
751
+ const ouvert = id => parId.has(id) && parId.get(id).status !== 'closed';
752
+ // Ready: open, every ticket that blocks it closed (Beads' own rule, without a second call).
753
+ const blocages = t =>
754
+ (t.dependencies ?? []).filter(d => d.type === 'blocks' && ouvert(d.depends_on_id));
755
+ const prets = tous.filter(t => t.status === 'open' && blocages(t).length === 0);
756
+ const aValider = tous.filter(
757
+ t => t.status !== 'closed' && (t.labels ?? []).includes('a-valider')
758
+ );
759
+ const nuit = tous.filter(t => t.status !== 'closed' && (t.labels ?? []).includes('nuit'));
760
+ const enfants = new Map();
761
+ for (const t of tous) {
762
+ if (t.parent && parId.has(t.parent)) {
763
+ (enfants.get(t.parent) ?? enfants.set(t.parent, []).get(t.parent)).push(t);
764
+ }
765
+ }
766
+ const ordre = (a, b) => a.id.localeCompare(b.id, undefined, { numeric: true });
767
+ for (const l of enfants.values()) {
768
+ l.sort(ordre);
769
+ }
770
+ // The circuits of the open tickets; a ticket whose circuit runs is in progress.
771
+ const parCircuit = new Map(circuits.filter(c => ouvert(c.ticket)).map(c => [c.ticket, c]));
772
+ const actifs = new Set([
773
+ ...enCoursMesures.map(v => v.ticket).filter(Boolean),
774
+ ...[...parCircuit.values()].filter(c => c.vivant).map(c => c.ticket),
775
+ ]);
776
+ const travailles = new Set(registre.map(r => r.ticket).filter(Boolean));
777
+ // A ticket is work unless it is an epic or a mother; a ticket an agent holds or has worked stays
778
+ // work, done or not, its discoveries under it notwithstanding.
779
+ const travail = t =>
780
+ t.issue_type !== 'epic' &&
781
+ (!enfants.has(t.id) || t.status === 'in_progress' || actifs.has(t.id) || travailles.has(t.id));
782
+ const pret = new Set(prets.map(t => t.id));
783
+ const avalider = new Set(aValider.map(t => t.id));
784
+ const bloquePar = t => blocages(t).map(d => numero(d.depends_on_id));
785
+ // The owner's go: the label `circuit`, set on his word.
786
+ const circuit = t => (t.labels ?? []).includes('circuit');
787
+ // A ticket's state: done, in progress, deferred, awaiting validation; else, with the owner's go,
788
+ // waiting (ready) or blocked (an open ticket it depends on); without it, awaiting his go.
789
+ const etat = t => {
790
+ if (t.status === 'closed') {
791
+ return 'fermes';
792
+ }
793
+ if (t.status === 'in_progress' || actifs.has(t.id)) {
794
+ return 'enCours';
795
+ }
796
+ if (t.status === 'deferred') {
797
+ return 'reportes';
798
+ }
799
+ if (avalider.has(t.id)) {
800
+ return 'aValider';
801
+ }
802
+ if (!circuit(t)) {
803
+ return 'sansGo';
804
+ }
805
+ return pret.has(t.id) ? 'enAttente' : 'bloques';
806
+ };
807
+
808
+ // Global.
809
+ const compteurs = {
810
+ enCours: 0,
811
+ enAttente: 0,
812
+ bloques: 0,
813
+ fermes: 0,
814
+ sansGo: 0,
815
+ aValider: 0,
816
+ reportes: 0,
817
+ };
818
+ // Each state's tickets, one per line in the states' view: the done ones, last closed first.
819
+ const parEtat = Object.fromEntries(Object.keys(compteurs).map(e => [e, []]));
820
+ for (const t of [...tous].sort(ordre)) {
821
+ if (travail(t)) {
822
+ compteurs[etat(t)]++;
823
+ parEtat[etat(t)].push(t);
824
+ }
825
+ }
826
+ parEtat.fermes.sort((a, b) => Date.parse(b.closed_at ?? '') - Date.parse(a.closed_at ?? ''));
827
+ // The states' view lists the tickets of the chantiers in progress only (`garde`, their ids); the
828
+ // counters keep every ticket.
829
+ const etatsDe = garde =>
830
+ ETATS.map(({ cle, nom, sens }) => ({
831
+ cle,
832
+ nom,
833
+ sens,
834
+ tickets: parEtat[cle]
835
+ .filter(t => garde.has(t.id))
836
+ .map(t => ({
837
+ id: t.id,
838
+ numero: numero(t.id),
839
+ sujet: titre(t).sujet,
840
+ bloquePar: bloquePar(t),
841
+ })),
842
+ }));
843
+ const desAgents = [
844
+ ...finis.map(r => ({ ...r, quand: Date.parse(r.fin) })),
845
+ ...enCoursMesures.map(v => ({ ...v, quand: maintenant })),
846
+ ];
847
+ // The steps a circuit leads itself: its register lines, and the step under way of a running
848
+ // circuit up to now. Each ticket's steps count out of its agents' turns, so a ticket's working
849
+ // time is the union of both, each instant once; they spend no tokens.
850
+ const etapes = new Map();
851
+ const etapeDe = (ticket, debut, fin) => {
852
+ if (ticket && Number.isFinite(debut) && Number.isFinite(fin) && fin > debut) {
853
+ (etapes.get(ticket) ?? etapes.set(ticket, []).get(ticket)).push([debut, fin]);
854
+ }
855
+ };
856
+ for (const r of etapesFinies) {
857
+ etapeDe(r.ticket, Date.parse(r.debut), Date.parse(r.fin));
858
+ }
859
+ for (const c of circuits) {
860
+ if (c.vivant && c.mene !== null && c.mene !== undefined) {
861
+ etapeDe(c.ticket, c.mene, maintenant);
862
+ }
863
+ }
864
+ const duCircuit = [...etapes].map(([ticket, l]) => {
865
+ const tours = sauf(
866
+ reunir(l),
867
+ reunir(desAgents.filter(m => m.ticket === ticket).flatMap(m => m.tours ?? []))
868
+ );
869
+ return {
870
+ ticket,
871
+ role: ROLE_DU_CIRCUIT,
872
+ travail: tours.reduce((s, [debut, fin]) => s + (fin - debut), 0),
873
+ jetons: 0,
874
+ tours,
875
+ quand: Math.max(-Infinity, ...tours.map(([, fin]) => fin)),
876
+ };
877
+ });
878
+ const mesures = [...desAgents, ...duCircuit];
879
+ const somme = l => ({
880
+ travail: l.reduce((s, x) => s + (x.travail ?? 0), 0),
881
+ jetons: l.reduce((s, x) => s + (x.jetons ?? 0), 0),
882
+ });
883
+ // A measure with its turns counts in the day the parts since midnight of their union; a register
884
+ // line written without them counts whole in the day it ended.
885
+ const partDuJour = tours =>
886
+ reunir(tours).reduce((s, [debut, fin]) => s + Math.max(0, fin - Math.max(debut, jour)), 0);
887
+ const duJour = mesures
888
+ .filter(m => m.quand >= jour)
889
+ .map(m => (m.tours ? { ...m, travail: partDuJour(m.tours) } : m));
890
+ const global = { compteurs, jour: somme(duJour), cumul: somme(mesures) };
891
+
892
+ // Chantiers.
893
+ const sous = t => [t, ...(enfants.get(t.id) ?? []).flatMap(sous)];
894
+ const mesure = ids => somme(mesures.filter(m => ids.has(m.ticket)));
895
+ const noeud = t => {
896
+ const liste = sous(t).filter(travail);
897
+ const faits = liste.filter(x => x.status === 'closed').length;
898
+ // A mother not in progress has her own state; an epic, the state of its tickets.
899
+ const e = travail(t)
900
+ ? etat(t)
901
+ : t.status === 'in_progress' || liste.some(x => etat(x) === 'enCours')
902
+ ? 'enCours'
903
+ : t.issue_type !== 'epic'
904
+ ? etat(t)
905
+ : faits === liste.length
906
+ ? 'fermes'
907
+ : bloquePar(t).length
908
+ ? 'bloques'
909
+ : 'sansGo';
910
+ return {
911
+ id: t.id,
912
+ ...titre(t),
913
+ mere: !travail(t),
914
+ etat: e,
915
+ faits,
916
+ total: liste.length,
917
+ bloquePar: bloquePar(t),
918
+ ...mesure(new Set(sous(t).map(x => x.id))),
919
+ enfants: ouverts(t).map(noeud),
920
+ };
921
+ };
922
+ // The tree holds open tickets only: a closed child leaves it, and a closed mother gives her place
923
+ // to her open descendants, at her level, in her order.
924
+ const ouverts = t =>
925
+ (enfants.get(t.id) ?? []).flatMap(x => (x.status === 'closed' ? ouverts(x) : [x]));
926
+ const racines = tous
927
+ .filter(t => t.issue_type === 'epic' && !t.parent && t.status !== 'closed')
928
+ .sort((a, b) => a.priority - b.priority || ordre(a, b));
929
+ const chantiers = [];
930
+ const autres = [];
931
+ for (const r of racines) {
932
+ const n = noeud(r);
933
+ if (n.etat === 'enCours') {
934
+ chantiers.push(n);
935
+ } else {
936
+ autres.push({ id: n.id, numero: n.numero, sujet: n.sujet, faits: n.faits, total: n.total });
937
+ }
938
+ }
939
+ const duChantier = new Set(
940
+ racines
941
+ .filter(r => chantiers.some(c => c.id === r.id))
942
+ .flatMap(sous)
943
+ .map(t => t.id)
944
+ );
945
+ const etats = etatsDe(duChantier);
946
+
947
+ // Agents.
948
+ const passages = new Map();
949
+ for (const r of [...registre].sort((a, b) => Date.parse(a.fin) - Date.parse(b.fin))) {
950
+ if (r.ticket) {
951
+ (passages.get(r.ticket) ?? passages.set(r.ticket, []).get(r.ticket)).push(r);
952
+ }
953
+ }
954
+ // One block per step and sub-step, in the order of the steps, with who works there, then the
955
+ // arbitrage's.
956
+ const blocs = [...SUITE, ARBITRAGE].map(e => ({
957
+ numero: e.numero,
958
+ titre: e.titre,
959
+ roles: e.qui.filter(r => r !== AUTO),
960
+ qui: quiDe(e),
961
+ enCours: [],
962
+ attend: [],
963
+ }));
964
+ const bloc = role => {
965
+ const e = etapeDuRole(role);
966
+ return e ? blocs.find(b => b.numero === e.numero) : undefined;
967
+ };
968
+ const places = new Set();
969
+ // One line per ticket: a ticket on which a role agent runs stands at its step, and at the
970
+ // regulator's only while none of its other agents runs.
971
+ const auTravail = new Set(
972
+ enCoursMesures.filter(v => v.role !== REGULATEUR && bloc(v.role)).map(v => v.ticket)
973
+ );
974
+ for (const v of enCoursMesures) {
975
+ const t = v.ticket && parId.get(v.ticket);
976
+ if (!t || !bloc(v.role) || (v.role === REGULATEUR && auTravail.has(v.ticket))) {
977
+ continue;
978
+ }
979
+ places.add(t.id);
980
+ bloc(v.role).enCours.push({
981
+ id: t.id,
982
+ numero: numero(t.id),
983
+ sujet: titre(t).sujet,
984
+ composant: titre(t).composant,
985
+ ligne: v.ligne,
986
+ depuis: v.depuis,
987
+ silence: v.silence ?? null,
988
+ ...mesure(new Set([t.id])),
989
+ });
990
+ }
991
+ // A ticket waits in AGENTS only with the owner's go (`circuit`): ready, blocked, or claimed with
992
+ // nothing running; a ticket without it stands in CHANTIER alone; a ticket with a circuit, running
993
+ // or stopped, stands in its circuit's entry, never waiting. The flow's deduction places the
994
+ // others, and a ticket no agent took yet waits for the testwriter, the circuit's first agent.
995
+ for (const t of [...tous].sort(ordre)) {
996
+ if (
997
+ !travail(t) ||
998
+ places.has(t.id) ||
999
+ parCircuit.has(t.id) ||
1000
+ ['fermes', 'reportes', 'aValider', 'sansGo'].includes(etat(t)) ||
1001
+ !circuit(t)
1002
+ ) {
1003
+ continue;
1004
+ }
1005
+ const p = passages.get(t.id) ?? [];
1006
+ // Claimed without any passage, the ticket keeps its line in its chantier.
1007
+ if (p.length === 0 && t.status === 'in_progress') {
1008
+ continue;
1009
+ }
1010
+ const r = etape(p, verdicts[t.id] ?? null);
1011
+ if (r && bloc(r)) {
1012
+ bloc(r).attend.push({
1013
+ id: t.id,
1014
+ numero: numero(t.id),
1015
+ sujet: titre(t).sujet,
1016
+ bloquePar: bloquePar(t),
1017
+ ...mesure(new Set([t.id])),
1018
+ });
1019
+ }
1020
+ }
1021
+ const debutJour = jour;
1022
+ const faitsDuJour = tous
1023
+ .filter(t => travail(t) && t.closed_at && Date.parse(t.closed_at) >= debutJour)
1024
+ .sort((a, b) => Date.parse(b.closed_at) - Date.parse(a.closed_at))
1025
+ .map(t => ({
1026
+ id: t.id,
1027
+ numero: numero(t.id),
1028
+ sujet: titre(t).sujet,
1029
+ ...mesure(new Set([t.id])),
1030
+ }));
1031
+
1032
+ // Alerts.
1033
+ const alertes = [];
1034
+ // A red night says the night it turned red, short enough to stand whole on its line: the local
1035
+ // day and month its ticket was opened, the night's date (scripts/nuit.sh), whatever its title names, a
1036
+ // commit in older ones.
1037
+ for (const t of nuit) {
1038
+ const { sujet: titreLu, numero: n } = titre(t);
1039
+ const ouvert = Date.parse(t.created_at ?? '');
1040
+ const sujet =
1041
+ /^La nuit est rouge\b/.test(titreLu) && !Number.isNaN(ouvert)
1042
+ ? `Nuit rouge depuis le ${jourEtMois(ouvert)}`
1043
+ : titreLu;
1044
+ alertes.push({ niveau: 'rouge', texte: `${sujet} ${n}`, sujet, numero: n });
1045
+ }
1046
+ for (const b of blocs) {
1047
+ for (const x of b.enCours) {
1048
+ if (x.ligne && x.silence !== null && x.silence > SILENCE) {
1049
+ alertes.push({
1050
+ niveau: 'orange',
1051
+ texte: `${x.numero}, agent ${b.numero} silencieux depuis ${Math.round(x.silence / MINUTE)} min`,
1052
+ });
1053
+ }
1054
+ }
1055
+ }
1056
+ // A ticket's circuit: from its highest mother still open (an epic is a chantier, not a circuit),
1057
+ // that mother and every ticket under it. A circuit lives while an agent runs on one of its
1058
+ // tickets: the others wait their turn in the same copy.
1059
+ const racineDuCircuit = t => {
1060
+ let r = t;
1061
+ for (let m = parId.get(r.parent); m && m.issue_type !== 'epic' && m.status !== 'closed';) {
1062
+ r = m;
1063
+ m = parId.get(r.parent);
1064
+ }
1065
+ return r;
1066
+ };
1067
+ const vit = t => actifs.has(t.id) || (enfants.get(t.id) ?? []).some(vit);
1068
+ for (const t of tous) {
1069
+ if (
1070
+ travail(t) &&
1071
+ t.status === 'in_progress' &&
1072
+ !vit(racineDuCircuit(t)) &&
1073
+ t.started_at &&
1074
+ maintenant - Date.parse(t.started_at) > SANS_AGENT
1075
+ ) {
1076
+ alertes.push({ niveau: 'orange', texte: `${numero(t.id)} en cours sans rien qui tourne` });
1077
+ }
1078
+ }
1079
+ // Each ticket's own measures: its agents' working time and tokens, running agents included.
1080
+ const parTicket = Object.fromEntries(
1081
+ [...new Set(mesures.map(m => m.ticket).filter(Boolean))].map(id => [id, mesure(new Set([id]))])
1082
+ );
1083
+ // The circuits, in the tickets' order.
1084
+ const lesCircuits = [...parCircuit.values()]
1085
+ .sort((a, b) => ordre({ id: a.ticket }, { id: b.ticket }))
1086
+ .map(c => ({
1087
+ ...c,
1088
+ id: c.ticket,
1089
+ numero: numero(c.ticket),
1090
+ sujet: titre(parId.get(c.ticket)).sujet,
1091
+ }));
1092
+ return {
1093
+ global,
1094
+ alertes,
1095
+ circuits: lesCircuits,
1096
+ chantiers,
1097
+ autres,
1098
+ agents: blocs,
1099
+ maintenant,
1100
+ horsTicket,
1101
+ faitsDuJour,
1102
+ etats,
1103
+ parTicket,
1104
+ declarations: lesDeclarations,
1105
+ };
1106
+ }
1107
+
1108
+ // The declarations of a project, the keys of `grillhouse` of its package.json <pkg> that hold
1109
+ // something (a non-empty list, a value): a step that names one (`declaration`, scripts/etapes.mjs)
1110
+ // shows only on a board that holds it.
1111
+ export const declarations = pkg =>
1112
+ Object.entries(pkg?.grillhouse ?? {})
1113
+ .filter(([, v]) => (Array.isArray(v) ? v.length > 0 : Boolean(v)))
1114
+ .map(([cle]) => cle);
1115
+
1116
+ export const k = n =>
1117
+ n >= 1e6
1118
+ ? `${(n / 1e6).toFixed(1).replace('.', ',')} M`
1119
+ : n >= 1e3
1120
+ ? `${Math.round(n / 1e3)} k`
1121
+ : `${n}`;
1122
+ export const duree = ms => {
1123
+ const min = Math.round(ms / MINUTE);
1124
+ return min < 60 ? `${min} min` : `${Math.floor(min / 60)} h ${String(min % 60).padStart(2, '0')}`;
1125
+ };
1126
+
1127
+ // The local day and month of the instant <t>, « 16/10 ».
1128
+ const jourEtMois = t => {
1129
+ const d = new Date(t);
1130
+ const deux = x => String(x).padStart(2, '0');
1131
+ return `${deux(d.getDate())}/${deux(d.getMonth() + 1)}`;
1132
+ };
1133
+
1134
+ // A time since a sign, « 8 s » under a minute, else as `duree`.
1135
+ const ilYa = ms => (ms < MINUTE ? `${Math.max(0, Math.round(ms / 1000))} s` : duree(ms));
1136
+
1137
+ // The witness of the automatic step <etape> of the running circuit <c> at <maintenant>: the
1138
+ // progress of its pass (` · faits/prévus · <durée>`, the figures once a task wrote, the time since
1139
+ // the start of the pass, as Bazel's counter), or the time since the start of its survey of the
1140
+ // components (1.1); past FIGEE without a sign, ` · ⚠ figée <durée> · faits/prévus`, the time since
1141
+ // the last sign, `fige`. Null without either.
1142
+ function temoinDe(c, etape, maintenant) {
1143
+ let age;
1144
+ let ecoule;
1145
+ let compte = '';
1146
+ if (c.avancement) {
1147
+ age = maintenant - c.avancement.dernier;
1148
+ ecoule = maintenant - (c.avancement.debut || c.avancement.dernier);
1149
+ const { faits, prevus } = c.avancement;
1150
+ if (faits || prevus) {
1151
+ compte = `${faits}/${prevus}`;
1152
+ }
1153
+ } else if (etape.releve && c.releve !== null && c.releve !== undefined) {
1154
+ age = maintenant - c.releve;
1155
+ ecoule = age;
1156
+ } else {
1157
+ return null;
1158
+ }
1159
+ const fige = age > FIGEE;
1160
+ return {
1161
+ fige,
1162
+ texte: fige
1163
+ ? ` · ⚠ figée ${ilYa(age)}${compte ? ` · ${compte}` : ''}`
1164
+ : ` · ${compte ? `${compte} · ` : ''}${ilYa(ecoule)}`,
1165
+ };
1166
+ }
1167
+
1168
+ // A ticket's time and tokens, « 12 min · 310 k »; nothing when both are zero.
1169
+ const conso = (ms, jetons) =>
1170
+ Math.round(ms / MINUTE) === 0 && !jetons ? '' : `${duree(ms)} · ${k(jetons)}`;
1171
+
1172
+ // A line of `largeur` columns: the left part cut to leave the right part whole.
1173
+ const couper = (gauche, droite, largeur) => {
1174
+ if (!droite) {
1175
+ return gauche.length > largeur ? `${gauche.slice(0, largeur - 1)}…` : gauche;
1176
+ }
1177
+ const place = largeur - droite.length - 1;
1178
+ const g =
1179
+ gauche.length > place ? `${gauche.slice(0, Math.max(place - 1, 0))}…` : gauche.padEnd(place);
1180
+ return `${g} ${droite}`;
1181
+ };
1182
+ // A line of `largeur` columns whose left part ends with `suite`, kept whole: what comes before it
1183
+ // is cut to leave it and the right part whole.
1184
+ const couperAvant = (gauche, suite, droite, largeur) => {
1185
+ if (!suite) {
1186
+ return couper(gauche, droite, largeur);
1187
+ }
1188
+ const place = (droite ? largeur - droite.length - 1 : largeur) - suite.length;
1189
+ const g = gauche.length > place ? `${gauche.slice(0, Math.max(place - 1, 0))}…` : gauche;
1190
+ return couper(`${g}${suite}`, droite, largeur);
1191
+ };
1192
+ // The board's lines, `largeur` columns wide, each with its tone (titre, alerte, attention, actif,
1193
+ // discret, or none), its section (`section`: agents, chantier or faits, in that order) and, for a
1194
+ // ticket's line, its id (`ticket`), which the pane makes selectable; an epic's line is marked
1195
+ // `chantier`. The head belongs to agents, the first section; a blank line belongs to the section
1196
+ // of the title after it. A chantier's « sans go » and « reporté » groups, its « à valider » group
1197
+ // past SEUIL_REPLI tickets, and the other chantiers beside one in progress fold to their title and
1198
+ // count: the folded title carries its group's key (`groupe`), and `groupes` receives, under that
1199
+ // key, the group's lines whole, which the pane opens on its own. The pane and the text show the
1200
+ // same lines.
1201
+ export const SEUIL_REPLI = 10;
1202
+ export function lignes(t, nom, largeur = LARGEUR, groupes = {}) {
1203
+ const out = [];
1204
+ let section = 'agents';
1205
+ // Each line laid down takes the current section.
1206
+ const marquer = l => ({ ...l, section });
1207
+ const poser = (...l) => out.push(...l.map(marquer));
1208
+ const cadre = couper;
1209
+ const L = (texte, ton, ticket) => poser(ticket ? { texte, ton, ticket } : { texte, ton });
1210
+ const g = t.global;
1211
+ const c = g.compteurs;
1212
+ L(nom.toUpperCase(), 'titre');
1213
+ // Two lines of states: those of the circuit, then those that wait for the owner.
1214
+ L(
1215
+ cadre(
1216
+ `▶ ${c.enCours} en cours · ${c.enAttente} en attente · ${c.bloques} bloqués · ${c.fermes} faits`,
1217
+ '',
1218
+ largeur
1219
+ )
1220
+ );
1221
+ L(
1222
+ cadre(`${c.sansGo} sans go · ${c.aValider} à valider · ${c.reportes} reportés`, '', largeur),
1223
+ c.sansGo || c.aValider ? 'attention' : undefined
1224
+ );
1225
+ L(
1226
+ cadre(
1227
+ `jour ${duree(g.jour.travail)} · ${k(g.jour.jetons)} · cumul ${duree(g.cumul.travail)} · ${k(g.cumul.jetons)}`,
1228
+ '',
1229
+ largeur
1230
+ ),
1231
+ 'discret'
1232
+ );
1233
+ for (const a of t.alertes) {
1234
+ L(
1235
+ a.numero ? cadre(`⚠ ${a.sujet}`, a.numero, largeur) : cadre(`⚠ ${a.texte}`, '', largeur),
1236
+ a.niveau === 'rouge' ? 'alerte' : 'attention'
1237
+ );
1238
+ }
1239
+
1240
+ L('', undefined);
1241
+ L('AGENTS ▶ en cours · en attente', 'titre');
1242
+ // A measure out of a ticket that holds the lock of the machine (scripts/avancement.mjs), at the
1243
+ // head: « ⏳ Mesure · <dépôt> · faits/prévus · <durée> », or past FIGEE without a sign
1244
+ // « ⚠ Mesure figée <durée> · <dépôt> · faits/prévus », in the tone of an alert; the repository
1245
+ // is cut, never the figures.
1246
+ const maintenant = t.maintenant ?? Date.now();
1247
+ if (t.horsTicket) {
1248
+ const m = t.horsTicket;
1249
+ const age = maintenant - m.dernier;
1250
+ const compte = m.faits || m.prevus ? `${m.faits}/${m.prevus}` : '';
1251
+ const fige = age > FIGEE;
1252
+ const quoi = m.depot;
1253
+ poser({
1254
+ texte: fige
1255
+ ? couperAvant(
1256
+ `⚠ Mesure figée ${ilYa(age)} · ${quoi}`,
1257
+ compte ? ` · ${compte}` : '',
1258
+ '',
1259
+ largeur
1260
+ )
1261
+ : couperAvant(
1262
+ `⏳ Mesure · ${quoi}`,
1263
+ ` · ${compte ? `${compte} · ` : ''}${ilYa(maintenant - (m.debut || m.dernier))}`,
1264
+ '',
1265
+ largeur
1266
+ ),
1267
+ ton: fige ? 'alerte' : 'actif',
1268
+ avancement: true,
1269
+ });
1270
+ }
1271
+ // The tree of the steps (scripts/etapes.mjs), in its order, as a pipeline of GitLab CI shows its
1272
+ // stages and folds the jobs of a stage that has nothing: a step with sub-steps is folded
1273
+ // (« ▸ 1 · Ouverture ») while none of them holds a ticket, unfolded (« ▾ ») once one does, all
1274
+ // its sub-steps then shown, indented, the empty ones in the discreet tone; a step without
1275
+ // sub-steps stands on one line « number · title · who », discreet when empty, a title with its
1276
+ // tickets under it. A circuit's ticket stands on one line, at the step of its state, and nowhere
1277
+ // else in AGENTS: the questioning of the plan at its step; stopped, or while a session of the
1278
+ // regulator runs on its stop or the questionneur questions his decision, under its step after a
1279
+ // line « ⤷ <the arbitrage> · régulateur ⇄ questionneur », which unfolds the step: the
1280
+ // arbitrage is out of the sequence, tied to the stopped step; a stop whose kind the table of the
1281
+ // stops sends to the supervisor (gesture `superviseur`, scripts/etapes.mjs), which no regulator
1282
+ // settles, stands there after a line « ⤷ Arrêt · superviseur » instead, its counters those of
1283
+ // its step. A regulator out of any circuit
1284
+ // stands under that line after the steps. A circuit's line gives its subject (`texte`) and, in
1285
+ // turn (`alterne`, which the pane shows every other 5 s), the component under way, with its rank
1286
+ // among the title's components when they are several; then the counters of the bounded loops of
1287
+ // its step from their second pass (`boucles` of the step, scripts/etapes.mjs, their counts in
1288
+ // the state's `boucles`), whole; then its ticket's working time and tokens. Its card says where
1289
+ // it stopped and why, or the regulator's question. The tickets' numbers line up in one column.
1290
+ const enCircuit = new Set((t.circuits ?? []).map(c => c.id));
1291
+ // The block of the step <e>, an empty one when the board holds none.
1292
+ const vides = new Map();
1293
+ const blocDe = e =>
1294
+ t.agents.find(b => b.numero === e.numero) ??
1295
+ vides.get(e.numero) ??
1296
+ vides
1297
+ .set(e.numero, {
1298
+ numero: e.numero,
1299
+ titre: e.titre,
1300
+ qui: quiDe(e),
1301
+ enCours: [],
1302
+ attend: [],
1303
+ })
1304
+ .get(e.numero);
1305
+ const arbitrage = blocDe(ARBITRAGE);
1306
+ const etapeDe = c => etapeDuCircuit({ ...c, raison: c.arret });
1307
+ const auSuperviseur = c =>
1308
+ !c.vivant &&
1309
+ !c.regulateur &&
1310
+ c.questionnement !== 'decision' &&
1311
+ gesteDe(c.arret)?.geste === 'superviseur';
1312
+ const aLArbitrage = c =>
1313
+ !auSuperviseur(c) && (!c.vivant || c.regulateur || c.questionnement === 'decision');
1314
+ const blocDuCircuit = c => {
1315
+ const e = etapeDe(c);
1316
+ return e ? blocDe(e) : aLArbitrage(c) ? arbitrage : undefined;
1317
+ };
1318
+ const montres = t.agents.flatMap(b =>
1319
+ [...b.enCours, ...b.attend].filter(x => !enCircuit.has(x.id))
1320
+ );
1321
+ const colonne = Math.max(
1322
+ 0,
1323
+ ...[...(t.circuits ?? []), ...montres].map(x => String(x.numero).length)
1324
+ );
1325
+ const numero = n => String(n).padEnd(colonne);
1326
+ const ligneDuCircuit = c => {
1327
+ const droite = conso(t.parTicket?.[c.id]?.travail ?? 0, t.parTicket?.[c.id]?.jetons ?? 0);
1328
+ const etape = aLArbitrage(c) ? ARBITRAGE : etapeDe(c);
1329
+ const boucles = (etape ? bouclesDe(etape) : [])
1330
+ .filter(b => b.max && c.boucles?.[b.cle] >= 2)
1331
+ .map(b => ` · ${b.libelle} ${c.boucles[b.cle]}/${b.max}`)
1332
+ .join('');
1333
+ const tete = `${c.vivant ? '▶' : '■'} ${numero(c.numero)} `;
1334
+ const composant = c.composant
1335
+ ? `${c.composant === '.' ? '.' : path.basename(c.composant)}${c.total > 1 ? ` ${c.rang}/${c.total}` : ''}`
1336
+ : null;
1337
+ const temoin = c.vivant && etape?.qui.includes(AUTO) ? temoinDe(c, etape, maintenant) : null;
1338
+ // The progress after the counters of the loops while the component (else the subject) keeps
1339
+ // its room beside them, in their place otherwise.
1340
+ const quoiCourt = composant ?? c.sujet;
1341
+ const reste = avec => largeur - (droite ? droite.length + 1 : 0) - tete.length - avec.length;
1342
+ const suite = !temoin
1343
+ ? boucles
1344
+ : reste(`${boucles}${temoin.texte}`) >= Math.min(PLACE_DU_SUJET, quoiCourt.length)
1345
+ ? `${boucles}${temoin.texte}`
1346
+ : temoin.texte;
1347
+ const texte = quoi => couperAvant(`${tete}${quoi}`, suite, droite, largeur);
1348
+ poser({
1349
+ texte: texte(c.sujet),
1350
+ ...(composant ? { alterne: texte(composant) } : {}),
1351
+ ton: temoin?.fige ? 'alerte' : c.vivant ? 'actif' : 'attention',
1352
+ ticket: c.id,
1353
+ ...(temoin ? { avancement: true } : {}),
1354
+ });
1355
+ };
1356
+ const contenu = b => {
1357
+ const tous = (t.circuits ?? []).filter(c => blocDuCircuit(c) === b);
1358
+ const circuits = tous.filter(c => !aLArbitrage(c) && !auSuperviseur(c));
1359
+ const arretes = tous.filter(aLArbitrage);
1360
+ const auxSuperviseur = tous.filter(auSuperviseur);
1361
+ const enCours = b.enCours.filter(x => !enCircuit.has(x.id));
1362
+ const attend = b.attend.filter(x => !enCircuit.has(x.id));
1363
+ return {
1364
+ circuits,
1365
+ arretes,
1366
+ auxSuperviseur,
1367
+ enCours,
1368
+ attend,
1369
+ vide: !tous.length && !enCours.length && !attend.length,
1370
+ };
1371
+ };
1372
+ const contenus = new Map([...SUITE, ARBITRAGE].map(blocDe).map(b => [b, contenu(b)]));
1373
+ const ligneDArbitrage = retrait =>
1374
+ L(cadre(`${retrait}⤷ ${arbitrage.titre} · ${arbitrage.qui}`, '', largeur), 'titre');
1375
+ // The tickets of the block <b>: its circuits that run, its agents, its waits, then the line of
1376
+ // the arbitrage, indented by <retrait> (none when <retrait> is null), and its stopped circuits,
1377
+ // then the line of the stops for the supervisor and theirs.
1378
+ const tickets = (b, retrait) => {
1379
+ const { circuits, arretes, auxSuperviseur, enCours, attend } = contenus.get(b);
1380
+ for (const c of circuits) {
1381
+ ligneDuCircuit(c);
1382
+ }
1383
+ for (const x of enCours) {
1384
+ L(
1385
+ cadre(`▶ ${numero(x.numero)} ${x.sujet}`, conso(x.travail, x.jetons), largeur),
1386
+ 'actif',
1387
+ x.id
1388
+ );
1389
+ }
1390
+ const attente = x => ({
1391
+ texte: cadre(
1392
+ `· ${numero(x.numero)} ${x.sujet}`,
1393
+ x.bloquePar?.length ? `attend ${x.bloquePar.join(', ')}` : conso(x.travail, x.jetons),
1394
+ largeur
1395
+ ),
1396
+ ton: 'discret',
1397
+ ticket: x.id,
1398
+ });
1399
+ poser(...attend.map(attente));
1400
+ if (arretes.length) {
1401
+ if (retrait !== null) {
1402
+ ligneDArbitrage(retrait);
1403
+ }
1404
+ for (const c of arretes) {
1405
+ ligneDuCircuit(c);
1406
+ }
1407
+ }
1408
+ if (auxSuperviseur.length) {
1409
+ L(cadre(`${retrait ?? ''}⤷ Arrêt · superviseur`, '', largeur), 'titre');
1410
+ for (const c of auxSuperviseur) {
1411
+ ligneDuCircuit(c);
1412
+ }
1413
+ }
1414
+ };
1415
+ const ligneDEtape = (b, retrait, plein = !contenus.get(b).vide) =>
1416
+ L(
1417
+ cadre(`${retrait}${b.numero} · ${b.titre}${b.qui ? ` · ${b.qui}` : ''}`, '', largeur),
1418
+ plein ? 'titre' : 'discret'
1419
+ );
1420
+ // A sub-step that names a declaration the project does not hold has no line.
1421
+ const declare = e => !e.declaration || (t.declarations ?? []).includes(e.declaration);
1422
+ for (const e of ETAPES) {
1423
+ const b = blocDe(e);
1424
+ const lesSous = (e.sous ?? []).filter(declare);
1425
+ if (!lesSous.length) {
1426
+ // Its number stands under those of the steps a fold mark opens.
1427
+ ligneDEtape(b, ' ');
1428
+ tickets(b, ' ');
1429
+ continue;
1430
+ }
1431
+ const sous = lesSous.map(blocDe);
1432
+ const ouverte = [b, ...sous].some(x => !contenus.get(x).vide);
1433
+ ligneDEtape(b, ouverte ? '▾ ' : '▸ ', ouverte);
1434
+ if (!ouverte) {
1435
+ continue;
1436
+ }
1437
+ tickets(b, '');
1438
+ for (const x of sous) {
1439
+ ligneDEtape(x, ' ');
1440
+ tickets(x, ' ');
1441
+ }
1442
+ }
1443
+ // A regulator out of any circuit, or a stopped circuit at no step of the list: under the line of
1444
+ // the arbitrage, after the steps.
1445
+ if (!contenus.get(arbitrage).vide) {
1446
+ ligneDArbitrage('');
1447
+ tickets(arbitrage, null);
1448
+ }
1449
+
1450
+ section = 'chantier';
1451
+ // The tickets the agents' blocks show: the chantier's tree does not show them a second time.
1452
+ const dansAgents = new Set([
1453
+ ...(t.circuits ?? []).map(c => c.id),
1454
+ ...t.agents.flatMap(b => [...b.enCours, ...b.attend].map(x => x.id)),
1455
+ ]);
1456
+ for (const ch of t.chantiers) {
1457
+ L('', undefined);
1458
+ poser({
1459
+ texte: cadre(`CHANTIER ${ch.numero} — ${ch.sujet}`, '', largeur),
1460
+ ton: 'titre',
1461
+ ticket: ch.id,
1462
+ chantier: true,
1463
+ });
1464
+ L(
1465
+ cadre(
1466
+ ` ${ch.faits}/${ch.total} faits · ${duree(ch.travail)} · ${k(ch.jetons)} jetons`,
1467
+ '',
1468
+ largeur
1469
+ ),
1470
+ 'discret'
1471
+ );
1472
+ const arbre = [];
1473
+ const marque = n =>
1474
+ n.etat === 'enCours'
1475
+ ? '▶'
1476
+ : n.etat === 'bloques' && n.bloquePar.length
1477
+ ? `attend ${n.bloquePar.join(', ')}`
1478
+ : n.etat === 'aValider'
1479
+ ? '? à valider'
1480
+ : n.etat === 'sansGo'
1481
+ ? 'sans go'
1482
+ : n.etat === 'enAttente'
1483
+ ? 'en attente'
1484
+ : n.etat === 'reportes'
1485
+ ? 'reporté'
1486
+ : n.etat === 'fermes'
1487
+ ? '✓'
1488
+ : '';
1489
+ // Every open ticket of the chantier has its line under the title of its state, the states in
1490
+ // their order, the tickets in their numbers' order. A ticket an agent block shows stands there
1491
+ // alone; its children keep their lines.
1492
+ const ligneDe = n => {
1493
+ const conso = n.jetons ? ` · ${k(n.jetons)}` : '';
1494
+ const droite = n.mere
1495
+ ? `${n.faits}/${n.total}${conso}${n.etat === 'bloques' ? ` ${marque(n)}` : ''}`
1496
+ : n.etat === 'enCours'
1497
+ ? `${duree(n.travail)}${conso}`
1498
+ : n.etat === 'bloques'
1499
+ ? marque(n)
1500
+ : '';
1501
+ return {
1502
+ texte: cadre(` ${n.numero} ${n.sujet}`, droite, largeur),
1503
+ ton: n.etat === 'enCours' ? 'actif' : undefined,
1504
+ ticket: n.id,
1505
+ };
1506
+ };
1507
+ const parEtat = new Map(ETATS.map(e => [e.cle, []]));
1508
+ const parcourir = noeuds => {
1509
+ for (const n of noeuds) {
1510
+ if (n.mere || !dansAgents.has(n.id)) {
1511
+ parEtat.get(n.etat)?.push(n);
1512
+ }
1513
+ parcourir(n.enfants);
1514
+ }
1515
+ };
1516
+ parcourir(ch.enfants);
1517
+ for (const e of ETATS) {
1518
+ const liste = parEtat.get(e.cle);
1519
+ if (!liste.length) {
1520
+ continue;
1521
+ }
1522
+ const titreDuGroupe = {
1523
+ texte: cadre(` ${e.nom}`, `${liste.length}`, largeur),
1524
+ ton: 'titre',
1525
+ };
1526
+ const replie =
1527
+ e.cle === 'sansGo' ||
1528
+ e.cle === 'reportes' ||
1529
+ (e.cle === 'aValider' && liste.length > SEUIL_REPLI);
1530
+ if (replie) {
1531
+ const cle = `${ch.id}/${e.cle}`;
1532
+ groupes[cle] = [titreDuGroupe, ...liste.map(ligneDe)].map(marquer);
1533
+ arbre.push({
1534
+ texte: cadre(` ${e.nom} ›`, `${liste.length}`, largeur),
1535
+ ton: 'titre',
1536
+ groupe: cle,
1537
+ });
1538
+ continue;
1539
+ }
1540
+ arbre.push(titreDuGroupe);
1541
+ arbre.push(...liste.map(ligneDe));
1542
+ }
1543
+ poser(...arbre);
1544
+ }
1545
+ // The epics not in progress, under their title (« AUTRES CHANTIERS » beside one in progress,
1546
+ // « CHANTIERS » alone), each on its line, selectable, and a chantier (`chantier`) the pane can
1547
+ // switch.
1548
+ if (t.autres.length) {
1549
+ L('', undefined);
1550
+ const autres = t.autres.map(a => ({
1551
+ texte: cadre(`${a.numero} ${a.sujet}`, `${a.faits}/${a.total}`, largeur),
1552
+ ticket: a.id,
1553
+ chantier: true,
1554
+ }));
1555
+ if (t.chantiers.length) {
1556
+ groupes.autres = [
1557
+ { texte: cadre('AUTRES CHANTIERS', `${t.autres.length}`, largeur), ton: 'titre' },
1558
+ ...autres,
1559
+ ].map(marquer);
1560
+ poser({
1561
+ texte: cadre('AUTRES CHANTIERS ›', `${t.autres.length}`, largeur),
1562
+ ton: 'titre',
1563
+ groupe: 'autres',
1564
+ });
1565
+ } else {
1566
+ L(cadre('CHANTIERS', `${t.autres.length}`, largeur), 'titre');
1567
+ poser(...autres);
1568
+ }
1569
+ }
1570
+
1571
+ if (t.faitsDuJour.length) {
1572
+ section = 'faits';
1573
+ L('', undefined);
1574
+ // The title carries the day's total: tickets, working time and tokens of the facts below.
1575
+ const n = t.faitsDuJour.length;
1576
+ const temps = t.faitsDuJour.reduce((s, x) => s + x.travail, 0);
1577
+ const jetons = t.faitsDuJour.reduce((s, x) => s + x.jetons, 0);
1578
+ L(
1579
+ cadre(
1580
+ "FAITS AUJOURD'HUI",
1581
+ `${n} ticket${n > 1 ? 's' : ''} · ${duree(temps)} · ${k(jetons)}`,
1582
+ largeur
1583
+ ),
1584
+ 'titre'
1585
+ );
1586
+ const fait = x => ({
1587
+ texte: cadre(`✓ ${x.numero} ${x.sujet}`, conso(x.travail, x.jetons), largeur),
1588
+ ton: 'discret',
1589
+ ticket: x.id,
1590
+ });
1591
+ poser(...t.faitsDuJour.map(fait));
1592
+ }
1593
+ return out;
1594
+ }
1595
+
1596
+ // The states' view, `largeur` columns wide: each state, its meaning, then the tickets of the
1597
+ // chantiers in progress one per line, each with its agents' time and tokens (a blocked one, what
1598
+ // it waits for), selectable as the board's are; section `etats`.
1599
+ export function lignesEtats(t, largeur = LARGEUR) {
1600
+ const out = [];
1601
+ for (const e of t.etats) {
1602
+ if (out.length) {
1603
+ out.push({ texte: '', section: 'etats' });
1604
+ }
1605
+ out.push({
1606
+ texte: couper(`${e.nom.toUpperCase()} — ${e.sens}`, `${e.tickets.length}`, largeur),
1607
+ ton: 'titre',
1608
+ section: 'etats',
1609
+ });
1610
+ if (!e.tickets.length) {
1611
+ out.push({ texte: ' —', ton: 'discret', section: 'etats' });
1612
+ }
1613
+ for (const x of e.tickets) {
1614
+ out.push({
1615
+ texte: couper(
1616
+ ` ${x.numero} ${x.sujet}`,
1617
+ e.cle === 'bloques' && x.bloquePar.length
1618
+ ? `attend ${x.bloquePar.join(', ')}`
1619
+ : conso(t.parTicket[x.id]?.travail ?? 0, t.parTicket[x.id]?.jetons ?? 0),
1620
+ largeur
1621
+ ),
1622
+ ton: e.cle === 'fermes' ? 'discret' : undefined,
1623
+ ticket: x.id,
1624
+ section: 'etats',
1625
+ });
1626
+ }
1627
+ }
1628
+ return out;
1629
+ }
1630
+
1631
+ // Where the stopped circuit <c> is: its step by number and title, its attempt, the last part of its
1632
+ // component's path (`.` for the root), and its stop (« interrompu » for a circuit dead without a
1633
+ // stop written).
1634
+ const ouEnEst = c => {
1635
+ const e = etapeDuCircuit({ ...c, raison: c.arret });
1636
+ const ou = [e ? libelle(e) : c.etape, c.tentative ?? null]
1637
+ .filter(x => x !== null && x !== undefined)
1638
+ .join(' ');
1639
+ const composant = c.composant
1640
+ ? ` · ${c.composant === '.' ? '.' : path.basename(c.composant)}`
1641
+ : '';
1642
+ const arret = c.code === null ? 'interrompu' : `arrêt ${c.arret ?? '?'} (${c.code})`;
1643
+ return `${ou}${composant}, ${arret}`;
1644
+ };
1645
+
1646
+ // The card of each ticket a line names: its number, whole title, component, state, its agents'
1647
+ // working time and tokens (`mesures`: ticket → { travail, jetons }) and the start of its
1648
+ // description (`resume`), after the key figures of its bilan when the circuit kept one (`bilans`:
1649
+ // ticket → its bilan, scripts/bilan.mjs). The pane reads the whole description from Beads when the
1650
+ // card fills it.
1651
+ export function fiches(
1652
+ tous,
1653
+ lignesDuTableau,
1654
+ prefix = '',
1655
+ mesures = {},
1656
+ circuits = [],
1657
+ bilans = {}
1658
+ ) {
1659
+ const arretes = new Map(circuits.filter(c => !c.vivant).map(c => [c.ticket, c]));
1660
+ const ids = new Set(lignesDuTableau.map(l => l.ticket).filter(Boolean));
1661
+ const out = {};
1662
+ for (const t of tous) {
1663
+ if (!ids.has(t.id)) {
1664
+ continue;
1665
+ }
1666
+ const resume = (t.description ?? '')
1667
+ .split('\n')
1668
+ .map(x => x.trim())
1669
+ .filter(x => x && !x.startsWith('#'))
1670
+ .join(' ');
1671
+ // A stopped circuit's card opens on the regulator's question, or on where it stopped and why.
1672
+ const a = arretes.get(t.id);
1673
+ const tete = a?.question
1674
+ ? `Question du régulateur : ${a.question.replace(/\s+/g, ' ')} `
1675
+ : a
1676
+ ? `Circuit arrêté en ${ouEnEst(a)} : ${a.raison ?? 'le circuit ne tourne plus'} `
1677
+ : '';
1678
+ const chiffres = bilans[t.id] ? `${enBref(bilans[t.id])} ` : '';
1679
+ const texte = `${tete}${chiffres}${resume}`;
1680
+ out[t.id] = {
1681
+ numero: lecture(t, prefix).numero,
1682
+ titre: lecture(t, prefix).sujet,
1683
+ composant: lecture(t, prefix).composant,
1684
+ statut: t.status,
1685
+ travail: mesures[t.id]?.travail ?? 0,
1686
+ jetons: mesures[t.id]?.jetons ?? 0,
1687
+ resume: texte.length > 400 ? `${texte.slice(0, 399)}…` : texte,
1688
+ };
1689
+ }
1690
+ return out;
1691
+ }
1692
+
1693
+ // Of the tickets the lines name, the one that moved last: a running agent's ticket at its last
1694
+ // activity, any other at its last update; null when none.
1695
+ export function dernierBouge(tous, lignesDuTableau, vivants = [], maintenant = Date.now()) {
1696
+ const ids = new Set(lignesDuTableau.map(l => l.ticket).filter(Boolean));
1697
+ const actif = new Map();
1698
+ for (const v of vivants) {
1699
+ if (v.ticket) {
1700
+ const quand = maintenant - (v.silence ?? 0);
1701
+ actif.set(v.ticket, Math.max(actif.get(v.ticket) ?? 0, quand));
1702
+ }
1703
+ }
1704
+ let dernier = null;
1705
+ let plusTard = -Infinity;
1706
+ for (const t of tous) {
1707
+ if (!ids.has(t.id)) {
1708
+ continue;
1709
+ }
1710
+ const quand = Math.max(
1711
+ actif.get(t.id) ?? -Infinity,
1712
+ Date.parse(t.updated_at ?? t.closed_at ?? t.created_at ?? '') || -Infinity
1713
+ );
1714
+ if (quand > plusTard) {
1715
+ plusTard = quand;
1716
+ dernier = t.id;
1717
+ }
1718
+ }
1719
+ return dernier;
1720
+ }
1721
+
1722
+ // The status line: the chantier in progress, alerts first.
1723
+ export function ligneEtat(t) {
1724
+ const alerte = t.alertes.length ? `⚠ ${t.alertes.length} · ` : '';
1725
+ const ch = t.chantiers[0];
1726
+ const c = t.global.compteurs;
1727
+ if (!ch) {
1728
+ return `${alerte}Grillhouse : ${c.enCours} en cours · ${c.enAttente} en attente · ${c.fermes} faits`;
1729
+ }
1730
+ return `${alerte}${ch.numero} ${ch.faits}/${ch.total} · ▶ ${c.enCours} en cours · ${k(t.global.jour.jetons)} aujourd'hui`;
1731
+ }
1732
+
1733
+ const bdJson = (racine, ...args) => {
1734
+ try {
1735
+ return JSON.parse(
1736
+ execFileSync('bd', [...args, '--json'], {
1737
+ cwd: racine,
1738
+ encoding: 'utf8',
1739
+ stdio: ['ignore', 'pipe', 'ignore'],
1740
+ maxBuffer: 256 * 1024 * 1024,
1741
+ })
1742
+ );
1743
+ } catch {
1744
+ return [];
1745
+ }
1746
+ };
1747
+
1748
+ // The package.json at the root of <racine>, or null.
1749
+ const lirePaquet = racine => {
1750
+ try {
1751
+ return JSON.parse(readFileSync(path.join(racine, 'package.json'), 'utf8'));
1752
+ } catch {
1753
+ return null;
1754
+ }
1755
+ };
1756
+
1757
+ // The board of the repository at <racine>.
1758
+ export function tableau(racine, { home = os.homedir(), maintenant = Date.now() } = {}) {
1759
+ let prefix = '';
1760
+ try {
1761
+ prefix = execFileSync('bd', ['config', 'get', 'issue_prefix'], {
1762
+ cwd: racine,
1763
+ encoding: 'utf8',
1764
+ maxBuffer: 256 * 1024 * 1024,
1765
+ }).trim();
1766
+ } catch {
1767
+ prefix = '';
1768
+ }
1769
+ let ps = '';
1770
+ try {
1771
+ ps = execFileSync('ps', ['-eo', 'pid=,etimes=,args='], { encoding: 'utf8' });
1772
+ } catch {
1773
+ ps = '';
1774
+ }
1775
+ const fichierRegistre =
1776
+ process.env.GRILLHOUSE_REGISTRE ?? path.join(home, '.claude', 'grillhouse', 'registre.jsonl');
1777
+ const registre = existsSync(fichierRegistre)
1778
+ ? lireRegistre(readFileSync(fichierRegistre, 'utf8'), racine)
1779
+ : [];
1780
+ const tous = bdJson(racine, 'list', '--all', '--limit', '0');
1781
+ // The reviewer's verdict matters once a developer or a reviewer ran: only those tickets are read.
1782
+ const dernier = {};
1783
+ for (const r of [...registre].sort((x, y) => Date.parse(x.fin) - Date.parse(y.fin))) {
1784
+ if (r.ticket && !juge(r.role) && r.role !== ROLE_DU_CIRCUIT) {
1785
+ dernier[r.ticket] = r.role;
1786
+ }
1787
+ }
1788
+ const verdicts = {};
1789
+ for (const t of tous) {
1790
+ if (t.status === 'closed' || !['developpeur', 'relecteur'].includes(dernier[t.id])) {
1791
+ continue;
1792
+ }
1793
+ const v = verdict(bdJson(racine, 'comments', t.id));
1794
+ if (v) {
1795
+ verdicts[t.id] = v;
1796
+ }
1797
+ }
1798
+ let commun = null;
1799
+ try {
1800
+ commun = execFileSync(
1801
+ 'git',
1802
+ ['-C', racine, 'rev-parse', '--path-format=absolute', '--git-common-dir'],
1803
+ { encoding: 'utf8' }
1804
+ ).trim();
1805
+ } catch {
1806
+ commun = null;
1807
+ }
1808
+ const fichierCache = commun && path.join(commun, 'tableau-cache.json');
1809
+ let cache = {};
1810
+ try {
1811
+ cache = JSON.parse(readFileSync(fichierCache, 'utf8'));
1812
+ } catch {
1813
+ cache = {};
1814
+ }
1815
+ const vivants = lireVivant({
1816
+ racine,
1817
+ home,
1818
+ prefix: prefix || '[A-Za-z0-9_]+',
1819
+ maintenant,
1820
+ ps,
1821
+ cache,
1822
+ });
1823
+ const circuits = lireCircuits({ racine, prefix: prefix || '[A-Za-z0-9_]+', ps });
1824
+ try {
1825
+ if (fichierCache) {
1826
+ writeFileSync(
1827
+ fichierCache,
1828
+ JSON.stringify(Object.fromEntries(Object.entries(cache).filter(([f]) => existsSync(f))))
1829
+ );
1830
+ }
1831
+ } catch {
1832
+ // A read-only place keeps the board working, without its cache.
1833
+ }
1834
+ const minuit = new Date(maintenant);
1835
+ minuit.setHours(0, 0, 0, 0);
1836
+ const t = assembler({
1837
+ tous,
1838
+ prefix,
1839
+ registre,
1840
+ vivants,
1841
+ circuits,
1842
+ verdicts,
1843
+ maintenant,
1844
+ jour: minuit.getTime(),
1845
+ horsTicket: lireLaMesure(process.env),
1846
+ declarations: declarations(lirePaquet(racine)),
1847
+ });
1848
+ // The project is named by its repository's folder, as a shell prompt names it.
1849
+ const nom = path.basename(racine);
1850
+ const groupes = {};
1851
+ const l = lignes(t, nom, LARGEUR, groupes);
1852
+ const vue = lignesEtats(t);
1853
+ const etat = ligneEtat(t);
1854
+ try {
1855
+ if (commun) {
1856
+ writeFileSync(path.join(commun, 'grillhouse-etat.txt'), `${etat}\n`);
1857
+ }
1858
+ } catch {
1859
+ // A read-only place keeps the board working, without its status line.
1860
+ }
1861
+ return {
1862
+ ...t,
1863
+ lignes: l,
1864
+ vueEtats: vue,
1865
+ groupes,
1866
+ fiches: fiches(
1867
+ tous,
1868
+ [...l, ...vue, ...Object.values(groupes).flat()],
1869
+ prefix,
1870
+ t.parTicket,
1871
+ t.circuits,
1872
+ bilansGardes(racine)
1873
+ ),
1874
+ dernier: dernierBouge(tous, l, vivants, maintenant),
1875
+ etat,
1876
+ };
1877
+ }
1878
+
1879
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
1880
+ const t = tableau(process.cwd());
1881
+ process.stdout.write(
1882
+ process.argv.includes('--texte')
1883
+ ? `${t.lignes.map(l => l.texte).join('\n')}\n`
1884
+ : JSON.stringify(t)
1885
+ );
1886
+ }