@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
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The envelope of a role agent: runs a command (the agent's `claude` session) inside a bwrap
|
|
3
|
-
// sandbox that shows the agent
|
|
4
|
-
//
|
|
3
|
+
// sandbox that shows the agent the whole copy and opens in writing only the packages of its plan,
|
|
4
|
+
// as the visibility of Bazel and the module boundaries of Nx refuse a dependency while the code
|
|
5
|
+
// stays readable. `npx grillhouse enveloppe <copie> <paquets> <rôle> -- <commande…>`
|
|
5
6
|
//
|
|
6
|
-
// - The root `/` is read-only
|
|
7
|
-
// package
|
|
8
|
-
// proper to the session, where only the scratchpad directory of the copy comes
|
|
9
|
-
// session's working directory inside it (`dossierDeTravail`; Claude Code's
|
|
10
|
-
// `CLAUDE_CODE_TMPDIR` else the launch's temporary directory, so that under a
|
|
11
|
-
// the base of the pass and goes with it); the npm
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// (
|
|
19
|
-
//
|
|
20
|
-
// agent's runs of `npm test` keep their tasks there, as every copy's runs do.
|
|
7
|
+
// - The root `/` is read-only, the copy included, its indexes (`.codegraph`, `.rtfm`) and every
|
|
8
|
+
// package outside the plan in sight. Only these are written: the packages of the plan, below; a
|
|
9
|
+
// `/tmp` empty and proper to the session, where only the scratchpad directory of the copy comes
|
|
10
|
+
// back, the session's working directory inside it (`dossierDeTravail`; Claude Code's
|
|
11
|
+
// scratchpads root, `CLAUDE_CODE_TMPDIR` else the launch's temporary directory, so that under a
|
|
12
|
+
// pass it lies in the base of the pass and goes with it); the npm cache; the Claude session
|
|
13
|
+
// state (`~/.claude` and the copy's project directory), whose configuration, skills, plugins and
|
|
14
|
+
// other projects stay read-only. `~/.claude.json` is a throwaway copy: the session writes it,
|
|
15
|
+
// the real file does not change. The tickets base (the main tree's `.beads/`) is written: the
|
|
16
|
+
// agent claims, plans, hands over and files what it finds. Every `.git`, the copy's included,
|
|
17
|
+
// stays read-only: the agent reads the git state and delivers a patch (`git diff`, and
|
|
18
|
+
// `git diff --no-index /dev/null <file>` for a new file); it never commits. The cache of
|
|
19
|
+
// Turborepo (`<main tree>/.turbo/cache`, shared by every copy) is written: the agent's runs of
|
|
20
|
+
// `npm test` keep their tasks there, as every copy's runs do.
|
|
21
21
|
// - The home and the session directories (`$XDG_RUNTIME_DIR`, `/run/user/<uid>`) show only what
|
|
22
22
|
// the envelope declares: each starts empty and sealed read-only, and the home gets back
|
|
23
23
|
// `~/.claude`, `~/.claude.json`, the npm cache, the paths of `MAISON_EN_LECTURE` (claude, node,
|
|
@@ -34,83 +34,52 @@
|
|
|
34
34
|
// (npm's `ignore-scripts`, docs/content/using-npm/config.md); no script of the frame has one. bwrap
|
|
35
35
|
// starts with that environment alone, so no other variable of the machine (`GH_TOKEN`,
|
|
36
36
|
// `NPM_TOKEN`…) enters, by name or by argument. The session also gets `GRILLHOUSE_ENVELOPPE`, the
|
|
37
|
-
//
|
|
37
|
+
// packages of the plan (`paquets-du-plan.mjs`): the frame's commands build, judge and write those
|
|
38
|
+
// packages only, and its lock refuses a write out of them. A declared variable that names a path
|
|
38
39
|
// (`VARIABLES_DE_CHEMIN`) enters only when its path is in sight inside the envelope, tested there
|
|
39
40
|
// before the command runs. The session has its own process space: it reads the environment of
|
|
40
41
|
// no process of the machine.
|
|
41
|
-
// - No index of the repository is in sight: neither codegraph nor rtfm comes back in the home,
|
|
42
|
-
// and every index directory (`INDEX`) of every worktree, the copy's included, is an empty
|
|
43
|
-
// directory sealed read-only; the worktree around it keeps its mount, the copy in writing.
|
|
44
42
|
// - A declared path that is a symbolic link comes back as that link, and its target with it
|
|
45
43
|
// (`avecLiens`).
|
|
46
44
|
// - A worktree of the repository under an emptied directory (the home, `/tmp`) comes back
|
|
47
|
-
// read-only, `.git` included; the copy and the tickets base are then
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// which the launcher produces before the session), when they exist inside it.
|
|
60
|
-
// The parent's own files (`src/index.ts`) come back read-only, and the emptied directories are
|
|
61
|
-
// then sealed read-only.
|
|
62
|
-
// - What the component's tests declare they read comes back read-only, as Bazel gives a test the
|
|
63
|
-
// `data` it declares: each path of `grillhouse.lit` of its `package.json`, and of the `lit` of
|
|
64
|
-
// each trace it declares (`cheminsLus`, each path from the root of <copie>), that lies in a
|
|
65
|
-
// sibling component and exists, a directory or a file. A path the envelope already shows (the
|
|
66
|
-
// root, the component itself, the shared test material, whose mount the role decides) adds no
|
|
67
|
-
// mount.
|
|
68
|
-
// - For a component that is a package of the copy (`paquets` of `scripts/paquets.mjs`), the agent
|
|
69
|
-
// writes that package and nothing else of the copy, as a sandbox of Bazel or of Nix opens in
|
|
70
|
-
// writing only what the action declares: the copy is read-only, the folders of its root that no
|
|
45
|
+
// read-only, `.git` included; the copy's packages of the plan and the tickets base are then
|
|
46
|
+
// written as above.
|
|
47
|
+
// - An envelope on the main tree is refused, so is a plan that names no package, and so is a path
|
|
48
|
+
// of the plan that does not exist; when it is the name of a package of the copy, the refusal
|
|
49
|
+
// gives its path.
|
|
50
|
+
// - <copie> is the agent's git worktree; <paquets> is the plan, the paths it names inside it
|
|
51
|
+
// (`packages/a`, or `src/parser` in a one-package project), in the variable's form
|
|
52
|
+
// (`ecrireLaListe` of paquets-du-plan.mjs); <rôle> is the agent's role (`testwriter`,
|
|
53
|
+
// `developpeur`…).
|
|
54
|
+
// - The plan writes, for each of its paths, the package of the copy that holds it (`ecritsDuPlan`,
|
|
55
|
+
// `paquets` of `scripts/paquets.mjs`), as a sandbox of Bazel or of Nix opens in writing only
|
|
56
|
+
// what the action declares: the rest of the copy is read-only, the folders of its root that no
|
|
71
57
|
// package holds (`libraries/`, `spec/`) included, and a write there fails on « Read-only file
|
|
72
|
-
// system », which the agent sees.
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// disk before it.
|
|
78
|
-
// - A component that is no package (a folder of the root, `scripts`; `src/parser` in a one-package
|
|
79
|
-
// project) writes the package that holds it, the root: the copy stays written. A folder of the
|
|
80
|
-
// root hides nothing: its scripts work on the whole repository. The rest holds: the root
|
|
81
|
-
// read-only, the copy written, its `.git` read-only.
|
|
58
|
+
// system », which the agent sees. Where a run of Turborepo writes at the root of the copy
|
|
59
|
+
// (`sortiesDeLaRacine`, its `.turbo`) is then an empty directory proper to the session, created
|
|
60
|
+
// on disk before it. A path that no package holds (a folder of the root, `scripts`; `src/parser`
|
|
61
|
+
// in a one-package project) writes the package that holds it, the root: the copy stays written,
|
|
62
|
+
// its `.git` read-only.
|
|
82
63
|
// - The package of the shared test material is every component of <copie> whose `package.json`
|
|
83
|
-
// exports `./test-fixtures` (`matieres` of `scripts/consommateurs.mjs`). It
|
|
84
|
-
// written for the `testwriter`, read-only for every other role,
|
|
85
|
-
// such a role, where a run of its tests writes in it (`sortiesDeTest`:
|
|
86
|
-
// Vite's cache) is an empty directory proper to the session, as Bazel
|
|
87
|
-
// of its source: what is written there leaves no trace. The envelope
|
|
88
|
-
// disk, empty, before the session, so that bwrap mounts over it inside
|
|
89
|
-
// -
|
|
90
|
-
// package of the shared test material, points into a hidden directory: such a link would cross
|
|
91
|
-
// the envelope.
|
|
64
|
+
// exports `./test-fixtures` (`matieres` of `scripts/consommateurs.mjs`). It is mounted after the
|
|
65
|
+
// packages of the plan: written for the `testwriter`, read-only for every other role, in the
|
|
66
|
+
// plan or not. For such a role, where a run of its tests writes in it (`sortiesDeTest`:
|
|
67
|
+
// Turborepo's task logs, Vite's cache) is an empty directory proper to the session, as Bazel
|
|
68
|
+
// writes a test's logs out of its source: what is written there leaves no trace. The envelope
|
|
69
|
+
// creates each of them on disk, empty, before the session, so that bwrap mounts over it inside
|
|
70
|
+
// the read-only package.
|
|
92
71
|
// - The exit codes are named in `codes.mjs`.
|
|
93
72
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
94
|
-
import {
|
|
95
|
-
copyFileSync,
|
|
96
|
-
existsSync,
|
|
97
|
-
mkdirSync,
|
|
98
|
-
lstatSync,
|
|
99
|
-
readdirSync,
|
|
100
|
-
readFileSync,
|
|
101
|
-
readlinkSync,
|
|
102
|
-
statSync,
|
|
103
|
-
} from 'node:fs';
|
|
73
|
+
import { copyFileSync, existsSync, mkdirSync, lstatSync, readlinkSync } from 'node:fs';
|
|
104
74
|
import os from 'node:os';
|
|
105
75
|
import path from 'node:path';
|
|
106
76
|
import { pathToFileURL } from 'node:url';
|
|
107
77
|
import { matieres as lesMatieres } from '../consommateurs.mjs';
|
|
108
78
|
import { paquets as lesPaquets } from '../paquets.mjs';
|
|
109
79
|
import { CODE_REFUS, CODE_USAGE } from './codes.mjs';
|
|
110
|
-
import { VARIABLE } from './
|
|
80
|
+
import { ecritsDuPlan, ecrireLaListe, lireLaListe, VARIABLE } from './paquets-du-plan.mjs';
|
|
111
81
|
import { ouvrirBase } from '../vitest-config/base-de-passe.mjs';
|
|
112
82
|
|
|
113
|
-
const PUBLISHED = ['package.json', 'docs/INTERFACE.md', 'dist', 'turbo.json', 'turbo.jsonc'];
|
|
114
83
|
// The role that writes the package of the shared test material.
|
|
115
84
|
const ROLE_MATIERE = 'testwriter';
|
|
116
85
|
// What a run of a package's tests writes in that package, relative to it: the logs of its tasks,
|
|
@@ -125,13 +94,6 @@ const sortiesDeTest = dir => SORTIES_DE_TEST.map(s => path.join(dir, s));
|
|
|
125
94
|
const SORTIES_DE_LA_RACINE = ['.turbo'];
|
|
126
95
|
// The directories of the copy <copie> where a run of Turborepo writes.
|
|
127
96
|
const sortiesDeLaRacine = copie => SORTIES_DE_LA_RACINE.map(s => path.join(copie, s));
|
|
128
|
-
// True when <composant> of <copie> is a package of the copy (`paquets`, the directory of each
|
|
129
|
-
// relative to it): its envelope then writes that package alone.
|
|
130
|
-
const estUnPaquet = (copie, composant, paquets) =>
|
|
131
|
-
paquets.some(p => path.resolve(copie, p.dossier) === path.resolve(copie, composant));
|
|
132
|
-
// The directories where codegraph and rtfm keep the index of a tree they index: the source of
|
|
133
|
-
// every component, which the envelope hides.
|
|
134
|
-
const INDEX = ['.codegraph', '.rtfm'];
|
|
135
97
|
// The machine's temporary directory: an empty one, proper to the session, replaces it.
|
|
136
98
|
const TEMPORAIRE = '/tmp';
|
|
137
99
|
// What stays read-only inside `~/.claude`: the configuration and what it loads.
|
|
@@ -149,8 +111,9 @@ const CLAUDE_EN_LECTURE = [
|
|
|
149
111
|
];
|
|
150
112
|
// What the home shows read-only besides `~/.claude`, as paths relative to it: claude's launcher
|
|
151
113
|
// and versions, node and its global tools (bd), the state of bd and of its dolt database, and the
|
|
152
|
-
// git configuration. The home shows nothing else: neither codegraph nor rtfm, whose
|
|
153
|
-
// the
|
|
114
|
+
// git configuration. The home shows nothing else: neither codegraph nor rtfm, whose servers the
|
|
115
|
+
// roles of the envelope do not receive (scripts/droits.mjs); the indexes of the repository are in
|
|
116
|
+
// sight with it.
|
|
154
117
|
const MAISON_EN_LECTURE = [
|
|
155
118
|
'.local/bin/claude',
|
|
156
119
|
'.local/share/claude',
|
|
@@ -222,8 +185,8 @@ export function dossierDeTravail(copie, env = process.env) {
|
|
|
222
185
|
// envelope mounts in writing, `npm_config_ignore_scripts` set to `true` (npm runs no lifecycle
|
|
223
186
|
// script: a `prepare` that writes under the read-only `.git` would fail `npm install`; the
|
|
224
187
|
// launcher installs outside the session), `TMPDIR` set to the session's working directory
|
|
225
|
-
// <travail>, and `GRILLHOUSE_ENVELOPPE` set to <
|
|
226
|
-
function environnementDeLaSeance(env, { cacheNpm,
|
|
188
|
+
// <travail>, and `GRILLHOUSE_ENVELOPPE` set to the plan <paquets>. Nothing else of <env> enters.
|
|
189
|
+
function environnementDeLaSeance(env, { cacheNpm, paquets, travail }) {
|
|
227
190
|
const declare = nom =>
|
|
228
191
|
ENVIRONNEMENT_DECLARE.includes(nom) ||
|
|
229
192
|
VARIABLES_DE_CHEMIN.includes(nom) ||
|
|
@@ -234,7 +197,7 @@ function environnementDeLaSeance(env, { cacheNpm, composant, travail }) {
|
|
|
234
197
|
npm_config_cache: cacheNpm,
|
|
235
198
|
npm_config_ignore_scripts: 'true',
|
|
236
199
|
TMPDIR: travail,
|
|
237
|
-
[VARIABLE]:
|
|
200
|
+
[VARIABLE]: ecrireLaListe(paquets),
|
|
238
201
|
};
|
|
239
202
|
}
|
|
240
203
|
|
|
@@ -316,64 +279,6 @@ function worktrees(copie) {
|
|
|
316
279
|
.map(l => l.slice('worktree '.length));
|
|
317
280
|
}
|
|
318
281
|
|
|
319
|
-
// What a directory of the real disk holds, as { name, dir } entries.
|
|
320
|
-
function lister(dir) {
|
|
321
|
-
return readdirSync(dir, { withFileTypes: true }).map(e => ({
|
|
322
|
-
name: e.name,
|
|
323
|
-
dir: e.isDirectory(),
|
|
324
|
-
}));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// The symbolic links under <dir> whose resolved target lies outside <dir>.
|
|
328
|
-
function liensSortants(dir) {
|
|
329
|
-
const found = [];
|
|
330
|
-
const walk = d => {
|
|
331
|
-
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
332
|
-
const p = path.join(d, e.name);
|
|
333
|
-
if (e.isSymbolicLink()) {
|
|
334
|
-
const target = path.resolve(d, readlinkSync(p));
|
|
335
|
-
const rel = path.relative(dir, target);
|
|
336
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) found.push({ lien: p, cible: target });
|
|
337
|
-
} else if (e.isDirectory() && e.name !== 'node_modules') {
|
|
338
|
-
walk(p);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
walk(dir);
|
|
343
|
-
return found;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// The paths, relative to <dir>, a package of <copie>, that its `package.json` publishes beyond the
|
|
347
|
-
// parts every package publishes: what its `files` field names, and the directory each trace it
|
|
348
|
-
// declares writes (`grillhouse.traces`, its `ecrit` from the root of <copie>); none without them.
|
|
349
|
-
function fichiersPublies(dir, copie) {
|
|
350
|
-
const manifeste = path.join(dir, 'package.json');
|
|
351
|
-
if (!existsSync(manifeste)) return [];
|
|
352
|
-
const { files, grillhouse } = JSON.parse(readFileSync(manifeste, 'utf8'));
|
|
353
|
-
const traces = Object.values(grillhouse?.traces ?? {})
|
|
354
|
-
.filter(t => typeof t?.ecrit === 'string')
|
|
355
|
-
.map(t => path.relative(dir, path.join(copie, t.ecrit)));
|
|
356
|
-
return [...(Array.isArray(files) ? files : []), ...traces];
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* The paths that the tests of the package at <dir> declare they read: its `grillhouse.lit`, then
|
|
361
|
-
* the `lit` of each trace it declares (`grillhouse.traces`), each path from the root; none without
|
|
362
|
-
* a package.json or a declaration.
|
|
363
|
-
* @param {string} dir the directory of the package
|
|
364
|
-
* @returns {string[]}
|
|
365
|
-
*/
|
|
366
|
-
export function cheminsLus(dir) {
|
|
367
|
-
const manifeste = path.join(dir, 'package.json');
|
|
368
|
-
if (!existsSync(manifeste)) return [];
|
|
369
|
-
const { grillhouse } = JSON.parse(readFileSync(manifeste, 'utf8'));
|
|
370
|
-
const liste = v => (Array.isArray(v) ? v.filter(l => typeof l === 'string') : []);
|
|
371
|
-
return [
|
|
372
|
-
...liste(grillhouse?.lit),
|
|
373
|
-
...Object.values(grillhouse?.traces ?? {}).flatMap(t => liste(t?.lit)),
|
|
374
|
-
];
|
|
375
|
-
}
|
|
376
|
-
|
|
377
282
|
const inside = (child, parent) => {
|
|
378
283
|
const rel = path.relative(parent, child);
|
|
379
284
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
@@ -397,21 +302,28 @@ export function jugerComposant({ depot, composant, paquets = [] }, fs) {
|
|
|
397
302
|
: `Le composant ${propre} n'existe pas.`;
|
|
398
303
|
}
|
|
399
304
|
|
|
400
|
-
// The bwrap arguments for this agent, or the refusal that stops its launch. `
|
|
401
|
-
//
|
|
402
|
-
// (`
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
// (`
|
|
406
|
-
// `
|
|
407
|
-
//
|
|
408
|
-
// `
|
|
409
|
-
//
|
|
410
|
-
// `fs` gives `
|
|
411
|
-
// (what the tests of the package at <dir> declare they read, `cheminsLus`), so the plan is testable
|
|
412
|
-
// without disk.
|
|
305
|
+
// The bwrap arguments for this agent, or the refusal that stops its launch. `paquets` is the plan,
|
|
306
|
+
// the paths it names relative to the copy; `paquetsDeLaCopie` lists the packages of the copy
|
|
307
|
+
// (`paquets` of `scripts/paquets.mjs`), which `jugerComposant` reads to judge each path and
|
|
308
|
+
// `ecritsDuPlan` to find the package that holds it. `montages` lists, in order, the paths outside
|
|
309
|
+
// the copy that are emptied, written, read back or made a symbolic link (`lien`, its text)
|
|
310
|
+
// (`montagesDeLaSeance`); an emptied one marked `scelle` is sealed read-only once every mount
|
|
311
|
+
// inside it is made. `matieres` lists the packages of the shared test material, relative to the
|
|
312
|
+
// copy (`matieres` of `scripts/consommateurs.mjs`); `role` is the agent's role, and only the
|
|
313
|
+
// `testwriter` writes them. `magasins` lists the task caches written by every run, the agent's
|
|
314
|
+
// included (Turborepo's `<main tree>/.turbo/cache`); one that does not exist is not mounted.
|
|
315
|
+
// `fs` gives `existe(path)`, so the plan is testable without disk.
|
|
413
316
|
export function plan(
|
|
414
|
-
{
|
|
317
|
+
{
|
|
318
|
+
trees,
|
|
319
|
+
copie,
|
|
320
|
+
paquets = [],
|
|
321
|
+
role,
|
|
322
|
+
matieres = [],
|
|
323
|
+
montages = [],
|
|
324
|
+
magasins = [],
|
|
325
|
+
paquetsDeLaCopie = [],
|
|
326
|
+
},
|
|
415
327
|
fs
|
|
416
328
|
) {
|
|
417
329
|
// The worktrees start with the main tree: an envelope on it would let the agent write its root.
|
|
@@ -420,23 +332,15 @@ export function plan(
|
|
|
420
332
|
refus: `La copie ${copie} est l'arbre principal : un agent travaille dans sa propre copie.`,
|
|
421
333
|
};
|
|
422
334
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (refusDuComposant) return { refus: refusDuComposant };
|
|
430
|
-
|
|
431
|
-
const caches = racine ? [] : trees.map(t => path.join(t, parentRel));
|
|
432
|
-
const matiere = matieres.map(m => path.join(copie, m));
|
|
433
|
-
for (const { lien, cible } of [propre, ...matiere].flatMap(d => fs.liens(d))) {
|
|
434
|
-
if (caches.some(c => inside(cible, c))) {
|
|
435
|
-
return {
|
|
436
|
-
refus: `Le lien ${lien} vise ${cible}, que l'enveloppe masque : il traverserait l'enveloppe.`,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
335
|
+
if (paquets.length === 0) {
|
|
336
|
+
return { refus: "Le plan ne nomme aucun paquet : l'enveloppe n'ouvre rien en écriture." };
|
|
337
|
+
}
|
|
338
|
+
for (const p of paquets) {
|
|
339
|
+
const refus = jugerComposant({ depot: copie, composant: p, paquets: paquetsDeLaCopie }, fs);
|
|
340
|
+
if (refus) return { refus };
|
|
439
341
|
}
|
|
342
|
+
const ecrits = ecritsDuPlan(paquets, paquetsDeLaCopie);
|
|
343
|
+
const toute = ecrits.includes('.');
|
|
440
344
|
|
|
441
345
|
// /dev/shm, which /dev brings in writing, becomes an empty directory proper to the session.
|
|
442
346
|
const args = [
|
|
@@ -462,50 +366,25 @@ export function plan(
|
|
|
462
366
|
for (const t of trees) {
|
|
463
367
|
if (videes.some(v => inside(t, v)) && fs.existe(t)) args.push('--ro-bind', t, t);
|
|
464
368
|
}
|
|
465
|
-
// The copy is mounted after them: an emptied /tmp does not hide it.
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (paquet) for (const s of sortiesDeLaRacine(copie)) if (fs.existe(s)) args.push('--tmpfs', s);
|
|
369
|
+
// The copy is mounted after them: an emptied /tmp does not hide it. It is read-only, and the
|
|
370
|
+
// packages of the plan come back in writing below, as a sandbox of Bazel opens only what the
|
|
371
|
+
// action declares; where a run of Turborepo writes at the root is an empty directory proper to
|
|
372
|
+
// the session. A plan that writes the root writes the whole copy. Its .git stays read-only.
|
|
373
|
+
args.push(toute ? '--bind' : '--ro-bind', copie, copie);
|
|
374
|
+
if (!toute) for (const s of sortiesDeLaRacine(copie)) if (fs.existe(s)) args.push('--tmpfs', s);
|
|
472
375
|
const gitDeLaCopie = path.join(copie, '.git');
|
|
473
376
|
if (fs.existe(gitDeLaCopie)) args.push('--ro-bind', gitDeLaCopie, gitDeLaCopie);
|
|
474
377
|
// The task cache (Turborepo's) is written by the agent's runs, as by every copy's.
|
|
475
378
|
for (const m of magasins) if (fs.existe(m)) args.push('--bind', m, m);
|
|
476
379
|
const tickets = trees.length > 0 ? path.join(trees[0], '.beads') : null;
|
|
477
380
|
if (tickets && fs.existe(tickets)) args.push('--bind', tickets, tickets);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
.filter(i => fs.existe(i));
|
|
483
|
-
for (const i of index) args.push('--tmpfs', i);
|
|
484
|
-
for (const c of caches) if (fs.existe(c)) args.push('--tmpfs', c);
|
|
485
|
-
const parent = path.join(copie, parentRel);
|
|
486
|
-
for (const e of racine ? [] : fs.lister(parent)) {
|
|
487
|
-
const p = path.join(parent, e.name);
|
|
488
|
-
if (!e.dir) {
|
|
489
|
-
args.push('--ro-bind', p, p);
|
|
490
|
-
} else if (e.name !== nom && !matiere.includes(p)) {
|
|
491
|
-
for (const part of new Set([...PUBLISHED, ...fs.publies(p, copie)])) {
|
|
492
|
-
const q = path.join(p, part);
|
|
493
|
-
if (inside(q, p) && fs.existe(q)) args.push('--ro-bind', q, q);
|
|
494
|
-
}
|
|
381
|
+
if (!toute) {
|
|
382
|
+
for (const e of ecrits) {
|
|
383
|
+
const d = path.join(copie, e);
|
|
384
|
+
args.push('--bind', d, d);
|
|
495
385
|
}
|
|
496
386
|
}
|
|
497
|
-
|
|
498
|
-
if (!racine) {
|
|
499
|
-
for (const l of new Set(fs.lus?.(propre) ?? [])) {
|
|
500
|
-
const q = path.join(copie, l);
|
|
501
|
-
const visible = inside(q, propre) || matiere.some(m => inside(q, m));
|
|
502
|
-
if (inside(q, parent) && q !== parent && !visible && fs.existe(q)) {
|
|
503
|
-
args.push('--ro-bind', q, q);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
args.push('--bind', propre, propre);
|
|
508
|
-
for (const m of matiere) {
|
|
387
|
+
for (const m of matieres.map(m => path.join(copie, m))) {
|
|
509
388
|
args.push(role === ROLE_MATIERE ? '--bind' : '--ro-bind', m, m);
|
|
510
389
|
// What a run of its tests writes goes, for a role that does not write it, to an empty
|
|
511
390
|
// directory proper to the session, as Bazel writes a test's logs out of its source.
|
|
@@ -513,31 +392,25 @@ export function plan(
|
|
|
513
392
|
for (const s of sortiesDeTest(m)) if (fs.existe(s)) args.push('--tmpfs', s);
|
|
514
393
|
}
|
|
515
394
|
// The emptied directories close behind the mounts: nothing new is written there.
|
|
516
|
-
for (const c of caches) if (fs.existe(c)) args.push('--remount-ro', c);
|
|
517
395
|
for (const { chemin } of montages.filter(m => m.scelle)) args.push('--remount-ro', chemin);
|
|
518
|
-
for (const i of index) args.push('--remount-ro', i);
|
|
519
396
|
args.push('--chdir', copie);
|
|
520
397
|
return { args };
|
|
521
398
|
}
|
|
522
399
|
|
|
523
|
-
const disque = {
|
|
524
|
-
lister,
|
|
525
|
-
existe: existsSync,
|
|
526
|
-
liens: d => (existsSync(d) && statSync(d).isDirectory() ? liensSortants(d) : []),
|
|
527
|
-
publies: fichiersPublies,
|
|
528
|
-
lus: cheminsLus,
|
|
529
|
-
};
|
|
400
|
+
const disque = { existe: existsSync };
|
|
530
401
|
|
|
531
|
-
// The command: the session of <commande> in the envelope of <
|
|
402
|
+
// The command: the session of <commande> in the envelope of the plan <paquets> in <copie>; its
|
|
403
|
+
// exit code.
|
|
532
404
|
function envelopper(argv) {
|
|
533
405
|
const sep = argv.indexOf('--');
|
|
534
|
-
const [copieArg,
|
|
406
|
+
const [copieArg, liste, role] = argv.slice(0, sep < 0 ? undefined : sep);
|
|
535
407
|
const commande = sep < 0 ? [] : argv.slice(sep + 1);
|
|
536
|
-
if (!copieArg || !
|
|
537
|
-
process.stderr.write('usage: enveloppe.mjs <copie> <
|
|
408
|
+
if (!copieArg || !liste || !role || commande.length === 0) {
|
|
409
|
+
process.stderr.write('usage: enveloppe.mjs <copie> <paquets> <rôle> -- <commande…>\n');
|
|
538
410
|
return CODE_USAGE;
|
|
539
411
|
}
|
|
540
412
|
const copie = path.resolve(copieArg);
|
|
413
|
+
const paquets = lireLaListe(liste);
|
|
541
414
|
// A launch from a package of a repository of workspaces sets npm's `workspace`, which
|
|
542
415
|
// `npm config` refuses: the cache is the user's, whatever the workspace.
|
|
543
416
|
const cacheNpm = execFileSync('npm', ['config', 'get', 'cache', '--workspaces=false'], {
|
|
@@ -579,10 +452,10 @@ function envelopper(argv) {
|
|
|
579
452
|
for (const s of sortiesDeTest(path.join(copie, m))) mkdirSync(s, { recursive: true });
|
|
580
453
|
}
|
|
581
454
|
}
|
|
582
|
-
// For a
|
|
583
|
-
// created here, so that bwrap mounts an empty one over it inside the read-only copy.
|
|
584
|
-
const
|
|
585
|
-
if (
|
|
455
|
+
// For a plan that leaves the root read-only, where a run of Turborepo writes at the root of the
|
|
456
|
+
// copy is created here, so that bwrap mounts an empty one over it inside the read-only copy.
|
|
457
|
+
const paquetsDeLaCopie = lesPaquets(copie);
|
|
458
|
+
if (!ecritsDuPlan(paquets, paquetsDeLaCopie).includes('.')) {
|
|
586
459
|
for (const s of sortiesDeLaRacine(copie)) mkdirSync(s, { recursive: true });
|
|
587
460
|
}
|
|
588
461
|
// The cache of Turborepo, in the main tree (the folder of git's common directory), created here
|
|
@@ -598,12 +471,12 @@ function envelopper(argv) {
|
|
|
598
471
|
{
|
|
599
472
|
trees: worktrees(copie),
|
|
600
473
|
copie,
|
|
601
|
-
|
|
474
|
+
paquets,
|
|
602
475
|
role,
|
|
603
476
|
matieres,
|
|
604
477
|
montages,
|
|
605
478
|
magasins,
|
|
606
|
-
|
|
479
|
+
paquetsDeLaCopie,
|
|
607
480
|
},
|
|
608
481
|
disque
|
|
609
482
|
);
|
|
@@ -614,7 +487,7 @@ function envelopper(argv) {
|
|
|
614
487
|
} else {
|
|
615
488
|
const r = spawnSync('bwrap', [...args, ...commandeEprouvee(commande)], {
|
|
616
489
|
stdio: 'inherit',
|
|
617
|
-
env: environnementDeLaSeance(process.env, { cacheNpm,
|
|
490
|
+
env: environnementDeLaSeance(process.env, { cacheNpm, paquets, travail }),
|
|
618
491
|
});
|
|
619
492
|
if (r.error) process.stderr.write(`⛔ ENVELOPPE IMPOSSIBLE — ${r.error.message}\n`);
|
|
620
493
|
code = r.error ? CODE_REFUS : (r.status ?? 1);
|