@roomi-fields/grillhouse 0.23.1 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/METACADRE.md +1 -1
  3. package/agents/explorateur.md +4 -2
  4. package/agents/questionneur.md +1 -1
  5. package/bin/grillhouse.mjs +3 -58
  6. package/cadre/templates/projet/CLAUDE.md +5 -1
  7. package/cadre/templates/projet/docs/agents/issue-tracker.md +4 -4
  8. package/charte.md +23 -6
  9. package/docs/ARCHITECTURE.md +130 -1
  10. package/hooks/register.tsx +3 -13
  11. package/package.json +13 -1
  12. package/scripts/bilan.mjs +134 -88
  13. package/scripts/circuit.mjs +1547 -2248
  14. package/scripts/composants-affectes.mjs +27 -108
  15. package/scripts/consommateurs-arbitres.mjs +15 -21
  16. package/scripts/consommateurs.d.ts +8 -0
  17. package/scripts/consommateurs.mjs +5 -1
  18. package/scripts/dette.d.ts +23 -0
  19. package/scripts/dette.mjs +76 -13
  20. package/scripts/droits.mjs +38 -44
  21. package/scripts/elements-du-lot.mjs +246 -0
  22. package/scripts/enveloppe/enveloppe.mjs +105 -232
  23. package/scripts/enveloppe/lancer.sh +79 -49
  24. package/scripts/enveloppe/paquets-du-plan.mjs +49 -0
  25. package/scripts/etapes.mjs +148 -213
  26. package/scripts/frontieres.mjs +4 -2
  27. package/scripts/gardes-du-cadre.mjs +8 -6
  28. package/scripts/grillhouse-maj.mjs +113 -19
  29. package/scripts/index-interfaces.mjs +21 -1
  30. package/scripts/integration/integrer.mjs +2 -2
  31. package/scripts/interfaces-contre-code.mjs +25 -17
  32. package/scripts/migrations/0.23.0-config-vitest.mjs +26 -23
  33. package/scripts/migrations/0.23.2-copies-du-cadre.mjs +201 -0
  34. package/scripts/migrations/0.24.0-circuit-simplifie.mjs +274 -0
  35. package/scripts/migrations.mjs +41 -6
  36. package/scripts/paquets.d.ts +5 -0
  37. package/scripts/paquets.mjs +3 -0
  38. package/scripts/references-par-les-types.mjs +55 -36
  39. package/scripts/registre.mjs +7 -15
  40. package/scripts/resultats.mjs +283 -142
  41. package/scripts/resumes.mjs +1 -1
  42. package/scripts/session-start.sh +1 -2
  43. package/scripts/sources.mjs +28 -0
  44. package/scripts/sous-commandes.mjs +62 -0
  45. package/scripts/tableau.mjs +85 -191
  46. package/scripts/tests.mjs +19 -17
  47. package/scripts/verifier.mjs +5 -4
  48. package/scripts/verrou-de-machine.mjs +131 -24
  49. package/scripts/verrous/verrou.mjs +66 -5
  50. package/skills/developpeur/SKILL.md +60 -50
  51. package/skills/explorateur/SKILL.md +90 -64
  52. package/skills/grill/SKILL.md +1 -1
  53. package/skills/mesure/SKILL.md +2 -1
  54. package/skills/pitmaster/SKILL.md +132 -118
  55. package/skills/pitmaster/references/consigne-agent.md +54 -148
  56. package/skills/questionneur/SKILL.md +18 -18
  57. package/skills/relecteur/SKILL.md +69 -23
  58. package/skills/testwriter/SKILL.md +46 -35
  59. package/types/index.d.ts +1 -3
  60. package/agents/referent-technique.md +0 -9
  61. package/agents/regulateur.md +0 -11
  62. package/scripts/enveloppe/composant.mjs +0 -11
  63. package/scripts/etat-du-circuit.mjs +0 -104
  64. package/skills/referent-technique/SKILL.md +0 -145
  65. 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 its own component, and of every other component only what it
