geneseed 3.1.0 → 3.1.2

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 (102) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/DESIGN.md +31 -20
  3. package/README.md +2 -2
  4. package/SETUP.md +30 -3
  5. package/SHIPPED.md +1 -1
  6. package/bin/{geneseed.mjs → build-driver.mjs} +88 -42
  7. package/bin/geneseed-cli.mjs +33 -32
  8. package/bin/geneseed-hook.mjs +7 -7
  9. package/docs/design-history.md +40 -40
  10. package/docs/extending.md +113 -85
  11. package/docs/web/rules.md +2 -0
  12. package/docs/web/setup-choices.md +13 -1
  13. package/harness.config.json +1 -1
  14. package/js/README.md +203 -0
  15. package/js/build/bundle.mjs +231 -0
  16. package/js/{catalog.mjs → build/catalog.mjs} +7 -7
  17. package/js/build/emit-claude.mjs +287 -0
  18. package/js/build/emit-common.mjs +255 -0
  19. package/js/build/emit-opencode.mjs +214 -0
  20. package/js/{generate.mjs → build/generate.mjs} +45 -31
  21. package/js/{render.mjs → build/render.mjs} +72 -11
  22. package/js/{checkout.mjs → build/source.mjs} +52 -11
  23. package/js/build/stubs.mjs +256 -0
  24. package/js/{themes.mjs → build/themes.mjs} +24 -24
  25. package/js/build/version.mjs +155 -0
  26. package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
  27. package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
  28. package/js/{installs.mjs → hosts/installs.mjs} +68 -16
  29. package/js/{link.mjs → hosts/link.mjs} +29 -28
  30. package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
  31. package/js/{native.mjs → hosts/native.mjs} +24 -22
  32. package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
  33. package/js/{settings.mjs → hosts/settings.mjs} +94 -69
  34. package/js/inspect/checks-authoring.mjs +761 -0
  35. package/js/inspect/checks-build.mjs +231 -0
  36. package/js/inspect/checks-repo.mjs +308 -0
  37. package/js/{diff.mjs → inspect/diff.mjs} +34 -31
  38. package/js/inspect/doctor.mjs +286 -0
  39. package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
  40. package/js/{inventory.mjs → inspect/inventory.mjs} +34 -20
  41. package/js/{registry.mjs → inspect/registry.mjs} +12 -10
  42. package/js/inspect/scan.mjs +125 -0
  43. package/js/{status.mjs → inspect/status.mjs} +27 -25
  44. package/js/inspect/validate.mjs +199 -0
  45. package/js/lib/fs.mjs +37 -759
  46. package/js/lib/json.mjs +344 -0
  47. package/js/lib/paths.mjs +242 -0
  48. package/js/lib/proc.mjs +3 -3
  49. package/js/lib/text.mjs +181 -0
  50. package/js/lib/udiff.mjs +1 -1
  51. package/js/{memory.mjs → maintain/memory.mjs} +10 -10
  52. package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
  53. package/js/{setup.mjs → maintain/setup.mjs} +49 -37
  54. package/js/{uninstall.mjs → maintain/uninstall.mjs} +62 -58
  55. package/js/{update.mjs → maintain/update.mjs} +51 -51
  56. package/js/{anim.mjs → ui/anim.mjs} +30 -28
  57. package/js/{cli.mjs → ui/cli.mjs} +10 -10
  58. package/js/{menu.mjs → ui/menu.mjs} +10 -9
  59. package/js/{tui.mjs → ui/tui.mjs} +36 -34
  60. package/js/web/actions.mjs +124 -89
  61. package/js/web/activity.mjs +22 -21
  62. package/js/web/api.mjs +88 -58
  63. package/js/web/daemon.mjs +319 -0
  64. package/js/web/docs.mjs +24 -21
  65. package/js/web/graph.mjs +0 -0
  66. package/js/web/handler.mjs +442 -0
  67. package/js/web/jobs.mjs +29 -21
  68. package/js/web/routes.mjs +223 -0
  69. package/js/web/server.mjs +56 -987
  70. package/package.json +11 -3
  71. package/src/AGENT.md.tmpl +1 -1
  72. package/src/doctrines/README.md +1 -1
  73. package/themes/opencode/README.md +1 -1
  74. package/web/dist/assets/{About-_MnJcwb_.js → About-CPvIFZGh.js} +1 -1
  75. package/web/dist/assets/{Activity-DpjqO-FR.js → Activity-z0uXdS7J.js} +1 -1
  76. package/web/dist/assets/{ActivityDetail-Bvrjv8oW.js → ActivityDetail-CEj5IJpF.js} +1 -1
  77. package/web/dist/assets/{Diff-DZ0IehBV.js → Diff-BMR8fPuG.js} +1 -1
  78. package/web/dist/assets/{Doctor-cu4krooO.js → Doctor-Bz5qE4r9.js} +1 -1
  79. package/web/dist/assets/{Graph-DUOPY65j.js → Graph-BV6a6t2l.js} +1 -1
  80. package/web/dist/assets/{Harnesses-B5Osg-BF.js → Harnesses-C5Ag5PaQ.js} +1 -1
  81. package/web/dist/assets/Laws-BK07NKw9.js +3 -0
  82. package/web/dist/assets/{Library-8y69I3_z.js → Library-CiXN9sC3.js} +1 -1
  83. package/web/dist/assets/{Markdown-Ds5GS1OD.js → Markdown-DG6FjUz-.js} +1 -1
  84. package/web/dist/assets/{Profile-7Jxa7ZUX.js → Profile-B2oSvdxH.js} +1 -1
  85. package/web/dist/assets/{Rules-UZ0jWkjg.js → Rules-DobVPd80.js} +1 -1
  86. package/web/dist/assets/{Skills-Bgkl0xLd.js → Skills-DQPsvsn9.js} +1 -1
  87. package/web/dist/assets/{StatusBadge-Cx4caF_2.js → StatusBadge-BhJ3HhdI.js} +1 -1
  88. package/web/dist/assets/{Themes-De8az7Sv.js → Themes-BP38FBU9.js} +1 -1
  89. package/web/dist/assets/{activity-VDAdVQ3q.js → activity-D9mfv7pY.js} +1 -1
  90. package/web/dist/assets/index-BE226Gv6.css +1 -0
  91. package/web/dist/assets/{index-BKrjBRV4.js → index-BZ7cPLMI.js} +1 -1
  92. package/web/dist/assets/index-BgIs5eIa.js +12 -0
  93. package/web/dist/assets/{index-wNiNzFFQ.js → index-DGM5j2hB.js} +1 -1
  94. package/web/dist/index.html +2 -2
  95. package/web/src/pages/Laws.jsx +135 -69
  96. package/docs/declined.md +0 -200
  97. package/docs/limits.md +0 -329
  98. package/js/doctor.mjs +0 -1712
  99. package/js/emit.mjs +0 -1410
  100. package/web/dist/assets/Laws-D8DJgTBp.js +0 -3
  101. package/web/dist/assets/index-1pPSz7Kl.css +0 -1
  102. package/web/dist/assets/index-C-gb-q-r.js +0 -12
