geneseed 3.1.1 → 3.1.3

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 (106) hide show
  1. package/CHANGELOG.md +123 -1
  2. package/DESIGN.md +15 -15
  3. package/README.md +1 -1
  4. package/SETUP.md +1 -1
  5. package/SHIPPED.md +1 -1
  6. package/bin/{geneseed.mjs → build-driver.mjs} +39 -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 +111 -83
  11. package/docs/web/exclusions.md +1 -1
  12. package/docs/web/footprint.md +1 -1
  13. package/docs/web/foreman-mode.md +2 -2
  14. package/docs/web/themes-author.md +1 -1
  15. package/docs/web-ui.md +41 -29
  16. package/harness.config.json +1 -1
  17. package/js/README.md +203 -0
  18. package/js/build/bundle.mjs +231 -0
  19. package/js/{catalog.mjs → build/catalog.mjs} +7 -7
  20. package/js/build/emit-claude.mjs +287 -0
  21. package/js/build/emit-common.mjs +255 -0
  22. package/js/build/emit-opencode.mjs +214 -0
  23. package/js/{generate.mjs → build/generate.mjs} +27 -28
  24. package/js/{render.mjs → build/render.mjs} +13 -9
  25. package/js/{checkout.mjs → build/source.mjs} +9 -9
  26. package/js/build/stubs.mjs +256 -0
  27. package/js/{themes.mjs → build/themes.mjs} +24 -24
  28. package/js/build/version.mjs +155 -0
  29. package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
  30. package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
  31. package/js/{installs.mjs → hosts/installs.mjs} +18 -16
  32. package/js/{link.mjs → hosts/link.mjs} +29 -28
  33. package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
  34. package/js/{native.mjs → hosts/native.mjs} +24 -22
  35. package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
  36. package/js/{settings.mjs → hosts/settings.mjs} +63 -55
  37. package/js/inspect/checks-authoring.mjs +761 -0
  38. package/js/inspect/checks-build.mjs +231 -0
  39. package/js/inspect/checks-repo.mjs +308 -0
  40. package/js/{diff.mjs → inspect/diff.mjs} +34 -31
  41. package/js/inspect/doctor.mjs +286 -0
  42. package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
  43. package/js/{inventory.mjs → inspect/inventory.mjs} +20 -17
  44. package/js/{registry.mjs → inspect/registry.mjs} +12 -10
  45. package/js/inspect/scan.mjs +125 -0
  46. package/js/{status.mjs → inspect/status.mjs} +27 -25
  47. package/js/inspect/validate.mjs +199 -0
  48. package/js/lib/fs.mjs +37 -759
  49. package/js/lib/json.mjs +344 -0
  50. package/js/lib/paths.mjs +242 -0
  51. package/js/lib/proc.mjs +3 -3
  52. package/js/lib/text.mjs +181 -0
  53. package/js/lib/udiff.mjs +1 -1
  54. package/js/{memory.mjs → maintain/memory.mjs} +10 -10
  55. package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
  56. package/js/{setup.mjs → maintain/setup.mjs} +34 -32
  57. package/js/{uninstall.mjs → maintain/uninstall.mjs} +55 -56
  58. package/js/{update.mjs → maintain/update.mjs} +51 -51
  59. package/js/{anim.mjs → ui/anim.mjs} +30 -28
  60. package/js/{cli.mjs → ui/cli.mjs} +10 -10
  61. package/js/{menu.mjs → ui/menu.mjs} +10 -9
  62. package/js/{tui.mjs → ui/tui.mjs} +36 -34
  63. package/js/web/actions.mjs +84 -85
  64. package/js/web/activity.mjs +22 -21
  65. package/js/web/api.mjs +154 -51
  66. package/js/web/daemon.mjs +319 -0
  67. package/js/web/docs.mjs +24 -21
  68. package/js/web/graph.mjs +0 -0
  69. package/js/web/handler.mjs +442 -0
  70. package/js/web/jobs.mjs +21 -19
  71. package/js/web/routes.mjs +245 -0
  72. package/js/web/server.mjs +56 -987
  73. package/package.json +11 -3
  74. package/src/doctrines/README.md +1 -1
  75. package/themes/opencode/README.md +1 -1
  76. package/web/dist/assets/{Activity-D_sYBlW-.js → Activity-B49fE_ug.js} +1 -1
  77. package/web/dist/assets/{ActivityDetail-BomKcMhy.js → ActivityDetail-abt7hzD2.js} +1 -1
  78. package/web/dist/assets/{Diff-BSvi1WUg.js → Diff-D3_bVkih.js} +1 -1
  79. package/web/dist/assets/{Doctor-Cz1s-y1n.js → Doctor-hXp7srsx.js} +1 -1
  80. package/web/dist/assets/{Graph-CuntzFJ0.js → Graph-DvYMtlvl.js} +1 -1
  81. package/web/dist/assets/Harness-BIWei7N5.js +1 -0
  82. package/web/dist/assets/Laws-CxmnRdwt.js +3 -0
  83. package/web/dist/assets/{Library-DXhTN9ju.js → Library-MjbOkHIW.js} +1 -1
  84. package/web/dist/assets/{Markdown-BoiZcdUl.js → Markdown-Dx_w0faR.js} +1 -1
  85. package/web/dist/assets/{Profile-DfCHlZ03.js → Profile-BRFvv2gr.js} +1 -1
  86. package/web/dist/assets/{Rules-eND7hJv9.js → Rules-CfsV1EVy.js} +1 -1
  87. package/web/dist/assets/{Skills-CDl9opZO.js → Skills-DArA6IHS.js} +1 -1
  88. package/web/dist/assets/{StatusBadge-DSDtcArX.js → StatusBadge-C9bZOEZk.js} +1 -1
  89. package/web/dist/assets/{activity-Dmb0FTAx.js → activity-Cc0n2jwx.js} +1 -1
  90. package/web/dist/assets/index-Bx3oxZ1o.css +1 -0
  91. package/web/dist/assets/index-DC3-GlCY.js +1 -0
  92. package/web/dist/assets/index-DP035wPa.js +12 -0
  93. package/web/dist/assets/{index-Dg58ScBe.js → index-dHi-7M-F.js} +1 -1
  94. package/web/dist/index.html +2 -2
  95. package/web/src/pages/Laws.jsx +5 -19
  96. package/docs/declined.md +0 -200
  97. package/docs/limits.md +0 -329
  98. package/js/doctor.mjs +0 -1750
  99. package/js/emit.mjs +0 -1411
  100. package/web/dist/assets/About-DyK7yRy7.js +0 -1
  101. package/web/dist/assets/Harnesses-DgRyOviS.js +0 -1
  102. package/web/dist/assets/Laws-CWk3Mpri.js +0 -3
  103. package/web/dist/assets/Themes-CoAWuIeM.js +0 -2
  104. package/web/dist/assets/index-B-mXquBZ.css +0 -1
  105. package/web/dist/assets/index-CH_Z725C.js +0 -12
  106. package/web/dist/assets/index-jrY4m9jP.js +0 -1
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.
@@ -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
@@ -3,7 +3,7 @@ group: concepts
3
3
  order: 9
