geneseed 3.7.0 → 3.9.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 (194) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/DESIGN.md +13 -10
  3. package/README.md +5 -5
  4. package/SETUP.md +2 -2
  5. package/SHIPPED.md +3 -3
  6. package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
  7. package/adapters/opencode/README.md +3 -3
  8. package/adapters/opencode/plugins/geneseed-guard.js +9 -9
  9. package/adapters/opencode/plugins/geneseed-workflow.js +1 -1
  10. package/adapters/opencode/workflows/_runtime.js +2 -2
  11. package/adapters/opencode/workflows/dispatch.js +1 -1
  12. package/docs/extending.md +72 -40
  13. package/docs/web/harnesses.md +2 -2
  14. package/docs/web/plugin-guard.md +2 -2
  15. package/docs/web/rules.md +1 -1
  16. package/docs/web/themes-author.md +2 -2
  17. package/docs/web-ui.md +1 -1
  18. package/harness.config.json +1 -1
  19. package/js/README.md +4 -3
  20. package/js/build/driver.mjs +14 -5
  21. package/js/build/generate.mjs +90 -41
  22. package/js/build/render.mjs +6 -3
  23. package/js/build/source.mjs +105 -1
  24. package/js/build/stubs.mjs +7 -2
  25. package/js/cli-table.json +154 -5
  26. package/js/hosts/hooks.mjs +5 -5
  27. package/js/hosts/native.mjs +1 -1
  28. package/js/inspect/checks-authoring.mjs +149 -116
  29. package/js/inspect/inventory.mjs +1 -2
  30. package/js/inspect/status.mjs +40 -4
  31. package/js/ui/tui.mjs +1 -1
  32. package/js/web/api.mjs +6 -1
  33. package/package.json +1 -1
  34. package/registry.json +7 -0
  35. package/src/AGENT.md.tmpl +19 -15
  36. package/src/agents/_template.md +3 -3
  37. package/src/agents/advocate.md +3 -3
  38. package/src/agents/architect.md +3 -3
  39. package/src/agents/developer.md +4 -4
  40. package/src/agents/docs.md +3 -3
  41. package/src/agents/empiricist.md +4 -4
  42. package/src/agents/explorer.md +4 -4
  43. package/src/agents/framer.md +3 -3
  44. package/src/agents/historian.md +3 -3
  45. package/src/agents/operator.md +3 -3
  46. package/src/agents/pragmatist.md +3 -3
  47. package/src/agents/researcher.md +7 -7
  48. package/src/agents/reviewer.md +3 -3
  49. package/src/agents/security.md +3 -3
  50. package/src/agents/skeptic.md +3 -3
  51. package/src/agents/steward.md +3 -3
  52. package/src/agents/tester.md +3 -3
  53. package/src/agents/user-advocate.md +3 -3
  54. package/src/agents/visionary.md +3 -3
  55. package/src/doctrines/README.md +4 -3
  56. package/src/doctrines/comms.md +4 -4
  57. package/src/doctrines/craft.md +20 -20
  58. package/src/doctrines/ops.md +19 -19
  59. package/src/doctrines/process.md +20 -20
  60. package/src/doctrines/rigor.md +22 -22
  61. package/src/laws/universal.md +23 -44
  62. package/src/memory/README.md +3 -3
  63. package/src/modes/foreman.md +1 -1
  64. package/src/notebook/README.md +1 -1
  65. package/src/ontology/universal.md +7 -9
  66. package/src/postures/artisan.md +2 -2
  67. package/src/skills/_self-improvement.md +4 -4
  68. package/src/skills/_template.md +1 -1
  69. package/src/skills/brainstorm.md +3 -3
  70. package/src/skills/bruno-collection-generator.md +6 -6
  71. package/src/skills/bruno-test-writer.md +5 -5
  72. package/src/skills/ci-fix.md +4 -4
  73. package/src/skills/commit.md +11 -10
  74. package/src/skills/consolidate-memory.md +4 -4
  75. package/src/skills/council.md +2 -2
  76. package/src/skills/debug.md +4 -4
  77. package/src/skills/deps-audit.md +3 -3
  78. package/src/skills/develop.md +7 -7
  79. package/src/skills/document-project.md +6 -6
  80. package/src/skills/explain-changes/SKILL.md +69 -0
  81. package/src/skills/explain-changes/VENDOR.md +20 -0
  82. package/src/skills/explain-changes/scripts/render_changes.mjs +558 -0
  83. package/src/skills/forge-mcp.md +4 -4
  84. package/src/skills/fresh-eyes.md +4 -4
  85. package/src/skills/frontend-design.md +1 -1
  86. package/src/skills/geneseed.md +9 -8
  87. package/src/skills/git-archaeology.md +5 -5
  88. package/src/skills/git-rescue.md +7 -7
  89. package/src/skills/handoff.md +3 -3
  90. package/src/skills/ingest.md +5 -5
  91. package/src/skills/migrate.md +8 -8
  92. package/src/skills/parallel-agents.md +2 -2
  93. package/src/skills/pipeline.md +2 -2
  94. package/src/skills/plan.md +3 -3
  95. package/src/skills/ponytail.md +2 -2
  96. package/src/skills/profile.md +2 -2
  97. package/src/skills/prose.md +1 -1
  98. package/src/skills/prototype.md +1 -1
  99. package/src/skills/refactor.md +3 -3
  100. package/src/skills/release.md +5 -5
  101. package/src/skills/repo-map.md +4 -4
  102. package/src/skills/research.md +9 -9
  103. package/src/skills/review-response.md +4 -4
  104. package/src/skills/rule.md +3 -3
  105. package/src/skills/security-audit.md +5 -5
  106. package/src/skills/ship.md +5 -5
  107. package/src/skills/skill-forge.md +4 -4
  108. package/src/skills/wiki.md +5 -5
  109. package/src/skills/workflow.md +2 -3
  110. package/src/skills/worktree.md +4 -4
  111. package/themes/_TEMPLATE.json +38 -40
  112. package/themes/biker.json +38 -40
  113. package/themes/commentator.json +38 -40
  114. package/themes/cyberpunk.json +38 -40
  115. package/themes/gamer.json +38 -40
  116. package/themes/imperial.json +38 -40
  117. package/themes/joker.json +38 -40
  118. package/themes/marvin.json +38 -40
  119. package/themes/mean.json +38 -40
  120. package/themes/military.json +38 -40
  121. package/themes/neutral.json +38 -40
  122. package/themes/pirate.json +38 -40
  123. package/themes/sports.json +38 -40
  124. package/themes/verstappen.json +38 -40
  125. package/themes/wizard.json +38 -40
  126. package/web/dist/assets/Activity-DAzV-qQj.js +1 -0
  127. package/web/dist/assets/ActivityDetail-CwEwMtKl.js +1 -0
  128. package/web/dist/assets/Dashboard-C2ELBW-X.js +2 -0
  129. package/web/dist/assets/{Docs-DrJNDPwI.js → Docs-iZrvQP8J.js} +2 -2
  130. package/web/dist/assets/FilterInput-CVyqmYWM.js +1 -0
  131. package/web/dist/assets/GreenhouseView-Bq1QmaGO.js +2 -0
  132. package/web/dist/assets/Installs-BiqR8xdO.js +2 -0
  133. package/web/dist/assets/JournalView-6wilG5Zd.js +1 -0
  134. package/web/dist/assets/Laws-D8YXD_XE.js +3 -0
  135. package/web/dist/assets/Library-DwuTZ2-L.js +1 -0
  136. package/web/dist/assets/{Markdown-BQAvN8xx.js → Markdown-CkFyfgNf.js} +14 -14
  137. package/web/dist/assets/OperatorHudView-B-HVBqfY.js +2 -0
  138. package/web/dist/assets/Personal-BbUTmZQR.js +1 -0
  139. package/web/dist/assets/Tabs-_pjUFIEI.js +1 -0
  140. package/web/dist/assets/drawable-CoIulEH2.js +1 -0
  141. package/web/dist/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
  142. package/web/dist/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
  143. package/web/dist/assets/index-BMwSp8BP.js +2 -0
  144. package/web/dist/assets/index-CDPDk2vj.css +1 -0
  145. package/web/dist/assets/{react-DbkvxWG5.js → react-B9Z_tXRV.js} +1 -1
  146. package/web/dist/assets/sections-GalMS0Zv.js +1 -0
  147. package/web/dist/assets/usePoll-O0sJEw1Y.js +1 -0
  148. package/web/dist/index.html +4 -4
  149. package/web/src/pages/Laws.jsx +616 -361
  150. package/web/dist/assets/Activity-CH-7cPIM.js +0 -1
  151. package/web/dist/assets/ActivityDetail-DZvz0xS5.js +0 -1
  152. package/web/dist/assets/CatalogRow-DftXpZHY.js +0 -1
  153. package/web/dist/assets/Diff-DvwdG0L9.js +0 -2
  154. package/web/dist/assets/Doctor-OTmwmYZg.js +0 -1
  155. package/web/dist/assets/FilterInput-BakJLvaV.js +0 -1
  156. package/web/dist/assets/GreenhouseView-XxrAdriX.js +0 -2
  157. package/web/dist/assets/Harness-C_LYNk5c.js +0 -1
  158. package/web/dist/assets/JournalView-BcuyeEaR.js +0 -1
  159. package/web/dist/assets/Laws-CWHrTKY4.js +0 -3
  160. package/web/dist/assets/Library-CA9IHghn.js +0 -1
  161. package/web/dist/assets/OperatorHudView-BMRY1Tt4.js +0 -2
  162. package/web/dist/assets/Profile-wceET1JY.js +0 -1
  163. package/web/dist/assets/Rules-BEXvY4Tx.js +0 -1
  164. package/web/dist/assets/Settings-DS2zF0Ez.js +0 -1
  165. package/web/dist/assets/Skills-DahlOJuI.js +0 -1
  166. package/web/dist/assets/StatusBadge-Cg6j-NJL.js +0 -1
  167. package/web/dist/assets/activity-iWaWVtsR.js +0 -1
  168. package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
  169. package/web/dist/assets/drawable-BTHjTzF7.js +0 -1
  170. package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
  171. package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
  172. package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
  173. package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  174. package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  175. package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  176. package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
  177. package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
  178. package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
  179. package/web/dist/assets/index-AN5Emnrg.js +0 -2
  180. package/web/dist/assets/index-Ruh93A3A.css +0 -1
  181. package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  182. package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  183. package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  184. package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  185. package/web/dist/assets/lawCats-Btjp3Hk_.js +0 -1
  186. package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
  187. package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
  188. package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
  189. package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
  190. package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  191. package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  192. package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  193. package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  194. package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +0 -1
