@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.
- package/.claude-plugin/plugin.json +9 -0
- package/METACADRE.md +41 -0
- package/agents/developpeur.md +10 -0
- package/agents/explorateur.md +9 -0
- package/agents/questionneur.md +11 -0
- package/agents/referent-technique.md +9 -0
- package/agents/regulateur.md +11 -0
- package/agents/relecteur.md +9 -0
- package/agents/testwriter.md +10 -0
- package/bin/grillhouse.mjs +125 -0
- package/cadre/templates/projet/.claude/settings.json +30 -0
- package/cadre/templates/projet/.editorconfig +47 -0
- package/cadre/templates/projet/.env.example +57 -0
- package/cadre/templates/projet/.eslintrc.cjs +56 -0
- package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
- package/cadre/templates/projet/.mcp.json +13 -0
- package/cadre/templates/projet/.prettierrc +11 -0
- package/cadre/templates/projet/CHANGELOG.md +61 -0
- package/cadre/templates/projet/CLAUDE.md +46 -0
- package/cadre/templates/projet/CONTRIBUTING.md +243 -0
- package/cadre/templates/projet/CREDITS.md +41 -0
- package/cadre/templates/projet/README.md +152 -0
- package/cadre/templates/projet/_gitignore +91 -0
- package/cadre/templates/projet/_npmignore +46 -0
- package/cadre/templates/projet/deployment/README.md +98 -0
- package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
- package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
- package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
- package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
- package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
- package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
- package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
- package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
- package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
- package/cadre/templates/projet/ecosystem.config.cjs +73 -0
- package/cadre/templates/projet/package.json +90 -0
- package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
- package/cadre/templates/projet/src/config.ts +68 -0
- package/cadre/templates/projet/src/errors.ts +102 -0
- package/cadre/templates/projet/src/index.ts +46 -0
- package/cadre/templates/projet/src/services/.gitkeep +8 -0
- package/cadre/templates/projet/src/types/index.ts +24 -0
- package/cadre/templates/projet/src/utils/logger.ts +106 -0
- package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
- package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
- package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
- package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
- package/cadre/templates/projet/tsconfig.eslint.json +9 -0
- package/cadre/templates/projet/tsconfig.json +41 -0
- package/cadre/templates/projet/tsdoc.json +6 -0
- package/cadre/templates/projet/vitest.config.ts +44 -0
- package/cadre/templates/turbo/paquets.json +29 -0
- package/cadre/templates/turbo/sans-paquets.json +28 -0
- package/charte.md +107 -0
- package/docs/ARCHITECTURE.md +90 -0
- package/hooks/crochet.mjs +33 -0
- package/hooks/hooks.json +58 -0
- package/hooks/register.tsx +558 -0
- package/package.json +75 -4
- package/scripts/avancement.mjs +244 -0
- package/scripts/bilan.mjs +523 -0
- package/scripts/branche-de-nuit.mjs +59 -0
- package/scripts/cadre.mjs +109 -0
- package/scripts/circuit.mjs +4375 -0
- package/scripts/code-mort.mjs +371 -0
- package/scripts/composants-affectes.mjs +598 -0
- package/scripts/consommateurs-arbitres.mjs +192 -0
- package/scripts/consommateurs.mjs +310 -0
- package/scripts/crochet.mjs +24 -0
- package/scripts/crochets.mjs +135 -0
- package/scripts/dernier-vert.mjs +208 -0
- package/scripts/dette.mjs +181 -0
- package/scripts/droits.mjs +354 -0
- package/scripts/enveloppe/codes.mjs +17 -0
- package/scripts/enveloppe/composant.mjs +11 -0
- package/scripts/enveloppe/enveloppe.mjs +628 -0
- package/scripts/enveloppe/lancer.sh +311 -0
- package/scripts/etapes.mjs +323 -0
- package/scripts/etat-du-circuit.mjs +104 -0
- package/scripts/frontieres.mjs +456 -0
- package/scripts/gardes-du-cadre.mjs +72 -0
- package/scripts/greffons.mjs +338 -0
- package/scripts/grillhouse-maj.mjs +514 -0
- package/scripts/index-interfaces.mjs +293 -0
- package/scripts/init.mjs +117 -0
- package/scripts/integration/file-attente.mjs +61 -0
- package/scripts/integration/file.mjs +494 -0
- package/scripts/integration/integrer.mjs +381 -0
- package/scripts/integration/rejeu.mjs +139 -0
- package/scripts/integration/verdict.mjs +13 -0
- package/scripts/interfaces-contre-code.mjs +547 -0
- package/scripts/ligne-etat/defaut.sh +110 -0
- package/scripts/ligne-etat/ligne-etat.mjs +93 -0
- package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
- package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
- package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
- package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
- package/scripts/migrations.mjs +43 -0
- package/scripts/nuit.sh +269 -0
- package/scripts/paquets.mjs +79 -0
- package/scripts/poussee-cles.mjs +46 -0
- package/scripts/preparation.mjs +84 -0
- package/scripts/references-par-les-types.mjs +420 -0
- package/scripts/registre.mjs +336 -0
- package/scripts/resultats.mjs +580 -0
- package/scripts/resumes.mjs +57 -0
- package/scripts/retirer-copie.sh +127 -0
- package/scripts/session-start.sh +203 -0
- package/scripts/sortie.mjs +155 -0
- package/scripts/sous-modules.sh +316 -0
- package/scripts/structure-guard.sh +27 -0
- package/scripts/superviseur.mjs +102 -0
- package/scripts/tableau.mjs +1886 -0
- package/scripts/tests.mjs +236 -0
- package/scripts/ticket.sh +23 -0
- package/scripts/turbo-des-paquets.mjs +899 -0
- package/scripts/un-commit-par-ticket.mjs +212 -0
- package/scripts/verifier.mjs +469 -0
- package/scripts/verrou-de-fichier.mjs +130 -0
- package/scripts/verrou-de-machine.mjs +211 -0
- package/scripts/verrous/commande.mjs +180 -0
- package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
- package/scripts/verrous/verrou.mjs +165 -0
- package/scripts/vitest-config/avancement.mjs +39 -0
- package/scripts/vitest-config/avancement.reporter.mjs +56 -0
- package/scripts/vitest-config/base-de-passe.mjs +128 -0
- package/scripts/vitest-config/base.d.ts +6 -0
- package/scripts/vitest-config/base.mjs +87 -0
- package/scripts/vitest-config/changements.mjs +173 -0
- package/scripts/vitest-config/cibles.mjs +207 -0
- package/scripts/vitest-config/duree.reporter.mjs +98 -0
- package/scripts/vitest-config/machine.setup.mjs +14 -0
- package/scripts/vitest-config/mesure.d.ts +6 -0
- package/scripts/vitest-config/mesure.mjs +13 -0
- package/scripts/vitest-config/mesure.setup.mjs +12 -0
- package/scripts/vitest-config/pression.mjs +90 -0
- package/scripts/vitest-config/resultats.mjs +23 -0
- package/scripts/vitest-config/resultats.setup.mjs +19 -0
- package/scripts/vitest.nuit.config.mjs +26 -0
- package/skills/developpeur/SKILL.md +175 -0
- package/skills/explorateur/SKILL.md +84 -0
- package/skills/grill/SKILL.md +238 -0
- package/skills/grill/references/releve-structurel.md +38 -0
- package/skills/mesure/SKILL.md +168 -0
- package/skills/pitmaster/SKILL.md +382 -0
- package/skills/pitmaster/references/cadre.md +119 -0
- package/skills/pitmaster/references/consigne-agent.md +308 -0
- package/skills/pitmaster/references/erreurs.md +25 -0
- package/skills/pitmaster/references/tour.md +124 -0
- package/skills/questionneur/SKILL.md +64 -0
- package/skills/redacteur/SKILL.md +84 -0
- package/skills/redacteur/references/documents-d-un-composant.md +105 -0
- package/skills/referent-technique/SKILL.md +145 -0
- package/skills/regulateur/SKILL.md +220 -0
- package/skills/release/SKILL.md +134 -0
- package/skills/release/scripts/update-version.cjs +92 -0
- package/skills/relecteur/SKILL.md +152 -0
- package/skills/relecteur/references/LICENSE-MIT +30 -0
- package/skills/relecteur/references/SOURCE.md +8 -0
- package/skills/relecteur/references/claims-check.md +14 -0
- package/skills/relecteur/references/deletion-check.md +14 -0
- package/skills/relecteur/references/edge-case-hunter.md +80 -0
- package/skills/relecteur/references/verification-gap.md +113 -0
- package/skills/testwriter/SKILL.md +187 -0
- package/types/index.d.ts +56 -0
- 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
|
+
}
|