4
4
  title: "Sovereign repos (exclusions)"
5
5
  kind: "concept"
6
- link: {"hash": "#/harnesses", "label": "Manage in Harnesses →"}
6
+ link: {"hash": "#/harness", "label": "Manage in Harness →"}
7
7
  ---
8
8
  A **sovereign repo** is a folder where every GLOBAL harness install goes fully dormant: hooks stay silent and the global preamble (`AGENT.md`/`CLAUDE.md`) is never loaded there. A repo that ships its own agent config you don't want a global harness layered onto is the usual case.
9
9
 
@@ -29,7 +29,7 @@ Neither setting hides text from the agent; they hide it from *every turn*.
29
29
  It's set-and-forget — stored in the `.geneseed-footprint` marker, preserved across every rebuild, identical on every host (OpenCode, Claude Code, Bob, Copilot). Changing it re-emits the install.
30
30
 
31
31
  - **Settings** — the **Footprint** toggle flips the current install (full ⇄ lean) and rebuilds it in place.
32
- - **Harnesses tab** — a per-harness dropdown sets it for any one install independently, then **Apply**.
32
+ - **Harness tab** — on an active install, a per-harness dropdown sets it for that install independently, then **Apply**. On a host that is not installed yet, it is step 2 of **Install…**.
33
33
  - **Setup / re-theme wizard** — asks for footprint alongside voice, posture, mode, and doctrine packs.
34
34
  - **CLI** — `geneseed-build --footprint lean` (with any `--emit`).
35
35
 
