geneseed 3.4.0 → 3.6.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/CHANGELOG.md +316 -0
  2. package/DESIGN.md +11 -14
  3. package/QUICKSTART.md +3 -1
  4. package/README.md +51 -42
  5. package/SETUP.md +25 -27
  6. package/SHIPPED.md +26 -32
  7. package/adapters/bob/README.md +12 -9
  8. package/adapters/copilot/README.md +26 -25
  9. package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
  10. package/adapters/opencode/README.md +2 -2
  11. package/adapters/opencode/plugins/geneseed-context.js +5 -3
  12. package/adapters/opencode/plugins/geneseed-workflow.js +30 -3
  13. package/adapters/opencode/workflows/_runtime.js +97 -7
  14. package/bin/build-driver.mjs +5 -1248
  15. package/bin/geneseed-cli.mjs +11 -6
  16. package/bin/geneseed-hook.mjs +4 -2
  17. package/docs/candidate-hosts.md +1 -1
  18. package/docs/design-history.md +37 -37
  19. package/docs/extending.md +49 -24
  20. package/docs/token-footprint.md +52 -26
  21. package/docs/web/claude-hooks.md +3 -2
  22. package/docs/web/collaboration.md +2 -2
  23. package/docs/web/first-session.md +1 -1
  24. package/docs/web/footprint.md +2 -2
  25. package/docs/web/harnesses.md +1 -1
  26. package/docs/web/model.md +1 -1
  27. package/docs/web/plugin-workflow.md +1 -0
  28. package/docs/web/rules.md +3 -3
  29. package/docs/web/setup-choices.md +2 -2
  30. package/docs/web/themes-author.md +1 -1
  31. package/docs/web-ui.md +18 -10
  32. package/harness.config.json +1 -1
  33. package/js/README.md +6 -5
  34. package/js/build/driver.mjs +1191 -0
  35. package/js/build/emit-claude.mjs +17 -10
  36. package/js/build/emit-common.mjs +4 -4
  37. package/js/build/generate.mjs +1 -1
  38. package/js/build/render.mjs +48 -44
  39. package/js/build/stubs.mjs +1 -1
  40. package/js/build/themes.mjs +4 -4
  41. package/js/build/version.mjs +12 -0
  42. package/js/cli-table.json +21 -6
  43. package/js/hosts/hooks.mjs +89 -74
  44. package/js/hosts/hosts.mjs +19 -2
  45. package/js/hosts/mcp.mjs +14 -20
  46. package/js/hosts/native.mjs +23 -17
  47. package/js/hosts/settings.mjs +52 -104
  48. package/js/inspect/checks-authoring.mjs +117 -3
  49. package/js/inspect/diff.mjs +1 -1
  50. package/js/inspect/doctor.mjs +1 -1
  51. package/js/inspect/excludes.mjs +1 -1
  52. package/js/inspect/scan.mjs +3 -14
  53. package/js/inspect/status.mjs +2 -2
  54. package/js/inspect/validate.mjs +1 -1
  55. package/js/lib/fs.mjs +23 -8
  56. package/js/lib/json.mjs +7 -1
  57. package/js/lib/proc.mjs +6 -4
  58. package/js/maintain/migrate.mjs +1 -1
  59. package/js/maintain/setup.mjs +1 -1
  60. package/js/maintain/uninstall.mjs +17 -15
  61. package/js/ui/cli.mjs +19 -0
  62. package/js/ui/menu.mjs +1 -1
  63. package/js/web/actions.mjs +4 -4
  64. package/js/web/activity.mjs +4 -4
  65. package/js/web/api.mjs +14 -11
  66. package/js/web/daemon.mjs +1 -1
  67. package/js/web/docs.mjs +12 -12
  68. package/js/web/handler.mjs +12 -5
  69. package/js/web/jobs.mjs +1 -1
  70. package/package.json +1 -1
  71. package/src/AGENT.md.tmpl +74 -38
  72. package/src/agents/developer.md +2 -2
  73. package/src/agents/reviewer.md +5 -2
  74. package/src/doctrines/README.md +26 -3
  75. package/src/doctrines/craft.md +78 -5
  76. package/src/doctrines/ops.md +62 -0
  77. package/src/doctrines/process.md +91 -4
  78. package/src/doctrines/rigor.md +40 -0
  79. package/src/laws/universal.md +27 -21
  80. package/src/memory/README.md +4 -2
  81. package/src/modes/foreman.md +5 -5
  82. package/src/notebook/README.md +2 -1
  83. package/src/ontology/universal.md +71 -67
  84. package/src/postures/peer.md +3 -0
  85. package/src/skills/bruno-collection-generator.md +1 -1
  86. package/src/skills/bruno-test-writer.md +2 -2
  87. package/src/skills/daydream/LICENSE +21 -0
  88. package/src/skills/daydream/VENDOR.md +3 -1
  89. package/src/skills/debug.md +1 -1
  90. package/src/skills/document-project.md +127 -25
  91. package/src/skills/geneseed-code-review.md +6 -2
  92. package/src/skills/parallel-agents.md +3 -3
  93. package/src/skills/pipeline.md +4 -4
  94. package/src/skills/react-view-transitions/VENDOR.md +7 -1
  95. package/src/skills/workflow.md +12 -4
  96. package/themes/_TEMPLATE.json +7 -5
  97. package/themes/biker.json +6 -4
  98. package/themes/commentator.json +6 -4
  99. package/themes/cyberpunk.json +6 -4
  100. package/themes/gamer.json +6 -4
  101. package/themes/imperial.json +6 -4
  102. package/themes/joker.json +6 -4
  103. package/themes/marvin.json +6 -4
  104. package/themes/mean.json +6 -4
  105. package/themes/military.json +6 -4
  106. package/themes/neutral.json +7 -5
  107. package/themes/opencode/README.md +2 -2
  108. package/themes/pirate.json +6 -4
  109. package/themes/sports.json +6 -4
  110. package/themes/verstappen.json +6 -4
  111. package/themes/wizard.json +6 -4
  112. package/web/dist/assets/Activity-Flgf4kHy.js +1 -0
  113. package/web/dist/assets/ActivityDetail-DIpr85nh.js +1 -0
  114. package/web/dist/assets/CatalogRow-CjyLjaLb.js +1 -0
  115. package/web/dist/assets/Diff-Deisdha8.js +2 -0
  116. package/web/dist/assets/Docs-DGUdY4l4.js +2 -0
  117. package/web/dist/assets/Doctor-F74lJ69P.js +1 -0
  118. package/web/dist/assets/FilterInput-BakJLvaV.js +1 -0
  119. package/web/dist/assets/GreenhouseView-D072lBiD.js +2 -0
  120. package/web/dist/assets/Harness-DNRP2Va7.js +1 -0
  121. package/web/dist/assets/JournalView-DWFqZ0Iw.js +1 -0
  122. package/web/dist/assets/Laws-BN6g3HU7.js +3 -0
  123. package/web/dist/assets/Library-CDOv4emM.js +1 -0
  124. package/web/dist/assets/Markdown-BQAvN8xx.js +38 -0
  125. package/web/dist/assets/OperatorHudView-CwNIUVX8.js +2 -0
  126. package/web/dist/assets/Profile-CpP9BTne.js +1 -0
  127. package/web/dist/assets/Rules-DFXsUCB7.js +1 -0
  128. package/web/dist/assets/Settings-BqZVUtJ5.js +1 -0
  129. package/web/dist/assets/Skills-Cu6kBgfF.js +1 -0
  130. package/web/dist/assets/StatusBadge-Cg6j-NJL.js +1 -0
  131. package/web/dist/assets/activity-iWaWVtsR.js +1 -0
  132. package/web/dist/assets/drawable-BgfrEBer.js +1 -0
  133. package/web/dist/assets/index-CAqrWhKl.js +2 -0
  134. package/web/dist/assets/index-Ruh93A3A.css +1 -0
  135. package/web/dist/assets/jobBuckets-Dbq8ZlWW.js +1 -0
  136. package/web/dist/assets/lawCats-BLgxl151.js +1 -0
  137. package/web/dist/assets/react-DbkvxWG5.js +8 -0
  138. package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +1 -0
  139. package/web/dist/index.html +4 -3
  140. package/web/src/pages/Laws.jsx +22 -8
  141. package/src/skills/react-view-transitions/README.md +0 -42
  142. package/web/dist/assets/Activity-DMGhhjQc.js +0 -1
  143. package/web/dist/assets/ActivityDetail-CeQcDA7e.js +0 -1
  144. package/web/dist/assets/CatalogRow-CnSbDPyg.js +0 -1
  145. package/web/dist/assets/Diff-D_ksMBD1.js +0 -5
  146. package/web/dist/assets/Doctor-Brb3_oCT.js +0 -1
  147. package/web/dist/assets/FilterInput-DbRWPhtT.js +0 -1
  148. package/web/dist/assets/GreenhouseView-F3VNFr73.js +0 -2
  149. package/web/dist/assets/Harness-DJSwfYeF.js +0 -1
  150. package/web/dist/assets/JournalView-BLRYRnB3.js +0 -1
  151. package/web/dist/assets/Laws-CKYns9qe.js +0 -3
  152. package/web/dist/assets/Library-BK0YloSv.js +0 -1
  153. package/web/dist/assets/Markdown-BzTRzc7a.js +0 -46
  154. package/web/dist/assets/OperatorHudView-D3OQSGp5.js +0 -2
  155. package/web/dist/assets/Profile-CPA2u_0g.js +0 -1
  156. package/web/dist/assets/Rules-Lukel-Ru.js +0 -1
  157. package/web/dist/assets/Skills-BKRLHH7m.js +0 -1
  158. package/web/dist/assets/StatusBadge-DI3weRKd.js +0 -1
  159. package/web/dist/assets/activity-C_H2Bx1P.js +0 -1
  160. package/web/dist/assets/index-B7_Wga23.js +0 -2
  161. package/web/dist/assets/index-DzdQjvM1.css +0 -1
  162. package/web/dist/assets/index-N8dTrVro.js +0 -1
  163. package/web/dist/assets/index-Py_IQjgK.js +0 -10
  164. package/web/dist/assets/jobBuckets-DfTUJUxR.js +0 -1
  165. package/web/dist/assets/lawCats-DUS9UTH4.js +0 -1
  166. package/web/dist/assets/react-DOpiTn5v.js +0 -32