package/docs/web-ui.md CHANGED
@@ -160,7 +160,7 @@ streams the output of background jobs and keeps their history across reloads.
160
160
  global harness goes dormant (hooks stay silent, the global preamble never loads);
161
161
  add or remove one here, the web mirror of `harness exclude add|remove|list`.
162
162
  - **Settings** (`#/settings`) — a **Console direction** picker (the visual flavour of the
163
- console) with a **Dashboard layout** control (Auto follows each theme's designed Status
163
+ console: Cultivar, Operator, Matrix or Atlas) with a **Dashboard layout** control (Auto follows each theme's designed Status
164
164
  lens; Cultivar / Greenhouse / Operator / Journal force one regardless of skin), the
165
165
  install snapshot (deployed/target/theme/version/memory store), a Maintenance card (PATH
166
166
  link/unlink, uninstall), an offline package download, server controls (Stop), and the
@@ -2,5 +2,5 @@
2
2
  "_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
3
3
  "theme": "neutral",
4
4
  "product": "Geneseed",
5
- "version": "3.7.0"
5
+ "version": "3.9.0"
6
6
  }
package/js/README.md CHANGED
@@ -37,7 +37,7 @@ as a per-host install at project or global scope.
37
37
 
38
38
  | module | owns |
39
39
  |---|---|