@@ -3,7 +3,7 @@ group: concepts
3
3
  order: 12
4
4
  title: "Foreman mode"
5
5
  kind: "concept"
6
- link: {"hash": "#/harnesses", "label": "Set the mode in Harnesses →"}
6
+ link: {"hash": "#/harness", "label": "Set the mode in Harness →"}
7
7
  ---
8
8
  **Mode** is the session's *operating* register — how work gets executed — one of four build-time dials, beside posture (the *relationship* register), [doctrine packs](#/docs/setup-choices) (which practice rules bind) and footprint (how much of the constitution loads inline). Two modes ship, set at build time so the choice holds steady instead of drifting mid-session, and preserved across every rebuild and re-theme.
9
9
 
@@ -24,7 +24,7 @@ Because the crew runs apart from the conversation, the session stays responsive
24
24
 
25
25
  Set-and-forget, stored beside the install and preserved across rebuilds and re-themes:
26
26
 
27
- - **Harnesses tab** — the per-install **Mode** dropdown (beside Voice, Footprint, and Posture), then **Apply**.
27
+ - **Harness tab** — on an active install, the per-install **Mode** dropdown (beside Voice, Footprint, and Posture), then **Apply**. On a host that is not installed yet, the same four choices are steps inside **Install…**.
28
28
  - **Setup / re-theme wizard** — asks for mode alongside voice, posture, doctrine packs, and footprint.
29
29
  - **CLI** — `geneseed-build --mode foreman`; switch back with `--mode direct`.
30
30
 
@@ -3,7 +3,7 @@ group: extend
3
3
  order: 1
4
4
  title: "Write a new theme"
5
5
  kind: "concept"
6
- link: {"hash": "#/themes", "label": "Open the theme gallery →"}
6
+ link: {"hash": "#/harness", "label": "Open the voice gallery →"}
7
7
  ---
8
8
  A theme is one JSON file under `themes/` declaring voice tokens only: `BANNER`, `TAGLINE`, `LOADED_SIGIL`, `VOICE`, the core nouns (`ONTOLOGY`/`LAW(S)`/`DOCTRINE(S)`/`AGENT(S)`/`SKILL(S)`/`MEMORY`/`NOTEBOOK`/`WIKI`), the section intros `INTRO_*`, the epigraphs `EPI_*`, the `BENEDICTION`, the `ROAST_PERSONA`, and `DESC_*` blurbs. Copy `themes/neutral.json` and edit. `geneseed build --theme yours` renders it; `doctor` checks for missing tokens.
9
9
 
package/docs/web-ui.md CHANGED
@@ -50,12 +50,17 @@ The left rail mirrors the harness's own shape:
50
50
 
51
51
  | Group | Views |
52
52
  | --- | --- |
53
- | **🧬 Harness** | Dashboard · Activity · Constitution · Rules · Profile · Skills · Agents · Library · Docs · Graph |
54
- | **🔧 Maintain** | Changes · Doctor |
55
- | **🎨 Configure** | Harnesses · Themes · Settings |
56
- | **ℹ️ About** | About |
53
+ | *(ungrouped)* | Dashboard |
54
+ | **📖 Codex** | Constitution · Rules · Profile · Skills · Agents · Library · Docs · Graph |
55
+ | **🩺 Care** | Activity · Changes · Doctor |
56
+ | **🎨 Setup** | Harness · Settings |
57
57
 
58
- A global **Spotlight** search lives in the topbar — press <kbd>/</kbd> to focus it, type
58
+ Above the index sits a **vitals** card — the germination ring, drift, the doctor's verdict
59
+ and how long ago the harness was built — so those four facts are on screen from every page.
60
+ It is hidden where the rail collapses to icons (≤960px) and returns with the phone drawer.
61
+
62
+ A global **Spotlight** search lives in the topbar — press <kbd>/</kbd> to focus it (or
63
+ <kbd>Ctrl</kbd>/<kbd>⌘</kbd>+<kbd>K</kbd>, which also works from inside a text field), type
59
64
  to jump to any agent, skill, doc, spec, MCP server, or constitutional rule — the `laws`
60
65
  catalog it indexes is the whole constitution, so an ontology section, an invariant and a
61
66
  doctrine rule are all reachable by name; <kbd>↑</kbd>/<kbd>↓</kbd> to
