@roomi-fields/grillhouse 0.23.1 → 0.24.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 +1 -1
- package/METACADRE.md +1 -1
- package/agents/explorateur.md +4 -2
- package/agents/questionneur.md +1 -1
- package/bin/grillhouse.mjs +3 -58
- package/cadre/templates/projet/CLAUDE.md +5 -1
- package/cadre/templates/projet/docs/agents/issue-tracker.md +4 -4
- package/charte.md +23 -6
- package/docs/ARCHITECTURE.md +127 -1
- package/hooks/register.tsx +3 -13
- package/package.json +13 -1
- package/scripts/bilan.mjs +134 -88
- package/scripts/circuit.mjs +1547 -2248
- package/scripts/composants-affectes.mjs +27 -108
- package/scripts/consommateurs-arbitres.mjs +15 -21
- package/scripts/consommateurs.d.ts +8 -0
- package/scripts/consommateurs.mjs +5 -1
- package/scripts/dette.d.ts +23 -0
- package/scripts/dette.mjs +76 -13
- package/scripts/droits.mjs +38 -44
- package/scripts/elements-du-lot.mjs +246 -0
- package/scripts/enveloppe/enveloppe.mjs +105 -232
- package/scripts/enveloppe/lancer.sh +79 -49
- package/scripts/enveloppe/paquets-du-plan.mjs +49 -0
- package/scripts/etapes.mjs +148 -213
- package/scripts/gardes-du-cadre.mjs +8 -6
- package/scripts/grillhouse-maj.mjs +31 -9
- package/scripts/index-interfaces.mjs +21 -1
- package/scripts/integration/integrer.mjs +2 -2
- package/scripts/interfaces-contre-code.mjs +25 -17
- package/scripts/migrations/0.23.0-config-vitest.mjs +26 -23
- package/scripts/migrations/0.23.2-copies-du-cadre.mjs +201 -0
- package/scripts/migrations/0.24.0-circuit-simplifie.mjs +231 -0
- package/scripts/migrations.mjs +41 -6
- package/scripts/paquets.d.ts +5 -0
- package/scripts/paquets.mjs +3 -0
- package/scripts/references-par-les-types.mjs +55 -36
- package/scripts/registre.mjs +7 -15
- package/scripts/resultats.mjs +283 -142
- package/scripts/resumes.mjs +1 -1
- package/scripts/session-start.sh +1 -2
- package/scripts/sources.mjs +28 -0
- package/scripts/sous-commandes.mjs +62 -0
- package/scripts/tableau.mjs +85 -191
- package/scripts/tests.mjs +19 -17
- package/scripts/verifier.mjs +5 -4
- package/scripts/verrou-de-machine.mjs +131 -24
- package/scripts/verrous/verrou.mjs +66 -5
- package/skills/developpeur/SKILL.md +60 -50
- package/skills/explorateur/SKILL.md +90 -64
- package/skills/grill/SKILL.md +1 -1
- package/skills/mesure/SKILL.md +2 -1
- package/skills/pitmaster/SKILL.md +132 -118
- package/skills/pitmaster/references/consigne-agent.md +54 -148
- package/skills/questionneur/SKILL.md +18 -18
- package/skills/relecteur/SKILL.md +69 -23
- package/skills/testwriter/SKILL.md +46 -35
- package/types/index.d.ts +1 -3
- package/agents/referent-technique.md +0 -9
- package/agents/regulateur.md +0 -11
- package/scripts/enveloppe/composant.mjs +0 -11
- package/scripts/etat-du-circuit.mjs +0 -104
- package/skills/referent-technique/SKILL.md +0 -145
- package/skills/regulateur/SKILL.md +0 -220
package/scripts/tests.mjs
CHANGED
|
@@ -37,21 +37,23 @@
|
|
|
37
37
|
// Turborepo's `--force` re-executes every task of the run, so the variable carries the moment of
|
|
38
38
|
// the run instead (`rejouer`), a value no cached run of the tests has. A test suspected unstable
|
|
39
39
|
// is played again, and a build, which the same inputs give again, is not.
|
|
40
|
-
// - In the envelope of an agent (`GRILLHOUSE_ENVELOPPE`, scripts/enveloppe/
|
|
41
|
-
//
|
|
42
|
-
// Bazel: the build is the named packages' alone, with the traces of those
|
|
43
|
-
// wait for, which live in the package the agent writes
|
|
44
|
-
// --only --filter=<name>`, their task ids read in
|
|
45
|
-
// `turbo run test:vitest --filter=<name> --dry=json`), never
|
|
46
|
-
// neighbour's trace; then the tests alone
|
|
47
|
-
// paths in the variable. A named package
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// and
|
|
40
|
+
// - In the envelope of an agent (`GRILLHOUSE_ENVELOPPE`, scripts/enveloppe/paquets-du-plan.mjs),
|
|
41
|
+
// the packages out of its plan and their traces arrive built by the launcher and read-only, as
|
|
42
|
+
// in a sandbox of Bazel: the build is the named packages' alone, with the traces of those
|
|
43
|
+
// packages their tests wait for, which live in the package the agent writes
|
|
44
|
+
// (`turbo run build <name>#trace:<t>… --only --filter=<name>`, their task ids read in
|
|
45
|
+
// Turborepo's plan of the tests, `turbo run test:vitest --filter=<name> --dry=json`), never
|
|
46
|
+
// their dependencies nor a neighbour's trace; then the tests alone
|
|
47
|
+
// (`turbo run test:vitest --only --filter=<name>`), the paths in the variable. A named package
|
|
48
|
+
// that the envelope shows read-only (one out of the plan; the shared test material, for every
|
|
49
|
+
// role but the testwriter) is not built, nor are its traces: it arrives built, as a read-only
|
|
50
|
+
// input of Bazel; the frame knows it read-only by testing its directory for writing
|
|
51
|
+
// (`inscriptible`), where the envelope mounts it. The shared test material plays its tests as
|
|
52
|
+
// it is: the logs of its tasks go to the envelope's directories proper to the session
|
|
53
|
+
// (`sortiesDeTest`, scripts/enveloppe/enveloppe.mjs), never into its source. A package out of
|
|
54
|
+
// the plan has no such directories: Turborepo and Vite cannot write its logs and its cache, and
|
|
55
|
+
// a run of its tests fails on « Read-only file system »; its tests play out of the envelope. The
|
|
56
|
+
// verdict that counts is the reviewer's and the integration's, out of the envelope.
|
|
55
57
|
// - An argument that begins with « - » is an option of Vitest, written `--<name>=<value>`, carried
|
|
56
58
|
// in the variable beside the paths.
|
|
57
59
|
// - A night test or a time budget bench, which the day leaves out, plays when a path names its own
|
|
@@ -79,7 +81,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
79
81
|
import { accessSync, constants, existsSync, readdirSync, statSync } from 'node:fs';
|
|
80
82
|
import path from 'node:path';
|
|
81
83
|
import { planifier } from './avancement.mjs';
|
|
82
|
-
import {
|
|
84
|
+
import { paquetsDeLEnveloppe } from './enveloppe/paquets-du-plan.mjs';
|
|
83
85
|
import { paquets } from './paquets.mjs';
|
|
84
86
|
import { garderLesResumes, RESULTATS } from './resultats.mjs';
|
|
85
87
|
import { tracesDuPlan } from './turbo-des-paquets.mjs';
|
|
@@ -94,7 +96,7 @@ const racine = process.cwd();
|
|
|
94
96
|
// Turborepo write in it (TMPDIR), and it goes at the end of the run.
|
|
95
97
|
const base = ouvrirBase('tests');
|
|
96
98
|
process.env.TMPDIR = base.dossier;
|
|
97
|
-
const enveloppe =
|
|
99
|
+
const enveloppe = paquetsDeLEnveloppe() !== null;
|
|
98
100
|
const args = process.argv.slice(2);
|
|
99
101
|
const force = args.includes('--force');
|
|
100
102
|
const options = args.filter(a => a.startsWith('-') && a !== '--force');
|
package/scripts/verifier.mjs
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
// shared by every repository of the machine: a pass that finds it held waits for the end of the
|
|
24
24
|
// other measure, and says so every 30 s with who holds it. The wait bounded (60 min), the pass
|
|
25
25
|
// ends at 75, « à repasser »; a lock held by an ancestor of the pass ends it at once, red (1);
|
|
26
|
-
// either way without playing, the holder named.
|
|
26
|
+
// either way without playing, the holder named. A pass in the frame's own repository is of the
|
|
27
|
+
// second rank: it waits while a project's measure waits too.
|
|
27
28
|
// - Turborepo plays the build, then the guards (the frame's, `grillhouse:gardes`, and the
|
|
28
29
|
// project's own, its root script `gardes`) and the tests (`test:vitest`) beside each other:
|
|
29
30
|
// `turbo run grillhouse:gardes gardes test:vitest --continue=dependencies-successful
|
|
@@ -92,7 +93,7 @@ import { availableParallelism, tmpdir } from 'node:os';
|
|
|
92
93
|
import path from 'node:path';
|
|
93
94
|
import { pathToFileURL } from 'node:url';
|
|
94
95
|
import { ecrireLaMesure, garderLeTotal, ouvrirAvancement, planifier } from './avancement.mjs';
|
|
95
|
-
import {
|
|
96
|
+
import { paquetsDeLEnveloppe } from './enveloppe/paquets-du-plan.mjs';
|
|
96
97
|
import { paquets } from './paquets.mjs';
|
|
97
98
|
import { AttenteDeLaMachine, avecLaMachine } from './verrou-de-machine.mjs';
|
|
98
99
|
import {
|
|
@@ -197,7 +198,7 @@ export function verifier({
|
|
|
197
198
|
const dossier =
|
|
198
199
|
envMachine[AVANCEMENT] || ouvrirAvancement({ racine, genre: 'verification' });
|
|
199
200
|
const effacer =
|
|
200
|
-
propre && !
|
|
201
|
+
propre && !paquetsDeLEnveloppe(envMachine)
|
|
201
202
|
? ecrireLaMesure({
|
|
202
203
|
env: envMachine,
|
|
203
204
|
depot: nomDuDepot(racine),
|
|
@@ -226,7 +227,7 @@ export function verifier({
|
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
},
|
|
229
|
-
{ env, dire }
|
|
230
|
+
{ env, racine, dire }
|
|
230
231
|
);
|
|
231
232
|
} catch (e) {
|
|
232
233
|
if (!(e instanceof AttenteDeLaMachine)) throw e;
|
|
@@ -31,21 +31,59 @@
|
|
|
31
31
|
// variable GRILLHOUSE_MESURE_TENUE carries the mark of the taking, and a process whose variable
|
|
32
32
|
// names the mark the lock holds plays under it without taking it again (the final pass of the
|
|
33
33
|
// circuit, `npm run verifier`, under the circuit's lock).
|
|
34
|
-
// - In the envelope of an agent (GRILLHOUSE_ENVELOPPE set, scripts/enveloppe/
|
|
35
|
-
// is taken: the envelope has its own process space, where an owner's number means nothing
|
|
34
|
+
// - In the envelope of an agent (GRILLHOUSE_ENVELOPPE set, scripts/enveloppe/paquets-du-plan.mjs)
|
|
35
|
+
// no lock is taken: the envelope has its own process space, where an owner's number means nothing
|
|
36
36
|
// outside, and its home shows no cache.
|
|
37
|
+
// - A measure has a rank, as the job server of GNU make serves its own build first and Bazel's
|
|
38
|
+
// `--experimental_local_test_priority` orders the tests that wait for the machine. A measure
|
|
39
|
+
// played in the frame's own repository (one whose workspaces hold the package
|
|
40
|
+
// @roomi-fields/grillhouse in development, its agent copies included: `rangDeLaMesure`) is of
|
|
41
|
+
// the second rank; a project's (a circuit, a `npm run verifier`) is of the first. A measure of
|
|
42
|
+
// the first rank that waits names itself in the directory `<lock>.premier-rang/` (one file per
|
|
43
|
+
// waiting process, named by its number, removed once it holds the lock or stops); a measure of
|
|
44
|
+
// the second rank does not take the lock, even free, while a live process waits there, so a
|
|
45
|
+
// project's circuit never waits behind the frame's own checks (BPScript 320.9: 208 min of its
|
|
46
|
+
// wait went to the checks of the frame's agent copies). A waiting file whose process is gone
|
|
47
|
+
// names no one and goes. Between measures of one rank, the first to find the lock free takes
|
|
48
|
+
// it. The bound of the wait is the same for both ranks, and the wait says its rank.
|
|
37
49
|
// - The lock is let go at the end of the measure, whether it succeeds or throws.
|
|
38
|
-
import {
|
|
50
|
+
import { randomUUID } from 'node:crypto';
|
|
51
|
+
import {
|
|
52
|
+
mkdirSync,
|
|
53
|
+
readdirSync,
|
|
54
|
+
readFileSync,
|
|
55
|
+
readlinkSync,
|
|
56
|
+
rmSync,
|
|
57
|
+
statSync,
|
|
58
|
+
writeFileSync,
|
|
59
|
+
} from 'node:fs';
|
|
39
60
|
import { homedir } from 'node:os';
|
|
40
61
|
import path from 'node:path';
|
|
41
|
-
import {
|
|
62
|
+
import { paquetsDeLEnveloppe } from './enveloppe/paquets-du-plan.mjs';
|
|
63
|
+
import { paquets } from './paquets.mjs';
|
|
42
64
|
import { attendreUnPeu, lacherVerrou, prendreVerrou } from './verrou-de-fichier.mjs';
|
|
65
|
+
import { vivant } from './vitest-config/base-de-passe.mjs';
|
|
43
66
|
import { A_REPASSER } from './vitest-config/pression.mjs';
|
|
44
67
|
|
|
45
68
|
const VARIABLE_VERROU = 'GRILLHOUSE_VERROU_MACHINE';
|
|
46
69
|
const VARIABLE_TENUE = 'GRILLHOUSE_MESURE_TENUE';
|
|
47
70
|
const VARIABLE_ATTENTE = 'GRILLHOUSE_ATTENTE_MACHINE_MS';
|
|
48
71
|
|
|
72
|
+
// The name of the frame's package, which a repository of the second rank develops.
|
|
73
|
+
const CADRE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).name;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The rank of a measure played at <racine>: 2 in the frame's own repository (its workspaces hold
|
|
77
|
+
* the package @roomi-fields/grillhouse in development), 1 elsewhere.
|
|
78
|
+
* @param {string} racine the root of the repository the measure plays in
|
|
79
|
+
* @returns {1 | 2}
|
|
80
|
+
*/
|
|
81
|
+
export function rangDeLaMesure(racine) {
|
|
82
|
+
return paquets(racine).some(p => p.pkg.name === CADRE) ? 2 : 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const RANGS = { 1: 'mesure de premier rang', 2: 'mesure de second rang (le dépôt du cadre)' };
|
|
86
|
+
|
|
49
87
|
/** The longest wait for the lock of the machine, in ms, unless GRILLHOUSE_ATTENTE_MACHINE_MS names another. */
|
|
50
88
|
export const ATTENTE_MACHINE_MS = 60 * 60_000;
|
|
51
89
|
// The gap between two lines that say the wait goes on.
|
|
@@ -90,12 +128,9 @@ const heure = ms => new Date(ms).toTimeString().slice(0, 8);
|
|
|
90
128
|
const duree = ms =>
|
|
91
129
|
ms < 120_000 ? `${Math.round(ms / 1000)} s` : `${Math.round(ms / 60_000)} min`;
|
|
92
130
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
function qui(verrou) {
|
|
97
|
-
const pid = detenteur(verrou);
|
|
98
|
-
if (pid === null) return 'une autre mesure';
|
|
131
|
+
// The process <pid>, its working directory and command when they read (on one line, cut at 200
|
|
132
|
+
// characters): « le processus 12 (dans /d) : node x ».
|
|
133
|
+
function processus(pid) {
|
|
99
134
|
let ou = '';
|
|
100
135
|
try {
|
|
101
136
|
ou = ` (dans ${readlinkSync(`/proc/${pid}/cwd`)})`;
|
|
@@ -110,6 +145,14 @@ function qui(verrou) {
|
|
|
110
145
|
const quoi = commande
|
|
111
146
|
? ` : ${commande.length > 200 ? `${commande.slice(0, 200)}…` : commande}`
|
|
112
147
|
: '';
|
|
148
|
+
return `le processus ${pid}${ou}${quoi}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Who holds the lock <verrou>: its process (`processus`) and since when the lock stands
|
|
152
|
+
// (« le processus 12 (dans /d) : node x, depuis 10:04:31 (3 min) »).
|
|
153
|
+
function qui(verrou) {
|
|
154
|
+
const pid = detenteur(verrou);
|
|
155
|
+
if (pid === null) return 'une autre mesure';
|
|
113
156
|
let depuis = '';
|
|
114
157
|
try {
|
|
115
158
|
const t = statSync(verrou).mtimeMs;
|
|
@@ -117,9 +160,58 @@ function qui(verrou) {
|
|
|
117
160
|
} catch {
|
|
118
161
|
// the lock is gone meanwhile
|
|
119
162
|
}
|
|
120
|
-
return
|
|
163
|
+
return `${processus(pid)}${depuis}`;
|
|
121
164
|
}
|
|
122
165
|
|
|
166
|
+
// The directory where the measures of the first rank that wait for <verrou> name themselves.
|
|
167
|
+
const fileDuPremierRang = verrou => `${verrou}.premier-rang`;
|
|
168
|
+
|
|
169
|
+
// Names this process among the measures of the first rank that wait for <verrou>; a function that
|
|
170
|
+
// removes its name.
|
|
171
|
+
function attendreAuPremierRang(verrou) {
|
|
172
|
+
const dossier = fileDuPremierRang(verrou);
|
|
173
|
+
const f = path.join(dossier, `${process.pid}-${randomUUID()}`);
|
|
174
|
+
mkdirSync(dossier, { recursive: true });
|
|
175
|
+
writeFileSync(f, String(process.pid));
|
|
176
|
+
return () => rmSync(f, { force: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The live processes of the measures of the first rank that wait for <verrou>; the name of a
|
|
180
|
+
// process that is gone is removed.
|
|
181
|
+
function premiersQuiAttendent(verrou) {
|
|
182
|
+
const dossier = fileDuPremierRang(verrou);
|
|
183
|
+
let noms;
|
|
184
|
+
try {
|
|
185
|
+
noms = readdirSync(dossier);
|
|
186
|
+
} catch {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const pids = [];
|
|
190
|
+
for (const nom of noms) {
|
|
191
|
+
const pid = Number(/^(\d+)-/.exec(nom)?.[1]);
|
|
192
|
+
if (pid && vivant(pid)) pids.push(pid);
|
|
193
|
+
else rmSync(path.join(dossier, nom), { force: true });
|
|
194
|
+
}
|
|
195
|
+
return pids;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Whom a measure of the rank <rang> waits for at <verrou>: the holder of the lock, and for the
|
|
199
|
+
// second rank the measures of the first that wait too, which pass first.
|
|
200
|
+
function attendus(verrou, rang) {
|
|
201
|
+
const tenu = detenteur(verrou) === null ? 'libre' : `tenu par ${qui(verrou)}`;
|
|
202
|
+
const premiers = rang === 2 ? premiersQuiAttendent(verrou) : [];
|
|
203
|
+
const avant = !premiers.length
|
|
204
|
+
? ''
|
|
205
|
+
: premiers.length === 1
|
|
206
|
+
? ` ; une mesure de premier rang attend aussi et passe d'abord : ${processus(premiers[0])}`
|
|
207
|
+
: ` ; ${premiers.length} mesures de premier rang attendent aussi et passent d'abord : ${premiers.map(processus).join(' ; ')}`;
|
|
208
|
+
return `le verrou de la mesure lourde (${verrou}) est ${tenu}${avant}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Whether a measure of the rank <rang> may take the lock <verrou>: the second rank only while no
|
|
212
|
+
// measure of the first waits.
|
|
213
|
+
const peutPrendre = (verrou, rang) => rang === 1 || premiersQuiAttendent(verrou).length === 0;
|
|
214
|
+
|
|
123
215
|
// The parent of the process <pid>, read in /proc (the field after the state, which follows the
|
|
124
216
|
// command, itself free to hold spaces and parentheses), or null.
|
|
125
217
|
function parent(pid) {
|
|
@@ -140,10 +232,20 @@ function borneDe(env) {
|
|
|
140
232
|
return env[VARIABLE_ATTENTE] && Number.isFinite(n) && n >= 0 ? n : ATTENTE_MACHINE_MS;
|
|
141
233
|
}
|
|
142
234
|
|
|
143
|
-
// Waits for the lock <verrou>, saying so (`dire`) when it begins and every
|
|
144
|
-
// time waited, in ms. Throws an AttenteDeLaMachine at once when an ancestor
|
|
145
|
-
// the bound <borneMs>.
|
|
146
|
-
|
|
235
|
+
// Waits for the lock <verrou> at the rank <rang>, saying so (`dire`) when it begins and every
|
|
236
|
+
// <rappelMs>; returns the time waited, in ms. Throws an AttenteDeLaMachine at once when an ancestor
|
|
237
|
+
// holds the lock, and at the bound <borneMs>. A measure of the first rank names itself among those
|
|
238
|
+
// that wait while it waits.
|
|
239
|
+
function attendreLaMachine(verrou, { rang, dire, borneMs, rappelMs }) {
|
|
240
|
+
const retirer = rang === 1 ? attendreAuPremierRang(verrou) : () => {};
|
|
241
|
+
try {
|
|
242
|
+
return attendreAuRang(verrou, { rang, dire, borneMs, rappelMs });
|
|
243
|
+
} finally {
|
|
244
|
+
retirer();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function attendreAuRang(verrou, { rang, dire, borneMs, rappelMs }) {
|
|
147
249
|
const debut = Date.now();
|
|
148
250
|
let dit = null;
|
|
149
251
|
for (;;) {
|
|
@@ -156,19 +258,20 @@ function attendreLaMachine(verrou, { dire, borneMs, rappelMs }) {
|
|
|
156
258
|
const t = Date.now();
|
|
157
259
|
if (t - debut >= borneMs)
|
|
158
260
|
throw new AttenteDeLaMachine(
|
|
159
|
-
`⏱ attente du verrou de la machine bornée à ${duree(borneMs)} (${VARIABLE_ATTENTE})
|
|
261
|
+
`⏱ attente du verrou de la machine bornée à ${duree(borneMs)} (${VARIABLE_ATTENTE}), ${RANGS[rang]} : ${attendus(verrou, rang)} ; à repasser (code ${A_REPASSER}).`,
|
|
160
262
|
A_REPASSER
|
|
161
263
|
);
|
|
162
264
|
if (dit === null)
|
|
163
265
|
dire(
|
|
164
|
-
`⏳ la machine mesure déjà :
|
|
266
|
+
`⏳ ${RANGS[rang]}, la machine mesure déjà : ${attendus(verrou, rang)} ; attente de son tour, ${duree(borneMs)} au plus.`
|
|
165
267
|
);
|
|
166
268
|
else if (t - dit >= rappelMs)
|
|
167
269
|
dire(
|
|
168
|
-
`⏳ attente du verrou de la machine depuis ${duree(t - debut)} :
|
|
270
|
+
`⏳ ${RANGS[rang]}, attente du verrou de la machine depuis ${duree(t - debut)} : ${attendus(verrou, rang)}.`
|
|
169
271
|
);
|
|
170
272
|
if (dit === null || t - dit >= rappelMs) dit = t;
|
|
171
|
-
if (prendreVerrou(verrou, { attendre: false }))
|
|
273
|
+
if (peutPrendre(verrou, rang) && prendreVerrou(verrou, { attendre: false }))
|
|
274
|
+
return Date.now() - debut;
|
|
172
275
|
attendreUnPeu(Math.max(1, Math.min(200, borneMs - (Date.now() - debut))));
|
|
173
276
|
}
|
|
174
277
|
}
|
|
@@ -177,8 +280,10 @@ function attendreLaMachine(verrou, { dire, borneMs, rappelMs }) {
|
|
|
177
280
|
* Runs <mesure> under the lock of the machine and returns what it returns. <mesure> receives the
|
|
178
281
|
* environment of its children, which carries the lock to them.
|
|
179
282
|
* @param {(envEnfant: Record<string, string | undefined>) => T} mesure a synchronous measure
|
|
180
|
-
* @param {{ env?: Record<string, string | undefined>,
|
|
181
|
-
* borneMs?: number, rappelMs?: number }} options `
|
|
283
|
+
* @param {{ env?: Record<string, string | undefined>, racine?: string,
|
|
284
|
+
* dire?: (ligne: string) => void, borneMs?: number, rappelMs?: number }} options `racine`, the
|
|
285
|
+
* root of the repository the measure plays in, which gives its rank (`rangDeLaMesure`; the
|
|
286
|
+
* working directory by default); `borneMs`, the bound of the wait
|
|
182
287
|
* (GRILLHOUSE_ATTENTE_MACHINE_MS, else ATTENTE_MACHINE_MS); `rappelMs`, the gap between two
|
|
183
288
|
* lines that say the wait goes on (30 s)
|
|
184
289
|
* @returns {T}
|
|
@@ -190,17 +295,19 @@ export function avecLaMachine(
|
|
|
190
295
|
mesure,
|
|
191
296
|
{
|
|
192
297
|
env = process.env,
|
|
298
|
+
racine = process.cwd(),
|
|
193
299
|
dire = l => process.stdout.write(`${l}\n`),
|
|
194
300
|
borneMs = borneDe(env),
|
|
195
301
|
rappelMs = RAPPEL_MS,
|
|
196
302
|
} = {}
|
|
197
303
|
) {
|
|
198
|
-
if (
|
|
304
|
+
if (paquetsDeLEnveloppe(env)) return mesure(env);
|
|
199
305
|
const verrou = verrouDeMachine(env);
|
|
200
306
|
if (env[VARIABLE_TENUE] && lire(verrou) === env[VARIABLE_TENUE]) return mesure(env);
|
|
201
307
|
mkdirSync(path.dirname(verrou), { recursive: true });
|
|
202
|
-
|
|
203
|
-
|
|
308
|
+
const rang = rangDeLaMesure(racine);
|
|
309
|
+
if (!(peutPrendre(verrou, rang) && prendreVerrou(verrou, { attendre: false }))) {
|
|
310
|
+
const attendu = attendreLaMachine(verrou, { rang, dire, borneMs, rappelMs });
|
|
204
311
|
dire(`✓ verrou de la machine pris après ${Math.round(attendu / 1000)} s.`);
|
|
205
312
|
}
|
|
206
313
|
try {
|
|
@@ -11,12 +11,27 @@
|
|
|
11
11
|
// - developpeur writes none of the testwriter's files, and no other file until its ticket's
|
|
12
12
|
// description holds a "## Architecture" section. The ticket is the "TON TICKET : <id>" line of the agent's
|
|
13
13
|
// launch prompt, read from its transcript.
|
|
14
|
+
// - In an envelope (`GRILLHOUSE_ENVELOPPE`, the packages of the plan, scripts/enveloppe/
|
|
15
|
+
// paquets-du-plan.mjs), a write in the copy out of the packages the plan writes (`ecritsDuPlan`)
|
|
16
|
+
// and out of the shared test material, whose rule is the one above, stops the session, as the
|
|
17
|
+
// visibility of Bazel refuses a target the rule does not open: the hook answers Claude Code's
|
|
18
|
+
// `continue: false`, the tool refused (`permissionDecision: deny`), its reason naming the file's
|
|
19
|
+
// package and the plan, and leaves that reason in the session's working directory
|
|
20
|
+
// (`MARQUE_HORS_PLAN` under TMPDIR), which the launcher hands to the circuit. The mount of the
|
|
21
|
+
// envelope refuses the same write to any tool; the hook says why, and stops.
|
|
14
22
|
import { execFileSync } from 'node:child_process';
|
|
15
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
23
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
24
|
import { tmpdir } from 'node:os';
|
|
17
25
|
import path from 'node:path';
|
|
18
26
|
import { pathToFileURL } from 'node:url';
|
|
19
27
|
import { matieres, NOM_DE_TEST } from '../consommateurs.mjs';
|
|
28
|
+
import {
|
|
29
|
+
ecritsDuPlan,
|
|
30
|
+
MARQUE_HORS_PLAN,
|
|
31
|
+
paquetQuiTient,
|
|
32
|
+
paquetsDeLEnveloppe,
|
|
33
|
+
} from '../enveloppe/paquets-du-plan.mjs';
|
|
34
|
+
import { paquets } from '../paquets.mjs';
|
|
20
35
|
|
|
21
36
|
const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
|
|
22
37
|
|
|
@@ -68,6 +83,31 @@ function racineDe(file) {
|
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
// The stop of a write out of the plan of the envelope <env>, `{ paquet, raison }`, or null: a
|
|
87
|
+
// write in the repository holding the file, out of the packages the plan writes and out of the
|
|
88
|
+
// shared test material.
|
|
89
|
+
function horsDuPlan(input, { tmp = tmpdir(), env = process.env } = {}) {
|
|
90
|
+
const plan = paquetsDeLEnveloppe(env);
|
|
91
|
+
if (!plan || !WRITE_TOOLS.has(input.tool_name)) return null;
|
|
92
|
+
const raw = input.tool_input?.file_path ?? input.tool_input?.notebook_path;
|
|
93
|
+
if (!raw) return null;
|
|
94
|
+
const file = path.resolve(input.cwd ?? process.cwd(), raw);
|
|
95
|
+
if (isScratch(file, tmp)) return null;
|
|
96
|
+
const copie = racineDe(file);
|
|
97
|
+
if (!copie) return null;
|
|
98
|
+
const liste = paquets(copie);
|
|
99
|
+
const ecrits = ecritsDuPlan(plan, liste);
|
|
100
|
+
const rel = path.relative(copie, file);
|
|
101
|
+
if (ecrits.includes('.') || ecrits.some(d => rel.startsWith(`${d}/`))) return null;
|
|
102
|
+
if (matieres(copie).some(m => isInside(file, m.dir))) return null;
|
|
103
|
+
const tenant = paquetQuiTient(rel, liste);
|
|
104
|
+
const paquet = tenant === '.' ? 'la racine du dépôt' : tenant;
|
|
105
|
+
return {
|
|
106
|
+
paquet,
|
|
107
|
+
raison: `⛔ hors-plan — ${rel} est dans ${paquet}, hors des paquets du plan (${plan.join(', ')}) : ta séance s'arrête là, et le superviseur étend le plan ou refuse l'écriture.`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
71
111
|
// The ticket id of the launch prompt. A sub-agent's prompt opens its own transcript, next to the
|
|
72
112
|
// session's; an agent launched as its own session (`claude --agent`) has it in the session's.
|
|
73
113
|
function ticketOf(input) {
|
|
@@ -157,9 +197,30 @@ export function refusal(role, input, { tmp = tmpdir(), describe = ticketDescript
|
|
|
157
197
|
|
|
158
198
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
159
199
|
const input = JSON.parse(readFileSync(0, 'utf8'));
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
200
|
+
const stop = horsDuPlan(input);
|
|
201
|
+
if (stop) {
|
|
202
|
+
// The mark for the circuit; the stop below is the hook's answer, mark or not.
|
|
203
|
+
try {
|
|
204
|
+
writeFileSync(path.join(tmpdir(), MARQUE_HORS_PLAN), stop.raison);
|
|
205
|
+
} catch {
|
|
206
|
+
// A working directory gone leaves no mark; the session still stops.
|
|
207
|
+
}
|
|
208
|
+
process.stdout.write(
|
|
209
|
+
`${JSON.stringify({
|
|
210
|
+
continue: false,
|
|
211
|
+
stopReason: stop.raison,
|
|
212
|
+
hookSpecificOutput: {
|
|
213
|
+
hookEventName: 'PreToolUse',
|
|
214
|
+
permissionDecision: 'deny',
|
|
215
|
+
permissionDecisionReason: stop.raison,
|
|
216
|
+
},
|
|
217
|
+
})}\n`
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
const reason = refusal(process.argv[2], input);
|
|
221
|
+
if (reason) {
|
|
222
|
+
process.stderr.write(reason + '\n');
|
|
223
|
+
process.exitCode = 2;
|
|
224
|
+
}
|
|
164
225
|
}
|
|
165
226
|
}
|
|
@@ -5,26 +5,26 @@ description: "Le développeur : travailler un ticket du dépôt courant en agent
|
|
|
5
5
|
|
|
6
6
|
# Développeur — travailler un ticket
|
|
7
7
|
|
|
8
|
-
Tu travailles un seul ticket, dans la copie qui porte son numéro
|
|
9
|
-
|
|
10
|
-
prime sur tout le
|
|
8
|
+
Tu travailles un seul ticket, dans la copie qui porte son numéro : une seule séance porte tout son
|
|
9
|
+
code, dans tous les paquets de son plan. Le superviseur (`pitmaster`) te l'a confié ; le
|
|
10
|
+
responsable du projet l'a validé. La charte du projet, déjà dans ton contexte, prime sur tout le
|
|
11
|
+
reste.
|
|
11
12
|
|
|
12
13
|
## Au démarrage
|
|
13
14
|
|
|
14
15
|
1. Ta consigne porte ton ticket, lu au lancement de ta séance : sa description et ses critères,
|
|
15
|
-
ses notes, la dernière passation de ton rôle
|
|
16
|
-
|
|
17
|
-
`bd update <id> --claim`. Des notes au ticket
|
|
18
|
-
reprends au point qu'elles nomment, sur l'état de
|
|
19
|
-
`git -C <copie> diff`).
|
|
20
|
-
2. Lis les documents des
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
3. Lis
|
|
24
|
-
fournisseur, sa ligne, les noms déclarés pour ton composant et l'adresse de sa rubrique « Rend ».
|
|
25
|
-
Le reste se lit à deux renvois : tous les éléments avec leur phrase dans
|
|
16
|
+
ses notes, la dernière passation de ton rôle (celle du testwriter, pour ta première séance), les
|
|
17
|
+
paquets de son plan et les constats du relecteur qui restent ouverts pour toi. Tu ne le relis
|
|
18
|
+
pas par bd, qui le refuse à ta séance. Puis `bd update <id> --claim`. Des notes au ticket
|
|
19
|
+
disent qu'un agent s'est arrêté avant toi : tu reprends au point qu'elles nomment, sur l'état de
|
|
20
|
+
ta copie (`git -C <copie> status`, `git -C <copie> diff`).
|
|
21
|
+
2. Lis les documents des paquets du plan : `CADRE.md`, `INTERFACE.md`, `ARCHITECTURE.md`
|
|
22
|
+
(`docs/`, ou `packages/<x>/docs/`), et le lexique `CONTEXT.md`. Ainsi ton code part de
|
|
23
|
+
l'architecture écrite. Un document absent ne t'arrête pas.
|
|
24
|
+
3. Lis ce que publient les fournisseurs des paquets du plan : leur ligne et leurs éléments dans
|
|
26
25
|
`docs/agents/index-des-interfaces.md`, puis la section d'un élément dans l'interface du
|
|
27
|
-
fournisseur
|
|
26
|
+
fournisseur, et la ligne « Déclaré pour » de sa section « Consommateurs ». Ainsi tu sais ce qui
|
|
27
|
+
est déjà calculé, et où.
|
|
28
28
|
4. Lance les tests du testwriter, déjà dans ta copie (`npm test -- <fichiers>`, chemins depuis la
|
|
29
29
|
racine du dépôt), et vois-les rouges ; sans test neuf, ce sont les rouges existants que le
|
|
30
30
|
rapport de la preuve nomme.
|
|
@@ -47,16 +47,20 @@ prime sur tout le reste.
|
|
|
47
47
|
- **Tu nommes la règle que tu appliques** : dans le ticket, la règle du cadre (`R…` du `CADRE.md`)
|
|
48
48
|
et la règle d'architecture ou de spécification. Quand aucune ne s'applique, la décision manque
|
|
49
49
|
et remonte au superviseur.
|
|
50
|
-
- **Ton plan porte
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
- **Ton plan porte la livraison, sur les seuls paquets du plan** : il va au ticket avant le code,
|
|
51
|
+
le fournisseur d'abord. Un plan qui demande un autre paquet t'arrête : ta fiche le nomme, et le
|
|
52
|
+
superviseur étend le plan ou refuse. Ainsi un ticket reste dans son plan.
|
|
53
53
|
|
|
54
54
|
## Le code
|
|
55
55
|
|
|
56
|
-
- **
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
- **Chaque paquet reçoit les faits de ses fournisseurs** : ceux que publie leur interface, déclarés
|
|
57
|
+
pour lui. Tu lis chacun à son adresse, et ton code le prend tel que son fournisseur le publie ; un
|
|
58
|
+
calcul qui refait l'un d'eux se refuse. Ainsi chaque fait a un seul calcul, comme une passe LLVM
|
|
59
|
+
reçoit son analyse du gestionnaire.
|
|
60
|
+
- **L'index d'abord** : toute recherche commence par `rtfm_search` (mode `hybrid`) pour le quoi, et
|
|
61
|
+
`rtfm_expand` lit en entier un résultat qu'elle rend ; l'appel se cherche par
|
|
62
|
+
`codegraph explore <symboles>`, ou l'outil `codegraph_explore` ; le shell lit un fichier déjà
|
|
63
|
+
nommé. Ainsi ton code part de ce que le dépôt contient.
|
|
60
64
|
- **L'existant d'abord** : avant d'écrire un calcul ou d'inventer un nom, tu cherches celui qui
|
|
61
65
|
existe (ton paquet, le lexique, les interfaces voisines) et tu vérifies ce qu'il fait.
|
|
62
66
|
- **Une entrée ambiguë se refuse** : ce que le produit ne sait pas interpréter d'une seule façon
|
|
@@ -68,12 +72,13 @@ prime sur tout le reste.
|
|
|
68
72
|
- **Le comportement tient dans les critères** : ton code fait ce que disent les critères
|
|
69
73
|
d'acceptation, et rien d'autre.
|
|
70
74
|
|
|
71
|
-
Tu
|
|
72
|
-
|
|
73
|
-
échoue (« Read-only file system »)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
Tu lis toute la copie, index compris. Tu écris les seuls paquets du plan de ton ticket (les
|
|
76
|
+
paquets de son titre) : le reste de la copie est en lecture seule, une écriture hors d'eux
|
|
77
|
+
échoue (« Read-only file system »), et ton verrou arrête ta séance sur une écriture hors du plan,
|
|
78
|
+
que le superviseur étend ou refuse ; un dossier hors des paquets (des données comme `libraries/`)
|
|
79
|
+
s'écrit par le paquet qui le possède, que ta `raison` nomme. Les paquets hors du plan
|
|
80
|
+
arrivent construits, et les traces que lisent tes tests produites : `npm test -- <fichiers>` construit les paquets du plan et régénère leurs propres traces, et `npm run api`,
|
|
81
|
+
après lui, écrit le rapport d'API et la part générée des paquets du plan. La matière de test
|
|
77
82
|
partagée, en lecture seule pour toi, arrive construite : `npm test -- <un test de la matière>` la
|
|
78
83
|
joue sans la construire, et les journaux et le cache du lancement restent dans ta séance. Les
|
|
79
84
|
gardes du cadre (frontières, consommateurs, index des interfaces, rapports d'API) tournent à côté
|
|
@@ -98,7 +103,7 @@ avec ta fiche. Ainsi le circuit reprend dès ta fiche rendue.
|
|
|
98
103
|
recommandation ; ton code suit ensuite le verdict « Arbitrage » au ticket.
|
|
99
104
|
- **Un fait que ton code prend chez un fournisseur hors de sa ligne « Déclaré pour »** (un élément
|
|
100
105
|
publié pas encore déclaré pour toi, ou un fait que ta section ne porte pas) : une découverte qui
|
|
101
|
-
bloque, que le superviseur porte au
|
|
106
|
+
bloque, que le superviseur tranche ou porte au responsable, et qui entre ensuite dans la section
|
|
102
107
|
« Consommateurs » du fournisseur.
|
|
103
108
|
- **Une donnée qui manque chez un fournisseur** : tu t'arrêtes, et ta fiche finit sur BLOQUÉ, sa
|
|
104
109
|
`raison` nommant la donnée et le fournisseur ; tu ne la calcules pas dans ton paquet. Le superviseur ajoute le fournisseur
|
|
@@ -117,45 +122,50 @@ son adresse) : le circuit la consigne fermée sous ton ticket, et tu continues.
|
|
|
117
122
|
## Vérifier et livrer
|
|
118
123
|
|
|
119
124
|
- **Le circuit contrôle après toi** : ta séance finie, le circuit du ticket joue les tests du lot
|
|
120
|
-
et de chaque paquet
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
et de chaque paquet du plan, puis les gardes dans l'enveloppe du plan ; puis la passe finale, la
|
|
126
|
+
passe complète que joue l'intégration. Un rouge te relance avec sa sortie quand il est dans un
|
|
127
|
+
paquet du plan ; hors du plan, il attend la passe finale, où il monte au superviseur. Tu es lancé
|
|
128
|
+
sur un rouge (de la preuve, un `rouge existant` compris, ou des contrôles), sur un constat du
|
|
129
|
+
relecteur ouvert pour toi, ou directement sur un plan qui ne porte aucun test (aucun paquet avec
|
|
130
|
+
un script `test:vitest`), sans testwriter ni preuve : une séance qui ne change aucun fichier du
|
|
131
|
+
plan n'a rien à livrer, et ses contrôles rouges arrêtent le circuit (`sans-livraison`) ; ta
|
|
132
|
+
passation dit pourquoi. Tes propres lancements pendant le travail restent les tiens. Ainsi ton
|
|
133
|
+
lot arrive au relecteur vert.
|
|
134
|
+
- **Un rouge qui reste dans les tests va au testwriter** : quand le rouge qui reste est dans les
|
|
135
|
+
tests du lot (un test existant que ton changement de type casse), ta fiche porte `renvoi`, le
|
|
136
|
+
rôle testwriter. Le circuit relance le testwriter, ta `raison` et ta passation pour rapport, puis
|
|
137
|
+
la preuve et ta séance, sans te relancer sur ce rouge ; le même renvoi deux fois de suite, sans
|
|
138
|
+
contrôles verts entre eux, l'arrête (`sans-livraison`). Ainsi le rouge va à l'agent qui écrit
|
|
139
|
+
les tests.
|
|
135
140
|
- **Ta séance finit sur ta fiche**, que ton schéma décrit et que le circuit lit seule :
|
|
136
141
|
- `statut` FAIT : ton code est écrit, et les contrôles le jugent ;
|
|
137
142
|
- `statut` BLOQUÉ, avec sa `raison` : ce qui reste est hors de ta portée (un test, la matière
|
|
138
|
-
de test partagée, un fichier hors
|
|
143
|
+
de test partagée, un fichier hors du plan, une décision hors de ton rôle) ; avec
|
|
139
144
|
`renvoi`, c'est le renvoi au testwriter ; sans lui, le circuit s'arrête (`hors-portee`) ;
|
|
140
145
|
- `statut` BESOIN, avec sa `question` : une réponse te manque pour avancer ; le circuit
|
|
141
146
|
s'arrête (`besoin`) ;
|
|
142
147
|
- `ecarts` : ce que le message de commit nomme, chacun sous son `sujet` (ce qui s'écarte :
|
|
143
148
|
une référence, un test, un plafond), avec son `texte` ;
|
|
144
149
|
- `decouvertes` : ce que le circuit consigne fermé sous le ticket.
|
|
145
|
-
Sur un arrêt,
|
|
150
|
+
Sur un arrêt, ta question monte au superviseur, et le circuit ne te relance pas sur ce rouge. Ainsi
|
|
146
151
|
tu ne tournes jamais sur un rouge que tu as dit hors de ta portée.
|
|
152
|
+
- **Un fait repris porte sa pièce** : un fait que tu reprends d'un autre agent (une passation, un
|
|
153
|
+
rapport, un constat) se relit à sa pièce avant que tu t'appuies dessus, et ta raison ou ta
|
|
154
|
+
question porte la sienne, `piece` (fichier:ligne depuis la racine de la copie, ou « $ commande »
|
|
155
|
+
et sa sortie), que le circuit cherche dans la copie. Ainsi une ligne mal lue s'arrête à toi.
|
|
147
156
|
- **Une relance reprend ta séance** : après des contrôles rouges, le circuit reprend ta séance avec
|
|
148
|
-
une consigne courte, le rapport neuf
|
|
157
|
+
une consigne courte, le rapport neuf ; ta consigne, ta compétence et ce que tu as lu restent
|
|
149
158
|
valables. La dernière tentative, sur le modèle fort, est une séance neuve et entière. Ainsi tu
|
|
150
|
-
repars de ce que tu sais.
|
|
159
|
+
repars de ce que tu sais. Toute relance (des contrôles rouges, un `RENDU`, une reprise du
|
|
160
|
+
superviseur) porte les constats du relecteur qui restent ouverts pour toi, tous, jusqu'à la
|
|
161
|
+
relecture qui les juge levés. Ainsi aucun constat ne se perd d'une relance à l'autre.
|
|
151
162
|
- **Les tests ciblés** : les tests touchés et voisins, plus ceux des composants en aval quand une
|
|
152
163
|
interface change (`npm test -- <fichiers>`).
|
|
153
164
|
- **Une suite se juge sur tous ses échecs** : leur liste se compare nom par nom aux échecs connus.
|
|
154
165
|
Ainsi « vert » veut dire que rien de neuf n'a cassé.
|
|
155
166
|
- **Un écart dit l'état que le lot laisse au commit** : un écart voulu, une référence changée, un
|
|
156
167
|
test mis en quarantaine avec le ticket qui le referme entrent dans `ecarts` de ta fiche, avec leur
|
|
157
|
-
cause ;
|
|
158
|
-
finale est verte avant le commit. Un sujet qu'une fiche d'avant nomme se reprend tel quel, et ta
|
|
168
|
+
cause ; la passe finale est verte avant le commit. Un sujet qu'une fiche d'avant nomme se reprend tel quel, et ta
|
|
159
169
|
valeur remplace la sienne : le circuit écrit chaque sujet une fois, de la dernière fiche qui le
|
|
160
170
|
porte. Ainsi le message de commit ne se répète ni ne se contredit.
|
|
161
171
|
- **Tu retires ce que tu crées pour mesurer**, chemin par chemin, avant de rendre la main.
|