40
- | `source.mjs` | Where the SOURCE is: `ROOT`/`SRC`/`THEMES`/`CONFIG`, `PACK_ORDER`, rule ids, the cfg object |
40
+ | `source.mjs` | Where the SOURCE is: `ROOT`/`SRC`/`THEMES`/`CONFIG`, `PACK_ORDER`, the rule canon (stable ids → positional numbers, citations → names), the cfg object |
41
41
  | `render.mjs` | The pure text pipeline: theme load, `INCLUDE` inlining, CATALOG blocks, `{{TOKENS}}`, dest paths |
42
42
  | `bundle.mjs` | `build` — render a theme into a bundle dir, plus the source-completeness refusal. Sits UNDER both emits |
43
43
  | `emit-claude.mjs` | The Claude/Bob/Copilot/OpenClaude emit — the render half plus the settings + CLAUDE.md managed-block wire |
@@ -182,8 +182,9 @@ and inline lists are held against a surface list written out in
182
182
 
183
183
  | I want to | open first | then |
184
184
  |---|---|---|
185
- | a doctrine rule | `src/doctrines/<pack>.md` — APPEND, ids run contiguously | `docs/extending.md` §2a — five files, `--sync-themes`, restyle 14 voices by hand. Author BOTH LEAN halves |
186
- | a law (invariant) | `src/laws/universal.md` — **APPEND, never insert** | `docs/extending.md` §2c. Author both LEAN halves: the lean footprint ships nothing else |
185
+ | a doctrine rule | `src/doctrines/<pack>.md` — `### {{DOCTRINE:<id>}} <Principle>`; the number is its position | `docs/extending.md` §2a — five files, `--sync-themes`, restyle 14 voices by hand. Author BOTH LEAN halves |
186
+ | a law (invariant) | `src/laws/universal.md` — `### {{LAW:<id>}} <Principle>`; cite by id, never by number | `docs/extending.md` §2c. Author both LEAN halves: the lean footprint ships nothing else |
187
+ | to remove or retire a rule | `docs/extending.md` §2e | `RETIRED_RULE_IDS` in `js/build/source.mjs` — a retired id is never reused |
187
188
  | a skill or an agent | `src/skills/<name>.md` or `src/agents/<name>.md` | `docs/extending.md` §3 — eight steps. Write the FILE before the table row or the build aborts |
188
189
  | a doctor check | `js/inspect/checks-{build,repo,authoring}.mjs` | register with `ran(...)` in `doctor.mjs`, then a PLANTED FAULT in `tests/unit/harness.test.mjs` |
189
190
  | a web endpoint | GET → `js/web/api.mjs`; POST → `js/web/actions.mjs` | declare it in `js/web/routes.mjs`; the partition test probes the real handler |
@@ -166,7 +166,7 @@ const VALUED = {
166
166
  '--theme': 'theme', '--posture': 'posture', '--mode': 'mode',
167
167
  '--doctrines': 'doctrines', '--exclude-rules': 'excludeRules',
168
168
  '--out': 'out', '--target': 'out', '--emit': 'emit',
169
- '--footprint': 'footprint', '--root': 'root',
169
+ '--footprint': 'footprint', '--root': 'root', '--config-dir': 'cfgDir',
170
170
  };