@@ -129,32 +134,39 @@ streams the output of background jobs and keeps their history across reloads.
129
134
  theme, parity, links, authoring gates, drift), grouping any problems per check. Re-run
130
135
  on demand.
131
136
 
132
- ### 🎨 Configure
133
-
134
- - **Harnesses** (`#/harnesses`) — every detected install (host × scope), one row each,
135
- with per-row **Voice**, **Footprint**, **Posture**, and **Mode** pickers plus
136
- Install/Apply, an on/off switch, and remove. **Posture** is the relationship register
137
- (peer/mentor/expert/assistant/artisan — default **peer**); **Mode** is the operating
138
- register (direct/foreman — default **direct**; foreman triages tasks into isolated
139
- pipelines instead of working every one itself). Both are orthogonal to voice and to
140
- each other, picked here or via the setup wizard, and preserved across every rebuild.
141
- "Deploy to folder…" and "Rebuild all" apply to the whole set. An **Excluded folders**
142
- card (shown once a global install exists) manages sovereign repos — folders where every
143
- global harness goes dormant (hooks stay silent, the global preamble never loads);
144
- add or remove one here, the web mirror of `harness exclude add|remove|list`.
145
- - **Themes** (`#/themes`) — a gallery of the shipped voices (name, tagline, sigil, accent
146
- glow). **Apply** a voice to rebuild the deployed harness in that theme; the current one
147
- is marked.
137
+ ### 🎨 Setup
138
+
139
+ - **Harness** (`#/harness`) — this machine's own install first (path, voice, footprint,
140
+ and when it was built), then the **Voice** reference, then every detected install.
141
+ `#/harnesses` and `#/themes` both still resolve here, so old links and bookmarks keep
142
+ working.
143
+ - The **Voice** card is the former Themes tab, kept as a REFERENCE rather than a second
144
+ control: a collapsed row that opens to every shipped voice with its tagline, the
145
+ deployed one pinned first and marked. It is the one place you can read what a voice
146
+ sounds like — the per-install picker below is a list of bare names. Applying happens
147
+ there, per install, beside that install's own footprint, posture and mode; a gallery at
148
+ the top of the page could only ever act on one of them.
149
+ - **Every install** lists each detected install (host × scope), one row each. An
150
+ ACTIVE row carries per-row **Voice**, **Footprint**, **Posture** and **Mode**
151
+ pickers plus **Apply**, an on/off switch, and remove; a host that is NOT installed
152
+ yet shows a single **Install…** that discloses the same four choices as steps.
153
+ **Posture** is the relationship register (peer/mentor/expert/assistant/artisan —
154
+ default **peer**); **Mode** is the operating register (direct/foreman — default
155
+ **direct**; foreman triages tasks into isolated pipelines instead of working every
156
+ one itself). Both are orthogonal to voice and to each other, picked here or via the
157
+ setup wizard, and preserved across every rebuild.
158
+ - "Deploy to folder…" and "Rebuild all" apply to the whole set. An **Excluded folders**
159
+ card (shown once a global install exists) manages sovereign repos — folders where every
160
+ global harness goes dormant (hooks stay silent, the global preamble never loads);
161
+ add or remove one here, the web mirror of `harness exclude add|remove|list`.
148
162
  - **Settings** (`#/settings`) — a **Console direction** picker (the visual flavour of the
149
163
  console) with a **Dashboard layout** control (Auto follows each theme's designed Status
150
- lens; Cultivar / Greenhouse / Operator force one regardless of skin), the install snapshot
151
- (deployed/target/theme/version/memory store), a Maintenance card (PATH link/unlink,
152
- uninstall), an offline package download, and server controls (Stop). Build, update, and MCP
153
- wiring live in the Harnesses tab.
154
-
155
- ### ℹ️ About
156
-
157
- - **About** (`#/about`) — project + creator credits and the source link.
164
+ lens; Cultivar / Greenhouse / Operator / Journal force one regardless of skin), the
165
+ install snapshot (deployed/target/theme/version/memory store), a Maintenance card (PATH
166
+ link/unlink, uninstall), an offline package download, server controls (Stop), and the
167
+ **About** colophon — project + creator credits and the source link, folded in from the
168
+ retired `#/about` tab (which still resolves here). Build, update, and MCP wiring live in
169
+ the Harness tab.
158
170
 
159
171
  ## Rebuilding the UI
160
172
 
@@ -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.1"
5
+ "version": "3.1.3"
6
6
  }