@roomi-fields/grillhouse 0.0.0-stage → 0.23.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 (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,316 @@
1
+ #!/usr/bin/env bash
2
+ # The preparation of a working copy's submodules, called by the three places that create a copy:
3
+ # the integration copy (scripts/integration/integrer.mjs), the agent copy
4
+ # (scripts/enveloppe/lancer.sh) and the night copy (scripts/nuit.sh).
5
+ #
6
+ # npx grillhouse sous-modules pose <copie> every submodule, set or shared
7
+ # npx grillhouse sous-modules garde <copie> refuses a copy whose submodule carries local work
8
+ # npx grillhouse sous-modules monter <copie> --partages <liste> <commande…>
9
+ # plays a command with the shared submodules read-only
10
+ #
11
+ # - `grillhouse.partage` in the package.json of the copy lists the paths of the submodules the
12
+ # project shares (`npm pkg set 'grillhouse.partage[0]=vendor/natif'`). A shared submodule is not
13
+ # set in a copy: the copy reads the one of the main repository at its usual path, through a
14
+ # symbolic link marked « ignored in the copy » (`git update-index --skip-worktree`), so that
15
+ # `git status` of the copy does not report it. The copy therefore reads the shared submodule at
16
+ # the commit checked out in the main repository, not at the commit its own HEAD references, and
17
+ # with the files the main repository holds there, untracked ones included. No repository holds
18
+ # one more copy of it. Without the key, or with an empty list, nothing is shared.
19
+ # - `pose` reads the declaration first and refuses before setting anything (exit 1, its first line
20
+ # on stderr citing `grillhouse.partage` and the faulty value, or naming the shared submodule):
21
+ # a value that is not a list, an element that is not a non-empty relative path inside the
22
+ # repository, a path that is not a submodule of .gitmodules, a shared submodule absent or not
23
+ # initialized in the main repository (never set in the copy instead). Then it puts the link of
24
+ # each shared submodule back (git replaces it by an empty folder when moving a copy forward
25
+ # changes the submodule's commit), sets each other submodule, one by one, at the commit of HEAD,
26
+ # nested ones included (`submodule update --init --recursive --force`), and removes their
27
+ # untracked files: a local modification is overwritten. A submodule that was shared and no longer
28
+ # is gets its own again. A submodule that cannot be set stops it, exit 1, its first line on
29
+ # stderr naming the submodule and its commit, git's output after. A submodule whose address is
30
+ # local (a path or a file:// URL) is set too: the command alone carries
31
+ # `-c protocol.file.allow=always`, the authorization of the file transport; no repository or
32
+ # machine configuration holds it.
33
+ # - The address a submodule is set from is the one the main repository uses: its
34
+ # `submodule.<name>.url` (git's own rule: the local configuration wins), else the url of
35
+ # .gitmodules. A copy shares the configuration of the main repository, so the pose reads it
36
+ # there. The command carries that address (`-c submodule.<name>.url=…`) and, in a copy whose
37
+ # submodule was already set, the `origin` remote of that submodule is moved to it first (git
38
+ # fetches through `origin`, fixed at the first clone). The pose never writes the main
39
+ # repository's configuration. A url relative to the superproject (`./`, `../`), in the
40
+ # configuration or in .gitmodules, is resolved as `git submodule init` resolves it for the main
41
+ # repository (relative_url and chop_last_dir of git), against the url of the remote the current
42
+ # branch of the main repository follows (`branch.<b>.remote`), else its `origin`, else its working
43
+ # tree folder. A remote url that is a local path not absolute is relative to the main repository.
44
+ # Each `../` removes the last component of that url, at its last slash, else at its colon:
45
+ # `host:repo.git` with `../natif.git` gives `host:natif.git`, `git@github.com:org/repo.git` with
46
+ # `../../x.git` gives `git@github.com:x.git`, `upstream.git` with `../natif.git` gives `natif.git`.
47
+ # One final « / » of the url goes. A local path not absolute that results is taken from the main
48
+ # repository's folder, which need not exist (`../up/repo.git` with `../../natif.git` gives
49
+ # `<main repository>/../up/natif.git`). A url git refuses for the main repository stops the pose
50
+ # before the submodule is set — a `../` that finds neither slash nor colon left in a relative
51
+ # remote url (`upstream.git` with `../../natif.git`), or in the `.` an absolute one becomes once
52
+ # emptied of its components: exit 1, its first line on stderr naming the submodule, and the
53
+ # main repository's configuration stays as it is. The resolved address is the one the command
54
+ # carries, so a copy under .claude/worktrees/ never resolves it from its own folder.
55
+ # - `pose` never enters a shared submodule, nor a submodule nested in it: they belong to the main
56
+ # repository, which neither sets nor cleans them.
57
+ # - `garde` reads the copy as it stands: a submodule modified in `git status` (an edited or
58
+ # untracked file, another commit checked out) stops it, exit 1, its first line on stderr naming
59
+ # the submodule. The agent copy calls it before moving forward, as its work stays in place.
60
+ # - `pose` prints on stdout the list it fixes: the target of each link it set (the path of the
61
+ # shared submodule in the main repository), one per line, nothing when nothing is shared.
62
+ # - `monter` plays the command, from the current folder, with each shared submodule of the main
63
+ # repository mounted read-only (bwrap), its working tree and its git repository (nested
64
+ # submodules included): a write into it, a commit, reset, checkout or fetch fails. Its caller
65
+ # hands it the list its pose printed (`--partages`, one path per line): the mount reads nothing
66
+ # the copy or a command of the project rewrites during a lot (neither the declaration, nor
67
+ # .gitmodules, nor the index, nor the main repository's state), only the git file of each shared
68
+ # submodule, read-only under every earlier mount. The list is required: a call without it is
69
+ # malformed. Every command of the project that the frame plays in a copy goes through it with
70
+ # the list of its pose: the integration (installation, build, suites, guards, hooks of its
71
+ # commit), the night, and the installation and build of the agent copy.
72
+ # With an empty list the command plays as it is. With a path and no possible mount (bwrap absent
73
+ # or refused, a shared submodule absent from the main repository or not initialized there),
74
+ # nothing plays: exit 1, its first line on stderr gives that reason.
75
+ # - Without .gitmodules and without a declaration in the copy, `pose` and `garde` do nothing.
76
+ # A malformed call exits 2.
77
+ set -uo pipefail
78
+ usage() {
79
+ echo "usage: sous-modules.sh pose|garde <copie> | monter <copie> --partages <liste> <commande…>" >&2
80
+ exit 2
81
+ }
82
+ [ $# -ge 2 ] || usage
83
+ action=$1 copie=$2
84
+ case "$action" in
85
+ pose | garde) [ $# -eq 2 ] || usage ;;
86
+ monter) [ $# -ge 5 ] && [ "$3" = --partages ] || usage ;;
87
+ *) usage ;;
88
+ esac
89
+
90
+ refuser() {
91
+ echo "$1" >&2
92
+ shift
93
+ [ $# -eq 0 ] || echo "$*" >&2
94
+ exit 1
95
+ }
96
+
97
+ # The declaration of the copy: the paths it shares, one per line in `partages`; a faulty one
98
+ # refuses. A package.json that does not read declares nothing.
99
+ lire_partage() {
100
+ local lecture ligne
101
+ lecture=$(node -e '
102
+ const fs = require("node:fs");
103
+ let pkg;
104
+ try { pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); } catch { process.exit(0); }
105
+ const v = pkg?.grillhouse?.partage;
106
+ if (v === undefined || v === null) process.exit(0);
107
+ const faute = (valeur, raison) => { console.log(`faute\t${JSON.stringify(valeur)}\t${raison}`); process.exit(0); };
108
+ if (!Array.isArray(v)) faute(v, "pas une liste de chemins de sous-modules");
109
+ for (const e of v) {
110
+ if (typeof e !== "string" || !e.trim()) faute(e, "pas un chemin non vide");
111
+ if (e.startsWith("/")) faute(e, "un chemin absolu");
112
+ if (e.split("/").includes("..")) faute(e, "un chemin qui sort du dépôt");
113
+ }
114
+ for (const e of v) console.log(`chemin\t${e}`);
115
+ ' "$copie/package.json") || refuser "la clé grillhouse.partage de $copie/package.json ne se lit pas."
116
+ partages=()
117
+ while IFS= read -r ligne; do
118
+ case "$ligne" in
119
+ faute$'\t'*)
120
+ local reste=${ligne#faute$'\t'}
121
+ refuser "la clé grillhouse.partage vaut ${reste%%$'\t'*}, ${reste#*$'\t'} : rien n'est posé." ;;
122
+ chemin$'\t'*) partages+=("${ligne#chemin$'\t'}") ;;
123
+ esac
124
+ done <<< "$lecture"
125
+ }
126
+
127
+ # The main repository's working tree: the first of the repository's copies.
128
+ dossier_principal() {
129
+ git -C "$copie" worktree list --porcelain | sed -n '1s/^worktree //p'
130
+ }
131
+
132
+ est_partage() {
133
+ local p
134
+ for p in "${partages[@]}"; do [ "$p" = "$1" ] && return 0; done
135
+ return 1
136
+ }
137
+
138
+ if [ "$action" = monter ]; then
139
+ # The paths to mount: the list the caller holds from the pose, absolute paths only.
140
+ cibles=()
141
+ while IFS= read -r c; do
142
+ [ -n "$c" ] || continue
143
+ case "$c" in /*) cibles+=("$c") ;; *) usage ;; esac
144
+ done <<< "$4"
145
+ shift 4
146
+ [ ${#cibles[@]} -gt 0 ] || exec "$@"
147
+ command -v bwrap > /dev/null || refuser "le montage en lecture seule des sous-modules partagés est impossible : bwrap est absent."
148
+ montage=(bwrap --dev-bind / / --die-with-parent)
149
+ for c in "${cibles[@]}"; do
150
+ [ -e "$c/.git" ] ||
151
+ refuser "le montage en lecture seule du sous-module partagé $c est impossible : il est absent ou non initialisé dans le dépôt principal."
152
+ depot=$(git -C "$c" rev-parse --absolute-git-dir) ||
153
+ refuser "le montage en lecture seule du sous-module partagé $c est impossible : son dépôt git ne se lit pas."
154
+ montage+=(--ro-bind "$c" "$c")
155
+ # Its git repository (nested submodules' ones live under it) is read-only too.
156
+ case "$depot" in "$c"/*) ;; *) montage+=(--ro-bind "$depot" "$depot") ;; esac
157
+ done
158
+ montage+=(--chdir "$PWD")
159
+ sortie=$("${montage[@]}" -- true 2>&1) ||
160
+ refuser "le montage en lecture seule des sous-modules partagés est impossible : bwrap est refusé." "$sortie"
161
+ exec "${montage[@]}" -- "$@"
162
+ fi
163
+
164
+ lire_partage
165
+ [ -f "$copie/.gitmodules" ] || [ ${#partages[@]} -eq 0 ] || refuser "la clé grillhouse.partage nomme ${partages[0]}, mais la copie n'a pas de .gitmodules : rien n'est posé."
166
+ [ -f "$copie/.gitmodules" ] || exit 0
167
+ # The submodules of .gitmodules: `chemins[i]` is a path, `noms[i]` its name. Read with -z so that a
168
+ # path or a name holding a space stays whole.
169
+ chemins=() noms=()
170
+ while IFS= read -r -d '' entree; do
171
+ cle=${entree%%$'\n'*}
172
+ cle=${cle#submodule.}
173
+ noms+=("${cle%.path}")
174
+ chemins+=("${entree#*$'\n'}")
175
+ done < <(git -C "$copie" config -f .gitmodules -z --get-regexp '^submodule\..*\.path$')
176
+
177
+ if [ "$action" = garde ]; then
178
+ for p in "${chemins[@]}"; do
179
+ etat=$(git -C "$copie" status --porcelain --ignore-submodules=none -- "$p") || exit 1
180
+ if [ -n "$etat" ]; then
181
+ echo "le sous-module $p porte des modifications locales dans $copie : elles restent en place, et la copie s'arrête." >&2
182
+ echo "$etat" >&2
183
+ exit 1
184
+ fi
185
+ done
186
+ exit 0
187
+ fi
188
+
189
+ # The name of the submodule at path $1, in .gitmodules.
190
+ nom_du_sous_module() {
191
+ local i
192
+ for i in "${!chemins[@]}"; do
193
+ [ "${chemins[$i]}" = "$1" ] && { printf '%s\n' "${noms[$i]}"; return; }
194
+ done
195
+ }
196
+
197
+ # The url of the main repository's remote a relative address resolves against, as git chooses it:
198
+ # the remote its current branch follows (`branch.<b>.remote`), else origin; no url, its folder.
199
+ base_relative() {
200
+ local principal=$1 branche remote base=
201
+ branche=$(git -C "$principal" symbolic-ref -q --short HEAD) || branche=
202
+ remote=
203
+ [ -z "$branche" ] || remote=$(git -C "$principal" config --get "branch.$branche.remote") || remote=
204
+ [ -n "$remote" ] || remote=origin
205
+ base=$(git -C "$principal" config --get "remote.$remote.url") || base=
206
+ [ -n "$base" ] || base=$principal
207
+ printf '%s\n' "$base"
208
+ }
209
+
210
+ # Whether a url is a local path, as git reads it (url_is_local_not_ssh): no colon, or a slash
211
+ # before its first colon.
212
+ est_locale() {
213
+ case "$1" in *:*) ;; *) return 0 ;; esac
214
+ case "${1%%:*}" in */*) return 0 ;; esac
215
+ return 1
216
+ }
217
+
218
+ # The url relative to the superproject (`./`, `../`) $1 of the submodule at path $2, resolved as
219
+ # `git submodule init` resolves it for the main repository (relative_url and chop_last_dir of git):
220
+ # the base loses one final « / »; a base that is a local path not absolute is relative to the main
221
+ # repository (`upstream.git` reads `./upstream.git`). Each `../` removes the last component of the
222
+ # base, at its last slash, else at its colon, which then joins the rest with « : »; a base left
223
+ # without either refuses when it is relative or `.`, and becomes `.` otherwise. One final « / » of
224
+ # the url goes. A local path not absolute that results is made absolute from the main repository's
225
+ # folder, without requiring it to exist. A refusal names the submodule, exit 1.
226
+ resoudre_relative() {
227
+ local url=$1 principal base relative=0 deuxpoints=0 resolue
228
+ principal=$(dossier_principal)
229
+ base=$(base_relative "$principal")
230
+ base=${base%/}
231
+ if est_locale "$base" && [ "${base#/}" = "$base" ]; then
232
+ relative=1
233
+ case "$base" in ./* | ../*) ;; *) base=./$base ;; esac
234
+ fi
235
+ while :; do
236
+ case "$url" in
237
+ ./*) url=${url#./} ;;
238
+ ../*)
239
+ url=${url#../}
240
+ case "$base" in
241
+ */*) base=${base%/*} ;;
242
+ *:*) base=${base%:*} deuxpoints=1 ;;
243
+ *)
244
+ [ "$relative" = 0 ] && [ "$base" != . ] ||
245
+ refuser "le sous-module $2 ne se pose pas : son adresse relative $1 remonte au-delà de l'adresse $(base_relative "$principal") du dépôt principal, que git refuse de même ; il n'est pas posé, et la configuration du dépôt principal reste intacte."
246
+ base=. ;;
247
+ esac ;;
248
+ *) break ;;
249
+ esac
250
+ done
251
+ if [ "$deuxpoints" = 1 ]; then resolue=$base:$url; else resolue=$base/$url; fi
252
+ case "$url" in */) resolue=${resolue%/} ;; esac
253
+ resolue=${resolue#./}
254
+ if est_locale "$resolue" && [ "${resolue#/}" = "$resolue" ]; then
255
+ resolue=$(realpath -m -s -- "$principal/$resolue") || return 1
256
+ fi
257
+ printf '%s\n' "$resolue"
258
+ }
259
+
260
+ adresse_du_sous_module() {
261
+ local nom url
262
+ nom=$(nom_du_sous_module "$1")
263
+ url=$(git -C "$copie" config --get "submodule.$nom.url") ||
264
+ url=$(git -C "$copie" config -f .gitmodules --get "submodule.$nom.url") || url=
265
+ case "$url" in ./* | ../*) url=$(resoudre_relative "$url" "$1") || return 1 ;; esac
266
+ printf '%s\n' "$url"
267
+ }
268
+
269
+ # Refuses before setting anything.
270
+ for p in "${partages[@]}"; do
271
+ printf '%s\n' "${chemins[@]}" | grep -qxF -- "$p" ||
272
+ refuser "la clé grillhouse.partage nomme « $p », qui n'est pas un sous-module de .gitmodules : rien n'est posé."
273
+ done
274
+ principal=
275
+ if [ ${#partages[@]} -gt 0 ]; then
276
+ principal=$(dossier_principal)
277
+ for p in "${partages[@]}"; do
278
+ [ -e "$principal/$p/.git" ] ||
279
+ refuser "le sous-module partagé $p est absent ou non initialisé dans le dépôt principal $principal : la copie ne le lit pas, rien n'est posé."
280
+ done
281
+ fi
282
+
283
+ # A shared submodule: the link to the main repository's, ignored in the copy.
284
+ for p in "${partages[@]}"; do
285
+ [ -L "$copie/$p" ] || rm -rf -- "${copie:?}/$p"
286
+ ln -sfn -- "$principal/$p" "$copie/$p"
287
+ git -C "$copie" update-index --skip-worktree -- "$p" || exit 1
288
+ done
289
+
290
+ # The others: set in the copy. A link left by an earlier declaration goes first, and the mark with it.
291
+ for p in "${chemins[@]}"; do
292
+ est_partage "$p" && continue
293
+ [ -L "$copie/$p" ] && rm -f -- "${copie:?}/$p"
294
+ git -C "$copie" update-index --no-skip-worktree -- "$p" || exit 1
295
+ adresse=$(adresse_du_sous_module "$p") || exit 1
296
+ options=(-c protocol.file.allow=always)
297
+ if [ -n "$adresse" ]; then
298
+ options+=(-c "submodule.$(nom_du_sous_module "$p").url=$adresse")
299
+ if [ -e "$copie/$p/.git" ] && ! sortie=$(git -C "$copie/$p" remote set-url origin "$adresse" 2>&1); then
300
+ echo "le sous-module $p ne se pose pas : son remote origin ne passe pas à $adresse :" >&2
301
+ echo "$sortie" >&2
302
+ exit 1
303
+ fi
304
+ fi
305
+ if ! sortie=$(git "${options[@]}" -C "$copie" submodule update --init --recursive --force -- "$p" 2>&1); then
306
+ commit=$(git -C "$copie" ls-tree HEAD -- "$p" | cut -d' ' -f3 | cut -f1)
307
+ echo "le sous-module $p ne se pose pas au commit ${commit:0:7} :" >&2
308
+ echo "$sortie" >&2
309
+ exit 1
310
+ fi
311
+ git -C "$copie/$p" clean -q -f -d || exit 1
312
+ git -C "$copie/$p" submodule foreach --quiet --recursive 'git clean -q -f -d' || exit 1
313
+ done
314
+
315
+ # The list the pose fixes: the target of each link, for the caller to hand to `monter`.
316
+ for p in "${partages[@]}"; do printf '%s\n' "$principal/$p"; done
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # UserPromptSubmit hook: a message that asks for a structure decision (split, merge, create or remove
3
+ # a package, a module, an interface, a boundary) reminds the session that such a decision goes through
4
+ # the grill. Read-only; silent on every other message, including one that merely mentions structure,
5
+ # and to a session of a role agent of the frame (`claude --agent <rôle>`: the input names it,
6
+ # `agent_type`, bare or as `grillhouse:<rôle>`, and the frame's plugin defines it,
7
+ # `$CLAUDE_PLUGIN_ROOT/agents/<rôle>.md`), whose instruction is a ticket's.
8
+ entree="$(cat)"
9
+ role="$(printf '%s' "$entree" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const a=JSON.parse(s).agent_type;if(typeof a==="string"&&/^[A-Za-z0-9_:-]+$/.test(a))process.stdout.write(a)}catch{}})')"
10
+ racine="${CLAUDE_PROJECT_DIR:-$PWD}"
11
+ role="${role#grillhouse:}"
12
+ [ -n "$role" ] && [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/agents/$role.md" ] && exit 0
13
+ prompt="$(printf '%s' "$entree" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(JSON.parse(s).prompt||"")}catch{}})')"
14
+ noun='paquets?|packages?|modules?|interfaces?|fronti[eè]res?|boundar(y|ies)|couches?|(é|e)tages?|layers?|monorepo|architecture'
15
+ decision="faut-il|doit-on|devrait-on|est-ce qu.?il faut|besoin (de|d.)|on (d[ée]coupe|s[ée]pare)|d[ée]couper|s[ée]parer|scinder|fusionner|regrouper|extraire|cr[ée]er (un|une|des)|supprimer (le|la|les|un|une)|should (we|i)|do we need|split|merge"
16
+ if printf '%s' "$prompt" | grep -qiE "($noun)" && printf '%s' "$prompt" | grep -qiE "($decision)"; then
17
+ cat <<'MSG'
18
+ ## Décision de structure
19
+ Ce message demande une décision de structure (découpage, paquets, modules, interfaces, frontières).
20
+ Elle se prend par le grill : charge la compétence `grill` (§2 « La règle de l'architecture », §3
21
+ relevé structurel, §4 grill), jamais par une réponse directe. Elle se tranche sur les objectifs, les
22
+ consommateurs, les représentations de données et les axes de changement, avec leurs pièces, et
23
+ s'arbitre par la charte (« Comment on arbitre ») — jamais sur la taille du code ni sur « pas besoin
24
+ pour l'instant ».
25
+ MSG
26
+ fi
27
+ exit 0
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ // One supervisor per project, as Zuul has one scheduler: a session of supervision holds the lock
3
+ // of its repository, and a second one reads it, says so, and launches no circuit. Played by the
4
+ // session start (scripts/session-start.sh), which writes what it says to the session.
5
+ //
6
+ // npx grillhouse superviseur <racine>
7
+ //
8
+ // - Out of a git repository, in an agent's copy (a worktree whose git directory is not the common
9
+ // one) and in a session of a role that the circuit launches (BEADS_ACTOR set: the author of its
10
+ // comments at the ticket), nothing is taken and nothing is said.
11
+ // - The lock is `<git common dir>/grillhouse-superviseur.lock`, one for the repository and its
12
+ // copies, taken through scripts/verrou-de-fichier.mjs for the session's own process: the first
13
+ // process above this one that is not a shell (sh, bash, dash, zsh), the claude of the session.
14
+ // It holds as long as that process lives: a lock whose process is gone is taken over.
15
+ // - Free, or held by a process that is gone: taken, nothing said. Held by the session itself (a
16
+ // session reopened in the same process): kept, nothing said. Held by another live session: the
17
+ // lock stays, and the section « ## Un autre superviseur tient ce dépôt » names its process and
18
+ // since when it holds the lock, and asks to say so to the owner and to launch no circuit
19
+ // (skill pitmaster, « Au lancement de la séance »).
20
+ // Exit 0, whatever it finds.
21
+ import { spawnSync } from 'node:child_process';
22
+ import { readFileSync, statSync } from 'node:fs';
23
+ import path from 'node:path';
24
+ import { pathToFileURL } from 'node:url';
25
+ import { prendreVerrou } from './verrou-de-fichier.mjs';
26
+ import { vivant } from './vitest-config/base-de-passe.mjs';
27
+
28
+ const VERROU_SUPERVISEUR = 'grillhouse-superviseur.lock';
29
+ const COQUILLES = new Set(['sh', 'bash', 'dash', 'zsh']);
30
+
31
+ // The parent and the name of the process <pid>, from /proc; null when it does not read.
32
+ function processus(pid) {
33
+ try {
34
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
35
+ const fin = stat.lastIndexOf(')');
36
+ const nom = stat.slice(stat.indexOf('(') + 1, fin);
37
+ const parent = Number(stat.slice(fin + 2).split(' ')[1]);
38
+ return { nom, parent };
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ // The session's process: the first process above <depart> that is not a shell.
45
+ function seance(depart = process.ppid) {
46
+ let pid = depart;
47
+ for (let i = 0; i < 32 && pid > 1; i++) {
48
+ const p = processus(pid);
49
+ if (!p) return depart;
50
+ if (!COQUILLES.has(p.nom)) return pid;
51
+ pid = p.parent;
52
+ }
53
+ return depart;
54
+ }
55
+
56
+ const git = (racine, ...args) =>
57
+ spawnSync('git', ['-C', racine, 'rev-parse', '--path-format=absolute', ...args], {
58
+ encoding: 'utf8',
59
+ });
60
+
61
+ /**
62
+ * The check of the supervisor at the opening of a session in <racine>: what it says (empty when
63
+ * nothing), the lock taken when it is free.
64
+ * @param {string} racine the directory the session opens in
65
+ * @param {{ env?: Record<string, string | undefined> }} options
66
+ * @returns {string}
67
+ */
68
+ function superviseur(racine, { env = process.env } = {}) {
69
+ if (env.BEADS_ACTOR) return '';
70
+ const r = git(racine, '--git-dir', '--git-common-dir');
71
+ if (r.status !== 0) return '';
72
+ const [dossierGit, commun] = r.stdout.trim().split('\n');
73
+ if (path.resolve(dossierGit) !== path.resolve(commun)) return '';
74
+ const verrou = path.join(commun, VERROU_SUPERVISEUR);
75
+ const moi = seance();
76
+ const tenant = () => {
77
+ try {
78
+ return Number(/^(\d+)/.exec(readFileSync(verrou, 'utf8'))?.[1]);
79
+ } catch {
80
+ return null;
81
+ }
82
+ };
83
+ if (tenant() === moi) return '';
84
+ if (prendreVerrou(verrou, { attendre: false, pid: moi })) return '';
85
+ const autre = tenant();
86
+ if (!autre || autre === moi || !vivant(autre)) return '';
87
+ let depuis = '';
88
+ try {
89
+ depuis = `, depuis le ${statSync(verrou).mtime.toLocaleString('fr-FR')}`;
90
+ } catch {
91
+ // the lock is gone meanwhile: its date is not said
92
+ }
93
+ return [
94
+ '## Un autre superviseur tient ce dépôt',
95
+ `La séance de supervision du processus ${autre} tient le verrou de ce dépôt (${verrou}${depuis}). Un projet a un seul superviseur, comme Zuul a un seul ordonnanceur : dis-le au responsable dès ta première réponse, et ne lance aucun circuit, ni à la main aucune livraison, tant que cette séance tient le verrou. Il se libère quand elle se ferme ; la séance suivante le reprend.`,
96
+ ].join('\n');
97
+ }
98
+
99
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
100
+ const texte = superviseur(process.argv[2] ?? process.cwd());
101
+ if (texte) process.stdout.write(`${texte}\n`);
102
+ }