171
171
  const FLAGS = {
172
172
  '--sync-themes': 'syncThemes', '--validate-only': 'validateOnly',
@@ -1032,10 +1032,10 @@ function writeMarkers(markerDir, emit, footprint) {
1032
1032
  * compares it file by file. The Python spells that `HOSTS[host]["emit_global"](theme,
1033
1033
  * out=..., cfg=<tmp>, footprint=...)`, where `cfg` overrides the target the emit would
1034
1034
  * otherwise resolve. This is that call, and the override is threaded as `args.cfgDir` through
1035
- * the four `emit*Global` bodies rather than added to `parseArgs`: `build.py` has no
1036
- * `--cfg-dir` flag, and a flag on one driver and not the other is a divergence
1037
- * `tests/golden.py` compares 259 times and could never see, because it drives both from the
1038
- * same argv.
1035
+ * the `emit*Global` bodies. The same key is also `--config-dir` on the command line: a global
1036
+ * emit never writes to `--out` (there `out` is the legacy bundle a memory store migrates FROM,
1037
+ * and `update` passes it for exactly that), so without the flag there was no way to preview a
1038
+ * global install anywhere but over the live one.
1039
1039
  *
1040
1040
  * WHICH SIDE OF THE BOUNDARY THIS SITS ON. P3c's rule was that a render CHILD must never
1041
1041
  * resolve a config dir, because a child that did would write 135 files into the developer's
@@ -1187,6 +1187,15 @@ function run(argv) {
1187
1187
  }
1188
1188
 
1189
1189
  const out = resolveOut(args.out);
1190
+ // Refused rather than ignored on a per-repo emit: an ignored target flag is how a preview
1191
+ // lands on a live install, which is the bug this flag exists to close.
1192
+ if (args.cfgDir !== undefined) {
1193
+ if (!args.emit.endsWith('-global')) {
1194
+ die(2, `argument --config-dir: only applies to a -global emit (got --emit ${args.emit}); `
1195
+ + 'a per-repo emit writes under --out/--root');
1196
+ }
1197
+ args.cfgDir = resolveOut(args.cfgDir);
1198
+ }
1190
1199
  const cfg = makeCfg(args);
1191
1200
 
1192
1201
  // The marker directory is the emit's TARGET, which for a global emit is the config dir it
@@ -80,12 +80,28 @@ function sysExit(msg) {
80
80
  * Calling the driver in-process keeps that: `resolveOut` resolves against `process.cwd()`
81
81
  * the same way. `build/the-bundle-follows-cwd-not-the-checkout` is the cell.
82
82
  *
83
- * Every other flag is the generator's own default, read from `harness.config.json` by the
84
- * driver — `harness build` forwards `--theme` and nothing else, and its own `--theme`
85
- * carries no `choices`, so an unknown one is refused by the generator rather than here.
83
+ * `geneseed build` used to forward `--theme` alone, which made `setup`'s own off-TTY hint
84
+ * (`geneseed build --emit … --theme …`) a usage error and left every other axis reachable
85
+ * only through the undocumented `geneseed-build`. Values are forwarded unchecked: the driver
86
+ * owns the choices, so the table does not repeat them.
87
+ *
88
+ * `--validate-only` is NOT one of these: it lives on `geneseed validate` only — that verb's
89
+ * source-tree half is the doctor, which starts a process, and this command's whole closure
90
+ * (`bin/geneseed-cli.mjs` → here → `driverMain`) is under the same transitive `child_process`
91
+ * ban as `bin/build-driver.mjs` itself. `driver.mjs`'s `run()` refuses the flag unconditionally
92
+ * for exactly that reason.
86
93
  */
94
+ const BUILD_FORWARD = [
95
+ ['theme', '--theme'], ['emit', '--emit'], ['footprint', '--footprint'],
96
+ ['posture', '--posture'], ['mode', '--mode'], ['doctrines', '--doctrines'],
97
+ ['excludeRules', '--exclude-rules'], ['out', '--out'], ['root', '--root'],
98
+ ['configDir', '--config-dir'],
99
+ ];
100
+
87
101
  export function cmdBuild(args) {
88
- return driverMain(args.theme ? ['--theme', args.theme] : []);
102
+ const argv = [];
103
+ for (const [key, flag] of BUILD_FORWARD) if (args[key] != null) argv.push(flag, args[key]);
104
+ return driverMain(argv);
89
105
  }
90
106
 
91
107
  // --------------------------------------------------------------------------------------
@@ -161,6 +177,66 @@ export const DEFAULT_EMIT = new Map([
161
177
  ['openclaude global', 'openclaude-global'], ['openclaude project', 'openclaude'],
162
178
  ]);
163
179
 
180
+ /**
181
+ * Everything `rebuild-all` reads back off one install, and the argv that re-emits it unchanged.
182
+ *
183
+ * EXTRACTED SO `status` SHOWS WHAT A REBUILD WOULD USE, not a second reading of the same
184
+ * markers that could drift from this one. An agent changing one setting takes `argv`, edits one
185
+ * flag and runs it; every flag it did not touch has to be the install's own, or the "one
186
+ * change" silently resets the rest — which is exactly the failure the seven read-backs below
187
+ * exist to prevent.
188
+ *
189
+ * THE MARKER IS TRUSTED ONLY FOR ITS OWN HOST. There is one `.geneseed-emit` per root and the
190
+ * last deploy wins, so in a dual-host repo (`.opencode/` and `.claude/` in one cwd) the other
191
+ * host's row would otherwise rebuild with the WRONG emit — turning a Claude install into an
192
+ * OpenCode one. `rebuild-all/a-dual-host-repo-rebuilds-each-row-in-its-own-emit` is the cell.
193
+ */
194
+ export function installProfile(host, scope, root) {
195
+ const raw = readMaybe(path.join(root, '.geneseed-emit'));
196
+ let marker = raw === null ? '' : raw.trim();
197
+ if (marker && (EMIT_HOST_SCOPE.get(marker) ?? ['', ''])[0] !== host) marker = '';
198
+ const emit = marker || DEFAULT_EMIT.get(`${host} ${scope}`) || 'opencode-global';
199
+ const theme = themeOfDir(root) || defaultTheme();
200
+ const footprint = footprintOfDir(root);
201
+ const posture = postureOfDir(root) || defaultPosture();
202
+ const mode = modeOfDir(root) || defaultMode();
203
+ // A rebuild that defaulted the pack selection would re-emit an install into a constitution
204
+ // its owner did not choose — and in one direction that is not merely surprising: dropping
205
+ // the process pack takes the commit/push consent RULES out of AGENT.md while
206
+ // `claudeHookGroups` keeps the gate wired, or the reverse. ⚠ AND THE FALLBACK IS NOT
207
+ // `harness.config.json`: a pre-migration carrier has no marker, and resolving that silence
208
+ // out of a machine-wide config narrows every such install on its first upgrade.
209
+ // `doctrinesForBuild` resolves unknown to ALL packs — see its docblock.
210
+ const doctrines = doctrinesForBuild(root);
211
+ // The per-rule axis is preserved for the same reason and with sharper teeth: an upgrade that
212
+ // dropped it would quietly hand back every rule its owner switched off, and `process 5` is
213
+ // one of them. `null` (no marker) stays `null` so the argv omits the flag, as it always did.
214
+ const excludeRules = excludedRulesOfDir(root);
215
+ const out = scope === 'global' ? null : root;
216
+ const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
217
+ PACK_ORDER, excludeRules);
218
+ // A GLOBAL ROW NAMES ITS OWN DIR. `root` came from the host's config-dir resolver, which
219
+ // honours env overrides (`OPENCODE_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`…); without the flag the
220
+ // same argv run from a shell lacking that variable re-emits into the DEFAULT dir — a second
221
+ // install beside the one it was meant to rebuild. Appended, so the argv's head is unchanged.
222
+ if (scope === 'global') argv.push('--config-dir', root);
223
+ return {
224
+ host, scope, root, state: installState(root, host, scope),
225
+ emit, theme, footprint, posture, mode, doctrines, excludeRules, argv,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * `argv` as one pasteable command, using the `geneseed build` launcher rather than the raw
231
+ * generator: `geneseed-build` is only on PATH for an npm global install, while a git-clone
232
+ * install has only the `geneseed` launcher, and `geneseed build` now forwards every flag
233
+ * `setupBuildArgs` produces. Only whitespace is quoted — a path holding shell metacharacters
234
+ * (`$`, `&`) would need quoting by hand.
235
+ */
236
+ export function rebuildCommand(argv) {
237
+ return ['geneseed', 'build', ...argv.map((a) => (/\s/.test(a) ? `"${a}"` : a))].join(' ');
238
+ }
239
+
164
240
  /**
165
241
  * `_harness_build.cmd_rebuild_all` — re-emit every ACTIVE install in place, best-effort.
166
242
  *
@@ -172,16 +248,12 @@ export const DEFAULT_EMIT = new Map([
172
248
  * `main` converts, and `rebuild-all/one-broken-install-does-not-stop-the-rest` is the cell
173
249
  * that fails if either half is undone.
174
250
  *
175
- * FIVE VALUES ARE READ BACK OFF EACH INSTALL, not defaulted. Theme and emit and footprint
176
- * come from markers; posture and mode are detected from the `**<Name>**` lead in the
177
- * deployed carrier's prose, because they were never markered. A rebuild that defaulted any
178
- * of them would silently re-emit an install into something its owner did not choose — which
179
- * is the same failure mode `_footprint_of_dir`'s WARN exists to prevent, at a larger scale.
180
- *
181
- * THE MARKER IS TRUSTED ONLY FOR ITS OWN HOST. There is one `.geneseed-emit` per root and the
182
- * last deploy wins, so in a dual-host repo (`.opencode/` and `.claude/` in one cwd) the other
183
- * host's row would otherwise rebuild with the WRONG emit — turning a Claude install into an
184
- * OpenCode one. `rebuild-all/a-dual-host-repo-rebuilds-each-row-in-its-own-emit` is the cell.
251
+ * SEVEN VALUES ARE READ BACK OFF EACH INSTALL, not defaulted — see `installProfile`. Theme and
252
+ * emit and footprint come from markers; posture and mode are detected from the `**<Name>**`
253
+ * lead in the deployed carrier's prose, because they were never markered. A rebuild that
254
+ * defaulted any of them would silently re-emit an install into something its owner did not
255
+ * choose — which is the same failure mode `_footprint_of_dir`'s WARN exists to prevent, at a
256
+ * larger scale.
185
257
  */
186
258
  export function cmdRebuildAll() {
187
259
  const targets = installTargets().filter(([h, s, r]) => installState(r, h, s) === 'active');
@@ -191,34 +263,11 @@ export function cmdRebuildAll() {
191
263
  }
192
264
  const failures = [];
193
265
  for (const [host, scope, root] of targets) {
194
- const raw = readMaybe(path.join(root, '.geneseed-emit'));
195
- let marker = raw === null ? '' : raw.trim();
196
- if (marker && (EMIT_HOST_SCOPE.get(marker) ?? ['', ''])[0] !== host) marker = '';
197
- const emit = marker || DEFAULT_EMIT.get(`${host} ${scope}`) || 'opencode-global';
198
- const theme = themeOfDir(root) || defaultTheme();
199
- const footprint = footprintOfDir(root);
200
- const posture = postureOfDir(root) || defaultPosture();
201
- const mode = modeOfDir(root) || defaultMode();
202
- const out = scope === 'global' ? null : root;
203
- // SIX values now, not five. A rebuild that defaulted the pack selection would re-emit an
204
- // install into a constitution its owner did not choose — and in one direction that is not
205
- // merely surprising: dropping the process pack takes the commit/push consent RULES out of
206
- // AGENT.md while `claudeHookGroups` keeps the gate wired, or the reverse. ⚠ AND THE
207
- // FALLBACK IS NOT `harness.config.json`: a pre-migration carrier has no marker, and
208
- // resolving that silence out of a machine-wide config narrows every such install on its
209
- // first upgrade. `doctrinesForBuild` resolves unknown to ALL packs — see its docblock.
210
- const doctrines = doctrinesForBuild(root);
211
- // SEVEN. The per-rule axis is preserved for the same reason and with sharper teeth: an
212
- // upgrade that dropped it would quietly hand back every rule its owner switched off, and
213
- // `process 5` is one of them — the consent gate would return to a harness that had been
214
- // built without it. No `doctrinesForBuild` rescue is needed here; see `apiInstallCmd`.
215
- const excludeRules = excludedRulesOfDir(root);
216
- const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
217
- PACK_ORDER, excludeRules);
266
+ const p = installProfile(host, scope, root);
218
267
  const label = `${host}:${scope} (${root})`;
219
- printOut(`[rebuild-all] ${label}: theme=${theme} emit=${emit} footprint=${footprint} `
220
- + `posture=${posture} mode=${mode}\n`);
221
- const rc = driverMain(argv);
268
+ printOut(`[rebuild-all] ${label}: theme=${p.theme} emit=${p.emit} footprint=${p.footprint} `
269
+ + `posture=${p.posture} mode=${p.mode}\n`);
270
+ const rc = driverMain(p.argv);
222
271
  if (rc !== 0) {
223
272
  failures.push(label);
224
273
  printErr(`[rebuild-all] FAILED ${label} (exit ${rc})\n`);
@@ -34,7 +34,7 @@ import { readText } from '../lib/fs.mjs';
34
34
  import { parseJson } from '../lib/json.mjs';
35
35
  import { normcase, comparePaths } from '../lib/paths.mjs';
36
36
  // `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
37
- import { PACK_ORDER, readPackText } from './source.mjs';
37
+ import { PACK_ORDER, readPackText, resolveRuleIds } from './source.mjs';
38
38
 
39
39
  /** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
40
40
  export const STRUCTURE = {
@@ -216,7 +216,8 @@ function readSource(cfg, p) {
216
216
  let seen = SRC_TEXT.get(cfg);
217
217
  if (!seen) SRC_TEXT.set(cfg, (seen = new Map()));
218
218
  let text = seen.get(p);
219
- if (text === undefined) seen.set(p, (text = readText(p)));
219
+ // Rule ids resolve here, once per file, so every stage after sees the addressed form.
220
+ if (text === undefined) seen.set(p, (text = resolveRuleIds(readText(p), cfg.src)));
220
221
  return text;
221
222
  }
222
223
 
@@ -250,7 +251,9 @@ export function renderFile(cfg, filePath, theme, footprint = 'full', lawsPrefix
250
251
  function registerBody(cfg, theme, dir, selected, fallback) {
251
252
  for (const name of [selected, fallback]) {
252
253
  const p = path.join(cfg.src, dir, `${name}.md`);
253
- if (existsSync(p) && statSync(p).isFile()) return substitute(readText(p), theme).trim();
254
+ if (existsSync(p) && statSync(p).isFile()) {
255
+ return substitute(resolveRuleIds(readText(p), cfg.src), theme).trim();
256
+ }
254
257
  }
255
258
  return '';
256
259
  }
@@ -37,7 +37,7 @@ import path from 'node:path';
37
37
  * on raw or folded text, so one behaviour serves both.
38
38
  */
39
39
  const _packTexts = new Map();
40
- export function readPackText(file) {
40
+ export function readRawText(file) {
41
41
  const key = path.resolve(file);
42
42
  const mtime = statSync(file).mtimeMs;
43
43
  const hit = _packTexts.get(key);
@@ -47,6 +47,110 @@ export function readPackText(file) {
47
47
  return text;
48
48
  }
49
49
 
50
+ /**
51
+ * A doctrine pack as the render sees it: ids resolved, so the heading reads
52
+ * `### {{DOCTRINE}} process 5 — {{DOC_CONSENT_BEFORE_PUSH}}` again. Every reader that keys on the
53
+ * `<pack> <n>` address (`knownRuleIds`, the exclusion filter, the survives gate) goes through
54
+ * here and keeps working unchanged — the address is computed, only the source stopped typing it.
55
+ */
56
+ export function readPackText(file) {
57
+ return resolveRuleIds(readRawText(file), path.dirname(path.dirname(file)));
58
+ }
59
+
60
+ /**
61
+ * STABLE RULE IDS. Every law and doctrine rule is DECLARED by its heading —
62
+ * `### {{LAW:verify-before-asserting}} Verify Before Asserting` — and CITED by its id —
63
+ * `{{LAW:verify-before-asserting}}`. The id is permanent; the number is not stored anywhere. It
64
+ * is the rule's position (Roman in `laws/universal.md`, Arabic within its pack), computed here
65
+ * at render time, so removing or reordering a rule renumbers every heading after it and rewires
66
+ * nothing: no citation ever named the number.
67
+ *
68
+ * WHY CITATIONS RENDER AS THE NAME, NOT THE NUMBER. A citation is the one place a number would
69
+ * leak into text that outlives the build — an agent reads "Law III", writes "Law III" into its
70
+ * memory, and the memory is wrong the day the canon moves. The rendered harness models what it
71
+ * asks of the agent: a rule is cited by its principle, in English, identical in every voice. The
72
+ * number survives only where it is structural — the rule's own heading.
73
+ *
74
+ * Retired ids live in `RETIRED_RULE_IDS` so the doctor can refuse their reuse: an id that meant
75
+ * one thing in someone's notes must never come back meaning another.
76
+ */
77
+ const DECL_RE = /^### \{\{(LAW|DOCTRINE):([a-z0-9]+(?:-[a-z0-9]+)*)\}\}[ \t]+(\S[^\n]*?)[ \t]*$/gm;
78
+ const CITE_RE = /\{\{(LAW|DOCTRINE):([a-z0-9-]+)\}\}/g;
79
+ export const RULE_ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
80
+
81
+ /**
82
+ * Ids that no longer name a live rule, and must never be declared again. `liveAs` names the one
83
+ * tier that may still declare it — `external-gate` was Law IX before it became a rigor rule, so
84
+ * the doctrine keeps it and `{{LAW:external-gate}}` is refused; `null` means gone for good.
85
+ */
86
+ export const RETIRED_RULE_IDS = {
87
+ 'external-gate': { was: 'Law IX', liveAs: 'DOCTRINE', note: 'moved to the rigor pack' },
88
+ 'absence-is-a-claim': { was: 'Law XI', liveAs: null, note: 'folded into verify-before-asserting' },
89
+ };
90
+
91
+ const ROMAN = [[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], [100, 'C'], [90, 'XC'],
92
+ [50, 'L'], [40, 'XL'], [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']];
93
+ export function toRoman(n) {
94
+ let out = '';
95
+ for (const [v, r] of ROMAN) while (n >= v) { out += r; n -= v; }
96
+ return out;
97
+ }
98
+
99
+ /** `sealed-secrets` -> `LEX_SEALED_SECRETS`; a doctrine rule's title key is `DOC_<ID>`. */
100
+ export const titleKey = (kind, id) =>
101
+ `${kind === 'LAW' ? 'LEX' : 'DOC'}_${id.toUpperCase().replaceAll('-', '_')}`;
102
+
103
+ /** The files that may declare rules, in canon order: `[kind, pack|null, file]`. */
104
+ export function canonFiles(src = SRC) {
105
+ return [['LAW', null, path.join(src, 'laws', 'universal.md')],
106
+ ...PACK_ORDER.map((p) => ['DOCTRINE', p, path.join(src, 'doctrines', `${p}.md`)])];
107
+ }
108
+
109
+ const _canons = new Map();
110
+ /**
111
+ * Every declared rule, in render order: `{ kind, pack, id, name, n, label }`, where `label` is
112
+ * the address the heading renders (`III`, `process 5`). Memoised on the raw texts' identity —
113
+ * `readRawText` hands back the same string until a file's mtime moves.
114
+ */
115
+ export function ruleCanon(src = SRC) {
116
+ const files = canonFiles(src);
117
+ const texts = files.map(([, , f]) => { try { return readRawText(f); } catch { return ''; } });
118
+ const key = path.resolve(src);
119
+ const hit = _canons.get(key);
120
+ if (hit && hit.texts.every((t, i) => t === texts[i])) return hit.canon;
121
+ const rules = [];
122
+ files.forEach(([kind, pack], i) => {
123
+ let n = 0;
124
+ for (const m of texts[i].matchAll(DECL_RE)) {
125
+ if (m[1] !== kind) continue; // a wrong-tier declaration is the doctor's to name
126
+ n += 1;
127
+ rules.push({ kind, pack, id: m[2], name: m[3], n, label: kind === 'LAW' ? toRoman(n) : `${pack} ${n}` });
128
+ }
129
+ });
130
+ const byId = new Map();
131
+ for (const r of rules) if (!byId.has(r.id)) byId.set(r.id, r);
132
+ const canon = { rules, byId };
133
+ _canons.set(key, { texts, canon });
134
+ return canon;
135
+ }
136
+
137
+ /**
138
+ * Resolve every declaration and citation in `text` against the canon under `src`. An unknown id
139
+ * is left exactly as written — visible in the output, and named by the doctor — rather than
140
+ * guessed at.
141
+ */
142
+ export function resolveRuleIds(text, src = SRC) {
143
+ if (!text.includes('{{LAW:') && !text.includes('{{DOCTRINE:')) return text;
144
+ const { byId } = ruleCanon(src);
145
+ const known = (kind, id) => { const r = byId.get(id); return r && r.kind === kind ? r : null; };
146
+ return text
147
+ .replace(DECL_RE, (whole, kind, id) => {
148
+ const r = known(kind, id);
149
+ return r ? `### {{${kind}}} ${r.label} — {{${titleKey(kind, id)}}}` : whole;
150
+ })
151
+ .replace(CITE_RE, (whole, kind, id) => { const r = known(kind, id); return r ? `*${r.name}*` : whole; });
152
+ }
153
+
50
154
  /**
51
155
  * `_build_core.ROOT` — the checkout, from this file's own location (`js/..`).
52
156
  *
@@ -137,8 +137,13 @@ the project's own. Follow the root \`AGENTS.md\`.
137
137
  /** `_build_render.PROFILE_FILE`. */
138
138
  const PROFILE_FILE = 'PROFILE.md';
139
139
 
140
- /** `_build_render.PROFILE_STUB`. */
141
- const PROFILE_STUB = `\
140
+ /**
141
+ * `_build_render.PROFILE_STUB`.
142
+ *
143
+ * Exported for the web console: `/api/profile` answers `seeded: true` while PROFILE.md is
144
+ * still byte-for-byte this text, which is how the Overview knows to ask for it to be filled in.
145
+ */
146
+ export const PROFILE_STUB = `\
142
147
  # Your profile
143
148
 
144
149
  Who you are and how you like to work — so the agent can meet you where you are
package/js/cli-table.json CHANGED
@@ -54,7 +54,7 @@
54
54
  },
55
55
  {
56
56
  "name": "build",
57
- "help": "render src/ -> Harness/",
57
+ "help": "render the harness (same flags as geneseed-build; an omitted flag takes the generator default, not the install's — see geneseed status --json for an install's own; dry run: geneseed validate)",
58
58
  "description": "",
59
59
  "positionals": [],
60
60
  "options": [
@@ -72,6 +72,141 @@
72
72
  "is_flag": false,
73
73
  "type": null,
74
74
  "hidden": false
75
+ },
76
+ {
77
+ "names": [
78
+ "--emit"
79
+ ],
80
+ "dest": "emit",
81
+ "metavar": null,
82
+ "help": "",
83
+ "choices": null,
84
+ "default": null,
85
+ "required": false,
86
+ "nargs": null,
87
+ "is_flag": false,
88
+ "type": null,
89
+ "hidden": false
90
+ },
91
+ {
92
+ "names": [
93
+ "--footprint"
94
+ ],
95
+ "dest": "footprint",
96
+ "metavar": null,
97
+ "help": "",
98
+ "choices": null,
99
+ "default": null,
100
+ "required": false,
101
+ "nargs": null,
102
+ "is_flag": false,
103
+ "type": null,
104
+ "hidden": false
105
+ },
106
+ {
107
+ "names": [
108
+ "--posture"
109
+ ],
110
+ "dest": "posture",
111
+ "metavar": null,
112
+ "help": "",
113
+ "choices": null,
114
+ "default": null,
115
+ "required": false,
116
+ "nargs": null,
117
+ "is_flag": false,
118
+ "type": null,
119
+ "hidden": false
120
+ },
121
+ {
122
+ "names": [
123
+ "--mode"
124
+ ],
125
+ "dest": "mode",
126
+ "metavar": null,
127
+ "help": "",
128
+ "choices": null,
129
+ "default": null,
130
+ "required": false,
131
+ "nargs": null,
132
+ "is_flag": false,
133
+ "type": null,
134
+ "hidden": false
135
+ },
136
+ {
137
+ "names": [
138
+ "--doctrines"
139
+ ],
140
+ "dest": "doctrines",
141
+ "metavar": null,
142
+ "help": "",
143
+ "choices": null,
144
+ "default": null,
145
+ "required": false,
146
+ "nargs": null,
147
+ "is_flag": false,
148
+ "type": null,
149
+ "hidden": false
150
+ },
151
+ {
152
+ "names": [
153
+ "--exclude-rules"
154
+ ],
155
+ "dest": "exclude_rules",
156
+ "metavar": null,
157
+ "help": "",
158
+ "choices": null,
159
+ "default": null,
160
+ "required": false,
161
+ "nargs": null,
162
+ "is_flag": false,
163
+ "type": null,
164
+ "hidden": false
165
+ },
166
+ {
167
+ "names": [
168
+ "--out"
169
+ ],
170
+ "dest": "out",
171
+ "metavar": null,
172
+ "help": "",
173
+ "choices": null,
174
+ "default": null,
175
+ "required": false,
176
+ "nargs": null,
177
+ "is_flag": false,
178
+ "type": null,
179
+ "hidden": false
180
+ },
181
+ {
182
+ "names": [
183
+ "--root"
184
+ ],
185
+ "dest": "root",
186
+ "metavar": null,
187
+ "help": "",
188
+ "choices": null,
189
+ "default": null,
190
+ "required": false,
191
+ "nargs": null,
192
+ "is_flag": false,
193
+ "type": null,
194
+ "hidden": false
195
+ },
196
+ {
197
+ "names": [
198
+ "--config-dir"
199
+ ],
200
+ "dest": "config_dir",
201
+ "metavar": null,
202
+ "help": "the config dir a -global emit writes to (default: the host's resolved one)",
203
+ "choices": null,
204
+ "default": null,
205
+ "required": false,
206
+ "nargs": null,
207
+ "is_flag": false,
208
+ "type": null,
209
+ "hidden": false
75
210
  }
76
211
  ],
77
212
  "mutex": []
@@ -353,7 +488,7 @@
353
488
  },
354
489
  {
355
490
  "name": "git-gate",
356
- "help": "PreToolUse hook: ASK before a history-discarding git act (Law IV) and before every git commit/push (process 5 backstop)",
491
+ "help": "PreToolUse hook: ASK before a history-discarding git act (Deletion Is Deliberate) and before every git commit/push (Consent Before Push backstop)",
357
492
  "description": "",
358
493
  "positionals": [],
359
494
  "options": [
@@ -393,7 +528,7 @@
393
528
  ],
394
529
  "dest": "no_consent",
395
530
  "metavar": null,
396
- "help": "skip the process-5 commit/push ask (set by the emit when the process pack or process 5 is off); Law IV still asks",
531
+ "help": "skip the Consent Before Push commit/push ask (set by the emit when the process pack or that rule is off); Deletion Is Deliberate still asks",
397
532
  "choices": null,
398
533
  "default": false,
399
534
  "required": false,
@@ -631,7 +766,7 @@
631
766
  },
632
767
  {
633
768
  "name": "rule-gate",
634
- "help": "PreToolUse hook: force an ASK before any write to user-rules.md or memory (process 1 backstop)",
769
+ "help": "PreToolUse hook: force an ASK before any write to user-rules.md or memory (Persist Insight backstop)",
635
770
  "description": "",
636
771
  "positionals": [],
637
772
  "options": [
@@ -681,7 +816,21 @@
681
816
  "help": "print the install dashboard as text (theme, mode, counts, memory, version)",
682
817
  "description": "",
683
818
  "positionals": [],
684
- "options": [],
819
+ "options": [
820
+ {
821
+ "names": ["--json"],
822
+ "dest": "json",
823
+ "metavar": null,
824
+ "help": "print the status payload as JSON — every install with its settings and the geneseed build command that rebuilds it",
825
+ "choices": null,
826
+ "default": false,
827
+ "required": false,
828
+ "nargs": "0",
829
+ "is_flag": true,
830
+ "type": null,
831
+ "hidden": false
832
+ }
833
+ ],
685
834
  "mutex": []
686
835
  },
687
836
  {
@@ -700,14 +700,14 @@ function gitDecide(args, payload) {
700
700
  const command = ((payload && payload.tool_input) || {}).command;
701
701
  if (typeof command !== 'string') return 0;
702
702
  if (DESTRUCTIVE_GIT_RE.test(command)) {
703
- return ask(args, 'git-gate', 'law-4', 'Geneseed Law IV \u2014 a history-rewriting or '
703
+ return ask(args, 'git-gate', 'law-4', 'Geneseed (Deletion Is Deliberate) \u2014 a history-rewriting or '
704
704
  + 'discarding git act needs confirmation bound to this specific command');
705
705
  }
706
706
  // `--no-consent`: the process pack (or process 5 alone) is off, so the commit/push ask has
707
707
  // no rule behind it — but Law IV above is universal and has already run.
708
708
  if (args.noConsent || !GIT_GATE_RE.test(command)) return 0;
709
- return ask(args, 'git-gate', 'process-5', 'Geneseed Doctrine process 5 \u2014 every git '
710
- + 'commit/push needs explicit approval');
709
+ return ask(args, 'git-gate', 'process-5', 'Geneseed (Consent Before Push) \u2014 every git '
710
+ + 'commit/push needs explicit approval; to see the change first, use the explain-changes skill');
711
711
  }
712
712
  export const cmdGitGate = guardGate(gitGate, 'git-gate');
713
713
 
@@ -764,13 +764,13 @@ function ruleDecide(args, payload) {
764
764
  const body = [ti.content, ti.new_string, ti.file_text, ti.new_str]
765
765
  .find((v) => typeof v === 'string') || '';
766
766
  if (body && !DOTENV_RE.test(p) && SECRET_RE.test(body)) {
767
- return ask(args, 'rule-gate', 'law-1', `Geneseed Law I — ${p} would carry a `
767
+ return ask(args, 'rule-gate', 'law-1', `Geneseed (Sealed Secrets) — ${p} would carry a `
768
768
  + 'credential-shaped string. Secrets live in .env or a secret manager, never in a '
769
769
  + 'tracked file.');
770
770
  }
771
771
  const target = ruleGateTarget(p, args.root);
772
772
  if (!target) return 0;
773
- return ask(args, 'rule-gate', 'process-1', `Geneseed Doctrine process 1 \u2014 writing to `
773
+ return ask(args, 'rule-gate', 'process-1', `Geneseed (Persist Insight) \u2014 writing to `
774
774
  + `${target}: a standing rule, or a fact to remember? That choice is the user's. Run the `
775
775
  + 'rule skill first.');
776
776
  }
@@ -34,7 +34,7 @@ import { writeText, copyFile, readText } from '../lib/fs.mjs';
34
34
  import { jsonDumps, parseJson, formatValue, formatRepr, isTruthy, isDict } from '../lib/json.mjs';
35
35
 
36
36
  /** Mirrors `_build_core.VENDORED_SKILL_DIRS`. */
37
- export const VENDORED_SKILL_DIRS = new Set(['react-view-transitions', 'daydream', 'token-report']);
37
+ export const VENDORED_SKILL_DIRS = new Set(['react-view-transitions', 'daydream', 'token-report', 'explain-changes']);
38
38
 
39
39
  /**
40
40
  * `PurePath(rel).parts` — split on EITHER separator, empties dropped.