@roomi-fields/grillhouse 0.0.0-stage → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/METACADRE.md +41 -0
  3. package/agents/developpeur.md +10 -0
  4. package/agents/explorateur.md +9 -0
  5. package/agents/questionneur.md +11 -0
  6. package/agents/referent-technique.md +9 -0
  7. package/agents/regulateur.md +11 -0
  8. package/agents/relecteur.md +9 -0
  9. package/agents/testwriter.md +10 -0
  10. package/bin/grillhouse.mjs +125 -0
  11. package/cadre/templates/projet/.claude/settings.json +30 -0
  12. package/cadre/templates/projet/.editorconfig +47 -0
  13. package/cadre/templates/projet/.env.example +57 -0
  14. package/cadre/templates/projet/.eslintrc.cjs +56 -0
  15. package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
  16. package/cadre/templates/projet/.mcp.json +13 -0
  17. package/cadre/templates/projet/.prettierrc +11 -0
  18. package/cadre/templates/projet/CHANGELOG.md +61 -0
  19. package/cadre/templates/projet/CLAUDE.md +46 -0
  20. package/cadre/templates/projet/CONTRIBUTING.md +243 -0
  21. package/cadre/templates/projet/CREDITS.md +41 -0
  22. package/cadre/templates/projet/README.md +152 -0
  23. package/cadre/templates/projet/_gitignore +91 -0
  24. package/cadre/templates/projet/_npmignore +46 -0
  25. package/cadre/templates/projet/deployment/README.md +98 -0
  26. package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
  27. package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
  28. package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
  29. package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
  30. package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
  31. package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
  32. package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
  33. package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
  34. package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
  35. package/cadre/templates/projet/ecosystem.config.cjs +73 -0
  36. package/cadre/templates/projet/package.json +90 -0
  37. package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
  38. package/cadre/templates/projet/src/config.ts +68 -0
  39. package/cadre/templates/projet/src/errors.ts +102 -0
  40. package/cadre/templates/projet/src/index.ts +46 -0
  41. package/cadre/templates/projet/src/services/.gitkeep +8 -0
  42. package/cadre/templates/projet/src/types/index.ts +24 -0
  43. package/cadre/templates/projet/src/utils/logger.ts +106 -0
  44. package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
  45. package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
  46. package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
  47. package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
  48. package/cadre/templates/projet/tsconfig.eslint.json +9 -0
  49. package/cadre/templates/projet/tsconfig.json +41 -0
  50. package/cadre/templates/projet/tsdoc.json +6 -0
  51. package/cadre/templates/projet/vitest.config.ts +44 -0
  52. package/cadre/templates/turbo/paquets.json +29 -0
  53. package/cadre/templates/turbo/sans-paquets.json +28 -0
  54. package/charte.md +107 -0
  55. package/docs/ARCHITECTURE.md +90 -0
  56. package/hooks/crochet.mjs +33 -0
  57. package/hooks/hooks.json +58 -0
  58. package/hooks/register.tsx +558 -0
  59. package/package.json +75 -4
  60. package/scripts/avancement.mjs +244 -0
  61. package/scripts/bilan.mjs +523 -0
  62. package/scripts/branche-de-nuit.mjs +59 -0
  63. package/scripts/cadre.mjs +109 -0
  64. package/scripts/circuit.mjs +4375 -0
  65. package/scripts/code-mort.mjs +371 -0
  66. package/scripts/composants-affectes.mjs +598 -0
  67. package/scripts/consommateurs-arbitres.mjs +192 -0
  68. package/scripts/consommateurs.mjs +310 -0
  69. package/scripts/crochet.mjs +24 -0
  70. package/scripts/crochets.mjs +135 -0
  71. package/scripts/dernier-vert.mjs +208 -0
  72. package/scripts/dette.mjs +181 -0
  73. package/scripts/droits.mjs +354 -0
  74. package/scripts/enveloppe/codes.mjs +17 -0
  75. package/scripts/enveloppe/composant.mjs +11 -0
  76. package/scripts/enveloppe/enveloppe.mjs +628 -0
  77. package/scripts/enveloppe/lancer.sh +311 -0
  78. package/scripts/etapes.mjs +323 -0
  79. package/scripts/etat-du-circuit.mjs +104 -0
  80. package/scripts/frontieres.mjs +456 -0
  81. package/scripts/gardes-du-cadre.mjs +72 -0
  82. package/scripts/greffons.mjs +338 -0
  83. package/scripts/grillhouse-maj.mjs +514 -0
  84. package/scripts/index-interfaces.mjs +293 -0
  85. package/scripts/init.mjs +117 -0
  86. package/scripts/integration/file-attente.mjs +61 -0
  87. package/scripts/integration/file.mjs +494 -0
  88. package/scripts/integration/integrer.mjs +381 -0
  89. package/scripts/integration/rejeu.mjs +139 -0
  90. package/scripts/integration/verdict.mjs +13 -0
  91. package/scripts/interfaces-contre-code.mjs +547 -0
  92. package/scripts/ligne-etat/defaut.sh +110 -0
  93. package/scripts/ligne-etat/ligne-etat.mjs +93 -0
  94. package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
  95. package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
  96. package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
  97. package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
  98. package/scripts/migrations.mjs +43 -0
  99. package/scripts/nuit.sh +269 -0
  100. package/scripts/paquets.mjs +79 -0
  101. package/scripts/poussee-cles.mjs +46 -0
  102. package/scripts/preparation.mjs +84 -0
  103. package/scripts/references-par-les-types.mjs +420 -0
  104. package/scripts/registre.mjs +336 -0
  105. package/scripts/resultats.mjs +580 -0
  106. package/scripts/resumes.mjs +57 -0
  107. package/scripts/retirer-copie.sh +127 -0
  108. package/scripts/session-start.sh +203 -0
  109. package/scripts/sortie.mjs +155 -0
  110. package/scripts/sous-modules.sh +316 -0
  111. package/scripts/structure-guard.sh +27 -0
  112. package/scripts/superviseur.mjs +102 -0
  113. package/scripts/tableau.mjs +1886 -0
  114. package/scripts/tests.mjs +236 -0
  115. package/scripts/ticket.sh +23 -0
  116. package/scripts/turbo-des-paquets.mjs +899 -0
  117. package/scripts/un-commit-par-ticket.mjs +212 -0
  118. package/scripts/verifier.mjs +469 -0
  119. package/scripts/verrou-de-fichier.mjs +130 -0
  120. package/scripts/verrou-de-machine.mjs +211 -0
  121. package/scripts/verrous/commande.mjs +180 -0
  122. package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
  123. package/scripts/verrous/verrou.mjs +165 -0
  124. package/scripts/vitest-config/avancement.mjs +39 -0
  125. package/scripts/vitest-config/avancement.reporter.mjs +56 -0
  126. package/scripts/vitest-config/base-de-passe.mjs +128 -0
  127. package/scripts/vitest-config/base.d.ts +6 -0
  128. package/scripts/vitest-config/base.mjs +87 -0
  129. package/scripts/vitest-config/changements.mjs +173 -0
  130. package/scripts/vitest-config/cibles.mjs +207 -0
  131. package/scripts/vitest-config/duree.reporter.mjs +98 -0
  132. package/scripts/vitest-config/machine.setup.mjs +14 -0
  133. package/scripts/vitest-config/mesure.d.ts +6 -0
  134. package/scripts/vitest-config/mesure.mjs +13 -0
  135. package/scripts/vitest-config/mesure.setup.mjs +12 -0
  136. package/scripts/vitest-config/pression.mjs +90 -0
  137. package/scripts/vitest-config/resultats.mjs +23 -0
  138. package/scripts/vitest-config/resultats.setup.mjs +19 -0
  139. package/scripts/vitest.nuit.config.mjs +26 -0
  140. package/skills/developpeur/SKILL.md +175 -0
  141. package/skills/explorateur/SKILL.md +84 -0
  142. package/skills/grill/SKILL.md +238 -0
  143. package/skills/grill/references/releve-structurel.md +38 -0
  144. package/skills/mesure/SKILL.md +168 -0
  145. package/skills/pitmaster/SKILL.md +382 -0
  146. package/skills/pitmaster/references/cadre.md +119 -0
  147. package/skills/pitmaster/references/consigne-agent.md +308 -0
  148. package/skills/pitmaster/references/erreurs.md +25 -0
  149. package/skills/pitmaster/references/tour.md +124 -0
  150. package/skills/questionneur/SKILL.md +64 -0
  151. package/skills/redacteur/SKILL.md +84 -0
  152. package/skills/redacteur/references/documents-d-un-composant.md +105 -0
  153. package/skills/referent-technique/SKILL.md +145 -0
  154. package/skills/regulateur/SKILL.md +220 -0
  155. package/skills/release/SKILL.md +134 -0
  156. package/skills/release/scripts/update-version.cjs +92 -0
  157. package/skills/relecteur/SKILL.md +152 -0
  158. package/skills/relecteur/references/LICENSE-MIT +30 -0
  159. package/skills/relecteur/references/SOURCE.md +8 -0
  160. package/skills/relecteur/references/claims-check.md +14 -0
  161. package/skills/relecteur/references/deletion-check.md +14 -0
  162. package/skills/relecteur/references/edge-case-hunter.md +80 -0
  163. package/skills/relecteur/references/verification-gap.md +113 -0
  164. package/skills/testwriter/SKILL.md +187 -0
  165. package/types/index.d.ts +56 -0
  166. package/README.md +0 -3
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env bash
2
+ # The night: once a day, the full pass played for real, then what the day never plays: the night
3
+ # tests and the time budget benches. During the day `npm run verifier` (the integration's) replays
4
+ # from Turborepo's cache every task whose inputs are unchanged, and plays the tests Vitest relates
5
+ # to the files changed since the last green (scripts/integration/integrer.mjs,
6
+ # scripts/dernier-vert.mjs).
7
+ #
8
+ # npx grillhouse nuit from the root of the project, by the crontab of the machine, e.g.
9
+ # « 0 3 * * * cd <root> && node_modules/.bin/grillhouse nuit »
10
+ #
11
+ # - It plays main's HEAD in its own clean copy (.claude/worktrees/nuit): detached, every untracked
12
+ # file removed, its submodules set at the commit of HEAD (scripts/sous-modules.sh, a local
13
+ # modification overwritten; one that cannot be set turns the night red; a submodule the project
14
+ # shares, `grillhouse.partage`, is linked to the main repository's instead, not set),
15
+ # dependencies reinstalled (npm ci) when package-lock.json changes, then
16
+ # `npm run verifier -- --force`: Turborepo's build, guards (the frame's and the project's own,
17
+ # its root script `gardes`) and every test of the day, each task
18
+ # played for real (`--force`, no log replayed from the cache, no measure against the last
19
+ # green: the complete pass, scripts/verifier.mjs), its result written to the cache again; green,
20
+ # its tree becomes the last green of the day's passes.
21
+ # A full pass « à repasser » (75, scripts/verifier.mjs: a failure of duration under load, which
22
+ # found no rest in its bound) makes the night « à repasser », below.
23
+ # An existing copy that does not get ready (its move onto HEAD, its cleaning or the setting of
24
+ # its submodules fails) is removed by `scripts/retirer-copie.sh --jetable` (local work does not
25
+ # hold it, a lock does) and created again, once; a copy that does not get ready the second time
26
+ # turns the night red, and so does a removal the script refuses, the log repeating its reason.
27
+ # - Every command of the project (installation, build, tests, night tests, night script) plays
28
+ # through `scripts/sous-modules.sh monter`, each shared submodule of the main repository
29
+ # read-only, its working tree and its git repository (the list the preparation's pose printed,
30
+ # whatever a command rewrites afterwards): a write into it fails. With a shared submodule and no possible mount (bwrap absent
31
+ # or refused), the night turns red, the log saying so, before any command runs.
32
+ # - The night tests: after a green full pass, the files `*.nuit.test.*`, `*.nuit.spec.*`,
33
+ # `*.budget.test.*` and `*.budget.spec.*` of the copy (outside node_modules, dist and .git): the
34
+ # night tests and the time budget benches, which `npm test` and the integration leave out
35
+ # (the package @roomi-fields/grillhouse/vitest-config, scripts/vitest-config/, which every day configuration
36
+ # extends), run by Vitest with the frame's configuration (scripts/vitest.nuit.config.mjs, beside
37
+ # this script), bench or night test alone as well. A red turns the night red, with a log line that says so, and the
38
+ # project's night script does not run. A copy with neither plays none. A run « à repasser » (75:
39
+ # every failure depended on a duration, one under load, scripts/vitest-config/duree.reporter.mjs)
40
+ # waits for rest (`npx grillhouse pression attendre`, bound 15 min) and plays
41
+ # once more; green then, the night goes on; à repasser again, or no rest in the bound, the night
42
+ # is « à repasser ».
43
+ # - « À repasser » (exit 75): neither green nor red. The last green stays, no ticket opens, nothing
44
+ # is pushed, the log says so; the next night plays again.
45
+ # - The project's night script: `grillhouse.nuit` in the package.json of the commit it plays names
46
+ # an npm script (`npm pkg set grillhouse.nuit=<script>`). After the night tests, the night plays
47
+ # `npm run <script>` in its copy; a red script turns the night red, with a log line that names it.
48
+ # The key is read as written, before any work: without it (or with null) the night plays
49
+ # no script of the project; a value that is not a non-blank string, or a name absent from the scripts of
50
+ # that package.json, turns the night red, with a log line that cites it, and nothing runs.
51
+ # - The night push: `grillhouse.poussee.nuit` in the package.json of the commit it plays names a
52
+ # remote branch (`npm pkg set grillhouse.poussee.nuit=<branch>`). After a green night, once the
53
+ # last green is written, the night runs `git push origin <commit>:<branch>` with GRILLHOUSE_NUIT=1
54
+ # set for that push alone, the mark by which the pre-push guard (scripts/branche-de-nuit.mjs)
55
+ # lets it through; any other push to that branch is refused there. Without the key
56
+ # (or with null, as for grillhouse.poussee itself) or after a red night it pushes nothing. The
57
+ # keys are read as written, before any work: a grillhouse.poussee that is neither null nor an
58
+ # object, or a grillhouse.poussee.nuit that is not a non-blank string, turns the night red with
59
+ # a log line that cites it, one line per faulty key. A refused push keeps the last green, opens
60
+ # (or completes) the night ticket « La poussée de la nuit est refusée sur <commit> », and exits
61
+ # 1; a remote already at the commit accepts the push.
62
+ # - It takes no lock of the integration or of the queue (scripts/integration/file.mjs): an
63
+ # integration and the queue run during the night, each in its own copy.
64
+ # - Its log is <git common dir>/nuit/<date>.log. Green: HEAD becomes the last green commit
65
+ # (<git common dir>/nuit/dernier-vert). Red: a ticket labelled `nuit`, with the commits since the
66
+ # last green, where `git bisect` looks for the culprit; an open one gets a comment instead.
67
+ # With a last green, each red task of the full pass (the file GRILLHOUSE_ROUGES names, which
68
+ # scripts/verifier.mjs writes: `<package>#<task>` per line, no task à repasser among them) gets
69
+ # its line, the command that looks for the culprit with that task alone:
70
+ # `git bisect start <head> <last green> && git bisect run npx turbo run <task> --filter=<package>`
71
+ # (`--filter=//` for the root), Turborepo's hash replaying at each step only what changed.
72
+ set -u
73
+ # The project is the working directory; the frame's scripts are this one's neighbours.
74
+ racine="$PWD"
75
+ ici="$(cd "$(dirname "$0")" && pwd)"
76
+ commun="$(git -C "$racine" rev-parse --path-format=absolute --git-common-dir)"
77
+ dossier="$commun/nuit"
78
+ mkdir -p "$dossier"
79
+ journal="$dossier/$(date +%Y-%m-%d).log"
80
+ # The red tasks of the full pass, written by scripts/verifier.mjs.
81
+ rouges="$dossier/rouges"
82
+ rm -f "$rouges"
83
+
84
+ tete="$(git -C "$racine" rev-parse HEAD)"
85
+ copie="$racine/.claude/worktrees/nuit"
86
+ branche=
87
+ # Gets the copy ready on HEAD: detached, cleaned, its submodules set; `partages` holds the list the
88
+ # pose fixes, which every mount gets. Prints the reason on failure.
89
+ partages=
90
+ preparer() {
91
+ if [ -d "$copie" ]; then
92
+ git -C "$copie" checkout -q --detach --force "$tete" || return 1
93
+ git -C "$copie" clean -q -f -d -x -e node_modules || return 1
94
+ else
95
+ git -C "$racine" worktree add -q --detach "$copie" "$tete" || return 1
96
+ fi
97
+ partages=$(bash "$ici/sous-modules.sh" pose "$copie")
98
+ }
99
+ # Plays the copy; its status is the night's verdict. An existing copy that does not get ready is
100
+ # removed (scripts/retirer-copie.sh --jetable) and created again, once.
101
+ jouer() {
102
+ echo "— la nuit du $(date '+%Y-%m-%d %H:%M') sur ${tete:0:7}"
103
+ local existait=0 raison
104
+ [ -d "$copie" ] && existait=1
105
+ if ! preparer; then
106
+ [ "$existait" -eq 1 ] || return 1
107
+ if ! raison=$(cd "$racine" && bash "$ici/retirer-copie.sh" --jetable "$copie" 2>&1); then
108
+ echo "✗ la copie de la nuit ne se prépare pas et ne se retire pas : $raison"
109
+ return 1
110
+ fi
111
+ preparer || return 1
112
+ fi
113
+ # The project's commands play with the shared submodules read-only; no possible mount stops here.
114
+ monter() { bash "$ici/sous-modules.sh" monter "$copie" --partages "$partages" "$@"; }
115
+ if ! raison=$(monter true 2>&1); then
116
+ echo "✗ le montage en lecture seule des sous-modules partagés est impossible : rien n'est joué."
117
+ echo "$raison"
118
+ return 1
119
+ fi
120
+ # The reading gives one line per finding: « script<TAB>name », « pousse<TAB>branch », or a fault
121
+ # « nuit-invalide<TAB>value as JSON », « nuit-absent<TAB>name », « poussee-invalide<TAB>value »,
122
+ # « poussee-nuit-invalide<TAB>value ». Every faulty key is reported before any work.
123
+ local lecture ligne nuit=
124
+ lecture="$(node --input-type=module -e '
125
+ import { readFileSync } from "node:fs";
126
+ import { pathToFileURL } from "node:url";
127
+ const pkg = JSON.parse(readFileSync(process.argv[1], "utf8"));
128
+ const { lirePoussee } = await import(pathToFileURL(process.argv[2]).href);
129
+ const out = (...l) => process.stdout.write(l.join("\t") + "\n");
130
+ const vide = v => typeof v !== "string" || !v.trim();
131
+ const v = pkg.grillhouse?.nuit;
132
+ if (v != null) {
133
+ if (vide(v)) out("nuit-invalide", JSON.stringify(v));
134
+ else if (!Object.hasOwn(pkg.scripts ?? {}, v)) out("nuit-absent", v);
135
+ else out("script", v);
136
+ }
137
+ const { nuit, fautes } = lirePoussee(pkg);
138
+ for (const f of fautes) out(f.cle === "grillhouse.poussee" ? "poussee-invalide" : "poussee-nuit-invalide", f.valeur);
139
+ if (nuit) out("pousse", nuit);
140
+ ' "$copie/package.json" "$ici/poussee-cles.mjs" 2>/dev/null)"
141
+ local faute=0
142
+ while IFS= read -r ligne; do
143
+ case "$ligne" in
144
+ script$'\t'*) nuit="${ligne#script$'\t'}" ;;
145
+ pousse$'\t'*) branche="${ligne#pousse$'\t'}" ;;
146
+ nuit-invalide$'\t'*)
147
+ echo "✗ la clé grillhouse.nuit vaut ${ligne#nuit-invalide$'\t'}, pas un nom de script npm : rien n'est joué."
148
+ faute=1 ;;
149
+ nuit-absent$'\t'*)
150
+ echo "✗ la clé grillhouse.nuit nomme « ${ligne#nuit-absent$'\t'} », absent des scripts du package.json : rien n'est joué."
151
+ faute=1 ;;
152
+ poussee-invalide$'\t'*)
153
+ echo "✗ la clé grillhouse.poussee vaut ${ligne#poussee-invalide$'\t'}, pas un objet : rien n'est joué."
154
+ faute=1 ;;
155
+ poussee-nuit-invalide$'\t'*)
156
+ echo "✗ la clé grillhouse.poussee.nuit vaut ${ligne#poussee-nuit-invalide$'\t'}, pas un nom de branche : rien n'est joué."
157
+ faute=1 ;;
158
+ esac
159
+ done <<< "$lecture"
160
+ [ "$faute" -eq 0 ] || return 1
161
+ if [ -f "$copie/package-lock.json" ] && ! cmp -s "$copie/package-lock.json" "$copie/node_modules/.nuit-lock"; then
162
+ monter npm --prefix "$copie" ci --no-audit --no-fund --silent || return 1
163
+ cp "$copie/package-lock.json" "$copie/node_modules/.nuit-lock"
164
+ fi
165
+ # The full pass, played for real: the night replays no log of the cache.
166
+ local code
167
+ GRILLHOUSE_ROUGES="$rouges" monter npm --prefix "$copie" run verifier -- --force
168
+ code=$?
169
+ if [ "$code" -eq 75 ]; then
170
+ echo "⏱ la passe complète est à repasser (code 75) : un échec de durée sous charge, sans repos dans la borne."
171
+ return 75
172
+ fi
173
+ [ "$code" -eq 0 ] || return 1
174
+ if [ -n "$(find "$copie" \( -name node_modules -o -name dist -o -name .git \) -prune -o \
175
+ -type f -regextype posix-extended -regex '.*\.(nuit|budget)\.(test|spec)\.(c|m)?[jt]sx?' -print -quit)" ]; then
176
+ bancs() { (cd "$copie" && monter npx --no-install vitest run --root "$copie" \
177
+ --config "$ici/vitest.nuit.config.mjs"); }
178
+ bancs
179
+ code=$?
180
+ if [ "$code" -eq 75 ]; then
181
+ echo "⏱ les bancs de temps ou les tests de nuit sont à repasser (code 75) : attente du repos, puis une relance."
182
+ if ! node "$ici/vitest-config/pression.mjs" attendre; then
183
+ echo "⏱ à repasser : pas de repos dans la borne."
184
+ return 75
185
+ fi
186
+ bancs
187
+ code=$?
188
+ if [ "$code" -eq 75 ]; then
189
+ echo "⏱ à repasser : les bancs échouent encore sous charge après la relance."
190
+ return 75
191
+ fi
192
+ fi
193
+ if [ "$code" -ne 0 ]; then
194
+ echo "✗ les bancs de temps ou les tests de nuit (*.budget.*, *.nuit.*) ont échoué."
195
+ return 1
196
+ fi
197
+ fi
198
+ [ -n "$nuit" ] || return 0
199
+ if ! monter npm --prefix "$copie" run "$nuit"; then
200
+ echo "✗ le script de nuit du projet \`npm run $nuit\` (grillhouse.nuit) a échoué."
201
+ return 1
202
+ fi
203
+ }
204
+ jouer >> "$journal" 2>&1
205
+ verdict=$?
206
+
207
+ if [ "$verdict" -eq 75 ]; then
208
+ echo "⏱ à repasser : ${tete:0:7} n'est ni vert ni rouge, la machine est restée chargée ; le dernier vert reste, aucun ticket, la nuit suivante rejoue." >> "$journal"
209
+ exit 75
210
+ fi
211
+
212
+ # Opens the night ticket (subject « number — subject — parent », the number being the id without the
213
+ # project's prefix as `bd config get issue_prefix` gives it), or comments the open one; prints its id.
214
+ signaler() {
215
+ local sujet="$1" texte="$2" ouvert neuf= prefixe
216
+ ouvert="$(cd "$racine" && bd list --status open --label nuit --json 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const l=JSON.parse(s);process.stdout.write(l[0]?.id??"")}catch{}})')"
217
+ if [ -n "$ouvert" ]; then
218
+ (cd "$racine" && bd comments add "$ouvert" "$texte" >/dev/null)
219
+ else
220
+ neuf="$(cd "$racine" && bd create "$sujet" -t bug -p 1 -l nuit -d "$texte" --silent)"
221
+ prefixe="$(cd "$racine" && bd config get issue_prefix 2>/dev/null)"
222
+ [ -n "$neuf" ] && (cd "$racine" && bd update "$neuf" --title "${neuf#"$prefixe"-} — $sujet — parent" >/dev/null)
223
+ fi
224
+ echo "${ouvert:-$neuf}"
225
+ }
226
+
227
+ if [ "$verdict" -eq 0 ]; then
228
+ echo "$tete" > "$dossier/dernier-vert"
229
+ echo "✓ vert : ${tete:0:7} est le dernier vert." >> "$journal"
230
+ if [ -n "$branche" ]; then
231
+ if GRILLHOUSE_NUIT=1 GIT_TERMINAL_PROMPT=0 git -C "$racine" push origin "$tete:$branche" >> "$journal" 2>&1; then
232
+ echo "✓ poussée : ${tete:0:7} est sur origin/$branche." >> "$journal"
233
+ else
234
+ echo "✗ la poussée de ${tete:0:7} vers origin/$branche (grillhouse.poussee.nuit) est refusée." >> "$journal"
235
+ texte="La poussée de la nuit du $(date +%Y-%m-%d) est refusée : ${tete:0:7} vers origin/$branche (grillhouse.poussee.nuit).
236
+ Le dernier vert reste ${tete:0:7}.
237
+ Journal : $journal
238
+ Fin du journal :
239
+ $(tail -40 "$journal")"
240
+ numero="$(signaler "La poussée de la nuit est refusée sur ${tete:0:7}" "$texte")"
241
+ echo "✗ vert, poussée refusée : ticket ${numero:-neuf} (étiquette nuit)." >> "$journal"
242
+ exit 1
243
+ fi
244
+ fi
245
+ exit 0
246
+ fi
247
+
248
+ vert="$(cat "$dossier/dernier-vert" 2>/dev/null || true)"
249
+ intervalle="${vert:+${vert:0:7}..}${tete:0:7}"
250
+ bissections=
251
+ if [ -n "$vert" ] && [ -s "$rouges" ]; then
252
+ bissections="Pour chaque tâche rouge, la commande qui cherche le commit fautif :"
253
+ while IFS= read -r tache; do
254
+ [ -n "$tache" ] || continue
255
+ bissections="$bissections
256
+ - $tache : git bisect start ${tete:0:7} ${vert:0:7} && git bisect run npx turbo run ${tache#*#} --filter=${tache%%#*}"
257
+ done < "$rouges"
258
+ bissections="$bissections
259
+ "
260
+ fi
261
+ texte="La nuit du $(date +%Y-%m-%d) est rouge sur ${tete:0:7}.
262
+ Commits à chercher (git bisect) : $intervalle.
263
+ ${bissections}Journal : $journal
264
+ Fin du journal :
265
+ $(tail -40 "$journal")"
266
+ # A night ticket is the mother of its fixes.
267
+ numero="$(signaler "La nuit est rouge depuis la nuit du $(date +%Y-%m-%d)" "$texte")"
268
+ echo "✗ rouge : ticket ${numero:-neuf} (étiquette nuit)." >> "$journal"
269
+ exit 1
@@ -0,0 +1,79 @@
1
+ // The packages of a repository: the frame's one list of them, each marked out of the frame or not.
2
+ // The update of the frame (scripts/grillhouse-maj.mjs), the generator of the packages' turbo.json
3
+ // (scripts/turbo-des-paquets.mjs) and the targeted test runs (scripts/tests.mjs) read it.
4
+ //
5
+ // The packages are the directories holding a `package.json` that the `workspaces` of the root
6
+ // `package.json` select (an array, or its `packages` key), mapped as npm maps them
7
+ // (@npmcli/map-workspaces), read with Node's glob: a pattern « ! » takes out what the other
8
+ // patterns select, unless a later pattern it matches puts it back; `node_modules` is never a
9
+ // package. A repository without `workspaces` has none. A package is out of the frame when
10
+ // `docs/agents/hors-cadre.txt` (read by `horsCadre`, below) lists it: a directory under
11
+ // `packages/` or `src/`.
12
+ //
13
+ // This module and the ones it imports use Node alone: the update runs them from a source checkout
14
+ // of the frame that installs no dependency.
15
+ import { existsSync, globSync, readFileSync } from 'node:fs';
16
+ import path from 'node:path';
17
+
18
+ // The list of the packages out of the frame: one line per package, « <package> <reason> », its
19
+ // first word the name of a directory under `packages/` or `src/`; a blank line or a line starting
20
+ // with `#` names nothing.
21
+ const HORS_CADRE = 'docs/agents/hors-cadre.txt';
22
+
23
+ // The names listed in <racine>'s `docs/agents/hors-cadre.txt`: a Set, empty without the file.
24
+ export function horsCadre(racine) {
25
+ const liste = path.join(racine, HORS_CADRE);
26
+ if (!existsSync(liste)) return new Set();
27
+ const noms = new Set();
28
+ for (const ligne of readFileSync(liste, 'utf8').split('\n')) {
29
+ const mot = ligne.trim().split(/\s+/)[0];
30
+ if (mot && !mot.startsWith('#')) noms.add(mot);
31
+ }
32
+ return noms;
33
+ }
34
+
35
+ const lireJson = f => {
36
+ try {
37
+ return JSON.parse(readFileSync(f, 'utf8'));
38
+ } catch {
39
+ return null;
40
+ }
41
+ };
42
+
43
+ // The packages of the repository at <racine>: [{ dossier, pkg, hors }], sorted by directory;
44
+ // `hors` whether `docs/agents/hors-cadre.txt` lists it (a directory under `packages/` or `src/`).
45
+ export function paquets(racine) {
46
+ const p = lireJson(path.join(racine, 'package.json'));
47
+ const declares = Array.isArray(p?.workspaces) ? p.workspaces : (p?.workspaces?.packages ?? []);
48
+ const motifs = [];
49
+ let nies = [];
50
+ for (const brut of declares.map(String)) {
51
+ const excl = /^!+/.exec(brut)?.[0] ?? '';
52
+ const motif = brut
53
+ .slice(excl.length)
54
+ .replace(/^\.?\/+/, '')
55
+ .replace(/\/$/, '');
56
+ if (excl.length % 2 === 1) {
57
+ nies.push(motif);
58
+ } else {
59
+ // A pattern puts back what an earlier negation it matches took out.
60
+ nies = nies.filter(n => !path.matchesGlob(motif, n));
61
+ motifs.push(motif);
62
+ }
63
+ }
64
+ const dossiers = new Set();
65
+ for (const motif of motifs) {
66
+ for (const f of globSync(`${motif}/package.json`, { cwd: racine })) {
67
+ const dossier = path.dirname(f);
68
+ if (f.split(path.sep).includes('node_modules')) continue;
69
+ if (nies.some(n => path.matchesGlob(dossier, n))) continue;
70
+ dossiers.add(dossier);
71
+ }
72
+ }
73
+ const listes = dossiers.size ? horsCadre(racine) : new Set();
74
+ return [...dossiers].sort().map(dossier => ({
75
+ dossier,
76
+ pkg: lireJson(path.join(racine, dossier, 'package.json')) ?? {},
77
+ hors: ['packages', 'src'].includes(path.dirname(dossier)) && listes.has(path.basename(dossier)),
78
+ }));
79
+ }
@@ -0,0 +1,46 @@
1
+ // The reading of the `grillhouse.poussee` keys of a package.json, shared by the night
2
+ // (scripts/nuit.sh), the pre-push guard (scripts/branche-de-nuit.mjs) and the queue
3
+ // (scripts/integration/file.mjs): the keys are read as written. `grillhouse.poussee` is absent,
4
+ // null or an object; `grillhouse.poussee.nuit` and `grillhouse.poussee.commit` are absent, null or
5
+ // a non-blank string.
6
+
7
+ // Returns { nuit, fautes }: `nuit` the night branch, or null when the key is absent or faulty;
8
+ // `fautes` one entry { cle, valeur } per faulty key, `valeur` as JSON.
9
+ export function lirePoussee(pkg) {
10
+ const fautes = [];
11
+ let nuit = null;
12
+ const p = pkg?.grillhouse?.poussee;
13
+ if (p !== undefined && p !== null) {
14
+ if (typeof p !== 'object' || Array.isArray(p)) {
15
+ fautes.push({ cle: 'grillhouse.poussee', valeur: JSON.stringify(p) });
16
+ } else if (p.nuit !== undefined && p.nuit !== null) {
17
+ if (typeof p.nuit !== 'string' || !p.nuit.trim()) {
18
+ fautes.push({ cle: 'grillhouse.poussee.nuit', valeur: JSON.stringify(p.nuit) });
19
+ } else {
20
+ nuit = p.nuit;
21
+ }
22
+ }
23
+ }
24
+ return { nuit, fautes };
25
+ }
26
+
27
+ // The branch the queue pushes main to: `grillhouse.poussee.commit`, `main` without the key (or with
28
+ // null). Returns { commit, faute }: `faute` is the faulty value as JSON, and `commit` null, when the
29
+ // key is not a non-blank string.
30
+ export function lireCommit(pkg) {
31
+ const p = pkg?.grillhouse?.poussee;
32
+ if (p === undefined || p === null) {
33
+ return { commit: 'main', faute: null };
34
+ }
35
+ if (typeof p !== 'object' || Array.isArray(p)) {
36
+ return { commit: null, faute: JSON.stringify(p) };
37
+ }
38
+ const c = p.commit;
39
+ if (c === undefined || c === null) {
40
+ return { commit: 'main', faute: null };
41
+ }
42
+ if (typeof c !== 'string' || !c.trim()) {
43
+ return { commit: null, faute: JSON.stringify(c) };
44
+ }
45
+ return { commit: c, faute: null };
46
+ }
@@ -0,0 +1,84 @@
1
+ // The preparation of a project for its first session, which `grillhouse init` runs on the project
2
+ // it creates (scripts/init.mjs). Idempotent: a second preparation changes nothing.
3
+ //
4
+ // - git: the directory becomes a repository when it is none.
5
+ // - Beads, the ticket store the skills work from: the tool (`bd`, installed by
6
+ // `npm install -g @beads/bd` when absent), then the store, under the prefix given, else the
7
+ // store's own, else the directory's name; the repository's `beads.role` is `maintainer`.
8
+ // - CodeGraph, the code knowledge graph the skills query (the MCP server of `.mcp.json`): the tool
9
+ // (installed by `npm install -g @colbymchenry/codegraph` when absent) and the index of the code.
10
+ // - The folder git reads the hooks from: `core.hooksPath` set to Beads' (`.beads/hooks`) when the
11
+ // repository names none. The frame's git hooks come by the project's `prepare`
12
+ // (`grillhouse hooks`), which the `npm install` below runs.
13
+ // - `npm install`, when the project has a package.json: it installs the frame's package.
14
+ // - Then the frame's plugins its settings declare, installed for the project (scripts/greffons.mjs):
15
+ // a plugin that cannot be installed has its line naming the command to type.
16
+ // A global tool whose installation fails stops the preparation, its message naming the command.
17
+ import { execFileSync, spawnSync } from 'node:child_process';
18
+ import { existsSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ import { installerGreffons } from './greffons.mjs';
21
+
22
+ // The prefix of the tickets: the one given, else the store's own, else the directory's name.
23
+ function prefixeDe(projet, donne) {
24
+ if (donne) return donne;
25
+ if (existsSync(path.join(projet, '.beads'))) {
26
+ const r = spawnSync('bd', ['config', 'get', 'issue_prefix'], {
27
+ cwd: projet,
28
+ encoding: 'utf8',
29
+ maxBuffer: 256 * 1024 * 1024,
30
+ });
31
+ const existant = (r.stdout ?? '').trim();
32
+ if (r.status === 0 && existant !== '' && !existant.includes('(')) return existant;
33
+ }
34
+ return path
35
+ .basename(projet)
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9]/g, '-');
38
+ }
39
+
40
+ // Runs <commande> in <projet>, its output this process's; throws on a failure.
41
+ const lancer = (projet, commande, args) =>
42
+ execFileSync(commande, args, { cwd: projet, stdio: 'inherit' });
43
+ const present = commande => spawnSync('sh', ['-c', `command -v ${commande}`]).status === 0;
44
+
45
+ // A global tool of the preparation, installed by npm when absent.
46
+ function outil(commande, paquet) {
47
+ if (present(commande)) return;
48
+ try {
49
+ execFileSync('npm', ['install', '-g', paquet], { stdio: 'inherit' });
50
+ } catch {
51
+ throw new Error(
52
+ `L'installation de ${paquet} échoue : lance npm install -g ${paquet}, puis recommence.`
53
+ );
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Prepares the project at <projet>, its tickets under <prefixe> (see the header). Returns the lines
59
+ * of failure of the plugins (empty when everything stands).
60
+ */
61
+ export function preparer(projet, prefixe) {
62
+ if (spawnSync('git', ['-C', projet, 'rev-parse', '--git-dir']).status !== 0) {
63
+ lancer(projet, 'git', ['init', '-q']);
64
+ }
65
+ outil('bd', '@beads/bd');
66
+ outil('codegraph', '@colbymchenry/codegraph');
67
+ if (!existsSync(path.join(projet, '.codegraph'))) lancer(projet, 'codegraph', ['init', projet]);
68
+ lancer(projet, 'bd', [
69
+ 'init',
70
+ '--init-if-missing',
71
+ '--non-interactive',
72
+ '--skip-agents',
73
+ '--quiet',
74
+ '--prefix',
75
+ prefixeDe(projet, prefixe),
76
+ ]);
77
+ lancer(projet, 'git', ['config', 'beads.role', 'maintainer']);
78
+ const r = spawnSync('git', ['-C', projet, 'config', 'core.hooksPath'], { encoding: 'utf8' });
79
+ if ((r.stdout ?? '').trim() === '') {
80
+ lancer(projet, 'git', ['config', 'core.hooksPath', '.beads/hooks']);
81
+ }
82
+ if (existsSync(path.join(projet, 'package.json'))) lancer(projet, 'npm', ['install']);
83
+ return installerGreffons(projet);
84
+ }