geneseed 2.0.0 → 3.0.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 (196) hide show
  1. package/CHANGELOG.md +217 -5
  2. package/DESIGN.md +149 -1326
  3. package/QUICKSTART.md +1 -1
  4. package/README.md +30 -22
  5. package/SETUP.md +98 -24
  6. package/SHIPPED.md +6 -5
  7. package/adapters/bob/README.md +3 -3
  8. package/adapters/claude-code/README.md +6 -4
  9. package/adapters/claude-code/skills/geneseed/SKILL.md +10 -6
  10. package/adapters/copilot/README.md +5 -4
  11. package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +11 -6
  12. package/adapters/opencode/HOW-OPENCODE-LOADS.md +1 -1
  13. package/adapters/opencode/README.md +15 -9
  14. package/adapters/opencode/agents/orchestrator.md +1 -1
  15. package/adapters/opencode/opencode.json +0 -9
  16. package/adapters/opencode/plugins/geneseed-context.js +7 -7
  17. package/adapters/opencode/plugins/geneseed-guard.js +6 -6
  18. package/adapters/opencode/workflows/dispatch.js +1 -1
  19. package/bin/geneseed.mjs +74 -8
  20. package/docs/declined.md +53 -2
  21. package/docs/design-history.md +1290 -0
  22. package/docs/extending.md +672 -0
  23. package/docs/limits.md +141 -82
  24. package/docs/token-footprint.md +15 -4
  25. package/docs/web/agents.md +1 -1
  26. package/docs/web/authoring.md +1 -1
  27. package/docs/web/claude-hooks.md +2 -2
  28. package/docs/web/collaboration.md +3 -3
  29. package/docs/web/first-session.md +2 -2
  30. package/docs/web/footprint.md +14 -7
  31. package/docs/web/foreman-mode.md +2 -2
  32. package/docs/web/harnesses.md +5 -3
  33. package/docs/web/install-quick.md +1 -1
  34. package/docs/web/model.md +3 -3
  35. package/docs/web/plugin-context.md +2 -2
  36. package/docs/web/plugin-guard.md +3 -3
  37. package/docs/web/plugin-ponytail.md +1 -1
  38. package/docs/web/rules.md +23 -3
  39. package/docs/web/setup-choices.md +20 -7
  40. package/docs/web/themes-author.md +13 -1
  41. package/docs/web-ui.md +36 -12
  42. package/geneseed +0 -0
  43. package/harness.config.json +1 -1
  44. package/js/catalog.mjs +29 -4
  45. package/js/checkout.mjs +24 -1
  46. package/js/cli-table.json +9 -7
  47. package/js/diff.mjs +8 -3
  48. package/js/doctor.mjs +374 -26
  49. package/js/emit.mjs +48 -11
  50. package/js/generate.mjs +39 -5
  51. package/js/hooks.mjs +8 -7
  52. package/js/installs.mjs +101 -4
  53. package/js/inventory.mjs +150 -20
  54. package/js/lib/fs.mjs +6 -2
  55. package/js/mcp.mjs +3 -15
  56. package/js/migrate.mjs +10 -4
  57. package/js/render.mjs +116 -11
  58. package/js/settings.mjs +297 -28
  59. package/js/setup.mjs +95 -5
  60. package/js/status.mjs +45 -4
  61. package/js/themes.mjs +1 -1
  62. package/js/tui.mjs +46 -3
  63. package/js/uninstall.mjs +30 -11
  64. package/js/web/actions.mjs +76 -9
  65. package/js/web/api.mjs +95 -16
  66. package/js/web/docs.mjs +33 -5
  67. package/js/web/jobs.mjs +12 -3
  68. package/js/web/server.mjs +59 -2
  69. package/package.json +1 -1
  70. package/src/AGENT.md.tmpl +75 -65
  71. package/src/agents/_template.md +4 -4
  72. package/src/agents/advocate.md +4 -4
  73. package/src/agents/architect.md +2 -2
  74. package/src/agents/developer.md +5 -5
  75. package/src/agents/docs.md +2 -2
  76. package/src/agents/empiricist.md +3 -3
  77. package/src/agents/explorer.md +3 -3
  78. package/src/agents/framer.md +3 -3
  79. package/src/agents/historian.md +3 -3
  80. package/src/agents/operator.md +3 -3
  81. package/src/agents/pragmatist.md +3 -3
  82. package/src/agents/reviewer.md +2 -2
  83. package/src/agents/security.md +2 -2
  84. package/src/agents/skeptic.md +4 -4
  85. package/src/agents/steward.md +3 -3
  86. package/src/agents/tester.md +2 -2
  87. package/src/agents/user-advocate.md +3 -3
  88. package/src/agents/visionary.md +3 -3
  89. package/src/doctrines/README.md +41 -0
  90. package/src/doctrines/craft.md +51 -0
  91. package/src/doctrines/ops.md +61 -0
  92. package/src/doctrines/process.md +87 -0
  93. package/src/doctrines/rigor.md +52 -0
  94. package/src/laws/universal.md +17 -301
  95. package/src/memory/README.md +2 -2
  96. package/src/modes/README.md +2 -2
  97. package/src/modes/foreman.md +1 -1
  98. package/src/notebook/README.md +2 -2
  99. package/src/ontology/universal.md +89 -0
  100. package/src/postures/README.md +3 -3
  101. package/src/postures/artisan.md +3 -3
  102. package/src/postures/assistant.md +1 -1
  103. package/src/postures/expert.md +1 -1
  104. package/src/postures/mentor.md +1 -1
  105. package/src/postures/peer.md +2 -2
  106. package/src/skills/_template.md +2 -2
  107. package/src/skills/brainstorm.md +2 -2
  108. package/src/skills/clarify.md +2 -2
  109. package/src/skills/codebase-design.md +1 -1
  110. package/src/skills/commit.md +5 -5
  111. package/src/skills/council.md +4 -4
  112. package/src/skills/crash-course.md +1 -1
  113. package/src/skills/daydream/SKILL.md +1 -1
  114. package/src/skills/debug.md +2 -2
  115. package/src/skills/decode.md +1 -1
  116. package/src/skills/develop.md +6 -6
  117. package/src/skills/document-project.md +4 -4
  118. package/src/skills/domain-modeling.md +1 -1
  119. package/src/skills/drill.md +1 -1
  120. package/src/skills/feynman.md +1 -1
  121. package/src/skills/forge-mcp.md +3 -3
  122. package/src/skills/fresh-eyes.md +4 -4
  123. package/src/skills/frontend-design.md +1 -1
  124. package/src/skills/gap-detector.md +1 -1
  125. package/src/skills/geneseed-code-review.md +1 -1
  126. package/src/skills/geneseed.md +4 -4
  127. package/src/skills/git-archaeology.md +1 -1
  128. package/src/skills/git-rescue.md +3 -3
  129. package/src/skills/handoff.md +4 -4
  130. package/src/skills/herdr.md +1 -1
  131. package/src/skills/ingest.md +3 -3
  132. package/src/skills/learning-path.md +1 -1
  133. package/src/skills/migrate.md +5 -5
  134. package/src/skills/opencode-theme.md +1 -1
  135. package/src/skills/parallel-agents.md +2 -2
  136. package/src/skills/pipeline.md +2 -2
  137. package/src/skills/plan.md +2 -2
  138. package/src/skills/ponytail.md +1 -1
  139. package/src/skills/profile.md +1 -1
  140. package/src/skills/prose.md +2 -2
  141. package/src/skills/prototype.md +2 -2
  142. package/src/skills/refactor.md +2 -2
  143. package/src/skills/release.md +4 -4
  144. package/src/skills/repo-map.md +5 -5
  145. package/src/skills/research.md +6 -6
  146. package/src/skills/review-response.md +3 -3
  147. package/src/skills/roast-me.md +1 -1
  148. package/src/skills/rule.md +5 -5
  149. package/src/skills/ship.md +3 -3
  150. package/src/skills/tdd.md +2 -2
  151. package/src/skills/tickets.md +1 -1
  152. package/src/skills/wayfinder.md +1 -1
  153. package/src/skills/wiki.md +4 -4
  154. package/src/skills/workflow.md +2 -2
  155. package/themes/README.md +17 -7
  156. package/themes/_TEMPLATE.json +37 -33
  157. package/themes/biker.json +37 -33
  158. package/themes/commentator.json +37 -33
  159. package/themes/cyberpunk.json +37 -33
  160. package/themes/gamer.json +37 -33
  161. package/themes/imperial.json +37 -33
  162. package/themes/joker.json +37 -33
  163. package/themes/marvin.json +37 -33
  164. package/themes/mean.json +37 -33
  165. package/themes/military.json +37 -33
  166. package/themes/neutral.json +37 -33
  167. package/themes/pirate.json +37 -33
  168. package/themes/sports.json +37 -33
  169. package/themes/verstappen.json +37 -33
  170. package/themes/wizard.json +37 -33
  171. package/web/dist/assets/{About-DX1-VTBP.js → About-C0Mk0igT.js} +1 -1
  172. package/web/dist/assets/{Activity-Dp9NaXOn.js → Activity-C4aJJVXz.js} +1 -1
  173. package/web/dist/assets/{ActivityDetail-HpOBtrXS.js → ActivityDetail-DaQ85za0.js} +1 -1
  174. package/web/dist/assets/{Diff-Ct4G3Se8.js → Diff-B3tlO2Yj.js} +1 -1
  175. package/web/dist/assets/{Doctor-DGjkZFuA.js → Doctor-BNC-5Dmo.js} +1 -1
  176. package/web/dist/assets/{Graph-b8o-S6kv.js → Graph-us0T6ZcV.js} +1 -1
  177. package/web/dist/assets/Harnesses-DAMgyfM_.js +1 -0
  178. package/web/dist/assets/Laws-rOxegJ75.js +1 -0
  179. package/web/dist/assets/{Library-BEZscyaQ.js → Library-KmwrHA1J.js} +1 -1
  180. package/web/dist/assets/{Markdown-k_LSLBGq.js → Markdown-0eQOVGSF.js} +1 -1
  181. package/web/dist/assets/{Profile-DydukTxP.js → Profile-ZU7WV6Xl.js} +1 -1
  182. package/web/dist/assets/{Rules-CIRG4Ldt.js → Rules-CtDoPhhf.js} +1 -1
  183. package/web/dist/assets/{Skills-Buoi1IBr.js → Skills-BhoOxdkD.js} +1 -1
  184. package/web/dist/assets/{StatusBadge-CE_AJttl.js → StatusBadge-CGvYVCwa.js} +1 -1
  185. package/web/dist/assets/{Themes-CI4o3gJC.js → Themes-Dr7OOkD7.js} +1 -1
  186. package/web/dist/assets/{activity-BCW7e6Cy.js → activity-B8aq3qtl.js} +1 -1
  187. package/web/dist/assets/index-2wqPDxuN.css +1 -0
  188. package/web/dist/assets/{index-B1PIx7RM.js → index-AhMzkdbI.js} +1 -1
  189. package/web/dist/assets/index-CRftEtBX.js +12 -0
  190. package/web/dist/assets/{index-Ck2iCdaC.js → index-D0lPN_qW.js} +1 -1
  191. package/web/dist/index.html +2 -2
  192. package/web/src/pages/Laws.jsx +282 -88
  193. package/web/dist/assets/Harnesses-gr3-aHBF.js +0 -1
  194. package/web/dist/assets/Laws-DTwF5tTC.js +0 -1
  195. package/web/dist/assets/index-BnTQUSUz.js +0 -12
  196. package/web/dist/assets/index-DUCsDv7z.css +0 -1