4
- // publishes. `npx grillhouse enveloppe <copie> <composant> <rôle> -- <commande…>`
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. Only these are written: the agent's copy (for a component that is a
7
- // package, that package alone, below); a `/tmp` empty and
8
- // proper to the session, where only the scratchpad directory of the copy comes back, the
9
- // session's working directory inside it (`dossierDeTravail`; Claude Code's scratchpads root,
10
- // `CLAUDE_CODE_TMPDIR` else the launch's temporary directory, so that under a pass it lies in
11
- // the base of the pass and goes with it); the npm
12
- // cache; the Claude session state (`~/.claude` and the copy's project directory), whose
13
- // configuration, skills, plugins and other projects stay read-only. `~/.claude.json` is a
14
- // throwaway copy: the session writes it, the real file does not change. The tickets base (the
15
- // main tree's `.beads/`) is written: the agent claims, plans, hands over and files what it
16
- // finds. Every `.git`, the
17
- // copy's included, stays read-only: the agent reads the git state and delivers a patch
18
- // (`git diff`, and `git diff --no-index /dev/null <file>` for a new file); it never commits.
19
- // The cache of Turborepo (`<main tree>/.turbo/cache`, shared by every copy) is written: the
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
- // component's path (`composant.mjs`): the frame's commands build and write that component only. A declared variable that names a path
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 written as above.
48
- // - An envelope on the main tree is refused, and so is a component that does not exist; when it is
49
- // the name of a package of the copy, the refusal gives its path.
50
- // - <copie> is the agent's git worktree; <composant> is the component's path inside it
51
- // (`packages/a`, or `src/parser` in a one-package project); <rôle> is the agent's role
52
- // (`testwriter`, `developpeur`…).
53
- // - In every worktree of the repository, the component's parent directory (`packages/`, `src/`)
54
- // is emptied. In <copie>, the component is mounted back in writing; every sibling component
55
- // gets back, read-only, its published parts: `package.json`, `docs/INTERFACE.md`, `dist/`, its
56
- // package configuration of Turborepo (`turbo.json`, `turbo.jsonc`), by which Turborepo reads in
57
- // the envelope the task graph it reads outside, the paths that the `files` field of its
58
- // `package.json` names, and the directory each trace it declares writes (`grillhouse.traces`,
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. Such a folder is written by the component that owns it, never
73
- // by a right the envelope adds: what the tests declare they read stays read-only. The package
74
- // of the shared test material, for the `testwriter`, the session's files and the mounts outside
75
- // the copy keep their rights; where a run of Turborepo writes at the root of the copy
76
- // (`sortiesDeLaRacine`, its `.turbo`) is an empty directory proper to the session, created on
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 comes back whole, after the agent's own component:
84
- // written for the `testwriter`, read-only for every other role, its own component included. For
85
- // such a role, where a run of its tests writes in it (`sortiesDeTest`: Turborepo's task logs,
86
- // Vite's cache) is an empty directory proper to the session, as Bazel writes a test's logs out
87
- // of its source: what is written there leaves no trace. The envelope creates each of them on
88
- // disk, empty, before the session, so that bwrap mounts over it inside the read-only package.
89
- // - The traversal guard refuses to launch when a symbolic link inside the component, or inside a
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 './composant.mjs';
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 index is for
153
- // the roles that see the whole repository.
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 <composant>. Nothing else of <env> enters.
226
- function environnementDeLaSeance(env, { cacheNpm, composant, travail }) {
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]: composant,
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. `montages` lists, in
401
- // order, the paths outside the copy that are emptied, written, read back or made a symbolic link
402
- // (`lien`, its text) (`montagesDeLaSeance`); an emptied one marked `scelle` is sealed read-only
403
- // once every mount inside it is made.
404
- // `matieres` lists the packages of the shared test material, relative to the copy
405
- // (`matieres` of `scripts/consommateurs.mjs`); `role` is the agent's role, and only the `testwriter` writes them.
406
- // `magasins` lists the task caches written by every run, the agent's included (Turborepo's
407
- // `<main tree>/.turbo/cache`); one that does not exist is not mounted.
408
- // `paquets` lists the packages of the copy, which `jugerComposant` reads to judge <composant>;
409
- // a <composant> among them writes itself alone of the copy.
410
- // `fs` gives `lister(dir)`, `existe(path)`, `liens(dir)`, `publies(dir, copie)` and `lus(dir)`
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
- { trees, copie, composant, role, matieres = [], montages = [], magasins = [], paquets = [] },
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
- const parentRel = path.dirname(composant);
424
- const nom = path.basename(composant);
425
- // A folder of the root (`scripts`) works on the whole repository: no neighbour to hide.
426
- const racine = parentRel === '.' || parentRel === '';
427
- const propre = path.join(copie, composant);
428
- const refusDuComposant = jugerComposant({ depot: copie, composant, paquets }, fs);
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. Its .git stays read-only.
466
- // For a component that is a package, the copy is read-only and its package alone comes back in
467
- // writing below, as a sandbox of Bazel opens only what the action declares; where a run of
468
- // Turborepo writes at the root is an empty directory proper to the session.
469
- const paquet = !racine && estUnPaquet(copie, composant, paquets);
470
- args.push(paquet ? '--ro-bind' : '--bind', copie, copie);
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
- // Every index directory of every worktree, the copy's included, becomes an empty directory in
479
- // its place; the worktree around it keeps its mount.
480
- const index = [...new Set([...trees, copie])]
481
- .flatMap(t => INDEX.map(i => path.join(t, i)))
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
- // What the component's tests declare they read, in a sibling the envelope hides.
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 <composant> in <copie>; its exit code.
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, composant, role] = argv.slice(0, sep < 0 ? undefined : sep);
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 || !composant || !role || commande.length === 0) {
537
- process.stderr.write('usage: enveloppe.mjs <copie> <composant> <rôle> -- <commande…>\n');
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 component that is a package, where a run of Turborepo writes at the root of the copy is
583
- // created here, so that bwrap mounts an empty one over it inside the read-only copy.
584
- const paquets = lesPaquets(copie);
585
- if (estUnPaquet(copie, composant, paquets)) {
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
- composant,
474
+ paquets,
602
475
  role,
603
476
  matieres,
604
477
  montages,
605
478
  magasins,
606
- paquets,
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, composant, travail }),
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);