geneseed 2.0.0 → 2.1.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 (68) hide show
  1. package/CHANGELOG.md +109 -5
  2. package/DESIGN.md +54 -1282
  3. package/README.md +8 -10
  4. package/SETUP.md +15 -7
  5. package/SHIPPED.md +3 -3
  6. package/adapters/opencode/opencode.json +0 -9
  7. package/bin/geneseed.mjs +2 -2
  8. package/docs/declined.md +53 -2
  9. package/docs/design-history.md +1290 -0
  10. package/docs/extending.md +563 -0
  11. package/docs/limits.md +141 -82
  12. package/docs/web/agents.md +1 -1
  13. package/docs/web/rules.md +1 -1
  14. package/geneseed +0 -0
  15. package/harness.config.json +1 -1
  16. package/js/cli-table.json +1 -1
  17. package/js/doctor.mjs +67 -22
  18. package/js/inventory.mjs +2 -1
  19. package/js/lib/fs.mjs +6 -2
  20. package/js/mcp.mjs +3 -15
  21. package/js/render.mjs +6 -4
  22. package/js/settings.mjs +79 -1
  23. package/js/themes.mjs +1 -1
  24. package/js/tui.mjs +2 -2
  25. package/js/uninstall.mjs +5 -5
  26. package/js/web/actions.mjs +20 -4
  27. package/js/web/server.mjs +53 -2
  28. package/package.json +1 -1
  29. package/src/laws/universal.md +35 -0
  30. package/themes/_TEMPLATE.json +3 -0
  31. package/themes/biker.json +3 -0
  32. package/themes/commentator.json +3 -0
  33. package/themes/cyberpunk.json +3 -0
  34. package/themes/gamer.json +3 -0
  35. package/themes/imperial.json +3 -0
  36. package/themes/joker.json +3 -0
  37. package/themes/marvin.json +3 -0
  38. package/themes/mean.json +3 -0
  39. package/themes/military.json +3 -0
  40. package/themes/neutral.json +3 -0
  41. package/themes/pirate.json +3 -0
  42. package/themes/sports.json +3 -0
  43. package/themes/verstappen.json +3 -0
  44. package/themes/wizard.json +3 -0
  45. package/web/dist/assets/{About-DX1-VTBP.js → About-BYXlmbyo.js} +1 -1
  46. package/web/dist/assets/{Activity-Dp9NaXOn.js → Activity-us5MPf4u.js} +1 -1
  47. package/web/dist/assets/{ActivityDetail-HpOBtrXS.js → ActivityDetail-CiaQXUk2.js} +1 -1
  48. package/web/dist/assets/{Diff-Ct4G3Se8.js → Diff-Bh0qbZ2s.js} +1 -1
  49. package/web/dist/assets/{Doctor-DGjkZFuA.js → Doctor-BIlQME45.js} +1 -1
  50. package/web/dist/assets/{Graph-b8o-S6kv.js → Graph-4dYD1dPG.js} +1 -1
  51. package/web/dist/assets/Harnesses-PzYZ3LOC.js +1 -0
  52. package/web/dist/assets/Laws-DtZweXal.js +1 -0
  53. package/web/dist/assets/{Library-BEZscyaQ.js → Library-CjMbksJW.js} +1 -1
  54. package/web/dist/assets/{Markdown-k_LSLBGq.js → Markdown-Bk81c3aZ.js} +1 -1
  55. package/web/dist/assets/{Profile-DydukTxP.js → Profile-Curui_-j.js} +1 -1
  56. package/web/dist/assets/{Rules-CIRG4Ldt.js → Rules-Bm283wdv.js} +1 -1
  57. package/web/dist/assets/{Skills-Buoi1IBr.js → Skills-gruqYuhl.js} +1 -1
  58. package/web/dist/assets/{StatusBadge-CE_AJttl.js → StatusBadge-EiM_FPqv.js} +1 -1
  59. package/web/dist/assets/{Themes-CI4o3gJC.js → Themes-DU99j4Hr.js} +1 -1
  60. package/web/dist/assets/{activity-BCW7e6Cy.js → activity-0rvQ6Vra.js} +1 -1
  61. package/web/dist/assets/{index-B1PIx7RM.js → index-0b2hobOo.js} +1 -1
  62. package/web/dist/assets/index-D5VY3TY_.js +12 -0
  63. package/web/dist/assets/{index-Ck2iCdaC.js → index-IiGFmCW_.js} +1 -1
  64. package/web/dist/index.html +1 -1
  65. package/web/src/pages/Laws.jsx +3 -0
  66. package/web/dist/assets/Harnesses-gr3-aHBF.js +0 -1
  67. package/web/dist/assets/Laws-DTwF5tTC.js +0 -1
  68. package/web/dist/assets/index-BnTQUSUz.js +0 -12