@@ -0,0 +1,672 @@
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 invariant, one
5
+ doctrine rule, one skill, one 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
+ The governance surface is three tiers and they cost very different amounts. **Adding a doctrine
14
+ rule is the cheap, common case** (§2a); **amending an invariant** is next (§2b); **adding an
15
+ invariant** is the most expensive single edit in the repo after the CLI surface (§2c); **adding a
16
+ whole pack** is rare and mechanical (§2d).
17
+
18
+ Written after the Python migration completed (2026-08-18), against `main`. Verified: every
19
+ file:line below was opened, and the commands were run.
20
+
21
+ ---
22
+
23
+ ## 0 — The frame
24
+
25
+ Three seams, and almost every question about "where do I edit?" resolves to one of them.
26
+
27
+ | Seam | Source of truth | How it reaches a user | Rebuild needed |
28
+ |---|---|---|---|
29
+ | **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 |
30
+ | **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 |
31
+ | **Console app** | `web/src/**` | built by vite into the **tracked** `web/dist/` | yes — rebuild *and commit* dist, then `geneseed web restart` |
32
+
33
+ Two rules follow from the table and they cause most of the surprises:
34
+
35
+ - **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`
37
+ reports the drift).
38
+ - **`docs/web/` is data, `web/src/` is code.** A new documentation page is one file and no
39
+ rebuild. A new screen is a new hashed chunk and a mandatory `web/dist` commit.
40
+
41
+ ### Counts are computed — never type one
42
+
43
+ Eight tokens are substituted at request time from the live inventory, by `docCounts` in
44
+ `js/web/docs.mjs`: `{N_LAWS}` `{N_AGENTS}` `{N_SKILLS}` `{N_PLUGINS}`, plus the four the
45
+ three-tier split added — `{N_ONTOLOGY}` (the ontology's sections), `{N_PACKS}` (packs that exist),
46
+ `{N_PACKS_ACTIVE}` (packs this install built in) and `{N_DOCTRINE_RULES}` (rules inside the active
47
+ packs). `{N_LAWS}` is the **invariant** count and nothing else; there is deliberately no token
48
+ meaning "the whole constitution", because `proseMirrorProblems` holds README and `SHIPPED.md`
49
+ 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.
51
+
52
+ ⚠ **Only a `kind: "concept"` page is substituted.** A `markdown`-kind page renders `{N_PACKS}`
53
+ literally, on the screen, as those nine characters.
54
+
55
+ Typing a literal number into a `docs/web` page is caught by `proseMirrorProblems`. The README
56
+ 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
58
+ badge keys `agents`/`skills`/`laws`/`themes`/`plugins`; `proseMirrorProblems` holds the prose).
59
+
60
+ ---
61
+
62
+ ## 1 — The baseline, and the five commands that reproduce CI
63
+
64
+ Verified on this checkout: `doctor --all` green on 14 themes; the Node suite reports
65
+ **1033 tests, 1029 pass, 4 skipped, 0 fail**.
66
+
67
+ CI is **four jobs expanding to six runs** (`.github/workflows/ci.yml`): `validate` and
68
+ `node-cells` on Linux *and* Windows, plus `package-no-python` and `web` on Linux. From the repo
69
+ root — never from a worktree, see §5:
70
+
71
+ ```bash
72
+ node bin/geneseed-cli.mjs doctor --all
73
+ ```
74
+
75
+ ```bash
76
+ node --test --test-reporter=tap "tests/**/*.test.mjs"
77
+ ```
78
+
79
+ ```bash
80
+ node tests/golden.mjs --idempotent
81
+ ```
82
+
83
+ ```bash
84
+ node tests/golden.mjs --deletion
85
+ ```
86
+
87
+ ```bash
88
+ node tests/mutate.mjs --verify
89
+ ```
90
+
91
+ ```bash
92
+ cd web && npm ci && npm run lint && npm run format:check && npm test && npm run build
93
+ ```
94
+
95
+ Notes that matter:
96
+
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.
100
+ - `--test-reporter=tap` is not decoration. Both workflows gate on the *count* (`# tests N ≥ 500`)
101
+ because `node --test` over a glob that matches nothing prints `# tests 0` and **exits 0**. Node's
102
+ default reporter changed between majors, which is how a green 1043-test suite once reported zero.
103
+ - `--deletion` is the only thing that exercises the write-before-delete prune. Anyone renaming or
104
+ removing a plugin, a skill or an emitted file needs it.
105
+ - **A local pass is weaker than CI.** ~18 tests in `tests/unit/package_manifest.test.mjs` carry
106
+ `{ skip: NO_NPM }` (`:83`) — every tarball, install and rename gate. On a machine without `npm`
107
+ on PATH they report *skipped* and the run is green.
108
+ - CI additionally re-runs the packaging suite under `npm@latest` (Linux, last in the job) and
109
+ packs the tarball into a `node:22-slim` container with no interpreter. Neither is reproducible
110
+ from the five commands; the container one is reachable locally through
111
+ `tests/helpers/no-python-container.sh` with a docker daemon.
112
+ - **Two assertions in `tests/unit/claude.test.mjs` silently stand down** on any machine where an
113
+ ancestor of the temp sandbox is a `.claude`/`.bob` install — which is every developer machine
114
+ with Claude Code installed under `~`. They are replaced by a `t.diagnostic`, so the TAP
115
+ `# skipped` count does **not** move. Locally you read `1035/1031/0/4` and see no sign of it.
116
+
117
+ ### The gates CI does not run
118
+
119
+ - **The mutation matrix itself.** `tests/mutate.mjs` — 31 rows, each a one-string edit to a
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.
122
+ Run `node tests/mutate.mjs --all` (or `--only M7`) by hand when you change something a gate is
123
+ supposed to be watching. ⚠ An interrupted run leaves the **product tree mutated** — check
124
+ `git status` before doing anything else.
125
+ Its *anchors* are now in CI (`--verify`, above): a row whose anchor a refactor has moved counts
126
+ as a **survivor**, not a skip, and until 2026-08-19 that only showed up for whoever ran the
127
+ matrix by hand — which nothing did.
128
+ - **Node 24.** No CI job runs it; `publish.yml` does. See §8.
129
+ - **`tests/golden.mjs`** is absent from the publish path entirely.
130
+
131
+ ### Gates that fire on changes you would not expect to be gated
132
+
133
+ - **A new subprocess anywhere in `js/` or `bin/`** — `tests/unit/spawn_hygiene.test.mjs` requires
134
+ capturing spawns to set `windowsHide` and inheriting spawns not to. (`adapters/` is *not*
135
+ scanned, so a plugin that spawns is on its own.)
136
+ - **Editing `.github/workflows/*.yml`** — seven structural assertions in
137
+ `tests/unit/docs.test.mjs:336-399` gate `publish.yml`'s shape, and `:422-444` discovers every
138
+ workflow with a test-count gate and pins `--test-reporter=tap` on it. A unit test gates the CI
139
+ files.
140
+ - **Adding any tracked file** — `tests/unit/package_manifest.test.mjs:401` demands every tracked
141
+ path fall on exactly one side of a declared partition, *with a written reason*. Adding this very
142
+ document required a `SHIPS` row; without one the tarball test fails, and it fails **before** the
143
+ file is committed too, because npm packs by glob while the partition reads `git ls-files`.
144
+
145
+ ---
146
+
147
+ ## 2 — Changing the constitution
148
+
149
+ Three tiers, four different costs. The addresses are `{{LAW}} <roman>` for an invariant and
150
+ `{{DOCTRINE}} <pack> <n>` for a doctrine rule; the ontology is cited by section name and adding a
151
+ section to it is not a checklist, it is a design change.
152
+
153
+ To count the cross-references either tier already carries, from the repo root:
154
+
155
+ ```bash
156
+ grep -rhoE "\{\{LAW\}\} [IVXL]+" src/ | wc -l
157
+ grep -rhoE "\{\{DOCTRINE\}\} [a-z]+ [0-9]+" src/ | wc -l
158
+ ```
159
+
160
+ On this checkout that reports 153 and 209. Both numbers move on every wave of editing, which is
161
+ why the commands are here and the answers are not load-bearing.
162
+
163
+ ### 2a — Adding a DOCTRINE RULE (the common case)
164
+
165
+ Five files, none of them a count. This is the slot most new material belongs in, and it is cheap
166
+ on purpose so that the invariant slot stays scarce.
167
+
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
170
+ the *heading* is what addresses the rule (not the filename, so a rule filed in the wrong file is
171
+ reported rather than silently renamed), and ids must run **contiguously from 1**, so appending
172
+ at the end is the only place `n` is free. Packs are numbered independently — nothing outside
173
+ this file renumbers.
174
+ 2. `themes/_TEMPLATE.json` — add `DOC_<PACK>_<n>`. ⚠ **This file *is* gated for this key family.**
175
+ `themeFiles()` filters `_`-prefixed names and theme parity never sees the template, but
176
+ `constitutionProblems` reads it alongside the fourteen voices for `DOC_*` and `LEX_*`
177
+ specifically — because it is what `--sync-themes` seeds the *next* voice from. Every other key
178
+ family in that file is still ungated.
179
+ 3. Seed the fourteen themes:
180
+
181
+ ```bash
182
+ node bin/geneseed.mjs --sync-themes
183
+ ```
184
+
185
+ ⚠ Four spellings of this command are in circulation and two of them fail — `doctor`'s own tip
186
+ is one of the two. See §7.2 before you copy a command from anywhere else. Then restyle all
187
+ fourteen by hand: `--sync-themes` writes the template's placeholder and only *prints*
188
+ `RESTYLE these`; a shipped placeholder passes every gate.
189
+ 4. `web/src/pages/Laws.jsx` — a `DOCTRINE_META` row keyed `'<pack>.<n>'`. Its first field is the
190
+ **pack**, not one of `LAW_CLASSES`' six; `doctrineMetaProblems` requires an *equality* between
191
+ that field and the key's own pack, which catches the copy error a six-way vocabulary cannot — a
192
+ row pasted from the pack above it, keeping the wrong pack. Its second field is the one-line
193
+ Principle, and **that copy exists nowhere else**; a rule absent here renders with a blank
194
+ description.
195
+ 5. `CHANGELOG.md`, then rebuild and commit `web/dist` (step 4 touched `web/src`).
196
+
197
+ **What a doctrine rule does *not* cost**, and each of these is a deliberate asymmetry:
198
+
199
+ - **No class entry.** There is no `LAW_CLASS` analogue: a doctrine rule's class *is* its pack, and
200
+ there is no second taxonomy over it.
201
+ - **No count, anywhere.** No badge, no `SHIPPED.md` triple, no prose mirror. The console spends
202
+ `{N_PACKS}` / `{N_PACKS_ACTIVE}` / `{N_DOCTRINE_RULES}`, all computed at request time.
203
+ - **No test fixture to bump**, and no renumber risk.
204
+
205
+ ⚠ **Cite only downward.** An always-on tier may never reference a toggleable one:
206
+ `constitutionProblems` refuses a `{{DOCTRINE}}` token anywhere under `src/ontology/` or
207
+ `src/laws/`, because a `--doctrines craft` build would then ship an invariant pointing at text its
208
+ `AGENT.md` does not contain. Doctrine→doctrine citations are fine and stay — every pack file ships
209
+ on disk whether or not it was built in. If the build's own default is narrowed,
210
+ `constitutionProblems` emits a `[note]` naming every citation that points at a shelved pack; notes
211
+ are printed and **not** counted, because a narrowed build is a configuration its owner chose, not
212
+ a defect.
213
+
214
+ ### 2b — Amending an INVARIANT
215
+
216
+ Steps 1 and 9 of §2c, plus `LAW_META` if the Principle line moved. Two cautions:
217
+
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
220
+ default build. Amend the first sentence, or the amendment does not exist for most installs. The
221
+ same is true of a doctrine rule; it is **not** true of the ontology, which ships whole at both
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
+
227
+ ### 2c — Adding an INVARIANT
228
+
229
+ Nine steps, and only the first is the rule itself. This is the expensive one, and it should be:
230
+ the bar is in `DESIGN.md` Decision 7, and most candidates belong in a doctrine pack instead.
231
+
232
+ 1. `src/laws/universal.md` — **append** `### {{LAW}} <roman> — {{LEX_<roman>}}` plus the body.
233
+ 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
235
+ against the canon**. Renumbering silently rewires every one of them.
236
+ 2. `themes/_TEMPLATE.json` — add `LEX_<roman>` in template order. Gated, as in §2a step 2:
237
+ `constitutionProblems` holds `LEX_I..LEX_IX` as an **equality** across all fifteen files (the
238
+ fourteen voices and the template), so both a missing key *and* a leftover one are reported. That
239
+ arm exists because the renumber left `LEX_XXII`, `LEX_XXIII`, `LEX_XXIV` and `LEX_XXXVI` behind
240
+ in every file and parity was silent — they were absent from nowhere.
241
+ 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.
243
+ 5. `LAW_META` in `web/src/pages/Laws.jsx` — same class, plus the one-line Principle. Note the map
244
+ is keyed by **arabic** number where `universal.md` heads with a Roman numeral. **This copy
245
+ exists nowhere else**; a rule absent here renders with a blank description. It is why that one
246
+ React file ships alone out of `web/src` in `package.json` — `doctor` regex-parses its literal.
247
+ 6. `README.md` — badge `badge/laws-N` and the `N universal laws` sentence.
248
+ 7. `SHIPPED.md` — the `N laws, N agents, N skills` triple.
249
+ 8. `tests/unit/harness.test.mjs` — the negative fixture for the `LAW_CLASS` gate plants a rule at
250
+ the *first free numeral*, which is `X` now that the invariants are I..IX. Bump it, or it stops
251
+ being a fixture and starts being a duplicate.
252
+ 9. `CHANGELOG.md`, then rebuild and commit `web/dist` (step 5 touched `web/src`).
253
+
254
+ ⚠ While restyling, `{{LAW}}` must stay a **single word** in every theme (and so must
255
+ `{{DOCTRINE}}`). Both heading parsers match the tier noun with `\S+`, so a two-word value does not
256
+ error — the heading simply stops matching and the whole tier parses to nothing, in silence.
257
+ `constitutionProblems` refuses one, which is the only reason it is visible at all.
258
+
259
+ The carrier itself is gated: `tests/unit/emit_smoke.test.mjs` checks every rule reaches every one
260
+ of nine emit modes in order and themed, under both footprints, with a `CEILING` **and** a floor
261
+ (half the ceiling) on the rendered size. Its docblock carries the current measurement: headroom is
262
+ 1,522 characters (2.8%) at full and 1,794 (4.8%) at lean, against the largest carrier — the
263
+ host-agnostic `files` bundle. Read that docblock before adding text to any tier; it is
264
+ re-measured, never nudged, and the ceiling is measured **with all four packs active**, which is
265
+ the only configuration it can speak for.
266
+
267
+ ### 2d — Adding a whole PACK
268
+
269
+ Rare, and mechanical. On top of §2a for each rule the pack carries:
270
+
271
+ 1. `src/doctrines/<pack>.md`, opening with its lead line — `**{{PACK_<NAME>}}** — how … .` A pack
272
+ file `PACK_ORDER` names but cannot read, or one carrying no `### {{DOCTRINE}} <pack> <n>`
273
+ heading at all, is a refusal.
274
+ 2. `PACK_ORDER` in `js/checkout.mjs` — the registration, and the render order. It is **narrative,
275
+ not alphabetical** (craft → rigor → ops → process); discovery sorts alphabetically and the
276
+ renderer refuses a `.md` under `src/doctrines/` that `PACK_ORDER` does not name, rather than
277
+ skipping it.
278
+ 3. `PACK_<NAME>` in `themes/_TEMPLATE.json` and all fourteen voices. Unlike `DOC_*`, this family is
279
+ 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,
281
+ and the only place a pack is ever explained to them. Missing entries fall back to the bare name.
282
+ 5. `harness.config.json` if the pack should be on by default; `constitutionProblems` refuses a
283
+ `doctrines` array naming a pack this checkout does not ship.
284
+
285
+ ---
286
+
287
+ ## 3 — Adding a SKILL or an AGENT
288
+
289
+ **Order matters at the first two steps.** `missingReferencedSpecs` (in `js/emit.mjs`) *aborts the
290
+ build* when `AGENT.md.tmpl` names a spec with no file — so the file comes before the table row, or
291
+ nothing builds at all. The refusal is clean: `tests/unit/emit_gates.test.mjs:265` proves a refused
292
+ emit leaves the prior install intact.
293
+
294
+ 1. `src/skills/<name>.md` or `src/agents/<name>.md`. The shape is load-bearing: `# {{SKILL}}: <name>`,
295
+ a blank line, then a `>` blockquote holding `{{DESC_<NAME>}}` — title, blockquote, nothing
296
+ between. `doctor` checks both the presence of the purpose line and that the blockquote is the
297
+ *first* block (`authoringProblems`, via `firstBlockquote` and `descBlockProblem`).
298
+ 2. One row in the hand-authored table in `src/AGENT.md.tmpl` (agents `125-141`, skills `183-229`).
299
+ Gated both ways: a row with no file, and a file with no row.
300
+ 3. `DESC_<NAME>` in **all fourteen** themes. Theme parity catches a missing one; nothing catches
301
+ an unstyled placeholder.
302
+ 4. **Skills only** — `SKILL_CLASS` in `js/inventory.mjs`. Gated both ways, inside
303
+ `countTableProblems`, which matters because the runtime fallback is silent: an unclassified
304
+ skill renders as `build`.
305
+ 5. `registry.json` — a row keyed `skills/<name>` or `agents/<name>` with `status` ∈
306
+ {`experimental`, `approved`, `deprecated`}, a semver `version`, a non-blank `owner`, an ISO
307
+ `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`).
309
+ 6. `README.md` — the badge, the `**🤖 Agents** (N)` / `**🛠 Skills** (N)` count, and for a skill the
310
+ `·`-separated name in the 🛠 row (that enumeration is gated two ways, unlike the laws' one).
311
+ 7. `SHIPPED.md` — the `N laws, N agents, N skills` triple.
312
+ 8. `doctor --all`, then the suite.
313
+
314
+ **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
316
+ `tests/unit/authoring_gates.test.mjs:312`), write a `VENDOR.md` carrying `**Upstream:**`,
317
+ `**Commit:**` (a 40-hex commit, never a moving branch) and `**License:**`, add the registry row —
318
+ and **skip steps 2, 4, 6, 7**: the flat-spec globs only see `*.md` at the top level.
319
+
320
+ Two traps worth knowing before you write the spec:
321
+
322
+ - **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
324
+ re-opens bash on all three hosts.
325
+ - **The console has a fourth, ungated copy of the skill taxonomy.** `SKILL_CATS` in
326
+ `web/src/pages/Skills.jsx:19` mirrors `SKILL_CLASS` with nothing comparing them (contrast
327
+ `LAW_META`, which *is* cross-checked). Inventing a new *category* means editing that file —
328
+ which drags in the `web/dist` rebuild — and an unknown category renders as Build with no warning.
329
+
330
+ Adding a spec reddens no recording: none of the surviving snapshots contains a skill or agent name.
331
+
332
+ ---
333
+
334
+ ## 4 — Adding a PLUGIN, a HOOK verb, or a doc page
335
+
336
+ ### 4a — An OpenCode plugin
337
+
338
+ The `geneseed-` prefix and the `.js` suffix are mechanically load-bearing: the badge count, the
339
+ `{N_PLUGINS}` substitution and the four-way gate all key off that glob.
340
+
341
+ 1. `adapters/opencode/plugins/geneseed-<name>.js` — ESM, zero dependencies. The factory-export
342
+ convention is real (OpenCode requires it) but **ungated**; the only generic gate on plugin
343
+ source is `node --check` (the one spawn in `authoringProblems`).
344
+ 2. `docs/web/plugin-<name>.md` — `group: plugins`, `title: "geneseed-<name>"`, `kind: "concept"`.
345
+ 3. `docs/web/plugins.md` — a bullet. The count above it renders from `{N_PLUGINS}`.
346
+ 4. `docs/web/model.md` — a second `{N_PLUGINS}` sentence whose **enumeration of the capabilities
347
+ is hand-written and ungated**.
348
+ 5. `README.md` — **three** sites: the badge, the `N plugins` prose, and the 🔌 Plugins row.
349
+ 6. `SHIPPED.md` — the bare name inside `plugins (…)`; that list must equal the directory exactly,
350
+ in both directions (`tests/unit/web_api.test.mjs:1655`).
351
+ 7. `adapters/opencode/README.md` and `docs/opencode-plugin-setup.md` — spelled-out counts, in
352
+ **word form** ("seven" → "eight"), the second file twice.
353
+ 8. `tests/plugins/<name>.test.mjs`. Auto-discovered; no workflow edit.
354
+
355
+ No `registry.json` row, no `package.json` edit — `files[]` ships `adapters/` whole.
356
+
357
+ ### 4b — A hook verb (claude + bob)
358
+
359
+ ⚠ **Read §5 first. A fifth hook verb has no green path today.**
360
+
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
363
+ shape); add the entry to `bin/geneseed-hook.mjs:43` `VERBS` with **exactly two-space indent**; add
364
+ a row to `js/cli-table.json` with a help string over 20 characters.
365
+
366
+ ### 4c — A documentation page
367
+
368
+ **One file, no rebuild, no server change.** `docs/web/<id>.md`, where the stem is the id.
369
+ Frontmatter is JSON-per-value, not YAML (`js/web/docs.mjs:63-82`):
370
+
371
+ - `group` must match an id in `_groups.json` — an unknown group is a **silent skip**.
372
+ - `kind` ∈ `markdown | concept | glossary | about | cli`; anything else 404s.
373
+ - `harness: "claude"|"opencode"` hides the page from the other host. Note the `plugins` group
374
+ carries this at the **group** level, and `hooks` symmetrically — a Claude-relevant page dropped
375
+ into `plugins` vanishes with no error.
376
+ - `kind: "markdown"` pulls from a repo file via `source:`, traversal-guarded, optionally sliced to
377
+ one section with `anchor:` + `slice: true`.
378
+
379
+ Inline `<!-- harness:claude -->` blocks strip per host and **fail open** on an unbalanced marker —
380
+ the page renders, wrong, with nothing red.
381
+
382
+ ---
383
+
384
+ ## 5 — The four things that are genuinely hard
385
+
386
+ These are not checklist items. They are structural, and each one should be settled *before* the
387
+ change that needs it, not during.
388
+
389
+ ### 5.1 ✅ A fifth hook verb — resolved 2026-08-19
390
+
391
+ **This was a hard blocker and it is now open.** Recorded here because the shape of the problem
392
+ recurs: any gate whose expected value lives in an artifact nothing can re-make will eventually
393
+ stop a legitimate change, and the fix is never to delete the artifact.
394
+
395
+ `tests/unit/hook_cli.test.mjs` asserted a strict equality between the hook verbs the frozen
396
+ matrix covers and the verbs `bin/geneseed-hook.mjs` carries. The CLI half of the same test
397
+ already exempted a named `NATIVE` list; the hook half exempted nothing — so a fifth hook verb
398
+ failed with **no green path**, since the recorder died on 2026-08-17 and the failure message
399
+ correctly forbids deleting cells to pass.
400
+
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
414
+
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:
418
+
419
+ | Probe | Frozen keys |
420
+ |---|---|
421
+ | `theme_preview` | `TAGLINE`, `LOADED_SIGIL`, `VOICE`, **`LEX_I`**, `BENEDICTION` |
422
+ | `theme_flair` | `ACCENT`, `BANNER` |
423
+
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.
426
+
427
+ This was proven, not inferred: prefixing `marvin.json`'s `TAGLINE` turned
428
+ `tests/snapshot/pure_snapshot.test.mjs` red at `case 1275`.
429
+
430
+ ### 5.3 ⚠ The hook shim is machine-wide and last-writer-wins
431
+
432
+ `~/.geneseed/bin/geneseed-hook[.cmd]` has no per-install component. The last checkout to emit
433
+ anything owns **every** install's hooks. Emitting from a git worktree or a temp copy used to
434
+ repoint the shim at that disposable path; removing the worktree then killed hooks for every
435
+ install on the machine.
436
+
437
+ **`hookPrefix` now refuses the claim** (`js/settings.mjs`, `ephemeralCheckout`): an emit whose
438
+ checkout is under the OS temp root, or is a linked git worktree (`.git` is a *file*), leaves a
439
+ shim that still resolves exactly as it is and wires the emitted hooks to it anyway. The bundle it
440
+ writes is byte-identical to a normal emit — the install simply keeps running the durable
441
+ checkout's entry, which is what last-writer-wins already handed every other install. A shim that
442
+ is **absent or already dead** is still written from anywhere, so a first emit on a fresh machine
443
+ works and the suite's own sandboxed emits are unchanged.
444
+
445
+ Two gates hold it: `tests/unit/hook_form.test.mjs` pins the rule (including the worktree arm,
446
+ reachable because `ephemeralCheckout` takes the temp root as a parameter), and
447
+ `tests/shim_intact.mjs` — a step in `ci.yml` and `publish.yml`, *after* `node --test`, because
448
+ each test file runs in its own process and the file that causes this is not the file that would
449
+ notice — fails if the suite left the shim naming anything that is not there.
450
+
451
+ This is not hypothetical — it happened while this file was being written. Several agents were
452
+ verifying gates out of copied checkouts (the `copyCheckout` fixture pattern
453
+ `tests/unit/harness.test.mjs:173` uses), one of them emitted, and the shim ended up pointing at
454
+ `…/Temp/gs-fix-8DYuJP/bin/geneseed-hook.mjs` — a directory that no longer existed. Every hook in
455
+ every install on the machine was dead and nothing said so.
456
+
457
+ The recovery is free, which is the part worth remembering: **`doctor` detects it and the run's own
458
+ emit repairs it**, reporting *"the checkout most likely moved … no further action needed"*. That
459
+ is still the answer for the case the guard cannot cover — a real checkout that genuinely moved —
460
+ so after any session that emitted from somewhere unusual, `doctor --all` from the real checkout
461
+ remains the cheapest thing to run.
462
+
463
+ Related: a stray `console.log` on a hook path silently disables a gate. Hooks signal through
464
+ stdout JSON and return 0 on every path — a printed byte is not a warning, it is a disabled gate.
465
+
466
+ ### 5.4 ⚠ A snapshot red is a finding, not a step
467
+
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.
473
+
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
483
+
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
+ ```
487
+
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.
491
+
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
497
+ `README.md`, `SETUP.md`, `QUICKSTART.md` and `SHIPPED.md` against the table.
498
+
499
+ ---
500
+
501
+ ## 6 — Hand-maintained, therefore rotting
502
+
503
+ Each of these is a copy of a decision that lives somewhere else, with no gate on its *accuracy* —
504
+ only, at best, on its presence.
505
+
506
+ | What | Where | Gated on |
507
+ |---|---|---|
508
+ | `§N` cross-references into `AGENT.md`'s anatomy | `src/skills/*`, `src/agents/*`, `adapters/**` | **range only.** `constitutionProblems` refuses a `§N` the template declares no section for — which catches a pointer past the end, the shape *removing* a section leaves. It cannot catch one that still resolves and now means something else, which is the shape *inserting* a section leaves, and is what actually happened when `## 2. Doctrines` pushed every later section down one. **Renumbering the anatomy means `grep -rn '§' src/ adapters/` and reading every hit.** Four satellites and a live OpenCode deny message shipped stale because the sweep stopped at `AGENT.md.tmpl` |
509
+ | Per-law Principle lines | `web/src/pages/Laws.jsx` | presence and class only, never accuracy |
510
+ | 504 themed constitution titles (36 keys × 14 voices) — `LEX_I..LEX_IX` (9), `DOC_<PACK>_<n>` (23), `PACK_<NAME>` (4) | `themes/*.json` | key presence only — a shipped placeholder is green. `LEX_*` and `DOC_*` are held across the template too, and in both directions; `PACK_*` only across the voices |
511
+ | The README keyword enumerations | `README.md`, `docs/web/rules.md` | nothing |
512
+ | The plugin capability enumeration | `docs/web/model.md` | nothing (the number substitutes; the list does not) |
513
+ | 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 |
515
+ | `LOADED_SIGIL` uniqueness | `themes/*.json` | nothing, and it is load-bearing for theme detection |
516
+ | `adapters/claude-code/settings.json` | — | nothing compares it to what the emitter writes; **it is already divergent** |
517
+ | `NATIVE`, twice | two test files | they check each other, which is the gate that makes the duplication safe |
518
+ | 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 |
520
+
521
+ ---
522
+
523
+ ## 7 — Stale on 2026-08-18, fixed on 2026-08-19
524
+
525
+ Nine items were found while writing this file, and fixed the next day. They are recorded as a
526
+ list rather than left as a diff, because the PATTERN is the useful part: every one was a document
527
+ or a comment pointing a reader at something that is not there, and in each case the gate that
528
+ should have caught it either did not cover that file — or was itself the frozen copy of a wrong
529
+ answer.
530
+
531
+ **Fixed.**
532
+
533
+ 1. **`DESIGN.md` taught the deleted Python build.** Its header promises *"the spec behind the
534
+ harness — read this before changing structure"*, and under it sat the six-edit checklist for
535
+ adding a Law, naming a deleted generator, `rituals/_harness_tui.py` for `LAW_CLASS`, and an
536
+ invocation of a program that no longer exists. Worse, `tests/unit/no_python.test.mjs` exempted
537
+ the file as a *historical record* — so the gate that hunts pointers-to-nothing was looking away
538
+ from the one document most likely to hold one. **Split**: the 190-line standing contract keeps
539
+ the name and is now scanned; the 1,290-line port narrative moved to
540
+ [design-history.md](design-history.md), which is exempt on the merits, and its dead links were
541
+ repaired because a link is a pointer whatever the prose around it says.
542
+ 2. **`doctor` printed a command that errors.** The theme-parity tip named
543
+ `./geneseed build --sync-themes`; `cmdBuild` forwards `--theme` and nothing else. The comment
544
+ above the tip argued that spelling on a premise the port had quietly stopped satisfying. It now
545
+ names `geneseed-build --sync-themes`, which is what README and SETUP said all along.
546
+ 3. **User-facing output named a program that does not exist.** `tui` and `uninstall` told users to
547
+ run `harness setup` / `harness uninstall` — the reference's name, dropped in P0. Five sites.
548
+ One of them was **pinned verbatim by a test**, so a gate was holding the wrong string in place;
549
+ that assertion is now a property — every backticked command a refusal offers must be a verb
550
+ `js/cli-table.json` declares — which is strictly stronger and fires on the next rename too.
551
+ 4. **The `tui` help promised a "full-screen curses control panel"** for a verb that returns 1 on
552
+ every path. `docs/limits.md` listed this as permanently unfixable, on the grounds that the help
553
+ corpus is frozen — but `formatHelp` never reads `cmd.help`, so it was an ordinary edit all
554
+ along. The limits row went with the fix.
555
+ 5. **Numbers in docblocks that had drifted** — `_TEMPLATE.json`'s "137 values" (140), "the 145
556
+ tokens" `src/` uses (148), and "sixteen `_*_problems` checks" (fifteen — the sixteenth was
557
+ named as `cli`, which was removed).
558
+ 6. **A claim that was simply false.** `js/render.mjs` said `--theme` is validated against
559
+ `choices` before anything renders, which is why its own refusal read as an unreachable nicety.
560
+ It is not in `choicesFor`; that refusal is the only one there is, on every path.
561
+ 7. **Five dead cross-references** to `tests/snapshot/no_python_in_corpus.test.mjs`, retired with
562
+ the corpora — three test files justified their own design by pointing at a gate that is gone.
563
+ Plus a skip message naming a `record-corpus` CI job that no longer exists.
564
+ 8. **`CHANGELOG.md` said 2.0.0 was "not yet published"**, with "no tag, no release and no install
565
+ 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.
567
+ 9. **`docs/web/agents.md`** enumerated 16 agents and omitted `developer`, where README says 17 —
568
+ the doctor arm cannot fire, because the page spells `{N_AGENTS}` and that arm only ever catches
569
+ a literal. **`docs/web/rules.md`** called each law "a short markdown file under `src/laws/`";
570
+ there is one file.
571
+
572
+ **Still open.**
573
+
574
+ - **`adapters/claude-code/settings.json` is divergent documentation.** It writes
575
+ `node bin/geneseed-hook.mjs <verb>` on seven lines, including one the emitter does not produce
576
+ at all, and carries none of the flags `claudeHookGroups` emits. Nothing compares the two; the
577
+ only reference to it anywhere is the error message that sends users to it. Either gate it
578
+ against the emitter, or delete it.
579
+ - **`AGENT_LESSON_PROMPT` has no drift gate.** Its sibling `LEARN_PROMPT_HEAD` is checked for
580
+ extractability; this one's parity test was retired. Reformat the declaration in
581
+ `geneseed-learn.js` and the per-agent lesson prompt silently degrades to a two-line fallback.
582
+ - **The fenced-command gate still covers only four documents.** `tests/unit/docs.test.mjs`
583
+ validates every `geneseed <verb> <flags>` line against `js/cli-table.json` — but only in
584
+ `README.md`, `SETUP.md`, `QUICKSTART.md` and `SHIPPED.md`. Adding `DESIGN.md` and the `docs/`
585
+ tree to that array is one line, and it would have caught item 2 on the day it was written.
586
+
587
+ ---
588
+
589
+ ## 8 — Release
590
+
591
+ Version lives in **`harness.config.json`** (the owner) and is mirrored into `package.json` — both
592
+ gated. Every other appearance is derived at runtime (`.geneseed-version` markers,
593
+ `agent-overrides.json` `_version`), and gated as a *relationship* rather than a literal, which is
594
+ why no test pins a version string and none should.
595
+
596
+ Do not run `npm version`. Bump the two files by hand, add the `CHANGELOG` section, update
597
+ `SHIPPED.md`, rebuild `web/dist` if `web/src` moved, run the five commands from §1, push, tag,
598
+ then rehearse and fire the workflow:
599
+
600
+ ```bash
601
+ gh workflow run publish.yml -f dry_run=true
602
+ ```
603
+
604
+ Two structural facts worth carrying:
605
+
606
+ - **`publish.yml` deliberately runs a newer toolchain than `ci.yml`** — Node 24 and `npm@latest`,
607
+ because trusted publishing requires them, against `ci.yml`'s Node 22. So toolchain-sensitive
608
+ defaults break *first and only* at the release. That has now happened twice: `npm pack --json`
609
+ changed its container between majors, and Node 24's default test reporter made the count gate
610
+ read zero on a green suite.
611
+ - **Renaming `publish.yml` breaks publishing silently** — npm keys the trusted publisher on the
612
+ filename.
613
+
614
+ Both known breaks have been patched at the symptom (`ci.yml` re-runs the packaging suite under
615
+ `npm@latest`; both workflows pin the TAP reporter). The *cause* is untouched: **no CI job runs
616
+ Node 24 at all**, so the release still executes a runtime nothing exercises — and `publish.yml`
617
+ never runs `tests/golden.mjs`, so the two self-comparisons are absent from the release path.
618
+
619
+ A sweep-and-replace version bump must not touch `registry.json`'s per-entity `"version": "1.0.0"`
620
+ rows, nor `web/package.json`.
621
+
622
+ ---
623
+
624
+ ## 9 — How an existing install picks up a change
625
+
626
+ | Install shape | Command | What it refreshes |
627
+ |---|---|---|
628
+ | 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` |
629
+ | npm | `npm install -g geneseed@latest`, then `geneseed rebuild-all` | there is no `postinstall`; the two steps are manual |
630
+ | any | `geneseed rebuild-all` | every **active** install, each re-read in its own five values (theme, emit, footprint, posture, mode) |
631
+
632
+ **Which installs get missed:** project installs that were never registered; installs disabled via
633
+ the `.geneseed-disabled` stash; registry rows whose directory is gone; and everything after the
634
+ point where a `rebuild-all` fails mid-loop — it is best-effort by contract and leaves a mixed
635
+ machine. Autostart entries are surveyed and reported, never rewritten.
636
+
637
+ **The web daemon serves the code it launched with**, plus an in-memory cache of every dist file.
638
+ After a `web/dist` rebuild the correct move is `geneseed web restart`, not a hard refresh —
639
+ otherwise the daemon serves an `index.html` naming assets that `emptyOutDir` deleted, the static
640
+ handler falls back to `index.html` at 200 with `text/html`, and the browser reports a MIME error.
641
+
642
+ ⚠ A bare `geneseed web` runs in the **foreground and writes no record**, so `web stop` reports
643
+ "no running server recorded" and exits 0, and `web restart` starts a *second* daemon. Recovery is
644
+ to kill every pid on the port, then `geneseed web start`.
645
+
646
+ ---
647
+
648
+ ## 10 — What to settle before the next wave
649
+
650
+ Ordered by what unblocks the most work per unit of effort. Each is small; none is urgent enough to
651
+ stop a change today, and all of them cost a contributor a wrong turn until they are done.
652
+
653
+ 1. **Extend the fenced-command gate to `DESIGN.md` and `docs/**/*.md` (§7.3).** One array literal.
654
+ Today it covers four documents, and both of the pointer-to-nothing defects §7 records — the
655
+ `DESIGN.md` authoring section and `doctor`'s theme-parity tip, since fixed — sat outside its
656
+ reach. It keeps catching them.
657
+ 2. **Decide the hook-verb question (§5.1) before it is asked under pressure.** Either mirror the
658
+ CLI side's exemption list onto the hook half, or write down that the hook verb set is closed.
659
+ Either answer is fine; discovering the question mid-feature is not.
660
+ 3. **Write down the frozen-key list (§5.2) where a theme author will see it** — `themes/README.md`
661
+ is the natural home. Seven keys × fifteen files is a small enough surface to name explicitly,
662
+ and today nothing warns you before the snapshot goes red.
663
+ 4. **Gate `AGENT_LESSON_PROMPT` like its sibling (§7.7), and either gate or delete
664
+ `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.
668
+
669
+ The list of hand-maintained satellites in §6 is the *design*, not a defect — this project
670
+ deliberately keeps tables and prose hand-written and gates them from the source tree. The
671
+ recommendations above are about the places where the gate is missing, wrong, or pointed at a file
672
+ that no longer exists.