@@ -54,7 +54,7 @@
54
54
  // table below now holds one LOADER per row (`() => import(…).then(m => m.cmdX)`), so a verb
55
55
  // loads its slice of the tree and no other verb's. The spawn allow-list gate still sees the
56
56
  // whole graph: `tests/unit/hook_cli.test.mjs`'s `importClosure` follows both import spellings.
57
- import { cliSpec, printHelp } from '../js/ui/cli.mjs';
57
+ import { cliSpec, printHelp, printVerbList } from '../js/ui/cli.mjs';
58
58
  import { parseIntStrict } from '../js/lib/text.mjs';
59
59
  import { printErr } from '../js/lib/fs.mjs';
60
60
 
@@ -279,10 +279,15 @@ async function main(argv) {
279
279
  // 'error' event and a stack trace.
280
280
  process.stdout.on('error', (e) => { if (e.code === 'EPIPE') process.exit(0); });
281
281
 
282
+ // Bare = `home`, as both launchers (`geneseed`, `geneseed.cmd`) already did — but the npm
283
+ // `bin` names THIS file, so a bare `geneseed` off PATH used to die asking for a verb.
284
+ if (!argv.length) argv = ['home'];
282
285
  const verb = argv[0];
283
- if (!verb || verb === '-h' || verb === '--help') {
284
- return die(2, `the following arguments are required: cmd (one of ${
285
- Object.keys(VERBS).join(', ')})`);
286
+ if (verb === '-h' || verb === '--help') {
287
+ return printVerbList('geneseed', [...Object.keys(VERBS), 'validate'], {
288
+ update: 'an alias of upgrade',
289
+ validate: "check the source tree without emitting (the generator's --validate-only)",
290
+ });
286
291
  }
287
292
  // `validate` — the generator's `--validate-only`, and the one verb this entry answers that is NOT
288
293
  // in the table above. It is dispatched here, before the lookup, for a reason that is a
@@ -302,7 +307,7 @@ async function main(argv) {
302
307
  // Lazy for the same reason the table rows are — `parseDriverArgs` alone drags the whole
303
308
  // generator driver in, and every OTHER verb was paying for it.
304
309
  const [{ cmdValidate }, { parseDriverArgs }] = await Promise.all([
305
- import('../js/inspect/validate.mjs'), import('./build-driver.mjs'),
310
+ import('../js/inspect/validate.mjs'), import('../js/build/driver.mjs'),
306
311
  ]);
307
312
  try {
308
313
  return cmdValidate(parseDriverArgs(argv.slice(1)));
@@ -319,7 +324,7 @@ async function main(argv) {
319
324
  // just about to become false — it was already claiming a third place for verbs that do
320
325
  // not exist.
321
326
  return die(2, `invalid choice: '${verb}'. This entry point carries only `
322
- + `${Object.keys(VERBS).join(', ')}; the four hook verbs live in `
327
+ + `${Object.keys(VERBS).join(', ')}; the five hook verbs live in `
323
328
  + 'bin/geneseed-hook.mjs.');
324
329
  }
325
330
  // `js/cli-table.json`, the CLI as data. A verb this table dispatches and the file does not
@@ -48,7 +48,7 @@ import { printErr } from '../js/lib/fs.mjs';
48
48
  const HOSTED = { '--root': 'root', '--host': 'host' };
49
49
  const VERBS = {
50
50
  context: { fn: cmdContext, flags: HOSTED },
51
- 'git-gate': { fn: cmdGitGate, flags: HOSTED },
51
+ 'git-gate': { fn: cmdGitGate, flags: HOSTED, switches: { '--no-consent': 'noConsent' } },
52
52
  'rule-gate': { fn: cmdRuleGate, flags: HOSTED },
53
53
  'tool-gate': { fn: cmdToolGate, flags: HOSTED },
54
54
  learn: {
@@ -72,7 +72,9 @@ function die(code, msg) {
72
72
  * positional.
73
73
  */
74
74
  function parse(spec, argv) {
75
- const args = { root: null, host: null, memory: null, consolidate: false, file: null };
75
+ const args = {
76
+ root: null, host: null, memory: null, consolidate: false, noConsent: false, file: null,
77
+ };
76
78
  for (let i = 0; i < argv.length; i += 1) {
77
79
  const tok = argv[i];
78
80
  const eq = tok.indexOf('=');
@@ -36,7 +36,7 @@ current host gets. Skills are the largest surface by file count and they cost no
36
36
  - **Codex CLI — cheapest, and the only one with an ask tier.** Its hook schema is
37
37
  Claude's with `hooks.json`/`config.toml` as the carrier, its payload field names are
38
38
  Claude's, and `permissionDecision: "ask"` exists. The Claude engine with `host: "codex"`
39
- covers instructions (`AGENTS.md`), skills (`.agents/skills`) and the four hook verbs
39
+ covers instructions (`AGENTS.md`), skills (`.agents/skills`) and the hook verbs
40
40
  unchanged; the only new code is the carrier writer (a `hooks.json` merge or a TOML
41
41
  table) and the config-dir resolver (`~/.codex`, `$CODEX_HOME`). Roughly the Copilot
42
42
  port minus the dialect work.
@@ -4,7 +4,7 @@
4
4
 
5
5
  **The Node port, P2 → P10 (2026-06 → 2026-08). A record, not a spec.**
6
6
 
7
- [← Back to DESIGN.md](../DESIGN.md) · [What this tool does not prove](limits.md) · [What it costs to add one thing](extending.md)
7
+ [← Back to DESIGN.md](../DESIGN.md) · [What it costs to add one thing](extending.md)
8
8
 
9
9
  </div>
10
10
 
@@ -31,7 +31,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
31
31
  ---
32
32
 
33
33
  ## ⚙️ Generator contract — the port narrative
34
- - **The render core has a Node twin: [`js/build/render.mjs`](js/build/render.mjs).** It is a
34
+ - **The render core has a Node twin: [`js/build/render.mjs`](../js/build/render.mjs).** It is a
35
35
  translation of `_build_render.py`'s pure pipeline, byte-identical by test rather than by
36
36
  intent — `tests/test_render_parity.py` renders both over every theme × footprint ×
37
37
  catalog × laws-prefix × posture × mode and compares the written trees byte for byte,
@@ -39,7 +39,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
39
39
  sort. `js/lib/fs.mjs` holds the primitives where the two runtimes disagree in silence;
40
40
  `Path.write_text`'s `\n` → `os.linesep` translation is the load-bearing one. Zero
41
41
  dependencies on that side too.
42
- - **The host-native layer has one too: [`js/hosts/native.mjs`](js/hosts/native.mjs)** — the point where
42
+ - **The host-native layer has one too: [`js/hosts/native.mjs`](../js/hosts/native.mjs)** — the point where
43
43
  RENDER stops being a pure function of `src/`. Its output depends on your
44
44
  `agent-overrides.json`, on which files already exist in the target (claim-on-create never
45
45
  overwrites a file Geneseed did not write), and on which of the three host dialects is
@@ -49,7 +49,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
49
49
  items and compares the written tree byte for byte, the returned ownership list *in order*,
50
50
  and the warning stream — and asserts the Node side printed nothing at all on stdout,
51
51
  because the emitted hook gates signal their verdict there and return 0 on every path.
52
- - **The OpenCode extras are ported too: [`js/hosts/opencode.mjs`](js/hosts/opencode.mjs)** — the
52
+ - **The OpenCode extras are ported too: [`js/hosts/opencode.mjs`](../js/hosts/opencode.mjs)** — the
53
53
  branded and curated colour themes, the opt-in primary agent and slash-command layer, the
54
54
  always-on `/ponytail` switch, the verbatim plugin/workflow copies, and the
55
55
  `agent-overrides.json` stub with its staleness notice. Half of it sits behind
@@ -61,7 +61,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
61
61
  streams** — the staleness notice goes to stdout where every native-layer warning goes to
62
62
  stderr, an asymmetry inherited from the Python rather than corrected.
63
63
  - **The seam is now a real process boundary: `build.py` spawns Node once per emit.**
64
- [`js/build/emit.mjs`](js/build/emit.mjs) writes the bundle (`build`) and the OpenCode layer's RENDER
64
+ `js/build/emit.mjs` writes the bundle (`build`) and the OpenCode layer's RENDER
65
65
  stage; Python keeps WIRE, PRUNE, MANIFEST and VERIFY, and drives — so doctor, web deploy,
66
66
  setup and rebuild-all keep calling `build.emit_*` in-process with no call site changed.
67
67
  Node falls back to Python when `node` is missing, silently and by design: the whole claim
@@ -121,7 +121,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
121
121
  (with the bundle in a subfolder, so `--out` and the target are not the same directory), a
122
122
  `.claude/` the user got to first, and every write-once file edited *between* two emits —
123
123
  the only way a byte comparison can tell "kept your store" from "re-seeded it".
124
- - **The wiring layer has a Node twin as well: [`js/hosts/settings.mjs`](js/hosts/settings.mjs)** — the
124
+ - **The wiring layer has a Node twin as well: [`js/hosts/settings.mjs`](../js/hosts/settings.mjs)** — the
125
125
  JSONC reader, the `opencode.json` and `settings.json` merges, the hook shim, the
126
126
  managed-block machinery and the settings integrity check. It was proven before it was
127
127
  wired, the way every piece before it was; the emit now drives it. It was the last unit to
@@ -203,7 +203,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
203
203
  writes — a write through the facade cannot be restored reliably, and a redirect that
204
204
  silently outlives its test poisons every later read. A bare `SRC` left in a submodule
205
205
  raises `NameError` rather than quietly reading a stale copy; that noise is the point.
206
- - **There is a second driver: [`bin/geneseed.mjs`](bin/geneseed.mjs), and it is ADDED beside
206
+ - **There is a second driver: `bin/geneseed.mjs`, and it is ADDED beside
207
207
  `main()` rather than replacing it.** `build.py` is also the `import build` facade that 19
208
208
  `rituals/` modules read 55 distinct names from, and roughly eleven sites spawn it as a
209
209
  subprocess; none of them flip, because flipping them would make Node mandatory for the
@@ -336,7 +336,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
336
336
  - **The runtime port starts with the four HOOK verbs, and with a gate rather than with
337
337
  code.** `context`, `git-gate`, `rule-gate` and `learn` are the four commands an emitted
338
338
  `settings.json` actually invokes, and they now have a Node twin:
339
- [`js/hosts/hooks.mjs`](js/hosts/hooks.mjs) behind [`bin/geneseed-hook.mjs`](bin/geneseed-hook.mjs), a
339
+ [`js/hosts/hooks.mjs`](../js/hosts/hooks.mjs) behind [`bin/geneseed-hook.mjs`](../bin/geneseed-hook.mjs), a
340
340
  **second binary** rather than a subcommand of the generator driver — `learn` must spawn
341
341
  whatever `$GENESEED_LLM` names, and `bin/geneseed.mjs` is under a hard `child_process`
342
342
  ban that is half the proof it is not a passthrough. The hook entry loads none of the
@@ -345,7 +345,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
345
345
  to import `build` for the marker filenames they need.
346
346
  **A cell is not an emit here, and that is the phase's first design question.** A verb's
347
347
  observable surface is stdout, stderr, the **exit code** and whatever it wrote, so
348
- [`tests/helpers/cli_golden.mjs`](tests/helpers/cli_golden.mjs) compares all four across 103 seeded
348
+ [`tests/helpers/cli_golden.mjs`](../tests/helpers/cli_golden.mjs) compares all four across 103 seeded
349
349
  worlds — reusing `golden.py`'s `cell_env`, `_normalise`, `_snapshot` and `_split` rather
350
350
  than growing a second set of rules about sandboxes and CRLF. `cwd` is an **input** to
351
351
  three of the four (discovery, the sovereign bypass, and learn's memory search all read
@@ -417,7 +417,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
417
417
  transitive relative imports instead — the same assertion, one level out, added because of
418
418
  this phase.
419
419
  - **`status` and `version` cross, and the phase's method is a CORPUS beside the matrix.**
420
- [`js/inspect/status.mjs`](js/inspect/status.mjs) answers both from `bin/geneseed-cli.mjs`. Two structural
420
+ [`js/inspect/status.mjs`](../js/inspect/status.mjs) answers both from `bin/geneseed-cli.mjs`. Two structural
421
421
  things came with them.
422
422
  **A closure walk counts what the callee COMPUTES; the caller consumes a fraction of it.**
423
423
  An `ast` walk puts `status` at 527 LOC across 7 modules — `_tui_inventory` returns every
@@ -441,7 +441,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
441
441
  the handoff offered.** `_color_enabled()` is `sys.stdout.isatty()`, every harness captures
442
442
  through a pipe, so deleting every escape code would be byte-identical across the whole
443
443
  matrix — ship it ungated or drop it and regress a real terminal. `_status_lines` is
444
- documented pure, so [`tests/snapshot/pure_snapshot.test.mjs`](tests/snapshot/pure_snapshot.test.mjs)
444
+ documented pure, so `tests/snapshot/pure_snapshot.test.mjs`
445
445
  calls it directly on both sides over a corpus of dashboards × colour × ASCII, and the tty
446
446
  question never arises. Three more cell-unreachable functions ride along for the same
447
447
  reason — `_version_verdict`'s up-to-date branch, `_manifest_is_claude` (only consulted for
@@ -459,8 +459,8 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
459
459
  now has one owner for the primitive instead of two.
460
460
  - **The first NON-hook verb crosses, and it needed a third binary rather than a row in an
461
461
  existing table.** `harness exclude add|remove|list` — the sovereign-repo exclusions —
462
- is now [`js/inspect/excludes.mjs`](js/inspect/excludes.mjs) behind
463
- [`bin/geneseed-cli.mjs`](bin/geneseed-cli.mjs). The *reader* had been ported since P5a
462
+ is now [`js/inspect/excludes.mjs`](../js/inspect/excludes.mjs) behind
463
+ [`bin/geneseed-cli.mjs`](../bin/geneseed-cli.mjs). The *reader* had been ported since P5a
464
464
  (`sovereignBypass` runs on every hook call); this is the writer, which maintains
465
465
  `excludes.json` across every global install and wires each host's native per-repo
466
466
  suppression into the excluded repo.
@@ -492,7 +492,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
492
492
  atomic (temp + rename) on both sides, so a concurrent pair loses an edit rather than
493
493
  tearing the file.
494
494
  - **`build`, `prompt` and `theme` cross — the generator's own CLI face —** as
495
- [`js/build/generate.mjs`](js/build/generate.mjs). **Ten of the 24 subcommands are now Node.** Three
495
+ [`js/build/generate.mjs`](../js/build/generate.mjs). **Ten of the 24 subcommands are now Node.** Three
496
496
  things are worth recording, and the first is about measurement rather than code.
497
497
  **A closure walk cannot see through a `subprocess`, and `build` is nothing but one.**
498
498
  `cmd_build` measures three lines because the work is `run([sys.executable, BUILD])`. Its
@@ -524,7 +524,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
524
524
  `test_the_fence_corpus_still_describes_the_real_tree` re-derives it and fails the day a
525
525
  source file grows a four-backtick run — at which point a cell becomes writable and should
526
526
  be written. The corpus file is renamed to
527
- [`tests/snapshot/pure_snapshot.test.mjs`](tests/snapshot/pure_snapshot.test.mjs) with it: it was
527
+ `tests/snapshot/pure_snapshot.test.mjs` with it: it was
528
528
  named for the status panel when the panel was all it held.
529
529
  **Three of the six verbs the phase was scoped around did not cross, each for a measured
530
530
  reason.** `sync-self` is a five-line alias whose body is `_update.sync_self` → `upgrade()`,
@@ -536,16 +536,16 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
536
536
  interpreter using the discovery the shim flip deleted; and a shim naming
537
537
  `bin/geneseed-cli.mjs` instead would answer three of the 24 verbs. The npm `bin` map is
538
538
  what replaces them, so they belong to the publish phase and not to this one.
539
- - **`diff` and `rebuild-all` cross** as [`js/inspect/diff.mjs`](js/inspect/diff.mjs) and `cmdRebuildAll` in
540
- [`js/build/generate.mjs`](js/build/generate.mjs), with the install detectors extracted to
541
- [`js/hosts/installs.mjs`](js/hosts/installs.mjs) and the registry to [`js/inspect/registry.mjs`](js/inspect/registry.mjs).
539
+ - **`diff` and `rebuild-all` cross** as [`js/inspect/diff.mjs`](../js/inspect/diff.mjs) and `cmdRebuildAll` in
540
+ [`js/build/generate.mjs`](../js/build/generate.mjs), with the install detectors extracted to
541
+ [`js/hosts/installs.mjs`](../js/hosts/installs.mjs) and the registry to [`js/inspect/registry.mjs`](../js/inspect/registry.mjs).
542
542
  **Twelve of the 24 subcommands are now Node.**
543
543
  **A closure walk cannot see through the standard library either.** An `ast` walk puts
544
544
  `diff` at 252 LOC of marker and manifest reading, and the verb's *entire* user-visible
545
545
  payload is `difflib.unified_diff`, which the walk counts as zero because `difflib` is not in
546
546
  `rituals/`. There is no lazier answer than reproducing it: a generic JS diff produces a
547
547
  correct diff and a *different* one, and what the acceptance matrix compares is bytes. So
548
- [`js/lib/udiff.mjs`](js/lib/udiff.mjs) is `SequenceMatcher` and `unified_diff`, including
548
+ [`js/lib/udiff.mjs`](../js/lib/udiff.mjs) is `SequenceMatcher` and `unified_diff`, including
549
549
  `autojunk` — the popular-line purge that engages at 200 elements and rewrites the hunks of
550
550
  every real harness file, and which no fixture is large enough to reach.
551
551
  **The rest of the diff is gated by cells and the algorithm by a corpus**, with the
@@ -582,10 +582,10 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
582
582
  does not exist yet: copy the checkout into the sandbox, plant one fault, and run *both*
583
583
  binaries from the copy so their two `ROOT`s move together. That is the next phase's first
584
584
  design decision rather than a corner of this one.
585
- - **`doctor` crosses** as [`js/inspect/doctor.mjs`](js/inspect/doctor.mjs), and the fixture came first.
585
+ - **`doctor` crosses** as [`js/inspect/doctor.mjs`](../js/inspect/doctor.mjs), and the fixture came first.
586
586
  **Thirteen of the 24 subcommands are now Node.**
587
587
  **The copy-the-checkout fixture, measured rather than feared.** `_copy_checkout` in
588
- [`tests/helpers/cli_golden.mjs`](tests/helpers/cli_golden.mjs) gives each cell its own copy of the
588
+ [`tests/helpers/cli_golden.mjs`](../tests/helpers/cli_golden.mjs) gives each cell its own copy of the
589
589
  working tree (`git ls-files --cached --others --exclude-standard`), plants one fault in it,
590
590
  and runs *both* binaries from the copy so the two `ROOT`s move together. 511 files, 5.2 MB,
591
591
  **0.31 s** against a `doctor` run of ~2.1 s — a seventh of the cell, so it is one copy per
@@ -621,7 +621,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
621
621
  indistinguishable), and one of the mutations is aimed at the FIXTURE: with `_copy_checkout`
622
622
  planting nothing, a planted-fault cell does not go red, it goes *vacuous* — which is the
623
623
  absolute half of a cell doing its job one layer further out than it ever has.
624
- - **`uninstall` crosses** as [`js/maintain/uninstall.mjs`](js/maintain/uninstall.mjs), and it is the first verb
624
+ - **`uninstall` crosses** as [`js/maintain/uninstall.mjs`](../js/maintain/uninstall.mjs), and it is the first verb
625
625
  in the port that **deletes**. **Fourteen of the 24 subcommands are now Node**; of the ten
626
626
  left, `setup`/`menu`/`home` are P5's and the rest belong to P6–P10.
627
627
  **A deletion needs the gate to prove what SURVIVED.** Every cell before this one asserts
@@ -660,7 +660,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
660
660
  question about the corpus of inputs before it is a question about the code. The control had to
661
661
  be re-chosen too; the first one shared mechanism with half the mutations and reported four
662
662
  over-reaches that were nothing of the kind.
663
- - **`setup` crosses** as [`js/maintain/setup.mjs`](js/maintain/setup.mjs), and it is the first **interactive**
663
+ - **`setup` crosses** as [`js/maintain/setup.mjs`](../js/maintain/setup.mjs), and it is the first **interactive**
664
664
  verb in the port. **Fifteen of the 24 subcommands are now Node**, and P5 is done: the nine
665
665
  left belong to P6–P10.
666
666
  **`cmd_setup` is a dispatcher, and that is the phase's first finding.** It is 21 lines: a
@@ -717,7 +717,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
717
717
  - **The web console's HTTP shell crosses, and it needed a third acceptance harness.**
718
718
  `tests/golden.py` compares the tree a generator writes; `tests/harness_golden.py` compares
719
719
  one verb invocation on stdout/stderr/exit/files. An endpoint writes nothing and never
720
- exits, so [`tests/helpers/web_fixture.mjs`](tests/helpers/web_fixture.mjs) asks a different question: one
720
+ exits, so [`tests/helpers/web_fixture.mjs`](../tests/helpers/web_fixture.mjs) asks a different question: one
721
721
  seeded world, one **sequence of requests** against a freshly started server, compared on
722
722
  the status line, the response body as bytes, the five headers the handler *chooses*, the
723
723
  daemon record, and the server's own streams. **Both sides are real processes**, started on
@@ -757,7 +757,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
757
757
  - **Eight web read endpoints cross, and six of them were a JSON face over work already
758
758
  done.** `/api/overview`, `/api/themes`, `/api/doctor`, `/api/diff`, `/api/installs`,
759
759
  `/api/excludes`, `/api/setup` and `/api/profile` now answer from
760
- [`js/web/api.mjs`](js/web/api.mjs) as well as from `rituals/_web_*.py`. **No subcommand
760
+ [`js/web/api.mjs`](../js/web/api.mjs) as well as from `rituals/_web_*.py`. **No subcommand
761
761
  crosses here and the count stays fifteen** — `web` the verb is P6h's, and until then the
762
762
  only caller of this code is the acceptance harness. `api_setup` is `statusData()` plus
763
763
  four fields; `api_diff` is `diffCollect()` reshaped; `api_excludes` is
@@ -788,7 +788,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
788
788
  seconds apart — so the harness normalises them, which means a twin formatting in UTC, or
789
789
  padding wrong, would compare equal in every web cell. The stamp is a pure function of an
790
790
  epoch second and the local zone, so it is gated in
791
- [`tests/snapshot/pure_snapshot.test.mjs`](tests/snapshot/pure_snapshot.test.mjs) over five
791
+ `tests/snapshot/pure_snapshot.test.mjs` over five
792
792
  instants chosen for what they break: single-digit month/day/hour/minute, both sides of a
793
793
  UTC date rollover, a DST transition, and a fractional second.
794
794
  **Two gates, two findings, and neither came from review.** The harness's own self-check
@@ -821,8 +821,8 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
821
821
  the control — without which every 501 in the list is vacuous.
822
822
  - **The catalog crosses, and the taxonomy it needed was already ported.**
823
823
  `/api/catalog/<section>`, `/api/item/<type>/<name>` and the wiki reader answer from
824
- [`js/web/api.mjs`](js/web/api.mjs), over a new
825
- [`js/inspect/inventory.mjs`](js/inspect/inventory.mjs) — `_harness_tui.py`'s catalog half. **Still
824
+ [`js/web/api.mjs`](../js/web/api.mjs), over a new
825
+ [`js/inspect/inventory.mjs`](../js/inspect/inventory.mjs) — `_harness_tui.py`'s catalog half. **Still
826
826
  fifteen subcommands; still no call site until P6h.**
827
827
  **The measurement is the finding.** `js/inspect/status.mjs` had said since P5d that "the ~111
828
828
  lines of TUI taxonomy" were P7's and not ported. `LAW_CLASS`, `SKILL_CLASS`,
@@ -858,7 +858,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
858
858
  cell whose target does not exist proves nothing.** Closed by seeding files that ARE there
859
859
  and naming, in each cell, the content that must not come back.
860
860
  - **The Docs pages cross for 57 of 59, and the two that do not are declared rather than
861
- discovered.** [`js/web/docs.mjs`](js/web/docs.mjs) answers `/api/docs` and
861
+ discovered.** [`js/web/docs.mjs`](../js/web/docs.mjs) answers `/api/docs` and
862
862
  `/api/docs/page/<id>` for the `markdown`, `concept` and `glossary` kinds — the harness-
863
863
  block stripping, the slug rules, the section slicing, the live-count substitution and the
864
864
  theme-aware glossary. **Still fifteen subcommands.** The measurement that scoped it is
@@ -878,7 +878,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
878
878
  Python's `\s` on a `str` pattern is not JavaScript's, and the standing note read "differs
879
879
  only at U+FEFF". Measured: `python \s = js \s − U+FEFF + U+001C..U+001F + U+0085`. A
880
880
  heading carrying an information separator slugged differently on each side. `PY_SPACE`
881
- in [`js/lib/fs.mjs`](js/lib/fs.mjs) is the measured set, and no cell could have found
881
+ in [`js/lib/fs.mjs`](../js/lib/fs.mjs) is the measured set, and no cell could have found
882
882
  it — nobody has written that heading.
883
883
  **Twelve mutations, eleven fire, six of them on the corpus rather than a cell** — which
884
884
  is what "a corpus phase" means in practice. The survivor is honest: dropping a blank
@@ -886,8 +886,8 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
886
886
  the empty string and a missing value reach the same fallback. Indistinguishable through
887
887
  its one consumer, not unreachable, and kept for the reason the POST body drain was.
888
888
  - **The capability graph and the live-activity surface cross, and the spawn the plan warned
889
- about was never there.** [`js/web/graph.mjs`](js/web/graph.mjs) and
890
- [`js/web/activity.mjs`](js/web/activity.mjs) answer `/api/graph`, `/api/activity` (GET)
889
+ about was never there.** `js/web/graph.mjs` and
890
+ [`js/web/activity.mjs`](../js/web/activity.mjs) answer `/api/graph`, `/api/activity` (GET)
891
891
  and `/api/activity/<sid>` — **22 of 29 web paths, still fifteen subcommands.** The plan
892
892
  scored the pid probe as a `tasklist` spawn needing a sixth allow-list row; the source is
893
893
  `ctypes` over `OpenProcess`, so **neither implementation spawns anything** and
@@ -912,7 +912,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
912
912
  **Eighteen mutations, eighteen fire, no survivors** — two of them visible only in the file
913
913
  snapshot, because the response body is byte-identical with the unlink removed.
914
914
  - **The web console starts WRITING, and the argv-in-a-response-body trap had a false
915
- premise.** [`js/web/actions.mjs`](js/web/actions.mjs) and [`js/hosts/mcp.mjs`](js/hosts/mcp.mjs) answer
915
+ premise.** [`js/web/actions.mjs`](../js/web/actions.mjs) and [`js/hosts/mcp.mjs`](../js/hosts/mcp.mjs) answer
916
916
  the eight mutating POSTs that own their own path plus the `/api/rules` and `/api/mcp` GETs
917
917
  that had to cross with them — **27 of 29 web paths, still fifteen subcommands.** The plan
918
918
  called `api_install_cmd`'s `{"cmd": [sys.executable, "build.py", …]}` "a Python argv, as
@@ -941,7 +941,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
941
941
  not be closed by any cell, so the 409 column itself became the thing under test, read out of
942
942
  the reference by `ast` and compared against the table `doPost` dispatches on.
943
943
  - **The console runs BACKGROUND JOBS, and the port's one sanctioned spawn is argued rather
944
- than assumed.** [`js/web/jobs.mjs`](js/web/jobs.mjs) answers the last two web paths —
944
+ than assumed.** [`js/web/jobs.mjs`](../js/web/jobs.mjs) answers the last two web paths —
945
945
  **29 of 29, still fifteen subcommands** — and it is the only module in the port allowed to
946
946
  start a copy of *this program*. **Isolation is the discriminator and it is a property of the
947
947
  RUNTIME, not of the verb**: the reference's daemon is threaded, Node's server is
@@ -975,7 +975,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
975
975
  get one writes a file whose NAME carries a clock. **A destamp that only reaches inside files
976
976
  leaves the filename as an ungateable axis.**
977
977
  - **The `web` VERB crosses — sixteen of 24 subcommands — and the TEARDOWN decided which cells
978
- exist.** [`js/web/server.mjs`](js/web/server.mjs) grows the daemon lifecycle in one file, as
978
+ exist.** [`js/web/server.mjs`](../js/web/server.mjs) grows the daemon lifecycle in one file, as
979
979
  the reference keeps it: `_probe`, `_live_daemon`, `_spawn_detached`, `start|stop|status|
980
980
  restart`, `_build_plan`'s consumer, `_npm_build`, the browser open, `cmd_web`, and the last
981
981
  POST — `/api/restart`. `bin/geneseed-cli.mjs` finally imports the web tree, which is what
@@ -1009,7 +1009,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
1009
1009
  every non-interactive run; the reference's `input()` raises there. The byte reader now
1010
1010
  reports that case as null, gated by a stdin-seeded corpus.
1011
1011
  - **The install TOGGLE crosses and the web console is whole — 29 of 29 paths ANSWER, still
1012
- sixteen subcommands.** [`js/maintain/uninstall.mjs`](js/maintain/uninstall.mjs) gains the reversible half it was
1012
+ sixteen subcommands.** [`js/maintain/uninstall.mjs`](../js/maintain/uninstall.mjs) gains the reversible half it was
1013
1013
  written expecting: deactivate MOVES every owned artifact into a sibling stash and drops the
1014
1014
  wiring, reactivate moves the same bytes back, and the stash directory's PRESENCE is the
1015
1015
  disabled flag — no recorded state that could drift from the filesystem. `/api/pick-folder` is
@@ -1038,7 +1038,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
1038
1038
  default the hook shim's runner and entry, so the Claude reactivate answered with that refusal
1039
1039
  on its first Node run instead of quietly wiring hooks at an invented path.
1040
1040
  - **`upgrade` crosses — seventeen of 24 — and the phase's real work was a fixture that could
1041
- WRITE.** [`js/maintain/update.mjs`](js/maintain/update.mjs) is `rituals/_update.py`'s upgrade half: the
1041
+ WRITE.** [`js/maintain/update.mjs`](../js/maintain/update.mjs) is `rituals/_update.py`'s upgrade half: the
1042
1042
  preflight, the streamed fetch, the four upstream classifications, the fast-forward, the
1043
1043
  doctor gate with its exact rollback, and the two rebuilds. Every other verb in this port
1044
1044
  READS the checkout; this one rewrites it, so its cells needed a git repository they could
package/docs/extending.md CHANGED
@@ -32,7 +32,7 @@ Three seams, and almost every question about "where do I edit?" resolves to one
32
32
  Two rules follow from the table and they cause most of the surprises:
33
33
 
34
34
  - **Nothing propagates by itself.** A bundle is a *render*. Editing `src/` changes nothing on any
35
- machine until something re-emits (`js/build/emit.mjs:362` re-fingerprints, `js/inspect/status.mjs:80`
35
+ machine until something re-emits (`sourceFingerprint` in `js/build/version.mjs` re-fingerprints, `js/inspect/status.mjs`
36
36
  reports the drift).
37
37
  - **`docs/web/` is data, `web/src/` is code.** A new documentation page is one file and no
38
38
  rebuild. A new screen is a new hashed chunk and a mandatory `web/dist` commit.
@@ -58,10 +58,10 @@ badge keys `agents`/`skills`/`laws`/`themes`/`plugins`; `proseMirrorProblems` ho
58
58
 
59
59
  ---
60
60
 
61
- ## 1 — The baseline, and the five commands that reproduce CI
61
+ ## 1 — The baseline, and the seven commands that reproduce CI
62
62
 
63
- Verified on this checkout: `doctor --all` green on 14 themes; the Node suite reports
64
- **1033 tests, 1029 pass, 4 skipped, 0 fail**.
63
+ The baseline is whatever these commands report on your checkout — run them; no count is
64
+ written here, because a typed count is stale by the next commit.
65
65
 
66
66
  CI is **four jobs expanding to six runs** (`.github/workflows/ci.yml`): `validate` and
67
67
  `node-cells` on Linux *and* Windows, plus `package-no-python` and `web` on Linux. From the repo
@@ -83,6 +83,10 @@ node tests/golden.mjs --idempotent
83
83
  node tests/golden.mjs --deletion
84
84
  ```
85
85
 
86
+ ```bash
87
+ node tests/golden.mjs --cli
88
+ ```
89
+
86
90
  ```bash
87
91
  node tests/mutate.mjs --verify
88
92
  ```
@@ -100,6 +104,11 @@ Notes that matter:
100
104
  - `--test-reporter=tap` is not decoration. Both workflows gate on the *count* (`# tests N ≥ 500`)
101
105
  because `node --test` over a glob that matches nothing prints `# tests 0` and **exits 0**. Node's
102
106
  default reporter changed between majors, which is how a green 1043-test suite once reported zero.
107
+ - `--cli` runs the ~320-cell CLI/hook matrix against the expectations written in each cell —
108
+ the only end-to-end gate on what a verb prints. CI runs it in `node-cells` on both platforms.
109
+ A cell that needs a broken checkout plants its fault as an **edit of the real file**
110
+ (`{ "edit": [[find, replace]] }` in `tests/helpers/matrix/cli.*.json`), never as a copy of it;
111
+ a `find` that stops matching exactly once fails the cell loudly.
103
112
  - `--deletion` is the only thing that exercises the write-before-delete prune. Anyone renaming or
104
113
  removing a plugin, a skill or an emitted file needs it.
105
114
  - **A local pass is weaker than CI.** ~18 tests in `tests/unit/package_manifest.test.mjs` carry
@@ -107,12 +116,12 @@ Notes that matter:
107
116
  on PATH they report *skipped* and the run is green.
108
117
  - CI additionally re-runs the packaging suite under `npm@latest` (Linux, last in the job) and
109
118
  packs the tarball into a `node:22-slim` container with no interpreter. Neither is reproducible
110
- from the five commands; the container one is reachable locally through
119
+ from the seven commands; the container one is reachable locally through
111
120
  `tests/helpers/no-python-container.sh` with a docker daemon.
112
121
  - **Two assertions in `tests/unit/claude.test.mjs` silently stand down** on any machine where an
113
122
  ancestor of the temp sandbox is a `.claude`/`.bob` install — which is every developer machine
114
123
  with Claude Code installed under `~`. They are replaced by a `t.diagnostic`, so the TAP
115
- `# skipped` count does **not** move. Locally you read `1035/1031/0/4` and see no sign of it.
124
+ `# skipped` count does **not** move, and a local run shows no sign of it.
116
125
 
117
126
  ### The gates CI does not run
118
127
 
@@ -162,11 +171,15 @@ why the commands are here and the answers are not load-bearing.
162
171
 
163
172
  ### 2a — Adding a DOCTRINE RULE (the common case)
164
173
 
165
- Five files, none of them a count. This is the slot most new material belongs in, and it is cheap
166
- on purpose so that the invariant slot stays scarce.
174
+ Five files, plus the written-out rule totals in four test files. This is the slot most new
175
+ material belongs in, and it is cheap on purpose so that the invariant slot stays scarce.
167
176
 
168
177
  1. `src/doctrines/<pack>.md` — **append** `### {{DOCTRINE}} <pack> <n> — {{DOC_<PACK>_<n>}}` plus
169
- the body. Two gates in `constitutionProblems` (`js/inspect/doctor.mjs`) sit on this: the pack named in
178
+ the body, wrapped in a `LEAN` block with **both halves authored** — the heading stays outside the
179
+ block, the full text goes in `LEAN:begin`, and the lean half is the rule *and its mechanism*, no
180
+ maxims (the style rule is in `src/doctrines/README.md`, and `leanBlockProblems` refuses a rule
181
+ with no block or an empty half). Two gates in `constitutionProblems`
182
+ (`js/inspect/checks-authoring.mjs`) sit on this too: the pack named in
170
183
  the *heading* is what addresses the rule (not the filename, so a rule filed in the wrong file is
171
184
  reported rather than silently renamed), and ids must run **contiguously from 1**, so appending
172
185
  at the end is the only place `n` is free. Packs are numbered independently — nothing outside
@@ -198,9 +211,18 @@ on purpose so that the invariant slot stays scarce.
198
211
 
199
212
  - **No class entry.** There is no `LAW_CLASS` analogue: a doctrine rule's class *is* its pack, and
200
213
  there is no second taxonomy over it.
201
- - **No count, anywhere.** No badge, no `SHIPPED.md` triple, no prose mirror. The console spends
214
+ - **No count in the product.** No badge, no `SHIPPED.md` triple, no prose mirror. The console spends
202
215
  `{N_PACKS}` / `{N_PACKS_ACTIVE}` / `{N_DOCTRINE_RULES}`, all computed at request time.
203
- - **No test fixture to bump**, and no renumber risk.
216
+ - **No renumber risk.** But there *are* written-out rule totals to bump — expected values are
217
+ written out, never recorded — in `tests/unit/emit_smoke.test.mjs`, `harness.test.mjs`,
218
+ `setup.test.mjs` and `web_api.test.mjs`. Each fails loudly with the new count; change the
219
+ number *and* the comment that explains it. A rule in **craft** moves more: `setup` and
220
+ `web_api` also pin the craft-only narrowed counts, `generate.test.mjs` excludes every craft
221
+ rule by address to empty the pack, and `harness.test.mjs`'s contiguity fixture plants a rule at
222
+ the first free craft id — the doctrine twin of §2c step 8. Grep `tests/` for the previous
223
+ rule's address before trusting this list. A rule long enough to matter can also breach the
224
+ `full` carrier ceiling in `emit_smoke` — raise it with a dated note there, as every earlier
225
+ move did.
204
226
 
205
227
  ⚠ **Cite only downward.** An always-on tier may never reference a toggleable one:
206
228
  `constitutionProblems` refuses a `{{DOCTRINE}}` token anywhere under `src/ontology/` or
@@ -217,10 +239,9 @@ Steps 1 and 9 of §2c, plus `LAW_META` if the Principle line moved. Two cautions
217
239
 
218
240
  - `--footprint` defaults to **lean**, and a law's lean text is the authored `LEAN:else` half of
219
241
  its block in `src/laws/universal.md` (`resolveLean` in `js/build/render.mjs`). Amend **both**
220
- halves, or the amendment does not exist for most installs. A doctrine rule is different: it is
221
- still machine-cut to its heading plus first sentence (`terseBlocks`), so a clause in its second
222
- paragraph is invisible in the default build. The on-disk `laws/` and `ontology/` copies always
223
- render at full, whatever the footprint.
242
+ halves, or the amendment does not exist for most installs — and a doctrine rule is no different,
243
+ since its rules got the same block. The on-disk `laws/`, `ontology/` and `doctrines/` copies
244
+ always render at full, whatever the footprint.
224
245
 
225
246
  ### 2c — Adding an INVARIANT
226
247
 
@@ -232,7 +253,7 @@ the bar is in `DESIGN.md` Decision 7, and most candidates belong in a doctrine p
232
253
  citations in `js/hosts/hooks.mjs` and two OpenCode plugins, and **nothing resolves a cross-reference
233
254
  against the canon**. Renumbering silently rewires every one of them.
234
255
  2. `themes/_TEMPLATE.json` — add `LEX_<roman>` in template order. Gated, as in §2a step 2:
235
- `constitutionProblems` holds `LEX_I..LEX_IX` as an **equality** across all fifteen files (the
256
+ `constitutionProblems` holds `LEX_I..LEX_XI` as an **equality** across all fifteen files (the
236
257
  fourteen voices and the template), so both a missing key *and* a leftover one are reported. That
237
258
  arm exists because the renumber left `LEX_XXII`, `LEX_XXIII`, `LEX_XXIV` and `LEX_XXXVI` behind
238
259
  in every file and parity was silent — they were absent from nowhere.
@@ -245,7 +266,7 @@ the bar is in `DESIGN.md` Decision 7, and most candidates belong in a doctrine p
245
266
  6. `README.md` — badge `badge/laws-N` and the `N universal laws` sentence.
246
267
  7. `SHIPPED.md` — the `N laws, N agents, N skills` triple.
247
268
  8. `tests/unit/harness.test.mjs` — the negative fixture for the `LAW_CLASS` gate plants a rule at
248
- the *first free numeral*, which is `X` now that the invariants are I..IX. Bump it, or it stops
269
+ the *first free numeral*, which is `XII` now that the invariants are I..XI. Bump it, or it stops
249
270
  being a fixture and starts being a duplicate.
250
271
  9. `CHANGELOG.md`, then rebuild and commit `web/dist` (step 5 touched `web/src`).
251
272
 
@@ -256,9 +277,9 @@ error — the heading simply stops matching and the whole tier parses to nothing
256
277
 
257
278
  The carrier itself is gated: `tests/unit/emit_smoke.test.mjs` checks every rule reaches every one
258
279
  of nine emit modes in order and themed, under both footprints, with a `CEILING` **and** a floor
259
- (half the ceiling) on the rendered size. Its docblock carries the current measurement: headroom is
260
- 1,522 characters (2.8%) at full and 1,794 (4.8%) at lean, against the largest carrier — the
261
- host-agnostic `files` bundle. Read that docblock before adding text to any tier; it is
280
+ (half the ceiling) on the rendered size. Its docblock carries the current measurement against
281
+ the largest carrier — the host-agnostic `files` bundle; no figure is repeated here, because a
282
+ typed headroom is stale by the next rule. Read that docblock before adding text to any tier; it is
262
283
  re-measured, never nudged, and the ceiling is measured **with all four packs active**, which is
263
284
  the only configuration it can speak for.
264
285
 
@@ -284,7 +305,7 @@ Rare, and mechanical. On top of §2a for each rule the pack carries:
284
305
 
285
306
  ## 3 — Adding a SKILL or an AGENT
286
307
 
287
- **Order matters at the first two steps.** `missingReferencedSpecs` (in `js/build/emit.mjs`) *aborts the
308
+ **Order matters at the first two steps.** `missingReferencedSpecs` (in `js/build/bundle.mjs`) *aborts the
288
309
  build* when `AGENT.md.tmpl` names a spec with no file — so the file comes before the table row, or
289
310
  nothing builds at all. The refusal is clean: `tests/unit/emit_gates.test.mjs:265` proves a refused
290
311
  emit leaves the prior install intact.
@@ -308,6 +329,10 @@ emit leaves the prior install intact.
308
329
  `·`-separated name in the 🛠 row (that enumeration is gated two ways, unlike the laws' one).
309
330
  7. `SHIPPED.md` — the `N laws, N agents, N skills` triple.
310
331
  8. `doctor --all`, then the suite.
332
+ 9. **Skills only, and optional** — cases in `tests/fixtures/skill_triggers.json` (positives *and*
333
+ near-miss negatives), then `node tests/skill_triggers.mjs --only <name>`. It is the one check
334
+ that asks a real model whether the description routes: every gate above passes a skill that
335
+ never fires. Hand-run, spends model calls on your own login, not part of the suite.
311
336
 
312
337
  **A vendored skill folder** (`src/skills/<name>/SKILL.md`) is a different shape: add it to
313
338
  `VENDORED_SKILL_DIRS` (`js/hosts/native.mjs:37` — the literal is frozen in
@@ -537,7 +562,7 @@ only, at best, on its presence.
537
562
  |---|---|---|
538
563
  | `§N` cross-references into `AGENT.md`'s anatomy | `src/skills/*`, `src/agents/*`, `adapters/**` | **range only.** `constitutionProblems` refuses a `§N` the template declares no section for — which catches a pointer past the end, the shape *removing* a section leaves. It cannot catch one that still resolves and now means something else, which is the shape *inserting* a section leaves, and is what actually happened when `## 2. Doctrines` pushed every later section down one. **Renumbering the anatomy means `grep -rn '§' src/ adapters/` and reading every hit.** Four satellites and a live OpenCode deny message shipped stale because the sweep stopped at `AGENT.md.tmpl` |
539
564
  | Per-law Principle lines | `web/src/pages/Laws.jsx` | presence and class only, never accuracy |
540
- | 504 themed constitution titles (36 keys × 14 voices) — `LEX_I..LEX_IX` (9), `DOC_<PACK>_<n>` (23), `PACK_<NAME>` (4) | `themes/*.json` | key presence only — a shipped placeholder is green. `LEX_*` and `DOC_*` are held across the template too, and in both directions; `PACK_*` only across the voices |
565
+ | 588 themed constitution titles (42 keys × 14 voices) — `LEX_I..LEX_XI` (11), `DOC_<PACK>_<n>` (27), `PACK_<NAME>` (4) | `themes/*.json` | key presence only — a shipped placeholder is green. `LEX_*` and `DOC_*` are held across the template too, and in both directions; `PACK_*` only across the voices |
541
566
  | The README keyword enumerations | `README.md`, `docs/web/rules.md` | nothing |
542
567
  | The plugin capability enumeration | `docs/web/model.md` | nothing (the number substitutes; the list does not) |
543
568
  | Section labels and page subtitles | `web/src/lib/sections.js`, `web/src/pages/Docs/index.jsx` | nothing |
@@ -545,7 +570,7 @@ only, at best, on its presence.
545
570
  | `LOADED_SIGIL` uniqueness | `themes/*.json` | nothing, and it is load-bearing for theme detection |
546
571
  | `adapters/claude-code/settings.json` | — | nothing compares it to what the emitter writes; **it is already divergent** |
547
572
  | `NATIVE`, twice | two test files | they check each other, which is the gate that makes the duplication safe |
548
- | The emit-size `CEILING` | `tests/unit/emit_smoke.test.mjs:54` | a hand-transcribed measurement |
573
+ | The emit-size `CEILING` | `tests/unit/emit_smoke.test.mjs` (`const CEILING`) | a hand-transcribed measurement |
549
574
  | Stale docblock counts | `js/build/themes.mjs:62` ("137 values" → 140), `js/build/render.mjs:116` ("145 tokens" → 148) | nothing |
550
575
 
551
576
  ---
@@ -624,7 +649,7 @@ gated. Every other appearance is derived at runtime (`.geneseed-version` markers
624
649
  why no test pins a version string and none should.
625
650
 
626
651
  Do not run `npm version`. Bump the two files by hand, add the `CHANGELOG` section, update
627
- `SHIPPED.md`, rebuild `web/dist` if `web/src` moved, run the five commands from §1, push, tag,
652
+ `SHIPPED.md`, rebuild `web/dist` if `web/src` moved, run the seven commands from §1, push, tag,
628
653
  then rehearse and fire the workflow:
629
654
 
630
655
  ```bash