@@ -0,0 +1,563 @@
1
+ # Extending — what it costs to change this tool
2
+
3
+ `docs/limits.md` is the standing answer to *"what does this tool not prove about itself?"*.
4
+ This file is its companion: **"what does it cost to add one thing?"** — one law, one skill, one
5
+ plugin, one hook verb, one doc page, one theme, one screen.
6
+
7
+ It exists because the answer is never one file. Every user-visible noun in Geneseed has
8
+ *satellites* — a themed title in fourteen voices, a class in a hand-written map, a count in a
9
+ badge, a row in a ledger, a page in the console — and the gates that keep them in step are spread
10
+ across `doctor`, the Node suite and four CI jobs. A contributor who knows the satellites ships in
11
+ one pass; one who does not discovers them one red gate at a time.
12
+
13
+ Written after the Python migration completed (2026-08-18), against `main`. Verified: every
14
+ file:line below was opened, and the commands were run.
15
+
16
+ ---
17
+
18
+ ## 0 — The frame
19
+
20
+ Three seams, and almost every question about "where do I edit?" resolves to one of them.
21
+
22
+ | Seam | Source of truth | How it reaches a user | Rebuild needed |
23
+ |---|---|---|---|
24
+ | **Harness content** | `src/` (laws, agents, skills, memory/notebook conventions, `AGENT.md.tmpl`) + `themes/*.json` for voice | rendered into a bundle by `geneseed build` / `update` / `rebuild-all` | yes — every active install must be re-emitted |
25
+ | **Console docs** | `docs/web/*.md` + `docs/web/_groups.json` | read off disk at request time by `js/web/docs.mjs:101` | no — not even a daemon restart for content |
26
+ | **Console app** | `web/src/**` | built by vite into the **tracked** `web/dist/` | yes — rebuild *and commit* dist, then `geneseed web restart` |
27
+
28
+ Two rules follow from the table and they cause most of the surprises:
29
+
30
+ - **Nothing propagates by itself.** A bundle is a *render*. Editing `src/` changes nothing on any
31
+ machine until something re-emits (`js/emit.mjs:362` re-fingerprints, `js/status.mjs:80`
32
+ reports the drift).
33
+ - **`docs/web/` is data, `web/src/` is code.** A new documentation page is one file and no
34
+ rebuild. A new screen is a new hashed chunk and a mandatory `web/dist` commit.
35
+
36
+ ### Counts are computed — never type one
37
+
38
+ `{N_LAWS}` `{N_AGENTS}` `{N_SKILLS}` `{N_PLUGINS}` are substituted at request time from the live
39
+ inventory (`js/web/docs.mjs:169-172`). Typing a literal number into a `docs/web` page is caught by
40
+ `proseMirrorProblems` (`js/doctor.mjs:715-737`). The README badges and prose, and the
41
+ `N laws, N agents, N skills` line in `SHIPPED.md`, are the hand-written exceptions — and each has
42
+ its own doctor arm (`js/doctor.mjs:676`, `:739`, `:822-829`).
43
+
44
+ ---
45
+
46
+ ## 1 — The baseline, and the five commands that reproduce CI
47
+
48
+ Verified on this checkout: `doctor --all` green on 14 themes; the Node suite reports
49
+ **1033 tests, 1029 pass, 4 skipped, 0 fail**.
50
+
51
+ CI is **four jobs expanding to six runs** (`.github/workflows/ci.yml`): `validate` and
52
+ `node-cells` on Linux *and* Windows, plus `package-no-python` and `web` on Linux. From the repo
53
+ root — never from a worktree, see §5:
54
+
55
+ ```bash
56
+ node bin/geneseed-cli.mjs doctor --all
57
+ ```
58
+
59
+ ```bash
60
+ node --test --test-reporter=tap "tests/**/*.test.mjs"
61
+ ```
62
+
63
+ ```bash
64
+ node tests/golden.mjs --idempotent
65
+ ```
66
+
67
+ ```bash
68
+ node tests/golden.mjs --deletion
69
+ ```
70
+
71
+ ```bash
72
+ node tests/mutate.mjs --verify
73
+ ```
74
+
75
+ ```bash
76
+ cd web && npm ci && npm run lint && npm run format:check && npm test && npm run build
77
+ ```
78
+
79
+ Notes that matter:
80
+
81
+ - The root `package.json` has **no `scripts` key**, and no pre-commit hook, Makefile or justfile
82
+ exists either. `npm test` at the root exits 1 with *Missing script: "test"*; the suite is run by
83
+ hand with the glob above.
84
+ - `--test-reporter=tap` is not decoration. Both workflows gate on the *count* (`# tests N ≥ 500`)
85
+ because `node --test` over a glob that matches nothing prints `# tests 0` and **exits 0**. Node's
86
+ default reporter changed between majors, which is how a green 1043-test suite once reported zero.
87
+ - `--deletion` is the only thing that exercises the write-before-delete prune. Anyone renaming or
88
+ removing a plugin, a skill or an emitted file needs it.
89
+ - **A local pass is weaker than CI.** ~18 tests in `tests/unit/package_manifest.test.mjs` carry
90
+ `{ skip: NO_NPM }` (`:83`) — every tarball, install and rename gate. On a machine without `npm`
91
+ on PATH they report *skipped* and the run is green.
92
+ - CI additionally re-runs the packaging suite under `npm@latest` (Linux, last in the job) and
93
+ packs the tarball into a `node:22-slim` container with no interpreter. Neither is reproducible
94
+ from the five commands; the container one is reachable locally through
95
+ `tests/helpers/no-python-container.sh` with a docker daemon.
96
+ - **Two assertions in `tests/unit/claude.test.mjs` silently stand down** on any machine where an
97
+ ancestor of the temp sandbox is a `.claude`/`.bob` install — which is every developer machine
98
+ with Claude Code installed under `~`. They are replaced by a `t.diagnostic`, so the TAP
99
+ `# skipped` count does **not** move. Locally you read `1035/1031/0/4` and see no sign of it.
100
+
101
+ ### The gates CI does not run
102
+
103
+ - **The mutation matrix itself.** `tests/mutate.mjs` — 31 rows, each a one-string edit to a
104
+ product file with a written argument for which gate should catch it. The full run is **in no
105
+ workflow** and should not be: it mutates the product tree 31 times and runs a gate per edit.
106
+ Run `node tests/mutate.mjs --all` (or `--only M7`) by hand when you change something a gate is
107
+ supposed to be watching. ⚠ An interrupted run leaves the **product tree mutated** — check
108
+ `git status` before doing anything else.
109
+ Its *anchors* are now in CI (`--verify`, above): a row whose anchor a refactor has moved counts
110
+ as a **survivor**, not a skip, and until 2026-08-19 that only showed up for whoever ran the
111
+ matrix by hand — which nothing did.
112
+ - **Node 24.** No CI job runs it; `publish.yml` does. See §8.
113
+ - **`tests/golden.mjs`** is absent from the publish path entirely.
114
+
115
+ ### Gates that fire on changes you would not expect to be gated
116
+
117
+ - **A new subprocess anywhere in `js/` or `bin/`** — `tests/unit/spawn_hygiene.test.mjs` requires
118
+ capturing spawns to set `windowsHide` and inheriting spawns not to. (`adapters/` is *not*
119
+ scanned, so a plugin that spawns is on its own.)
120
+ - **Editing `.github/workflows/*.yml`** — seven structural assertions in
121
+ `tests/unit/docs.test.mjs:336-399` gate `publish.yml`'s shape, and `:422-444` discovers every
122
+ workflow with a test-count gate and pins `--test-reporter=tap` on it. A unit test gates the CI
123
+ files.
124
+ - **Adding any tracked file** — `tests/unit/package_manifest.test.mjs:401` demands every tracked
125
+ path fall on exactly one side of a declared partition, *with a written reason*. Adding this very
126
+ document required a `SHIPS` row; without one the tarball test fails, and it fails **before** the
127
+ file is committed too, because npm packs by glob while the partition reads `git ls-files`.
128
+
129
+ ---
130
+
131
+ ## 2 — Adding a LAW
132
+
133
+ Ten files, of which five are pure satellites. Ground truth: `git show --stat 1b53701` (law XXXVIII)
134
+ touched 21 files, 14 of them themes.
135
+
136
+ 1. `src/laws/universal.md` — **append** `### {{LAW}} <roman> — {{LEX_<roman>}}` plus the body.
137
+ Never insert: 397 `{{LAW}} <numeral>` cross-references live in `src/`, plus hardcoded citations
138
+ in `js/hooks.mjs` and two OpenCode plugins, and **nothing resolves a cross-reference against the
139
+ canon**. Renumbering silently rewires every one of them.
140
+ 2. `themes/_TEMPLATE.json` — add `LEX_<roman>` in template order. **Nothing gates this file**
141
+ (`themeFiles()` filters `_`-prefixed names, `js/installs.mjs:113`), so forgetting it is green
142
+ today and starves the *next* law's sync.
143
+ 3. Seed the fourteen themes:
144
+
145
+ ```bash
146
+ node bin/geneseed.mjs --sync-themes
147
+ ```
148
+
149
+ ⚠ Four spellings of this command are in circulation and two of them fail — `doctor`'s own tip
150
+ is one of the two. See §7.2 before you copy a command from anywhere else.
151
+ 4. Restyle all fourteen by hand. `--sync-themes` writes the template's `<themed title for …>`
152
+ placeholder and only *prints* `RESTYLE these`; a shipped placeholder passes every gate.
153
+ 5. `js/inventory.mjs:40` — `LAW_CLASS`, one of the six in `LAW_CLASSES` (`:56`).
154
+ 6. `web/src/pages/Laws.jsx:28` — `LAW_META`: same class, plus the one-line Principle. **This copy
155
+ exists nowhere else**; a law absent here renders with a blank description. It is why that one
156
+ React file ships alone out of `web/src` in `package.json` — `doctor` regex-parses its literal.
157
+ 7. `README.md` — badge `badge/laws-N` and the `N universal laws` sentence.
158
+ 8. `SHIPPED.md` — the `N laws, N agents, N skills` triple.
159
+ 9. `tests/unit/harness.test.mjs:293` — the negative fixture hardcodes the *next* free numeral;
160
+ bump it or it stops being a fixture and starts being a duplicate.
161
+ 10. `CHANGELOG.md`, then rebuild and commit `web/dist` (step 6 touched `web/src`).
162
+
163
+ **Amending** a law is steps 1, 10 and — if the principle moved — 6. Two extra cautions:
164
+
165
+ - `--footprint` defaults to **lean**, and lean keeps only the heading plus the first sentence
166
+ (`js/render.mjs:167`). A clause added to the second paragraph is invisible in the default build.
167
+ Amend the first sentence, or the amendment does not exist for most installs.
168
+ - ⚠ **Law I is frozen.** `LEX_I` is one of seven theme keys recorded byte-for-byte in the
169
+ primitives corpus (§5). Retitling Law I in any theme reddens a snapshot that cannot be re-blessed.
170
+
171
+ The carrier itself is gated: `tests/unit/emit_smoke.test.mjs:211-265` checks every law reaches
172
+ every one of nine emit modes in order and themed, under both footprints, with a ceiling **and** a
173
+ floor on the rendered size. Headroom measured today: ~2 100 chars full, ~1 200 lean — about three
174
+ average laws at full footprint, which is the binding one.
175
+
176
+ ---
177
+
178
+ ## 3 — Adding a SKILL or an AGENT
179
+
180
+ **Order matters at the first two steps.** `missingReferencedSpecs` (`js/emit.mjs:493`) *aborts the
181
+ build* when `AGENT.md.tmpl` names a spec with no file — so the file comes before the table row, or
182
+ nothing builds at all. The refusal is clean: `tests/unit/emit_gates.test.mjs:265` proves a refused
183
+ emit leaves the prior install intact.
184
+
185
+ 1. `src/skills/<name>.md` or `src/agents/<name>.md`. The shape is load-bearing: `# {{SKILL}}: <name>`,
186
+ a blank line, then a `>` blockquote holding `{{DESC_<NAME>}}` — title, blockquote, nothing
187
+ between. `doctor` checks both the presence of the purpose line and that the blockquote is the
188
+ *first* block (`js/doctor.mjs:899-908`).
189
+ 2. One row in the hand-authored table in `src/AGENT.md.tmpl` (agents `125-141`, skills `183-229`).
190
+ Gated both ways: a row with no file, and a file with no row.
191
+ 3. `DESC_<NAME>` in **all fourteen** themes. Theme parity catches a missing one; nothing catches
192
+ an unstyled placeholder.
193
+ 4. **Skills only** — `SKILL_CLASS` in `js/inventory.mjs:59`. Gated both ways
194
+ (`js/doctor.mjs:785-791`), which matters because the runtime fallback is silent: an unclassified
195
+ skill renders as `build` (`js/inventory.mjs:166`).
196
+ 5. `registry.json` — a row keyed `skills/<name>` or `agents/<name>` with `status` ∈
197
+ {`experimental`, `approved`, `deprecated`}, a semver `version`, a non-blank `owner`, an ISO
198
+ `added`, and `last_verified: ""`. **This is the most forgettable step**: four hand-typed fields,
199
+ and the gate fires in both directions (`js/doctor.mjs:388-436`).
200
+ 6. `README.md` — the badge, the `**🤖 Agents** (N)` / `**🛠 Skills** (N)` count, and for a skill the
201
+ `·`-separated name in the 🛠 row (that enumeration is gated two ways, unlike the laws' one).
202
+ 7. `SHIPPED.md` — the `N laws, N agents, N skills` triple.
203
+ 8. `doctor --all`, then the suite.
204
+
205
+ **A vendored skill folder** (`src/skills/<name>/SKILL.md`) is a different shape: add it to
206
+ `VENDORED_SKILL_DIRS` (`js/native.mjs:37` — the literal is frozen in
207
+ `tests/unit/authoring_gates.test.mjs:312`), write a `VENDOR.md` carrying `**Upstream:**`,
208
+ `**Commit:**` (a 40-hex commit, never a moving branch) and `**License:**`, add the registry row —
209
+ and **skip steps 2, 4, 6, 7**: the flat-spec globs only see `*.md` at the top level.
210
+
211
+ Two traps worth knowing before you write the spec:
212
+
213
+ - **The word "Read-only" anywhere in an agent spec locks its emit.** It is a bare substring test
214
+ (`js/native.mjs:148`). The escape hatch is an explicit `<!-- bash: allow -->` marker, which
215
+ re-opens bash on all three hosts.
216
+ - **The console has a fourth, ungated copy of the skill taxonomy.** `SKILL_CATS` in
217
+ `web/src/pages/Skills.jsx:19` mirrors `SKILL_CLASS` with nothing comparing them (contrast
218
+ `LAW_META`, which *is* cross-checked). Inventing a new *category* means editing that file —
219
+ which drags in the `web/dist` rebuild — and an unknown category renders as Build with no warning.
220
+
221
+ Adding a spec reddens no recording: none of the surviving snapshots contains a skill or agent name.
222
+
223
+ ---
224
+
225
+ ## 4 — Adding a PLUGIN, a HOOK verb, or a doc page
226
+
227
+ ### 4a — An OpenCode plugin
228
+
229
+ The `geneseed-` prefix and the `.js` suffix are mechanically load-bearing: the badge count, the
230
+ `{N_PLUGINS}` substitution and the four-way gate all key off that glob.
231
+
232
+ 1. `adapters/opencode/plugins/geneseed-<name>.js` — ESM, zero dependencies. The factory-export
233
+ convention is real (OpenCode requires it) but **ungated**; the only generic gate on plugin
234
+ source is `node --check` (`js/doctor.mjs:924-941`).
235
+ 2. `docs/web/plugin-<name>.md` — `group: plugins`, `title: "geneseed-<name>"`, `kind: "concept"`.
236
+ 3. `docs/web/plugins.md` — a bullet. The count above it renders from `{N_PLUGINS}`.
237
+ 4. `docs/web/model.md` — a second `{N_PLUGINS}` sentence whose **enumeration of the capabilities
238
+ is hand-written and ungated**.
239
+ 5. `README.md` — **three** sites: the badge, the `N plugins` prose, and the 🔌 Plugins row.
240
+ 6. `SHIPPED.md` — the bare name inside `plugins (…)`; that list must equal the directory exactly,
241
+ in both directions (`tests/unit/web_api.test.mjs:1655`).
242
+ 7. `adapters/opencode/README.md` and `docs/opencode-plugin-setup.md` — spelled-out counts, in
243
+ **word form** ("seven" → "eight"), the second file twice.
244
+ 8. `tests/plugins/<name>.test.mjs`. Auto-discovered; no workflow edit.
245
+
246
+ No `registry.json` row, no `package.json` edit — `files[]` ships `adapters/` whole.
247
+
248
+ ### 4b — A hook verb (claude + bob)
249
+
250
+ ⚠ **Read §5 first. A fifth hook verb has no green path today.**
251
+
252
+ The mechanical part: implement `cmd<Verb>` in `js/hooks.mjs`; wire the command string in
253
+ `claudeHookGroups` (`js/settings.mjs:434`, spelled `` `${run} <verb> …` `` — the gate scrapes that
254
+ shape); add the entry to `bin/geneseed-hook.mjs:43` `VERBS` with **exactly two-space indent**; add
255
+ a row to `js/cli-table.json` with a help string over 20 characters.
256
+
257
+ ### 4c — A documentation page
258
+
259
+ **One file, no rebuild, no server change.** `docs/web/<id>.md`, where the stem is the id.
260
+ Frontmatter is JSON-per-value, not YAML (`js/web/docs.mjs:63-82`):
261
+
262
+ - `group` must match an id in `_groups.json` — an unknown group is a **silent skip**.
263
+ - `kind` ∈ `markdown | concept | glossary | about | cli`; anything else 404s.
264
+ - `harness: "claude"|"opencode"` hides the page from the other host. Note the `plugins` group
265
+ carries this at the **group** level, and `hooks` symmetrically — a Claude-relevant page dropped
266
+ into `plugins` vanishes with no error.
267
+ - `kind: "markdown"` pulls from a repo file via `source:`, traversal-guarded, optionally sliced to
268
+ one section with `anchor:` + `slice: true`.
269
+
270
+ Inline `<!-- harness:claude -->` blocks strip per host and **fail open** on an unbalanced marker —
271
+ the page renders, wrong, with nothing red.
272
+
273
+ ---
274
+
275
+ ## 5 — The four things that are genuinely hard
276
+
277
+ These are not checklist items. They are structural, and each one should be settled *before* the
278
+ change that needs it, not during.
279
+
280
+ ### 5.1 ✅ A fifth hook verb — resolved 2026-08-19
281
+
282
+ **This was a hard blocker and it is now open.** Recorded here because the shape of the problem
283
+ recurs: any gate whose expected value lives in an artifact nothing can re-make will eventually
284
+ stop a legitimate change, and the fix is never to delete the artifact.
285
+
286
+ `tests/unit/hook_cli.test.mjs` asserted a strict equality between the hook verbs the frozen
287
+ matrix covers and the verbs `bin/geneseed-hook.mjs` carries. The CLI half of the same test
288
+ already exempted a named `NATIVE` list; the hook half exempted nothing — so a fifth hook verb
289
+ failed with **no green path**, since the recorder died on 2026-08-17 and the failure message
290
+ correctly forbids deleting cells to pass.
291
+
292
+ Both halves now filter through the same `NATIVE` list, and the reverse check refuses an
293
+ exemption for a verb neither entry point carries. The argument is in `docs/declined.md` — the
294
+ short version is that the reason those verbs have no cell (*the reference had nothing to compare
295
+ against*) is the same reason a verb postdating the reference has none, so it is one exemption,
296
+ not two. Nothing measurable was lost: the hook verb set is pinned twice more, from editable
297
+ sources, twelve lines above.
298
+
299
+ **To add a hook verb today:** implement `cmd<Verb>` in `js/hooks.mjs`, wire the string in
300
+ `claudeHookGroups`, add the `VERBS` entry, add the `js/cli-table.json` row, then name the verb in
301
+ `NATIVE` in **both** `tests/unit/hook_cli.test.mjs` and `tests/snapshot/cli_help.test.mjs` (they
302
+ read each other's source, so one alone fails) and give it an absolute unit gate.
303
+
304
+ ### 5.2 ⚠ Seven theme keys are frozen byte-for-byte
305
+
306
+ `tests/__snapshots__/primitives/{posix,win32}.json` record `theme_preview` and `theme_flair`
307
+ answers for all fourteen themes **and the template**. Those probes read the live `themes/*.json`
308
+ at replay time, which pins:
309
+
310
+ | Probe | Frozen keys |
311
+ |---|---|
312
+ | `theme_preview` | `TAGLINE`, `LOADED_SIGIL`, `VOICE`, **`LEX_I`**, `BENEDICTION` |
313
+ | `theme_flair` | `ACCENT`, `BANNER` |
314
+
315
+ Editing any of those, in any theme, reddens a snapshot with **no re-bless path**. Adding a *new*
316
+ law is safe (only `LEX_I` is recorded). Restyling an existing theme's tagline or banner is not.
317
+
318
+ This was proven, not inferred: prefixing `marvin.json`'s `TAGLINE` turned
319
+ `tests/snapshot/pure_snapshot.test.mjs` red at `case 1275`.
320
+
321
+ ### 5.3 ⚠ The hook shim is machine-wide and last-writer-wins
322
+
323
+ `~/.geneseed/bin/geneseed-hook[.cmd]` has no per-install component. The last checkout to emit
324
+ anything owns **every** install's hooks. Emitting from a git worktree or a temp copy used to
325
+ repoint the shim at that disposable path; removing the worktree then killed hooks for every
326
+ install on the machine.
327
+
328
+ **`hookPrefix` now refuses the claim** (`js/settings.mjs`, `ephemeralCheckout`): an emit whose
329
+ checkout is under the OS temp root, or is a linked git worktree (`.git` is a *file*), leaves a
330
+ shim that still resolves exactly as it is and wires the emitted hooks to it anyway. The bundle it
331
+ writes is byte-identical to a normal emit — the install simply keeps running the durable
332
+ checkout's entry, which is what last-writer-wins already handed every other install. A shim that
333
+ is **absent or already dead** is still written from anywhere, so a first emit on a fresh machine
334
+ works and the suite's own sandboxed emits are unchanged.
335
+
336
+ Two gates hold it: `tests/unit/hook_form.test.mjs` pins the rule (including the worktree arm,
337
+ reachable because `ephemeralCheckout` takes the temp root as a parameter), and
338
+ `tests/shim_intact.mjs` — a step in `ci.yml` and `publish.yml`, *after* `node --test`, because
339
+ each test file runs in its own process and the file that causes this is not the file that would
340
+ notice — fails if the suite left the shim naming anything that is not there.
341
+
342
+ This is not hypothetical — it happened while this file was being written. Several agents were
343
+ verifying gates out of copied checkouts (the `copyCheckout` fixture pattern
344
+ `tests/unit/harness.test.mjs:173` uses), one of them emitted, and the shim ended up pointing at
345
+ `…/Temp/gs-fix-8DYuJP/bin/geneseed-hook.mjs` — a directory that no longer existed. Every hook in
346
+ every install on the machine was dead and nothing said so.
347
+
348
+ The recovery is free, which is the part worth remembering: **`doctor` detects it and the run's own
349
+ emit repairs it**, reporting *"the checkout most likely moved … no further action needed"*. That
350
+ is still the answer for the case the guard cannot cover — a real checkout that genuinely moved —
351
+ so after any session that emitted from somewhere unusual, `doctor --all` from the real checkout
352
+ remains the cheapest thing to run.
353
+
354
+ Related: a stray `console.log` on a hook path silently disables a gate. Hooks signal through
355
+ stdout JSON and return 0 on every path — a printed byte is not a warning, it is a disabled gate.
356
+
357
+ ### 5.4 ⚠ A snapshot red is a finding, not a step
358
+
359
+ Everything left under `tests/__snapshots__/` — the primitives, the 26 help texts, and the small
360
+ pure-function recordings — was produced by an implementation that no longer exists. There is no
361
+ `--record`. A change that moves a recorded byte leaves two honest options: revert it, or argue in
362
+ writing (in `docs/declined.md`) that the recording was wrong. `docs/limits.md` carries the full
363
+ argument and the measurement that licensed retiring the three larger corpora.
364
+
365
+ **A worked example, from the session that wrote this file.** Closing a real defect — three count
366
+ gates that go green when you *delete* the sentence they check — turned the primitives corpus red
367
+ at `case 371`: `proseMirrorProblems` is a recorded pure function, the recording holds a case that
368
+ passes an empty `shipped` string and expects `[]`, and the new absence arm answered otherwise.
369
+ Both obvious moves are wrong. Re-blessing is impossible. Abandoning the fix leaves a gate failing
370
+ open. The third move is the one to reach for: **the presence check moved up one level, into
371
+ `countTableProblems`, which is not recorded** — where it reads better anyway, beside the code that
372
+ already knows whether the file could be opened. The rule that falls out: *when a frozen recording
373
+ blocks a change, look for the nearest caller that is not frozen.* Check with
374
+
375
+ ```bash
376
+ node -e "const j=require('./tests/__snapshots__/primitives/win32.json');console.log([...new Set(j.cases.map(c=>c.fn))].join('\n'))"
377
+ ```
378
+
379
+ The concrete consequence for evolution: **editing the help text or the flags of any of the 26
380
+ recorded CLI verbs is a frozen surface.** New verbs have a path (`NATIVE`); changes to old ones
381
+ do not. `docs/limits.md:311-316` names this cluster as the one open standing decision.
382
+
383
+ The CLI surface is the most expensive thing in the repo to change, because four hand-written
384
+ equalities converge on it: `js/cli-table.json`'s 29-command count (`tests/unit/cli_table.test.mjs:67`),
385
+ the 5-entry hidden-argument list (`tests/snapshot/cli_help.test.mjs:219`), the 25-subparser count
386
+ (`tests/unit/docs.test.mjs:193`), and the 26 unblessable help fixtures. Plus
387
+ `tests/unit/docs.test.mjs:554`, which validates every fenced `geneseed <verb> <flags>` line in
388
+ `README.md`, `SETUP.md`, `QUICKSTART.md` and `SHIPPED.md` against the table.
389
+
390
+ ---
391
+
392
+ ## 6 — Hand-maintained, therefore rotting
393
+
394
+ Each of these is a copy of a decision that lives somewhere else, with no gate on its *accuracy* —
395
+ only, at best, on its presence.
396
+
397
+ | What | Where | Gated on |
398
+ |---|---|---|
399
+ | Per-law Principle lines | `web/src/pages/Laws.jsx` | presence and class only, never accuracy |
400
+ | 560 themed law titles (40 × 14) | `themes/*.json` | key presence only — a shipped placeholder is green |
401
+ | The README keyword enumerations | `README.md`, `docs/web/rules.md` | nothing |
402
+ | The plugin capability enumeration | `docs/web/model.md` | nothing (the number substitutes; the list does not) |
403
+ | Section labels and page subtitles | `web/src/lib/sections.js`, `web/src/pages/Docs/index.jsx` | nothing |
404
+ | `THEME_BLURBS` (8 of 14), `ART` (8 of 14) | `js/setup.mjs`, `js/anim.mjs` | nothing — missing entries fall back silently |
405
+ | `LOADED_SIGIL` uniqueness | `themes/*.json` | nothing, and it is load-bearing for theme detection |
406
+ | `adapters/claude-code/settings.json` | — | nothing compares it to what the emitter writes; **it is already divergent** |
407
+ | `NATIVE`, twice | two test files | they check each other, which is the gate that makes the duplication safe |
408
+ | The emit-size `CEILING` | `tests/unit/emit_smoke.test.mjs:54` | a hand-transcribed measurement |
409
+ | Stale docblock counts | `js/themes.mjs:62` ("137 values" → 140), `js/render.mjs:116` ("145 tokens" → 148) | nothing |
410
+
411
+ ---
412
+
413
+ ## 7 — Stale on 2026-08-18, fixed on 2026-08-19
414
+
415
+ Nine items were found while writing this file, and fixed the next day. They are recorded as a
416
+ list rather than left as a diff, because the PATTERN is the useful part: every one was a document
417
+ or a comment pointing a reader at something that is not there, and in each case the gate that
418
+ should have caught it either did not cover that file — or was itself the frozen copy of a wrong
419
+ answer.
420
+
421
+ **Fixed.**
422
+
423
+ 1. **`DESIGN.md` taught the deleted Python build.** Its header promises *"the spec behind the
424
+ harness — read this before changing structure"*, and under it sat the six-edit checklist for
425
+ adding a Law, naming a deleted generator, `rituals/_harness_tui.py` for `LAW_CLASS`, and an
426
+ invocation of a program that no longer exists. Worse, `tests/unit/no_python.test.mjs` exempted
427
+ the file as a *historical record* — so the gate that hunts pointers-to-nothing was looking away
428
+ from the one document most likely to hold one. **Split**: the 190-line standing contract keeps
429
+ the name and is now scanned; the 1,290-line port narrative moved to
430
+ [design-history.md](design-history.md), which is exempt on the merits, and its dead links were
431
+ repaired because a link is a pointer whatever the prose around it says.
432
+ 2. **`doctor` printed a command that errors.** The theme-parity tip named
433
+ `./geneseed build --sync-themes`; `cmdBuild` forwards `--theme` and nothing else. The comment
434
+ above the tip argued that spelling on a premise the port had quietly stopped satisfying. It now
435
+ names `geneseed-build --sync-themes`, which is what README and SETUP said all along.
436
+ 3. **User-facing output named a program that does not exist.** `tui` and `uninstall` told users to
437
+ run `harness setup` / `harness uninstall` — the reference's name, dropped in P0. Five sites.
438
+ One of them was **pinned verbatim by a test**, so a gate was holding the wrong string in place;
439
+ that assertion is now a property — every backticked command a refusal offers must be a verb
440
+ `js/cli-table.json` declares — which is strictly stronger and fires on the next rename too.
441
+ 4. **The `tui` help promised a "full-screen curses control panel"** for a verb that returns 1 on
442
+ every path. `docs/limits.md` listed this as permanently unfixable, on the grounds that the help
443
+ corpus is frozen — but `formatHelp` never reads `cmd.help`, so it was an ordinary edit all
444
+ along. The limits row went with the fix.
445
+ 5. **Numbers in docblocks that had drifted** — `_TEMPLATE.json`'s "137 values" (140), "the 145
446
+ tokens" `src/` uses (148), and "sixteen `_*_problems` checks" (fifteen — the sixteenth was
447
+ named as `cli`, which was removed).
448
+ 6. **A claim that was simply false.** `js/render.mjs` said `--theme` is validated against
449
+ `choices` before anything renders, which is why its own refusal read as an unreachable nicety.
450
+ It is not in `choicesFor`; that refusal is the only one there is, on every path.
451
+ 7. **Five dead cross-references** to `tests/snapshot/no_python_in_corpus.test.mjs`, retired with
452
+ the corpora — three test files justified their own design by pointing at a gate that is gone.
453
+ Plus a skip message naming a `record-corpus` CI job that no longer exists.
454
+ 8. **`CHANGELOG.md` said 2.0.0 was "not yet published"**, with "no tag, no release and no install
455
+ URL"; `geneseed@2.0.0` has been on npm since 2026-08-17. And **`SETUP.md`** documented
456
+ `geneseed upgrade v1.0.0 # pin to a tag`, which `js/update.mjs` prints is IGNORED.
457
+ 9. **`docs/web/agents.md`** enumerated 16 agents and omitted `developer`, where README says 17 —
458
+ the doctor arm cannot fire, because the page spells `{N_AGENTS}` and that arm only ever catches
459
+ a literal. **`docs/web/rules.md`** called each law "a short markdown file under `src/laws/`";
460
+ there is one file.
461
+
462
+ **Still open.**
463
+
464
+ - **`adapters/claude-code/settings.json` is divergent documentation.** It writes
465
+ `node bin/geneseed-hook.mjs <verb>` on seven lines, including one the emitter does not produce
466
+ at all, and carries none of the flags `claudeHookGroups` emits. Nothing compares the two; the
467
+ only reference to it anywhere is the error message that sends users to it. Either gate it
468
+ against the emitter, or delete it.
469
+ - **`AGENT_LESSON_PROMPT` has no drift gate.** Its sibling `LEARN_PROMPT_HEAD` is checked for
470
+ extractability; this one's parity test was retired. Reformat the declaration in
471
+ `geneseed-learn.js` and the per-agent lesson prompt silently degrades to a two-line fallback.
472
+ - **The fenced-command gate still covers only four documents.** `tests/unit/docs.test.mjs`
473
+ validates every `geneseed <verb> <flags>` line against `js/cli-table.json` — but only in
474
+ `README.md`, `SETUP.md`, `QUICKSTART.md` and `SHIPPED.md`. Adding `DESIGN.md` and the `docs/`
475
+ tree to that array is one line, and it would have caught item 2 on the day it was written.
476
+
477
+ ---
478
+
479
+ ## 8 — Release
480
+
481
+ Version lives in **`harness.config.json`** (the owner) and is mirrored into `package.json` — both
482
+ gated. Every other appearance is derived at runtime (`.geneseed-version` markers,
483
+ `agent-overrides.json` `_version`), and gated as a *relationship* rather than a literal, which is
484
+ why no test pins a version string and none should.
485
+
486
+ Do not run `npm version`. Bump the two files by hand, add the `CHANGELOG` section, update
487
+ `SHIPPED.md`, rebuild `web/dist` if `web/src` moved, run the five commands from §1, push, tag,
488
+ then rehearse and fire the workflow:
489
+
490
+ ```bash
491
+ gh workflow run publish.yml -f dry_run=true
492
+ ```
493
+
494
+ Two structural facts worth carrying:
495
+
496
+ - **`publish.yml` deliberately runs a newer toolchain than `ci.yml`** — Node 24 and `npm@latest`,
497
+ because trusted publishing requires them, against `ci.yml`'s Node 22. So toolchain-sensitive
498
+ defaults break *first and only* at the release. That has now happened twice: `npm pack --json`
499
+ changed its container between majors, and Node 24's default test reporter made the count gate
500
+ read zero on a green suite.
501
+ - **Renaming `publish.yml` breaks publishing silently** — npm keys the trusted publisher on the
502
+ filename.
503
+
504
+ Both known breaks have been patched at the symptom (`ci.yml` re-runs the packaging suite under
505
+ `npm@latest`; both workflows pin the TAP reporter). The *cause* is untouched: **no CI job runs
506
+ Node 24 at all**, so the release still executes a runtime nothing exercises — and `publish.yml`
507
+ never runs `tests/golden.mjs`, so the two self-comparisons are absent from the release path.
508
+
509
+ A sweep-and-replace version bump must not touch `registry.json`'s per-entity `"version": "1.0.0"`
510
+ rows, nor `web/package.json`.
511
+
512
+ ---
513
+
514
+ ## 9 — How an existing install picks up a change
515
+
516
+ | Install shape | Command | What it refreshes |
517
+ |---|---|---|
518
+ | git checkout | `geneseed upgrade` | export improvements → preflight → ff-only pull → `doctor` on the pulled tree → re-emit this bundle → bounce the web daemon → `rebuild-all` |
519
+ | npm | `npm install -g geneseed@latest`, then `geneseed rebuild-all` | there is no `postinstall`; the two steps are manual |
520
+ | any | `geneseed rebuild-all` | every **active** install, each re-read in its own five values (theme, emit, footprint, posture, mode) |
521
+
522
+ **Which installs get missed:** project installs that were never registered; installs disabled via
523
+ the `.geneseed-disabled` stash; registry rows whose directory is gone; and everything after the
524
+ point where a `rebuild-all` fails mid-loop — it is best-effort by contract and leaves a mixed
525
+ machine. Autostart entries are surveyed and reported, never rewritten.
526
+
527
+ **The web daemon serves the code it launched with**, plus an in-memory cache of every dist file.
528
+ After a `web/dist` rebuild the correct move is `geneseed web restart`, not a hard refresh —
529
+ otherwise the daemon serves an `index.html` naming assets that `emptyOutDir` deleted, the static
530
+ handler falls back to `index.html` at 200 with `text/html`, and the browser reports a MIME error.
531
+
532
+ ⚠ A bare `geneseed web` runs in the **foreground and writes no record**, so `web stop` reports
533
+ "no running server recorded" and exits 0, and `web restart` starts a *second* daemon. Recovery is
534
+ to kill every pid on the port, then `geneseed web start`.
535
+
536
+ ---
537
+
538
+ ## 10 — What to settle before the next wave
539
+
540
+ Ordered by what unblocks the most work per unit of effort. Each is small; none is urgent enough to
541
+ stop a change today, and all of them cost a contributor a wrong turn until they are done.
542
+
543
+ 1. **Rewrite the authoring section of `DESIGN.md` (§7.1) and fix `doctor`'s tip (§7.2).** These are
544
+ the two documents a contributor reads *first* when adding a law, and both name programs that no
545
+ longer exist. Half an hour, and it removes the most likely first wrong turn.
546
+ 2. **Extend the fenced-command gate to `DESIGN.md` and `docs/**/*.md` (§7.3).** One array literal.
547
+ It would have caught item 1 the day it was written, and it keeps catching it.
548
+ 3. **Decide the hook-verb question (§5.1) before it is asked under pressure.** Either mirror the
549
+ CLI side's exemption list onto the hook half, or write down that the hook verb set is closed.
550
+ Either answer is fine; discovering the question mid-feature is not.
551
+ 4. **Write down the frozen-key list (§5.2) where a theme author will see it** — `themes/README.md`
552
+ is the natural home. Seven keys × fifteen files is a small enough surface to name explicitly,
553
+ and today nothing warns you before the snapshot goes red.
554
+ 5. **Gate `AGENT_LESSON_PROMPT` like its sibling (§7.7), and either gate or delete
555
+ `adapters/claude-code/settings.json` (§7.10).** Two silent failure modes, both cheap to close.
556
+ 6. **Consider a `scripts` block in the root `package.json`.** There is currently no single command
557
+ that runs what CI runs; every contributor reassembles the five by hand from this file. A
558
+ `verify` script is three lines and makes §1 executable rather than descriptive.
559
+
560
+ The list of hand-maintained satellites in §6 is the *design*, not a defect — this project
561
+ deliberately keeps tables and prose hand-written and gates them from the source tree. The
562
+ recommendations above are about the places where the gate is missing, wrong, or pointed at a file
563
+ that no longer exists.