package/docs/extending.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # Extending — what it costs to change this tool
2
2
 
3
- `docs/limits.md` is the standing answer to *"what does this tool not prove about itself?"*.
4
3
  This file is its companion: **"what does it cost to add one thing?"** — one invariant, one
5
4
  doctrine rule, one skill, one plugin, one hook verb, one doc page, one theme, one screen.
6
5
 
@@ -33,7 +32,7 @@ Three seams, and almost every question about "where do I edit?" resolves to one
33
32
  Two rules follow from the table and they cause most of the surprises:
34
33
 
35
34
  - **Nothing propagates by itself.** A bundle is a *render*. Editing `src/` changes nothing on any
36
- machine until something re-emits (`js/emit.mjs:362` re-fingerprints, `js/status.mjs:80`
35
+ machine until something re-emits (`js/build/emit.mjs:362` re-fingerprints, `js/inspect/status.mjs:80`
37
36
  reports the drift).
38
37
  - **`docs/web/` is data, `web/src/` is code.** A new documentation page is one file and no
39
38
  rebuild. A new screen is a new hashed chunk and a mandatory `web/dist` commit.
@@ -47,14 +46,14 @@ three-tier split added — `{N_ONTOLOGY}` (the ontology's sections), `{N_PACKS}`
47
46
  packs). `{N_LAWS}` is the **invariant** count and nothing else; there is deliberately no token
48
47
  meaning "the whole constitution", because `proseMirrorProblems` holds README and `SHIPPED.md`
49
48
  prose to that same number and a token that quietly meant something wider would put the docs and
50
- the gate at odds with no way to re-bless either.
49
+ the gate at odds.
51
50
 
52
51
  ⚠ **Only a `kind: "concept"` page is substituted.** A `markdown`-kind page renders `{N_PACKS}`
53
52
  literally, on the screen, as those nine characters.
54
53
 
55
54
  Typing a literal number into a `docs/web` page is caught by `proseMirrorProblems`. The README
56
55
  badges and prose, and the `N laws, N agents, N skills` line in `SHIPPED.md`, are the hand-written
57
- exceptions — and each has its own arm in `js/doctor.mjs` (`countTableProblems` walks the five
56
+ exceptions — and each has its own arm in `js/inspect/doctor.mjs` (`countTableProblems` walks the five
58
57
  badge keys `agents`/`skills`/`laws`/`themes`/`plugins`; `proseMirrorProblems` holds the prose).
59
58
 
60
59
  ---
@@ -94,9 +93,10 @@ cd web && npm ci && npm run lint && npm run format:check && npm test && npm run
94
93
 
95
94
  Notes that matter:
96
95
 
97
- - The root `package.json` has **no `scripts` key**, and no pre-commit hook, Makefile or justfile
98
- exists either. `npm test` at the root exits 1 with *Missing script: "test"*; the suite is run by
99
- hand with the glob above.
96
+ - The root `package.json` has exactly one script, `lint`, and no pre-commit hook, Makefile or
97
+ justfile exists. `npm test` at the root exits 1 with *Missing script: "test"*; the suite is run
98
+ by hand with the glob above. `npm ci` once per clone installs the devDependencies `lint` needs
99
+ — it pulls no runtime dependency, because there are none.
100
100
  - `--test-reporter=tap` is not decoration. Both workflows gate on the *count* (`# tests N ≥ 500`)
101
101
  because `node --test` over a glob that matches nothing prints `# tests 0` and **exits 0**. Node's
102
102
  default reporter changed between majors, which is how a green 1043-test suite once reported zero.
@@ -116,9 +116,9 @@ Notes that matter:
116
116
 
117
117
  ### The gates CI does not run
118
118
 
119
- - **The mutation matrix itself.** `tests/mutate.mjs` — 31 rows, each a one-string edit to a
119
+ - **The mutation matrix itself.** `tests/mutate.mjs` — 33 rows, each a one-string edit to a
120
120
  product file with a written argument for which gate should catch it. The full run is **in no
121
- workflow** and should not be: it mutates the product tree 31 times and runs a gate per edit.
121
+ workflow** and should not be: it mutates the product tree 33 times and runs a gate per edit.
122
122
  Run `node tests/mutate.mjs --all` (or `--only M7`) by hand when you change something a gate is
123
123
  supposed to be watching. ⚠ An interrupted run leaves the **product tree mutated** — check
124
124
  `git status` before doing anything else.
@@ -166,7 +166,7 @@ Five files, none of them a count. This is the slot most new material belongs in,
166
166
  on purpose so that the invariant slot stays scarce.
167
167
 
168
168
  1. `src/doctrines/<pack>.md` — **append** `### {{DOCTRINE}} <pack> <n> — {{DOC_<PACK>_<n>}}` plus
169
- the body. Two gates in `constitutionProblems` (`js/doctor.mjs`) sit on this: the pack named in
169
+ the body. Two gates in `constitutionProblems` (`js/inspect/doctor.mjs`) sit on this: the pack named in
170
170
  the *heading* is what addresses the rule (not the filename, so a rule filed in the wrong file is
171
171
  reported rather than silently renamed), and ids must run **contiguously from 1**, so appending
172
172
  at the end is the only place `n` is free. Packs are numbered independently — nothing outside
@@ -179,7 +179,7 @@ on purpose so that the invariant slot stays scarce.
179
179
  3. Seed the fourteen themes:
180
180
 
181
181
  ```bash
182
- node bin/geneseed.mjs --sync-themes
182
+ node bin/build-driver.mjs --sync-themes
183
183
  ```
184
184
 
185
185
  ⚠ Four spellings of this command are in circulation and two of them fail — `doctor`'s own tip
@@ -216,13 +216,10 @@ a defect.
216
216
  Steps 1 and 9 of §2c, plus `LAW_META` if the Principle line moved. Two cautions:
217
217
 
218
218
  - `--footprint` defaults to **lean**, and lean keeps only the heading plus the first sentence
219
- (`terseBlocks` in `js/render.mjs`). A clause added to the second paragraph is invisible in the
219
+ (`terseBlocks` in `js/build/render.mjs`). A clause added to the second paragraph is invisible in the
220
220
  default build. Amend the first sentence, or the amendment does not exist for most installs. The
221
221
  same is true of a doctrine rule; it is **not** true of the ontology, which ships whole at both
222
222
  footprints.
223
- - ⚠ **Rule I is frozen.** `LEX_I` is one of seven theme keys recorded byte-for-byte in the
224
- primitives corpus (§5.2). Retitling Rule I in any theme reddens a snapshot that cannot be
225
- re-blessed.
226
223
 
227
224
  ### 2c — Adding an INVARIANT
228
225
 
@@ -231,7 +228,7 @@ the bar is in `DESIGN.md` Decision 7, and most candidates belong in a doctrine p
231
228
 
232
229
  1. `src/laws/universal.md` — **append** `### {{LAW}} <roman> — {{LEX_<roman>}}` plus the body.
233
230
  Never insert: the `{{LAW}}` cross-references counted above live all over `src/`, plus hardcoded
234
- citations in `js/hooks.mjs` and two OpenCode plugins, and **nothing resolves a cross-reference
231
+ citations in `js/hosts/hooks.mjs` and two OpenCode plugins, and **nothing resolves a cross-reference
235
232
  against the canon**. Renumbering silently rewires every one of them.
236
233
  2. `themes/_TEMPLATE.json` — add `LEX_<roman>` in template order. Gated, as in §2a step 2:
237
234
  `constitutionProblems` holds `LEX_I..LEX_IX` as an **equality** across all fifteen files (the
@@ -239,7 +236,7 @@ the bar is in `DESIGN.md` Decision 7, and most candidates belong in a doctrine p
239
236
  arm exists because the renumber left `LEX_XXII`, `LEX_XXIII`, `LEX_XXIV` and `LEX_XXXVI` behind
240
237
  in every file and parity was silent — they were absent from nowhere.
241
238
  3. `--sync-themes`, then restyle all fourteen by hand (§2a step 3).
242
- 4. `LAW_CLASS` in `js/inventory.mjs` — one of the six in `LAW_CLASSES` beside it.
239
+ 4. `LAW_CLASS` in `js/inspect/inventory.mjs` — one of the six in `LAW_CLASSES` beside it.
243
240
  5. `LAW_META` in `web/src/pages/Laws.jsx` — same class, plus the one-line Principle. Note the map
244
241
  is keyed by **arabic** number where `universal.md` heads with a Roman numeral. **This copy
245
242
  exists nowhere else**; a rule absent here renders with a blank description. It is why that one
@@ -271,13 +268,13 @@ Rare, and mechanical. On top of §2a for each rule the pack carries:
271
268
  1. `src/doctrines/<pack>.md`, opening with its lead line — `**{{PACK_<NAME>}}** — how … .` A pack
272
269
  file `PACK_ORDER` names but cannot read, or one carrying no `### {{DOCTRINE}} <pack> <n>`
273
270
  heading at all, is a refusal.
274
- 2. `PACK_ORDER` in `js/checkout.mjs` — the registration, and the render order. It is **narrative,
271
+ 2. `PACK_ORDER` in `js/build/source.mjs` — the registration, and the render order. It is **narrative,
275
272
  not alphabetical** (craft → rigor → ops → process); discovery sorts alphabetically and the
276
273
  renderer refuses a `.md` under `src/doctrines/` that `PACK_ORDER` does not name, rather than
277
274
  skipping it.
278
275
  3. `PACK_<NAME>` in `themes/_TEMPLATE.json` and all fourteen voices. Unlike `DOC_*`, this family is
279
276
  held only by `themeParityProblems` — presence, across the voices, template excluded.
280
- 4. `DOCTRINE_BLURBS` in `js/setup.mjs` — the one-line description an installer sees in the wizard,
277
+ 4. `DOCTRINE_BLURBS` in `js/maintain/setup.mjs` — the one-line description an installer sees in the wizard,
281
278
  and the only place a pack is ever explained to them. Missing entries fall back to the bare name.
282
279
  5. `harness.config.json` if the pack should be on by default; `constitutionProblems` refuses a
283
280
  `doctrines` array naming a pack this checkout does not ship.
@@ -286,7 +283,7 @@ Rare, and mechanical. On top of §2a for each rule the pack carries:
286
283
 
287
284
  ## 3 — Adding a SKILL or an AGENT
288
285
 
289
- **Order matters at the first two steps.** `missingReferencedSpecs` (in `js/emit.mjs`) *aborts the
286
+ **Order matters at the first two steps.** `missingReferencedSpecs` (in `js/build/emit.mjs`) *aborts the
290
287
  build* when `AGENT.md.tmpl` names a spec with no file — so the file comes before the table row, or
291
288
  nothing builds at all. The refusal is clean: `tests/unit/emit_gates.test.mjs:265` proves a refused
292
289
  emit leaves the prior install intact.
@@ -299,20 +296,20 @@ emit leaves the prior install intact.
299
296
  Gated both ways: a row with no file, and a file with no row.
300
297
  3. `DESC_<NAME>` in **all fourteen** themes. Theme parity catches a missing one; nothing catches
301
298
  an unstyled placeholder.
302
- 4. **Skills only** — `SKILL_CLASS` in `js/inventory.mjs`. Gated both ways, inside
299
+ 4. **Skills only** — `SKILL_CLASS` in `js/inspect/inventory.mjs`. Gated both ways, inside
303
300
  `countTableProblems`, which matters because the runtime fallback is silent: an unclassified
304
301
  skill renders as `build`.
305
302
  5. `registry.json` — a row keyed `skills/<name>` or `agents/<name>` with `status` ∈
306
303
  {`experimental`, `approved`, `deprecated`}, a semver `version`, a non-blank `owner`, an ISO
307
304
  `added`, and `last_verified: ""`. **This is the most forgettable step**: four hand-typed fields,
308
- and the gate fires in both directions (`js/doctor.mjs:388-436`).
305
+ and the gate fires in both directions (`js/inspect/doctor.mjs:388-436`).
309
306
  6. `README.md` — the badge, the `**🤖 Agents** (N)` / `**🛠 Skills** (N)` count, and for a skill the
310
307
  `·`-separated name in the 🛠 row (that enumeration is gated two ways, unlike the laws' one).
311
308
  7. `SHIPPED.md` — the `N laws, N agents, N skills` triple.
312
309
  8. `doctor --all`, then the suite.
313
310
 
314
311
  **A vendored skill folder** (`src/skills/<name>/SKILL.md`) is a different shape: add it to
315
- `VENDORED_SKILL_DIRS` (`js/native.mjs:37` — the literal is frozen in
312
+ `VENDORED_SKILL_DIRS` (`js/hosts/native.mjs:37` — the literal is frozen in
316
313
  `tests/unit/authoring_gates.test.mjs:312`), write a `VENDOR.md` carrying `**Upstream:**`,
317
314
  `**Commit:**` (a 40-hex commit, never a moving branch) and `**License:**`, add the registry row —
318
315
  and **skip steps 2, 4, 6, 7**: the flat-spec globs only see `*.md` at the top level.
@@ -320,14 +317,13 @@ and **skip steps 2, 4, 6, 7**: the flat-spec globs only see `*.md` at the top le
320
317
  Two traps worth knowing before you write the spec:
321
318
 
322
319
  - **The word "Read-only" anywhere in an agent spec locks its emit.** It is a bare substring test
323
- (`js/native.mjs:148`). The escape hatch is an explicit `<!-- bash: allow -->` marker, which
320
+ (`js/hosts/native.mjs:148`). The escape hatch is an explicit `<!-- bash: allow -->` marker, which
324
321
  re-opens bash on all three hosts.
325
322
  - **The console has a fourth, ungated copy of the skill taxonomy.** `SKILL_CATS` in
326
323
  `web/src/pages/Skills.jsx:19` mirrors `SKILL_CLASS` with nothing comparing them (contrast
327
324
  `LAW_META`, which *is* cross-checked). Inventing a new *category* means editing that file —
328
325
  which drags in the `web/dist` rebuild — and an unknown category renders as Build with no warning.
329
326
 
330
- Adding a spec reddens no recording: none of the surviving snapshots contains a skill or agent name.
331
327
 
332
328
  ---
333
329
 
@@ -358,8 +354,8 @@ No `registry.json` row, no `package.json` edit — `files[]` ships `adapters/` w
358
354
 
359
355
  ⚠ **Read §5 first. A fifth hook verb has no green path today.**
360
356
 
361
- The mechanical part: implement `cmd<Verb>` in `js/hooks.mjs`; wire the command string in
362
- `claudeHookGroups` (`js/settings.mjs:434`, spelled `` `${run} <verb> …` `` — the gate scrapes that
357
+ The mechanical part: implement `cmd<Verb>` in `js/hosts/hooks.mjs`; wire the command string in
358
+ `claudeHookGroups` (`js/hosts/settings.mjs:434`, spelled `` `${run} <verb> …` `` — the gate scrapes that
363
359
  shape); add the entry to `bin/geneseed-hook.mjs:43` `VERBS` with **exactly two-space indent**; add
364
360
  a row to `js/cli-table.json` with a help string over 20 characters.
365
361
 
@@ -381,6 +377,55 @@ the page renders, wrong, with nothing red.
381
377
 
382
378
  ---
383
379
 
380
+ ## 4bis — Adding a RUNTIME DEPENDENCY
381
+
382
+ **Short answer: you vendor it, or you do not add it.** A dependency arrives as tracked source under
383
+ `js/vendor/<name>/`, imported by relative path, landing in the same `git merge --ff-only` as the
384
+ code that imports it. It never comes from a registry at install time. `tests/unit/dependency_policy.test.mjs`
385
+ gates this, and its header carries the long form.
386
+
387
+ This is not conservatism. The npm route is *unbuildable* here, for three reasons that were measured
388
+ rather than assumed:
389
+
390
+ 1. **Every fixture would die at module load.** `copyCheckout` lists the tree with
391
+ `git ls-files --cached --others --exclude-standard`, which honours `.gitignore`, and
392
+ `.gitignore` ignores `/node_modules/`. The copy is written outside the repository, under the OS
393
+ temp root, and the product is run out of it as a real child process — so Node's upward resolver
394
+ walk starts in temp and finds nothing. Eleven test files build such a copy. And this is not a
395
+ fixture defect to work around: a user's fresh `git clone` has no `node_modules` either. **The
396
+ fixture is that clone.** Giving the fixture something the real install lacks makes it stop
397
+ reproducing the failure it exists to catch.
398
+
399
+ 2. **The clone channel cannot install, and it is the only channel that would need to.** `geneseed
400
+ update` is `git pull` + rebuild. `npm ci` deletes `node_modules` before refetching, so a blocked
401
+ download leaves an install whose CLI cannot start — the one outcome the update must never
402
+ produce. `npm install` writes the tracked `package-lock.json`, so `preflight()`'s
403
+ `git status --porcelain` then reports a dirty tree and the *next* update refuses with "You have
404
+ local changes": a self-poisoning update. Meanwhile npm/npx installs already resolve
405
+ `dependencies` at install time and need nothing from us. There is no overlap between the channel
406
+ that would need a sync step and the channel where one could work.
407
+
408
+ 3. **The hook path pays per tool call.** `bin/geneseed-hook.mjs` loads on every tool call of every
409
+ agent session (~14 ms). A relative import costs no resolver walk; a bare one does.
410
+
411
+ What vendoring costs, by contrast, is almost nothing: `files[]` already carries `js/` and the
412
+ manifest partition already carries the `js/` prefix row, so a vendored subtree costs **zero**
413
+ manifest lines and zero partition lines. The existing doctor gate already covers a bad drop, because
414
+ `runDoctor` validates the pulled tree in a fresh interpreter before the rebuild.
415
+
416
+ **The one exception, and it is not a loophole.** `adapters/` does not run in this tool's process — it
417
+ ships into OpenCode's. A module the *host* provides is resolvable there and nowhere else, so
418
+ reaching for one is legitimate. But it must be a **guarded dynamic import with a working degraded
419
+ path**, never a static one: an older host that lacks it would otherwise fail the whole plugin at
420
+ load instead of losing one capability. `geneseed-workflow.js`'s `@opencode-ai/plugin` is the shape
421
+ to copy. The allow-list in the gate is two-sided — an entry nothing imports fails as loudly as an
422
+ import nothing declares.
423
+
424
+ **Before proposing one at all, price it.** At the time this was written there were 463 import
425
+ specifiers under `js/`, `bin/` and `adapters/` and exactly one was bare — the host-provided
426
+ exception above. The invariant already held; it had simply never been written down. A library that
427
+ would delete fewer lines than its vendored source adds is not a saving.
428
+
384
429
  ## 5 — The four things that are genuinely hard
385
430
 
386
431
  These are not checklist items. They are structural, and each one should be settled *before* the
@@ -398,34 +443,24 @@ already exempted a named `NATIVE` list; the hook half exempted nothing — so a
398
443
  failed with **no green path**, since the recorder died on 2026-08-17 and the failure message
399
444
  correctly forbids deleting cells to pass.
400
445
 
401
- Both halves now filter through the same `NATIVE` list, and the reverse check refuses an
402
- exemption for a verb neither entry point carries. The argument is in `docs/declined.md` — the
403
- short version is that the reason those verbs have no cell (*the reference had nothing to compare
404
- against*) is the same reason a verb postdating the reference has none, so it is one exemption,
405
- not two. Nothing measurable was lost: the hook verb set is pinned twice more, from editable
406
- sources, twelve lines above.
407
-
408
- **To add a hook verb today:** implement `cmd<Verb>` in `js/hooks.mjs`, wire the string in
409
- `claudeHookGroups`, add the `VERBS` entry, add the `js/cli-table.json` row, then name the verb in
410
- `NATIVE` in **both** `tests/unit/hook_cli.test.mjs` and `tests/snapshot/cli_help.test.mjs` (they
411
- read each other's source, so one alone fails) and give it an absolute unit gate.
412
-
413
- ### 5.2 ⚠ Seven theme keys are frozen byte-for-byte
446
+ `NATIVE` in `tests/unit/hook_cli.test.mjs` names the verbs held to the structural rules rather
447
+ than to a per-verb gate, and the reverse check refuses an exemption for a verb neither entry point
448
+ carries — so a name that stops meaning anything fails rather than sitting there.
414
449
 
415
- `tests/__snapshots__/primitives/{posix,win32}.json` record `theme_preview` and `theme_flair`
416
- answers for all fourteen themes **and the template**. Those probes read the live `themes/*.json`
417
- at replay time, which pins:
450
+ **To add a hook verb today:** implement `cmd<Verb>` in `js/hosts/hooks.mjs`, wire the string in
451
+ `claudeHookGroups`, add the `VERBS` entry, add the `js/cli-table.json` row, then give it an
452
+ absolute unit gate. `NATIVE` used to be duplicated across two files that read each other's source
453
+ to stay in step; there is one copy now, so there is nothing to keep in step.
418
454
 
419
- | Probe | Frozen keys |
420
- |---|---|
421
- | `theme_preview` | `TAGLINE`, `LOADED_SIGIL`, `VOICE`, **`LEX_I`**, `BENEDICTION` |
422
- | `theme_flair` | `ACCENT`, `BANNER` |
455
+ ### 5.2 ✅ Theme text is free to edit — resolved 2026-08-23
423
456
 
424
- Editing any of those, in any theme, reddens a snapshot with **no re-bless path**. Adding a *new*
425
- law is safe (only `LEX_I` is recorded). Restyling an existing theme's tagline or banner is not.
457
+ Seven keys (`TAGLINE`, `LOADED_SIGIL`, `VOICE`, `LEX_I`, `BENEDICTION`, `ACCENT`, `BANNER`) used to
458
+ be pinned byte-for-byte by a recording with no re-bless path, so restyling a theme's tagline was a
459
+ red gate. **That recording is gone.** Theme text is now held only by `doctor --all` — key presence,
460
+ cross-theme parity, and no unresolved tokens — which is a claim about structure, not wording.
426
461
 
427
- This was proven, not inferred: prefixing `marvin.json`'s `TAGLINE` turned
428
- `tests/snapshot/pure_snapshot.test.mjs` red at `case 1275`.
462
+ Restyle freely. What still bites is `LOADED_SIGIL` uniqueness (§6): it is load-bearing for theme
463
+ detection and nothing checks it.
429
464
 
430
465
  ### 5.3 ⚠ The hook shim is machine-wide and last-writer-wins
431
466
 
@@ -434,7 +469,7 @@ anything owns **every** install's hooks. Emitting from a git worktree or a temp
434
469
  repoint the shim at that disposable path; removing the worktree then killed hooks for every
435
470
  install on the machine.
436
471
 
437
- **`hookPrefix` now refuses the claim** (`js/settings.mjs`, `ephemeralCheckout`): an emit whose
472
+ **`hookPrefix` now refuses the claim** (`js/hosts/settings.mjs`, `ephemeralCheckout`): an emit whose
438
473
  checkout is under the OS temp root, or is a linked git worktree (`.git` is a *file*), leaves a
439
474
  shim that still resolves exactly as it is and wires the emitted hooks to it anyway. The bundle it
440
475
  writes is byte-identical to a normal emit — the install simply keeps running the durable
@@ -463,37 +498,30 @@ remains the cheapest thing to run.
463
498
  Related: a stray `console.log` on a hook path silently disables a gate. Hooks signal through
464
499
  stdout JSON and return 0 on every path — a printed byte is not a warning, it is a disabled gate.
465
500
 
466
- ### 5.4 ⚠ A snapshot red is a finding, not a step
501
+ ### 5.4 ✅ Expected values are written out — resolved 2026-08-23
467
502
 
468
- Everything left under `tests/__snapshots__/` — the primitives, the 26 help texts, and the small
469
- pure-function recordings — was produced by an implementation that no longer exists. There is no
470
- `--record`. A change that moves a recorded byte leaves two honest options: revert it, or argue in
471
- writing (in `docs/declined.md`) that the recording was wrong. `docs/limits.md` carries the full
472
- argument and the measurement that licensed retiring the three larger corpora.
503
+ This section used to say that a snapshot red was a finding rather than a step: the recordings under
504
+ `tests/__snapshots__/` came from an implementation that no longer existed, there was no `--record`,
505
+ and a change that moved a recorded byte left only two honest options — revert it, or argue in
506
+ writing that the recording was wrong. The rule that fell out of it was *when a frozen recording
507
+ blocks a change, move the logic to the nearest caller that is not frozen*.
473
508
 
474
- **A worked example, from the session that wrote this file.** Closing a real defect — three count
475
- gates that go green when you *delete* the sentence they check — turned the primitives corpus red
476
- at `case 371`: `proseMirrorProblems` is a recorded pure function, the recording holds a case that
477
- passes an empty `shipped` string and expects `[]`, and the new absence arm answered otherwise.
478
- Both obvious moves are wrong. Re-blessing is impossible. Abandoning the fix leaves a gate failing
479
- open. The third move is the one to reach for: **the presence check moved up one level, into
480
- `countTableProblems`, which is not recorded** — where it reads better anyway, beside the code that
481
- already knows whether the file could be opened. The rule that falls out: *when a frozen recording
482
- blocks a change, look for the nearest caller that is not frozen.* Check with
509
+ **None of that applies any more.** The recordings are retired (tag `corpus-reference-v3.1.2` is the
510
+ only way back to them), and every expected value now lives in the test file that asserts it, above
511
+ a sentence saying which decision it pins. The rule is correspondingly ordinary:
483
512
 
484
- ```bash
485
- node -e "const j=require('./tests/__snapshots__/primitives/win32.json');console.log([...new Set(j.cases.map(c=>c.fn))].join('\n'))"
486
- ```
513
+ > **Change the expectation and the sentence that explains it in the same commit, and say why in
514
+ > the message.**
487
515
 
488
- The concrete consequence for evolution: **editing the help text or the flags of any of the 26
489
- recorded CLI verbs is a frozen surface.** New verbs have a path (`NATIVE`); changes to old ones
490
- do not. `docs/limits.md:311-316` names this cluster as the one open standing decision.
516
+ A red test is a step again. What it costs is an argument, in prose, that the new answer is better —
517
+ the cost that keeps the tables honest, and far cheaper than the one it replaced.
491
518
 
492
- The CLI surface is the most expensive thing in the repo to change, because four hand-written
493
- equalities converge on it: `js/cli-table.json`'s 29-command count (`tests/unit/cli_table.test.mjs:67`),
494
- the 5-entry hidden-argument list (`tests/snapshot/cli_help.test.mjs:219`), the 25-subparser count
495
- (`tests/unit/docs.test.mjs:193`), and the 26 unblessable help fixtures. Plus
496
- `tests/unit/docs.test.mjs:554`, which validates every fenced `geneseed <verb> <flags>` line in
519
+ The CLI surface is no longer the most expensive thing in the repo to change. Editing a verb's help
520
+ text or flags is now an ordinary edit: `js/cli-table.json` is the owned document, and
521
+ `tests/unit/text_layout.test.mjs` renders every verb through `formatHelp` to check the layout
522
+ holds. Three hand-written equalities still converge on it and are cheap to keep in step — the
523
+ 29-command count and the hidden-argument rule (`tests/unit/cli_table.test.mjs`), and
524
+ `tests/unit/docs.test.mjs`, which validates every fenced `geneseed <verb> <flags>` line in
497
525
  `README.md`, `SETUP.md`, `QUICKSTART.md` and `SHIPPED.md` against the table.
498
526
 
499
527
  ---
@@ -511,12 +539,12 @@ only, at best, on its presence.
511
539
  | The README keyword enumerations | `README.md`, `docs/web/rules.md` | nothing |
512
540
  | The plugin capability enumeration | `docs/web/model.md` | nothing (the number substitutes; the list does not) |
513
541
  | Section labels and page subtitles | `web/src/lib/sections.js`, `web/src/pages/Docs/index.jsx` | nothing |
514
- | `THEME_BLURBS` (8 of 14), `ART` (8 of 14) | `js/setup.mjs`, `js/anim.mjs` | nothing — missing entries fall back silently |
542
+ | `THEME_BLURBS` (8 of 14), `ART` (8 of 14) | `js/maintain/setup.mjs`, `js/ui/anim.mjs` | nothing — missing entries fall back silently |
515
543
  | `LOADED_SIGIL` uniqueness | `themes/*.json` | nothing, and it is load-bearing for theme detection |
516
544
  | `adapters/claude-code/settings.json` | — | nothing compares it to what the emitter writes; **it is already divergent** |
517
545
  | `NATIVE`, twice | two test files | they check each other, which is the gate that makes the duplication safe |
518
546
  | The emit-size `CEILING` | `tests/unit/emit_smoke.test.mjs:54` | a hand-transcribed measurement |
519
- | Stale docblock counts | `js/themes.mjs:62` ("137 values" → 140), `js/render.mjs:116` ("145 tokens" → 148) | nothing |
547
+ | Stale docblock counts | `js/build/themes.mjs:62` ("137 values" → 140), `js/build/render.mjs:116` ("145 tokens" → 148) | nothing |
520
548
 
521
549
  ---
522
550
 
@@ -555,7 +583,7 @@ answer.
555
583
  5. **Numbers in docblocks that had drifted** — `_TEMPLATE.json`'s "137 values" (140), "the 145
556
584
  tokens" `src/` uses (148), and "sixteen `_*_problems` checks" (fifteen — the sixteenth was
557
585
  named as `cli`, which was removed).
558
- 6. **A claim that was simply false.** `js/render.mjs` said `--theme` is validated against
586
+ 6. **A claim that was simply false.** `js/build/render.mjs` said `--theme` is validated against
559
587
  `choices` before anything renders, which is why its own refusal read as an unreachable nicety.
560
588
  It is not in `choicesFor`; that refusal is the only one there is, on every path.
561
589
  7. **Five dead cross-references** to `tests/snapshot/no_python_in_corpus.test.mjs`, retired with
@@ -563,7 +591,7 @@ answer.
563
591
  Plus a skip message naming a `record-corpus` CI job that no longer exists.
564
592
  8. **`CHANGELOG.md` said 2.0.0 was "not yet published"**, with "no tag, no release and no install
565
593
  URL"; `geneseed@2.0.0` has been on npm since 2026-08-17. And **`SETUP.md`** documented
566
- `geneseed upgrade v1.0.0 # pin to a tag`, which `js/update.mjs` prints is IGNORED.
594
+ `geneseed upgrade v1.0.0 # pin to a tag`, which `js/maintain/update.mjs` prints is IGNORED.
567
595
  9. **`docs/web/agents.md`** enumerated 16 agents and omitted `developer`, where README says 17 —
568
596
  the doctor arm cannot fire, because the page spells `{N_AGENTS}` and that arm only ever catches
569
597
  a literal. **`docs/web/rules.md`** called each law "a short markdown file under `src/laws/`";
@@ -662,9 +690,9 @@ stop a change today, and all of them cost a contributor a wrong turn until they
662
690
  and today nothing warns you before the snapshot goes red.
663
691
  4. **Gate `AGENT_LESSON_PROMPT` like its sibling (§7.7), and either gate or delete
664
692
  `adapters/claude-code/settings.json` (§7.10).** Two silent failure modes, both cheap to close.
665
- 5. **Consider a `scripts` block in the root `package.json`.** There is currently no single command
666
- that runs what CI runs; every contributor reassembles the five by hand from this file. A
667
- `verify` script is three lines and makes §1 executable rather than descriptive.
693
+ 5. **Finish the `scripts` block in the root `package.json`.** `lint` landed with the ESLint
694
+ config; the rest of what CI runs is still reassembled by hand from this file. A `verify` script
695
+ chaining lint + suite + doctor is three lines and makes §1 executable rather than descriptive.
668
696
 
669
697
  The list of hand-maintained satellites in §6 is the *design*, not a defect — this project
670
698
  deliberately keeps tables and prose hand-written and gates them from the source tree. The
package/docs/web/rules.md CHANGED
@@ -21,6 +21,8 @@ Practices rather than principles: **craft** (how code is written), **rigor** (ho
21
21
 
22
22
  A pack you leave out **still ships in the bundle**: every pack file lands under `doctrines/` beside `AGENT.md` whether or not it was built in. So a rule that cites one in an inactive pack still resolves on disk, and you can read the alternatives before turning one on. What an inactive pack loses is bindingness, not availability.
23
23
 
24
+ Rules toggle **individually** too — `geneseed-build --exclude-rules "process 7"`, or the switches on this page — so *keep all of process except one rule* is a state you can build. A pack whose every rule is excluded drops out of the active set on its own. What an install excluded is recorded beside the pack list, as `Excluded rules: process 7`, and every rebuild preserves it.
25
+
24
26
  ### Which outranks which
25
27
 
26
28
  Ontology and Invariants first, then your own `user-rules.md`, then the active Doctrines, then `PROFILE.md`. A rule you write in `user-rules.md` **outranks a doctrine rule** — a practice pack chosen at build time never overrides an instruction you wrote for your own repo — and nothing outranks an invariant: a user rule or a doctrine rule may tighten one, never repeal it.
@@ -36,10 +36,22 @@ Which practice rules this install adopts, fixed at build time. Unlike the dials
36
36
  - **ops** — how the machine is driven: tool discovery, commands that return, complete teardowns.
37
37
  - **process** — how a session runs: planning, context economy, docs first, and the **consent gate on every commit and push**.
38
38
 
39
- Turning **process** off takes that consent gate with it, at the tool boundary as well as in the prose: the OpenCode permission block and the Claude/Bob `PreToolUse` hook that ask before a commit or a push are wired only when the pack is built in, because a boundary that keeps asking for a rule the install never adopted is a gate arguing with its own harness. The invariant-territory refusals — `rm -rf`, a force-push — stay in every build; they are the Rules' territory, not the pack's.
39
+ Turning **process 5** off takes that consent gate with it, at the tool boundary as well as in the prose: the OpenCode permission block and the Claude/Bob `PreToolUse` hook that ask before a commit or a push are wired only when that rule is built in, because a boundary that keeps asking for a rule the install never adopted is a gate arguing with its own harness. The invariant-territory refusals — `rm -rf`, a force-push — stay in every build; they are the Rules' territory, not the pack's.
40
40
 
41
41
  A pack you leave out still ships in the bundle under `doctrines/`, so you can read it before turning it on, and a rule that cites it still resolves. Change the set with `geneseed-build --doctrines craft,rigor` (or `--doctrines none`), or in `harness.config.json`.
42
42
 
43
+ ### One rule at a time
44
+
45
+ The pack is the coarse axis. `--exclude-rules` is the fine one, and it takes rule addresses — so *keep all of process, drop just `process 7`* is expressible:
46
+
47
+ geneseed-build --exclude-rules "process 7"
48
+ geneseed-build --exclude-rules "process 5,craft 2"
49
+ geneseed-build --exclude-rules none
50
+
51
+ Either spelling of an address works, `process 7` or `process.7`. The two axes compose: a pack whose every rule is excluded leaves `Active packs:` entirely, so no pack header is ever rendered with nothing under it. An install records its exclusions in a second marker line — `Excluded rules: process 7` — written only when something *is* excluded, and every rebuild path reads it back and preserves it.
52
+
53
+ The switches are on the **Constitution** page, one per rule, staged locally and applied in a single rebuild.
54
+
43
55
  ### Footprint — how much of the constitution loads inline
44
56
 
45
57
  How much of the constitution `AGENT.md` carries inline every turn. A token-cost dial, not a rules cut — whatever this install adopted always applies:
@@ -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.1.0"
5
+ "version": "3.1.2"
6
6
  }