geneseed 3.1.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/DESIGN.md +15 -15
  3. package/README.md +1 -1
  4. package/SETUP.md +1 -1
  5. package/SHIPPED.md +1 -1
  6. package/bin/{geneseed.mjs → build-driver.mjs} +39 -42
  7. package/bin/geneseed-cli.mjs +33 -32
  8. package/bin/geneseed-hook.mjs +7 -7
  9. package/docs/design-history.md +40 -40
  10. package/docs/extending.md +111 -83
  11. package/harness.config.json +1 -1
  12. package/js/README.md +203 -0
  13. package/js/build/bundle.mjs +231 -0
  14. package/js/{catalog.mjs → build/catalog.mjs} +7 -7
  15. package/js/build/emit-claude.mjs +287 -0
  16. package/js/build/emit-common.mjs +255 -0
  17. package/js/build/emit-opencode.mjs +214 -0
  18. package/js/{generate.mjs → build/generate.mjs} +27 -28
  19. package/js/{render.mjs → build/render.mjs} +13 -9
  20. package/js/{checkout.mjs → build/source.mjs} +9 -9
  21. package/js/build/stubs.mjs +256 -0
  22. package/js/{themes.mjs → build/themes.mjs} +24 -24
  23. package/js/build/version.mjs +155 -0
  24. package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
  25. package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
  26. package/js/{installs.mjs → hosts/installs.mjs} +18 -16
  27. package/js/{link.mjs → hosts/link.mjs} +29 -28
  28. package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
  29. package/js/{native.mjs → hosts/native.mjs} +24 -22
  30. package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
  31. package/js/{settings.mjs → hosts/settings.mjs} +63 -55
  32. package/js/inspect/checks-authoring.mjs +761 -0
  33. package/js/inspect/checks-build.mjs +231 -0
  34. package/js/inspect/checks-repo.mjs +308 -0
  35. package/js/{diff.mjs → inspect/diff.mjs} +34 -31
  36. package/js/inspect/doctor.mjs +286 -0
  37. package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
  38. package/js/{inventory.mjs → inspect/inventory.mjs} +20 -17
  39. package/js/{registry.mjs → inspect/registry.mjs} +12 -10
  40. package/js/inspect/scan.mjs +125 -0
  41. package/js/{status.mjs → inspect/status.mjs} +27 -25
  42. package/js/inspect/validate.mjs +199 -0
  43. package/js/lib/fs.mjs +37 -759
  44. package/js/lib/json.mjs +344 -0
  45. package/js/lib/paths.mjs +242 -0
  46. package/js/lib/proc.mjs +3 -3
  47. package/js/lib/text.mjs +181 -0
  48. package/js/lib/udiff.mjs +1 -1
  49. package/js/{memory.mjs → maintain/memory.mjs} +10 -10
  50. package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
  51. package/js/{setup.mjs → maintain/setup.mjs} +34 -32
  52. package/js/{uninstall.mjs → maintain/uninstall.mjs} +55 -56
  53. package/js/{update.mjs → maintain/update.mjs} +51 -51
  54. package/js/{anim.mjs → ui/anim.mjs} +30 -28
  55. package/js/{cli.mjs → ui/cli.mjs} +10 -10
  56. package/js/{menu.mjs → ui/menu.mjs} +10 -9
  57. package/js/{tui.mjs → ui/tui.mjs} +36 -34
  58. package/js/web/actions.mjs +84 -85
  59. package/js/web/activity.mjs +22 -21
  60. package/js/web/api.mjs +72 -50
  61. package/js/web/daemon.mjs +319 -0
  62. package/js/web/docs.mjs +24 -21
  63. package/js/web/graph.mjs +0 -0
  64. package/js/web/handler.mjs +442 -0
  65. package/js/web/jobs.mjs +21 -19
  66. package/js/web/routes.mjs +223 -0
  67. package/js/web/server.mjs +56 -987
  68. package/package.json +11 -3
  69. package/src/doctrines/README.md +1 -1
  70. package/themes/opencode/README.md +1 -1
  71. package/web/dist/assets/{About-DyK7yRy7.js → About-CPvIFZGh.js} +1 -1
  72. package/web/dist/assets/{Activity-D_sYBlW-.js → Activity-z0uXdS7J.js} +1 -1
  73. package/web/dist/assets/{ActivityDetail-BomKcMhy.js → ActivityDetail-CEj5IJpF.js} +1 -1
  74. package/web/dist/assets/{Diff-BSvi1WUg.js → Diff-BMR8fPuG.js} +1 -1
  75. package/web/dist/assets/{Doctor-Cz1s-y1n.js → Doctor-Bz5qE4r9.js} +1 -1
  76. package/web/dist/assets/{Graph-CuntzFJ0.js → Graph-BV6a6t2l.js} +1 -1
  77. package/web/dist/assets/{Harnesses-DgRyOviS.js → Harnesses-C5Ag5PaQ.js} +1 -1
  78. package/web/dist/assets/{Laws-CWk3Mpri.js → Laws-BK07NKw9.js} +1 -1
  79. package/web/dist/assets/{Library-DXhTN9ju.js → Library-CiXN9sC3.js} +1 -1
  80. package/web/dist/assets/{Markdown-BoiZcdUl.js → Markdown-DG6FjUz-.js} +1 -1
  81. package/web/dist/assets/{Profile-DfCHlZ03.js → Profile-B2oSvdxH.js} +1 -1
  82. package/web/dist/assets/{Rules-eND7hJv9.js → Rules-DobVPd80.js} +1 -1
  83. package/web/dist/assets/{Skills-CDl9opZO.js → Skills-DQPsvsn9.js} +1 -1
  84. package/web/dist/assets/{StatusBadge-DSDtcArX.js → StatusBadge-BhJ3HhdI.js} +1 -1
  85. package/web/dist/assets/{Themes-CoAWuIeM.js → Themes-BP38FBU9.js} +1 -1
  86. package/web/dist/assets/{activity-Dmb0FTAx.js → activity-D9mfv7pY.js} +1 -1
  87. package/web/dist/assets/index-BE226Gv6.css +1 -0
  88. package/web/dist/assets/{index-jrY4m9jP.js → index-BZ7cPLMI.js} +1 -1
  89. package/web/dist/assets/index-BgIs5eIa.js +12 -0
  90. package/web/dist/assets/{index-Dg58ScBe.js → index-DGM5j2hB.js} +1 -1
  91. package/web/dist/index.html +2 -2
  92. package/web/src/pages/Laws.jsx +1 -1
  93. package/docs/declined.md +0 -200
  94. package/docs/limits.md +0 -329
  95. package/js/doctor.mjs +0 -1750
  96. package/js/emit.mjs +0 -1411
  97. package/web/dist/assets/index-B-mXquBZ.css +0 -1
  98. package/web/dist/assets/index-CH_Z725C.js +0 -12
package/CHANGELOG.md CHANGED
@@ -6,7 +6,74 @@ labels in `harness.config.json`. The canonical identity of an *installed* harnes
6
6
  is the source fingerprint in `.geneseed-version` (see `geneseed version`), not this
7
7
  label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
8
8
 
9
- ## [Unreleased]
9
+ ## [3.1.2] — 2026-08-24
10
+
11
+ ### Changed
12
+
13
+
14
+ - **`js/` is now domain folders, not a flat drawer.** The tree splits into `build/`, `hosts/`,
15
+ `inspect/`, and `main/`, and the four largest modules split with it: `server.mjs` into
16
+ routes / handler / daemon / server, `emit.mjs` into common / stubs / version / bundle,
17
+ `doctor.mjs` into scan plus three check groups, and `fs.mjs` into fs / json / paths / text.
18
+ The Node generator driver is renamed to `bin/build-driver.mjs`; the `geneseed-build` bin name
19
+ is unchanged, so nothing on an installed harness has to move.
20
+
21
+ **No emitted byte moves.** The 261-cell corpus replays identical across all 14 themes, which
22
+ is the whole warrant for a reorganization this size — 151 files, 8502 insertions, 6481
23
+ deletions, and zero observable difference for anyone who installed 3.1.1.
24
+
25
+ - **Docs point at the code that exists.** A module map and front door in `js/README.md`, with a
26
+ doctor arm that keeps them honest; 201 stale paths repointed in prose, the 35 split-module
27
+ pointers resolved, and 69 docblock claims corrected where the code had stopped supporting
28
+ what the comment promised. The vestigial `emit.mjs` is deleted.
29
+
30
+ - **The Python-era `py` identifier prefix is retired**, and `js/ bin/ tests/ adapters/` are now
31
+ linted — the half of the tree that never was.
32
+
33
+
34
+ - **Two of "the four things that are genuinely hard" are resolved rather than rewritten.**
35
+ `docs/extending.md` §5.2 — seven theme keys pinned byte-for-byte, so restyling a tagline was a
36
+ red gate — and §5.4 — *a snapshot red is a finding, not a step*. Theme text is free to edit,
37
+ and a red test is a step again. Editing a CLI verb's help text or flags is an ordinary edit.
38
+
39
+ - **The standing rule is now "expected values are written out, not recorded"**: change the
40
+ expectation and the sentence that explains it in the same commit, and say why in the message.
41
+
42
+ ### Added
43
+
44
+
45
+ - **A gate on how a runtime dependency may enter**: vendored under `js/vendor/<name>/` as tracked
46
+ source behind a relative import, never installed. Fixtures run from the OS temp root and cannot
47
+ resolve a bare specifier, and neither can a fresh `git clone`; devDependencies are unaffected.
48
+
49
+ ### Removed
50
+
51
+
52
+ - **The recorded corpora, and the notion of a corpus with them.** `tests/__snapshots__/` held
53
+ 2.6 MB of transcripts taken from the CPython reference before it was deleted, replayed by
54
+ `tests/snapshot/`. With no recorder left they could never be re-blessed, so a red run could
55
+ only ever mean *you changed something*, never *you broke something* — and every deliberate
56
+ change to emitted text arrived as a red gate. The 1381-cell retirement of 2026-08-17 had
57
+ already made the argument; this finishes it.
58
+
59
+ **What they owned is now written out.** `wrapText`, `dwidth` and `formatHelp` had zero unit
60
+ coverage — the recordings were their only gate — so `tests/unit/text_layout.test.mjs` states
61
+ their rules explicitly and renders all 29 verbs. The JSONC, Windows-PATH and token-report
62
+ tables moved inline; `sync_themes` became an owned fixture with plain-text expectations. The
63
+ "corpus contains what it is named for" gates went with them: when the apostrophe is visible in
64
+ the file, nothing has to assert it is there.
65
+
66
+ Recoverable at tag `corpus-reference-v3.1.2`. Nothing can regenerate them.
67
+
68
+ - **Three migration statistics.** "N of the M subcommands run from Node", "0 commands have no
69
+ Node twin", and a native-verb list duplicated across two files purely so each could police the
70
+ other. A migration percentage is not a fact about a finished tool. The durable property under
71
+ the first two — every declared verb is dispatched by an entry point — is now asserted directly.
72
+
73
+ - **`golden.mjs --against`**, which could not have worked since the emit corpus was retired, and
74
+ `orphanCheck`, `narrowingReason` and `tests/helpers/snapshot_io.mjs` with it. The three
75
+ surviving modes are all self-comparisons.
76
+
10
77
 
11
78
  ## [3.1.1] — 2026-08-21
12
79
 
package/DESIGN.md CHANGED
@@ -35,7 +35,7 @@ vault or a specific tool's hooks.
35
35
  names (`ONT_TELOS`, `ONT_EVIDENCE`, `ONT_DECISIONS`, `ONT_CONDUCT`), a few rare
36
36
  technical nouns (`Context`, `Scripts`, `Charter`), and the folder names (`laws/`,
37
37
  `ontology/`, `doctrines/`, `agents/`, `skills/`, `memory/`, `notebook/` via `DIR_*`).
38
- These live in the `STRUCTURE` map in `js/render.mjs` and are laid on top of every
38
+ These live in the `STRUCTURE` map in `js/build/render.mjs` and are laid on top of every
39
39
  render, so a theme can never move a path, a link, or a heading number. No theme file
40
40
  defines a `DIR_*` or an `ONT_*` key, and none may: an ontology citation spells the
41
41
  section on both sides (`{{ONTOLOGY}}: {{ONT_TELOS}}`), so heading and reference move
@@ -102,7 +102,7 @@ vault or a specific tool's hooks.
102
102
  Rules, `I`..`IX`, headed `### {{LAW}} <roman> — {{LEX_<roman>}}`.
103
103
  - **Doctrines** (`src/doctrines/{craft,rigor,ops,process}.md`) — *how work is done
104
104
  here*. Practice rules addressed by pack and number, cited as `Doctrine process 5`.
105
- `PACK_ORDER` (`js/checkout.mjs`) fixes the order craft → rigor → ops → process, which
105
+ `PACK_ORDER` (`js/build/source.mjs`) fixes the order craft → rigor → ops → process, which
106
106
  is narrative and deliberately not alphabetical.
107
107
 
108
108
  **The Ontology and the Invariants are never toggleable** — every build carries both,
@@ -128,7 +128,7 @@ vault or a specific tool's hooks.
128
128
  repeal one; a user rule outranks a doctrine rule outright; nothing outranks an
129
129
  invariant. And an always-on tier may never cite a toggleable one — a `--doctrines craft`
130
130
  build would otherwise ship an invariant pointing at text its `AGENT.md` does not
131
- contain — so `constitutionProblems` (`js/doctor.mjs`) refuses a `{{DOCTRINE}}` token
131
+ contain — so `constitutionProblems` (`js/inspect/checks-authoring.mjs`) refuses a `{{DOCTRINE}}` token
132
132
  anywhere under `src/ontology/` or `src/laws/`.
133
133
 
134
134
  The bar for a **new invariant** is the highest in the tree: universal (it binds *every*
@@ -146,9 +146,9 @@ vault or a specific tool's hooks.
146
146
  |---|---|---|---|
147
147
  | body | append to `src/laws/universal.md` — never insert; nothing resolves a cross-reference against the canon | append to `src/doctrines/<pack>.md`, `### {{DOCTRINE}} <pack> <n> — {{DOC_<PACK>_<n>}}`; ids must run contiguously from 1 | a new `src/doctrines/<pack>.md` with its `**Name** — lead line` |
148
148
  | themed title | `LEX_<roman>` in all 15 theme files | `DOC_<PACK>_<n>` in all 15 theme files | `PACK_<NAME>` in all 15, plus each rule's `DOC_*` |
149
- | class | `LAW_CLASS` in `js/inventory.mjs`, one of the six in `LAW_CLASSES` | **none** — a doctrine rule's class *is* its pack, and there is no second taxonomy over it | — |
149
+ | class | `LAW_CLASS` in `js/inspect/inventory.mjs`, one of the six in `LAW_CLASSES` | **none** — a doctrine rule's class *is* its pack, and there is no second taxonomy over it | — |
150
150
  | console copy | a `LAW_META` row in `web/src/pages/Laws.jsx` (keyed by arabic number) | a `DOCTRINE_META` row in the same file (keyed `<pack>.<n>`) | one `DOCTRINE_META` row per rule |
151
- | registration | the numeral is the registration | the number is the registration | the pack name in `PACK_ORDER`, `js/checkout.mjs` |
151
+ | registration | the numeral is the registration | the number is the registration | the pack name in `PACK_ORDER`, `js/build/source.mjs` |
152
152
  | renumber risk | high — appending is the only safe move | none — packs are numbered independently | none |
153
153
  | counts | README badge + the `N universal laws` prose, `SHIPPED.md`'s triple, the web onboarding copy | **none** — the console spends the `N_PACKS` / `N_PACKS_ACTIVE` / `N_DOCTRINE_RULES` count tokens, all computed at request time | none, same reason |
154
154
  | changelog | `CHANGELOG.md` | `CHANGELOG.md` | `CHANGELOG.md` |
@@ -160,7 +160,7 @@ vault or a specific tool's hooks.
160
160
  synonym. `--sync-themes` writes the template's placeholder and only *prints* which keys
161
161
  to restyle; a shipped placeholder passes every gate.
162
162
 
163
- Almost all of it is gated, across six named checks in `js/doctor.mjs` —
163
+ Almost all of it is gated, across six named checks in `js/inspect/checks-build.mjs` —
164
164
  `themeParityProblems` (key parity across the voices), `lawMetaProblems` and
165
165
  `doctrineMetaProblems` (the console's Principle column, in both directions),
166
166
  `constitutionProblems` (pack numbering and filing, `LEX_I..LEX_IX` as an *equality*, the
@@ -195,11 +195,11 @@ theme file defines a `DIR_*` key, so `laws/` is `laws/` in all fourteen voices.
195
195
  | Notebook | `src/notebook/` | `notebook/` | the agent's sovereign space — any medium, seed-once charter the agent may rewrite; only `.gitignore` re-asserted |
196
196
  | Posture | `src/postures/` | inlined into `AGENT.md` | how much ceremony the agent brings to a task |
197
197
  | Mode | `src/modes/` | inlined into `AGENT.md` | the working axis (solo, foreman, …) — orthogonal to posture |
198
- | Context | `js/emit.mjs` | `context.json` | empty per-repo manifest, written once and never overwritten; git-ignore it |
198
+ | Context | `js/build/stubs.mjs` | `context.json` | empty per-repo manifest, written once and never overwritten; git-ignore it |
199
199
  | Themes | `themes/*.json` | — | token → label maps; `themes/opencode/` is a separate colour system |
200
200
  | Release label | `harness.config.json` | `.geneseed-version` | the theme and the human-readable version the driver reads; the canonical identity is the source fingerprint, not this string |
201
201
  | Lifecycle | `registry.json` | — | maintainer-side status/version/owner per agent and skill; never rendered into a bundle |
202
- | Generator | `bin/geneseed.mjs` (`geneseed-build`) | — | substitution + `<!-- INCLUDE: -->` inlining, and the nine `--emit` targets |
202
+ | Generator | `bin/build-driver.mjs` (`geneseed-build`) | — | substitution + `<!-- INCLUDE: -->` inlining, and the nine `--emit` targets |
203
203
  | CLI, as data | `js/cli-table.json` | — | the owned document the entry point parses its own argv from |
204
204
  | Automation | `bin/geneseed-cli.mjs` (`geneseed`) | — | 25 verbs — `build` / `doctor` / `status` / `prompt` / `diff` / `setup` / `web` / `upgrade` / … |
205
205
  | Hooks | `bin/geneseed-hook.mjs` (`geneseed-hook`) | — | the four verbs an emitted `settings.json` invokes: `context` / `git-gate` / `rule-gate` / `learn` |
@@ -212,28 +212,28 @@ theme file defines a `DIR_*` key, so `laws/` is `laws/` in all fourteen voices.
212
212
  - Unknown tokens are left visible (debugging aid); `doctor` flags them.
213
213
  - Node built-ins only; no third-party dependencies, ever. (`package.json` carries neither a
214
214
  `dependencies` nor a `devDependencies` key, and a test asserts it.)
215
- - **`js/settings.mjs` is the half of the emit that edits files you co-own** — the
215
+ - **`js/hosts/settings.mjs` is the half of the emit that edits files you co-own** — the
216
216
  `settings.json` / `opencode.json` merges, JSONC parsing, the hook shim and the managed
217
217
  CLAUDE.md block. It is a separate module because every function in it reconciles
218
218
  Geneseed's claim with content Geneseed did not write, and because eleven of its names are
219
- driven by the *runtime* as well as by an emit (`js/mcp.mjs` uses ten of them
219
+ driven by the *runtime* as well as by an emit (`js/hosts/mcp.mjs` uses ten of them
220
220
  for deactivate, remerge, reactivate and uninstall; `exclude` and `doctor` use the rest).
221
- Its dependency closure points one way only: nothing in it calls into `js/render.mjs` or
222
- `js/emit.mjs`. Keep it that way — that closure is what makes it a unit.
221
+ Its dependency closure points one way only: nothing in it calls into `js/build/render.mjs` or
222
+ `js/build/bundle.mjs`. Keep it that way — that closure is what makes it a unit.
223
223
  - **Every emit runs five stages in one order: `RENDER* → WIRE* → PRUNE → MANIFEST →
224
- VERIFY`.** RENDER writes files Geneseed owns wholesale; WIRE is `js/settings.mjs`
224
+ VERIFY`.** RENDER writes files Geneseed owns wholesale; WIRE is `js/hosts/settings.mjs`
225
225
  reconciling files you co-own; PRUNE removes what the previous manifest owned and this
226
226
  emit no longer produces; MANIFEST records both; VERIFY re-reads the merge to confirm it
227
227
  stuck. WIRE must precede MANIFEST because wiring is what fills the `managed` record the
228
228
  manifest stores, and no RENDER may follow a WIRE because a render writes wholesale a file
229
229
  a wire has just reconciled. `tests/unit/emit_phase_order.test.mjs` fails the build if any of
230
230
  the nine emits drifts out of that order, or if a new file-mutating routine in
231
- `js/settings.mjs` is called from an emit without being classified. RENDER and WIRE now run
231
+ `js/hosts/settings.mjs` is called from an emit without being classified. RENDER and WIRE now run
232
232
  in the *same process* — the seam that once spawned a second runtime per emit is gone — so
233
233
  the render and wire dispatchers stay two separate functions and two separate statements on
234
234
  purpose: that shape is the only thing left for the walker to check, and a gate refuses an
235
235
  emit whose sequence has lost its WIRE.
236
- `bin/geneseed.mjs`'s `main()` is classified too, as the one stage no per-emit gate can see: it
236
+ `bin/build-driver.mjs`'s `main()` is classified too, as the one stage no per-emit gate can see: it
237
237
  writes the `.geneseed-emit` / `-footprint` / `-theme` markers and the install-registry
238
238
  record *after* every emit returns, and nothing that reconciles a file you co-own may run
239
239
  there, because there is no manifest to record the claim and no teardown able to undo it.
package/README.md CHANGED
@@ -93,7 +93,7 @@ Both launchers are thin shims over the Node CLI: they need `node` (22.3+) on `PA
93
93
 
94
94
  Node ≥ 22.3 is the entire dependency list. Every subcommand, all four hooks, every web-console endpoint and both generators run from it, and every commit runs the whole unit suite plus `doctor --all` across every theme × host × footprint, on Linux and Windows both. So an install needs no second interpreter — not for the harness, and not for anything it ships. (Until 2026-08-17 the generator's output was also replayed byte for byte against recordings taken from the Python implementation. Those were retired — see [`docs/limits.md`](docs/limits.md) for the measurement that licensed it and the coverage it cost.)
95
95
 
96
- - **0 commands have no Node twin**: 25 of the 25 subcommands, and all four hook verbs, answer from the Node entry points. `catalog`, `mcp` and `memory` are Node-native faces the CLI table declares on top of that roster. There is no full-screen browse panel — `geneseed tui` and `geneseed menu` are verbs that refuse it by name and print the command list instead, off a terminal as well as on one.
96
+ - **One runtime, no second one to install**: every command and all four hook verbs answer from the Node entry points. `catalog`, `mcp` and `memory` are console-facing verbs the CLI table declares alongside them. There is no full-screen browse panel — `geneseed tui` and `geneseed menu` are verbs that refuse it by name and print the command list instead, off a terminal as well as on one.
97
97
  - **Nothing you install needs an interpreter.** The `token-report` skill is a script rather than prose, and it ships as `scripts/token_report.mjs`, run with `node`; `daydream` and `herdr` hand their inline code to `node -e`. So **every bundle carries** nothing that needs one: no Python file, and no skill that shells out to an interpreter, on any host, theme or footprint. Three tests freeze that — one scans every tracked file for inline code handed to an interpreter, one globs the bundle source, and one replays the report script over seeded transcripts for all four hosts and compares the bytes.
98
98
  - **`upgrade` / `update` / `sync-self` / `bootstrap` are for a git checkout.** They `git pull` the install's own origin. From an npm install they stop before touching anything and name `npm install -g geneseed@latest` as the update instead.
99
99
 
package/SETUP.md CHANGED
@@ -100,7 +100,7 @@ hand? Pick a path below.
100
100
  > **Every `geneseed-build …` below is the generator.** `npm install -g geneseed` puts it
101
101
  > on your `PATH`; without installing anything, `npx -p geneseed geneseed-build --emit
102
102
  > opencode-global` is the same command. From a checkout it is
103
- > `node bin/geneseed.mjs` with the same flags. Every commit re-emits with it across every
103
+ > `node bin/build-driver.mjs` with the same flags. Every commit re-emits with it across every
104
104
  > theme × host × footprint combination and checks the result with `doctor --all`, on Linux
105
105
  > and Windows both. Until 2026-08-17 that output was also compared byte for byte against
106
106
  > recordings taken from the Python implementation; those were retired, and `docs/limits.md`
package/SHIPPED.md CHANGED
@@ -20,7 +20,7 @@ historical pointers, not files distributed with the repo.
20
20
  > **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
21
21
  > human-readable label only. The canonical identity of an installed harness is the
22
22
  > **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
23
- > string. Current label: **3.1.0**.
23
+ > string. Current label: **3.1.2**.
24
24
 
25
25
  > **Spec status audit (2026-06-15).** Every dated spec authored before today was
26
26
  > verified against the source tree and confirmed implemented (code + tests + docs);
@@ -20,7 +20,7 @@
20
20
  * `js_cfg()` (_build_core.py:199) always sends `structure` and `capabilityLinkRe`, because
21
21
  * the Python originals are module-level names that TESTS MUTATE — `_OWNED` membership
22
22
  * asked one level out. This driver has no Python module to mutate, so it sends neither and
23
- * `js/render.mjs:215`'s `cfg.structure ?? STRUCTURE` and `js/emit.mjs:680`'s
23
+ * `js/build/render.mjs:215`'s `cfg.structure ?? STRUCTURE` and `js/build/bundle.mjs`'s
24
24
  * `cfg.capabilityLinkRe ? ... : <default>` take their right-hand branches for the first
25
25
  * time. Those two fallbacks have been dead code since they were written — always
26
26
  * overridden by the driver that always supplies them. P4 is what makes them live.
@@ -36,35 +36,36 @@ import {
36
36
  } from 'node:fs';
37
37
  import path from 'node:path';
38
38
  import { fileURLToPath } from 'node:url';
39
- import {
40
- build, emitOpencodeRender, emitOpencodeGlobalRender, emitClaudeRender, phaseLog,
41
- } from '../js/emit.mjs';
42
- import { settingsIntegrityCheck } from '../js/settings.mjs';
43
- import { writeText, parseJson, jsonDumpsIndent, withPyNewlines } from '../js/lib/fs.mjs';
39
+ import { build, phaseLog } from '../js/build/bundle.mjs';
40
+ import { emitClaudeRender } from '../js/build/emit-claude.mjs';
41
+ import { emitOpencodeRender, emitOpencodeGlobalRender } from '../js/build/emit-opencode.mjs';
42
+ import { settingsIntegrityCheck } from '../js/hosts/settings.mjs';
43
+ import { writeText, withPlatformNewlines } from '../js/lib/fs.mjs';
44
+ import { parseJson, jsonDumpsIndent } from '../js/lib/json.mjs';
44
45
  // P5c moved these out of this file: `bin/geneseed-cli.mjs` needs the same four resolvers to
45
46
  // find a global install, and a resolver that decides WHERE a driver writes is the last thing
46
47
  // that should exist twice. golden.py's 259 cells are what made the move safe to attempt.
47
48
  import {
48
- GLOBAL_MANIFEST, pyResolve, opencodeConfigDir, claudeConfigDir, bobConfigDir,
49
+ GLOBAL_MANIFEST, resolvePath, opencodeConfigDir, claudeConfigDir, bobConfigDir,
49
50
  copilotConfigDir,
50
- } from '../js/hosts.mjs';
51
+ } from '../js/hosts/hosts.mjs';
51
52
 
52
53
  // P5d moved these out of this file for the reason P5c moved the host resolvers: `harness
53
54
  // status` renders to count, so `bin/geneseed-cli.mjs` needs the same checkout paths and the
54
55
  // same cfg. golden.py's 259 cells all build one, which is what made the move safe.
55
56
  import {
56
- ROOT, CONFIG, THEMES, discoverNames, makeCfg, PACK_ORDER, knownRuleIds,
57
- } from '../js/checkout.mjs';
57
+ ROOT, CONFIG, discoverNames, makeCfg, PACK_ORDER, knownRuleIds,
58
+ } from '../js/build/source.mjs';
58
59
 
59
60
  // P5f moved these, for the same arithmetic a third time: `harness rebuild-all` reads the
60
61
  // registry to find every install it must re-emit, and a CLI verb may not reach into a driver
61
- // for a reader. See js/registry.mjs.
62
- import { registryRecord, registryRoots } from '../js/registry.mjs';
62
+ // for a reader. See js/inspect/registry.mjs.
63
+ import { registryRecord, registryRoots } from '../js/inspect/registry.mjs';
63
64
 
64
65
  // P2. `--sync-themes` crossed into its own module rather than into this file: it is 90 lines
65
66
  // of textual surgery over committed files with a corpus of its own, and it is the half of the
66
67
  // maintainer pair that needs nothing this driver is banned from having.
67
- import { syncThemes } from '../js/themes.mjs';
68
+ import { syncThemes } from '../js/build/themes.mjs';
68
69
 
69
70
  /** The nine `--emit` choices, in build.py:337-338's order. */
70
71
  const EMITS = ['files', 'opencode', 'opencode-global', 'claude', 'claude-global',
@@ -95,24 +96,24 @@ const NATIVE_CATALOG = { opencode: true, claude: true, bob: false, copilot: fals
95
96
  /** `_build_render.resolve_out` — absolute, or relative to the CURRENT WORKING DIRECTORY
96
97
  * (not to ROOT), so the harness renders straight into any repository.
97
98
  *
98
- * EXPORTED IN P5i because `js/setup.mjs` became its second caller: `_setup_summary_lines`
99
+ * EXPORTED IN P5i because `js/maintain/setup.mjs` became its second caller: `_setup_summary_lines`
99
100
  * names the AGENT.md a bundle emit just wrote, and it names it by the same resolution the
100
101
  * emit used. Copying the one line would be a second owner of the rule that a bare `Harness`
101
102
  * follows the invocation and not the checkout — which is what
102
103
  * `build/the-bundle-follows-cwd-not-the-checkout` exists to gate.
103
104
  *
104
- * `pyResolve` AND NOT `path.resolve`, and the difference is the whole reason `pyResolve`
105
+ * `resolvePath` AND NOT `path.resolve`, and the difference is the whole reason `resolvePath`
105
106
  * exists: the reference ends in `.resolve()`, which canonicalises — 8.3 short names expanded
106
107
  * on Windows, symlinks followed on POSIX, the filesystem's own casing — where `path.resolve`
107
- * only normalises `.`/`..`. Nine `pyResolve` call sites had the rule and this one did not.
108
+ * only normalises `.`/`..`. Nine `resolvePath` call sites had the rule and this one did not.
108
109
  * It is invisible on a machine whose paths are already canonical, which is every machine
109
110
  * this ran on until GitHub's Windows runner handed it a `C:\\Users\\RUNNER~1\\...` and the
110
111
  * two CLIs printed the same directory under two different names — a 70-byte difference in a
111
112
  * line compared BYTE for byte by `test_the_two_entry_points_agree_on_stdout_BYTES`. The
112
- * argument to `pyResolve` is made absolute first so its `expanduser` cannot fire: the
113
+ * argument to `resolvePath` is made absolute first so its `expanduser` cannot fire: the
113
114
  * reference does not expand `~` here, and a bare `~` is a legal directory name. */
114
115
  export function resolveOut(raw) {
115
- return pyResolve(path.resolve(process.cwd(), raw));
116
+ return resolvePath(path.resolve(process.cwd(), raw));
116
117
  }
117
118
 
118
119
  /**
@@ -218,7 +219,7 @@ function choicesFor() {
218
219
  * carrier, and a marker whose contents depend on argument order is one that compares unequal
219
220
  * to itself. Validation is against DISCOVERY, not `PACK_ORDER`, so the refusal names the
220
221
  * packs the checkout actually has; a discovered pack missing from `PACK_ORDER` is a
221
- * different fault and `js/render.mjs` refuses the whole build for it.
222
+ * different fault and `js/build/render.mjs` refuses the whole build for it.
222
223
  */
223
224
  /**
224
225
  * `--exclude-rules "process 7,craft 3"` (or `process.7`) -> the cfg's exclusion array.
@@ -357,11 +358,11 @@ function parseArgs(argv, defaults) {
357
358
  * hand-rolled copy would have its own `--target` alias, its own `choices` lists and its own
358
359
  * `-h`, and `test_the_help_text_names_every_flag_the_reference_takes` gates only this one.
359
360
  *
360
- * `withPyNewlines` because the refusal and `-h` paths WRITE: called from `bin/geneseed.mjs`
361
+ * `withPlatformNewlines` because the refusal and `-h` paths WRITE: called from `bin/build-driver.mjs`
361
362
  * they sit inside `main`'s funnel, and called from the CLI binary they would not.
362
363
  */
363
364
  export function parseDriverArgs(argv) {
364
- return withPyNewlines(() => parseArgs(argv, configDefaults()));
365
+ return withPlatformNewlines(() => parseArgs(argv, configDefaults()));
365
366
  }
366
367
 
367
368
  function choice(flag, value, allowed) {
@@ -441,7 +442,7 @@ function die(code, msg) {
441
442
  * re-emit, which is P10's migration arriving five phases early.
442
443
  *
443
444
  * EXPORTED SINCE P6i, because the emitter is no longer its only caller.
444
- * `js/uninstall.mjs`'s `remergeClaudeHooks` re-merges the canonical hooks when a disabled
445
+ * `js/maintain/uninstall.mjs`'s `remergeClaudeHooks` re-merges the canonical hooks when a disabled
445
446
  * Claude install is turned back on, and `mergeClaudeSettings` refuses to guess the pair —
446
447
  * correctly. There is exactly one right answer on this side and it is this function, so the
447
448
  * second caller imports it rather than restating two lines that must never drift: a
@@ -464,14 +465,14 @@ export function hookRunnerEntry() {
464
465
  * through `_build_core._claude_config_dir`, an `_OWNED` name precisely because the suite
465
466
  * redirects it at a sandbox, and a redirect that stops at a subprocess half-works in
466
467
  * silence. The child must therefore never derive it — but this file is the PARENT, so
467
- * deriving it is exactly this file's job, and `js/emit.mjs` receives the answer.
468
+ * deriving it is exactly this file's job, and `the host emit` receives the answer.
468
469
  *
469
470
  * POSIX spelling, per the original: `claudeMdExcludes` entries are glob patterns, where a
470
471
  * backslash is an escape, so the Windows-native form risks never matching.
471
472
  */
472
473
  function preambleExclude(claudeMd) {
473
474
  if (path.basename(claudeMd) !== 'CLAUDE.md') return null;
474
- return pyResolve(path.join(claudeConfigDir(), 'CLAUDE.md')).split(path.sep).join('/');
475
+ return resolvePath(path.join(claudeConfigDir(), 'CLAUDE.md')).split(path.sep).join('/');
475
476
  }
476
477
 
477
478
  /**
@@ -566,10 +567,6 @@ function pruneOwned(oc, oldOwned, owned) {
566
567
  }
567
568
  }
568
569
 
569
- function isDir(p) {
570
- try { return statSync(p).isDirectory(); } catch { return false; }
571
- }
572
-
573
570
  /**
574
571
  * `_build_global._project_survivors` — registered per-repo PROJECT installs of `emitName`.
575
572
  *
@@ -736,7 +733,7 @@ function emitOpencodeGlobal(cfg, args, out) {
736
733
  * `_build_global._emit_claude_core` — the shared engine behind six emits.
737
734
  *
738
735
  * Only the two Copilot ones run through it so far, and that is a deliberate split rather
739
- * than an arbitrary stopping point: `js/emit.mjs:1166` gates the entire settings/hook path
736
+ * than an arbitrary stopping point: `js/build/emit-claude.mjs's wire` gates the entire settings/hook path
740
737
  * behind `if (!isCopilot)`, so a Copilot emit never reaches `hookPrefix` and therefore never
741
738
  * needs `hookOpts` — the pair of machine-absolute values (`sys.executable` and the harness
742
739
  * entry point) that a Node driver has no way to produce. Copilot crossing first isolates
@@ -1007,7 +1004,7 @@ export function emitProjectInto(host, { theme, out, root, footprint = 'full', do
1007
1004
  // same reason: a drift reader that renders `expected` at the DEFAULT pack set reports the
1008
1005
  // whole doctrines section as edited on every narrowed install. `null` means "no opinion" and
1009
1006
  // `makeCfg` renders all four, which is what every validation caller wants.
1010
- return withPyNewlines(() => emit(makeCfg(doctrines ? { doctrines } : {}),
1007
+ return withPlatformNewlines(() => emit(makeCfg(doctrines ? { doctrines } : {}),
1011
1008
  { theme, footprint, root }, out));
1012
1009
  }
1013
1010
 
@@ -1020,7 +1017,7 @@ export function emitProjectInto(host, { theme, out, root, footprint = 'full', do
1020
1017
  * exactly as `run` inherits it below.
1021
1018
  */
1022
1019
  export function buildInto({ theme, out, footprint = 'lean' }) {
1023
- return withPyNewlines(
1020
+ return withPlatformNewlines(
1024
1021
  () => build(makeCfg(), theme, out, { footprint, nativeCatalog: false }),
1025
1022
  );
1026
1023
  }
@@ -1042,7 +1039,7 @@ export function emitGlobalInto(host, {
1042
1039
  // rendering at `peer`/`direct`; the drift readers pass what the deployment actually says,
1043
1040
  // because rendering `expected` at the defaults reports AGENT.md as edited on every install
1044
1041
  // that chose a register — the same scar the footprint left in `diffCollect`, one axis over.
1045
- return withPyNewlines(
1042
+ return withPlatformNewlines(
1046
1043
  () => emit(
1047
1044
  // THE PACK SELECTION RIDES THE SAME RAIL, with one difference: `doctrinesOfDir` answers
1048
1045
  // `null` for "no marker / undetectable", and that must render at ALL packs rather than
@@ -1062,16 +1059,16 @@ export function emitGlobalInto(host, {
1062
1059
  * `_harness_build.cmd_build` is `run([sys.executable, BUILD, *extra]).returncode` — Python
1063
1060
  * needs a second process because `build.py` is a different PROGRAM. Here it is a module in
1064
1061
  * the same one, and `bin/geneseed-cli.mjs` is under a transitive `child_process` ban, so
1065
- * `js/generate.mjs` calls this directly. The export is what makes the auto-run below need a
1062
+ * `js/build/generate.mjs` calls this directly. The export is what makes the auto-run below need a
1066
1063
  * guard: without one, importing this file to reach `main` would RUN the generator with the
1067
1064
  * CLI's argv.
1068
1065
  */
1069
1066
  export function main(argv) {
1070
- // `withPyNewlines` is what makes this driver's bytes Python's bytes on Windows, for every
1067
+ // `withPlatformNewlines` is what makes this driver's bytes Python's bytes on Windows, for every
1071
1068
  // print site in the whole render tree at once — see its docblock for why a funnel and not
1072
- // 25 calls to `pyPrint`. It wraps the `catch` as well as `run`, because `die` writes its
1069
+ // 25 calls to `printOut`. It wraps the `catch` as well as `run`, because `die` writes its
1073
1070
  // line on the way out.
1074
- return withPyNewlines(() => {
1071
+ return withPlatformNewlines(() => {
1075
1072
  try {
1076
1073
  return run(argv);
1077
1074
  } catch (e) {
@@ -1092,12 +1089,12 @@ function run(argv) {
1092
1089
  // different places because the transitive `child_process` ban splits them.
1093
1090
  //
1094
1091
  // `--sync-themes` is HERE, because it needs nothing this driver may not have: it reads
1095
- // `themes/`, rewrites the files textually, and prints. `js/themes.mjs` carries it.
1092
+ // `themes/`, rewrites the files textually, and prints. `js/build/themes.mjs` carries it.
1096
1093
  // Non-zero when files were CHANGED (0 == already in sync), so CI can run it as a drift
1097
1094
  // check — build.py:393-397's mapping, and a refusal (exit 2) if the template is unreadable.
1098
1095
  if (args.syncThemes) return syncThemes() ? 1 : 0;
1099
1096
  // `--validate-only` is NOT, and cannot be: its source-tree half is the doctor, and
1100
- // `js/doctor.mjs` starts a process (`node --check` over the OpenCode plugins). This driver
1097
+ // `js/inspect/checks-authoring.mjs` starts a process (`node --check` over the OpenCode plugins). This driver
1101
1098
  // is under a transitive ban on reaching any such module, gated twice — by a source grep in
1102
1099
  // `tests/test_node_cli_parity.py` and by an import walk in `tests/test_hook_cli_parity.py`.
1103
1100
  // So the tool crossed onto the CLI binary, which already carries the doctor, and this flag
@@ -1172,10 +1169,10 @@ function run(argv) {
1172
1169
  }
1173
1170
 
1174
1171
  /**
1175
- * Run only when this file IS the entry point, not when `js/generate.mjs` imports it.
1172
+ * Run only when this file IS the entry point, not when `js/build/generate.mjs` imports it.
1176
1173
  *
1177
1174
  * `import.meta.main` would say this in one word and is Node 24; the spec's `engines` floor
1178
- * is still open and this machine runs v22, the same reason `js/emit.mjs` avoids it. So the
1175
+ * is still open and this machine runs v22, which is why the comparison is spelled out rather than using it. So the
1179
1176
  * check is the manual one, through `realpathSync` because `process.argv[1]` is whatever the
1180
1177
  * caller typed and `import.meta.url` is always the resolved file — an `npx`-generated shim
1181
1178
  * or a `geneseed link` symlink makes those differ as strings while naming the same file.
@@ -1183,9 +1180,9 @@ function run(argv) {
1183
1180
  * BOTH ways of getting this wrong are gated, which is why it is a guard and not a comment,
1184
1181
  * and both were measured rather than argued:
1185
1182
  *
1186
- * * stuck FALSE — `node bin/geneseed.mjs` becomes a silent no-op: 259 golden cells.
1183
+ * * stuck FALSE — `node bin/build-driver.mjs` becomes a silent no-op: 259 golden cells.
1187
1184
  * * stuck TRUE — importing this file runs the generator on the IMPORTER's argv, so it is
1188
- * not only `build` that breaks. `bin/geneseed-cli.mjs` reaches `js/generate.mjs` at
1185
+ * not only `build` that breaks. `bin/geneseed-cli.mjs` reaches `js/build/generate.mjs` at
1189
1186
  * module load, so every CLI verb dies in the driver's flag parser: **62 of the 166
1190
1187
  * acceptance cells**, `exclude` and `status` and `version` among them.
1191
1188
  */
@@ -4,24 +4,24 @@
4
4
  * invokes. `bin/geneseed-hook.mjs` is the twin of the four it does.
5
5
  *
6
6
  * WHY A THIRD BINARY. P5c's whole question, and the three candidates were: grow the hook
7
- * entry, put verbs in `bin/geneseed.mjs`, or this. Each of the other two costs something
7
+ * entry, put verbs in `bin/build-driver.mjs`, or this. Each of the other two costs something
8
8
  * specific.
9
9
  *
10
10
  * * GROWING `bin/geneseed-hook.mjs` weakens the gate the design now rests on.
11
11
  * `test_the_entry_carries_exactly_the_verbs_the_emitter_wires` asserts EQUALITY between
12
- * that file's table and the hooks `js/settings.mjs` wires, and P5b made it load-bearing:
12
+ * that file's table and the hooks `js/hosts/settings.mjs` wires, and P5b made it load-bearing:
13
13
  * the shim is machine-wide (`~/.geneseed/bin/geneseed-hook[.cmd]`, no per-install
14
14
  * component) and last-writer-wins, so a Node emit owns the hooks of installs Python
15
15
  * wrote, and the verb sets matching is what makes that safe. Relaxing it to a subset
16
16
  * relation to admit `exclude` would be the single most damaging edit available here.
17
17
  * Two more reasons point the same way, and the second is not in that file's own
18
- * argument: `exclude`'s writer imports `js/settings.mjs` and `js/emit.mjs`, ~2,100 lines
18
+ * argument: `exclude`'s writer imports `js/hosts/settings.mjs` and `the emit modules`, ~2,100 lines
19
19
  * that would then load on EVERY PreToolUse call — the spec's hook-latency budget says
20
20
  * the shim must exec a minimal entry, not the CLI. And the entry's refusal list, which
21
21
  * names every verb it does not carry, is what makes "silently accepted `doctor` and did
22
22
  * nothing" impossible; every Geneseed hook exits 0 and signals through stdout, so a
23
23
  * no-op and a success are one observation.
24
- * * `bin/geneseed.mjs` IS `build.py`'s `main()` — a different Python program from
24
+ * * `bin/build-driver.mjs` IS `build.py`'s `main()` — a different Python program from
25
25
  * `rituals/harness.py`, not a different half of the same one. It parses generator FLAGS
26
26
  * with a partition asserted over them (`test_the_node_driver_classifies_every_emit`),
27
27
  * `tests/golden.py` drives it 259 times per run, and it is under a hard `child_process`
@@ -31,7 +31,7 @@
31
31
  *
32
32
  * What this costs is what the P5b handoff predicted: a second acceptance matrix. It is paid
33
33
  * in `tests/harness_golden.py`, which now carries a `bin` per cell and takes `--new-cli`
34
- * beside `--new` — and it would have been paid identically by the `bin/geneseed.mjs` option,
34
+ * beside `--new` — and it would have been paid identically by the `bin/build-driver.mjs` option,
35
35
  * so it is not a discriminator between the two.
36
36
  *
37
37
  * THE VERB SET REFUSES THE REST BY NAME, exactly as the hook entry does. It carries 22 of the
@@ -46,24 +46,25 @@
46
46
  * for the file as a whole is that its transitive imports now reach two modules that spawn,
47
47
  * which `_ALLOWED_SPAWNS` in `tests/test_hook_cli_parity.py` declares argv by argv.
48
48
  */
49
- import { cmdCatalog } from '../js/catalog.mjs';
50
- import { cliSpec, printHelp } from '../js/cli.mjs';
51
- import { cmdDiff } from '../js/diff.mjs';
52
- import { cmdDoctor, cmdValidate } from '../js/doctor.mjs';
53
- import { parseDriverArgs } from './geneseed.mjs';
54
- import { cmdExclude } from '../js/excludes.mjs';
55
- import { cmdBuild, cmdPrompt, cmdRebuildAll, cmdTheme } from '../js/generate.mjs';
56
- import { cmdMigrate } from '../js/migrate.mjs';
57
- import { pyInt } from '../js/lib/fs.mjs';
58
- import { cmdLink, cmdUnlink } from '../js/link.mjs';
59
- import { cmdMcp } from '../js/mcp.mjs';
60
- import { cmdMemory } from '../js/memory.mjs';
61
- import { cmdHome, cmdMenu } from '../js/menu.mjs';
62
- import { cmdTui } from '../js/tui.mjs';
63
- import { cmdSetup } from '../js/setup.mjs';
64
- import { cmdStatus, cmdVersion } from '../js/status.mjs';
65
- import { cmdUninstall } from '../js/uninstall.mjs';
66
- import { cmdBootstrap, cmdSyncSelf, cmdUpgrade } from '../js/update.mjs';
49
+ import { cmdCatalog } from '../js/build/catalog.mjs';
50
+ import { cliSpec, printHelp } from '../js/ui/cli.mjs';
51
+ import { cmdDiff } from '../js/inspect/diff.mjs';
52
+ import { cmdDoctor } from '../js/inspect/doctor.mjs';
53
+ import { cmdValidate } from '../js/inspect/validate.mjs';
54
+ import { parseDriverArgs } from './build-driver.mjs';
55
+ import { cmdExclude } from '../js/inspect/excludes.mjs';
56
+ import { cmdBuild, cmdPrompt, cmdRebuildAll, cmdTheme } from '../js/build/generate.mjs';
57
+ import { cmdMigrate } from '../js/maintain/migrate.mjs';
58
+ import { parseIntStrict } from '../js/lib/text.mjs';
59
+ import { cmdLink, cmdUnlink } from '../js/hosts/link.mjs';
60
+ import { cmdMcp } from '../js/hosts/mcp.mjs';
61
+ import { cmdMemory } from '../js/maintain/memory.mjs';
62
+ import { cmdHome, cmdMenu } from '../js/ui/menu.mjs';
63
+ import { cmdTui } from '../js/ui/tui.mjs';
64
+ import { cmdSetup } from '../js/maintain/setup.mjs';
65
+ import { cmdStatus, cmdVersion } from '../js/inspect/status.mjs';
66
+ import { cmdUninstall } from '../js/maintain/uninstall.mjs';
67
+ import { cmdBootstrap, cmdSyncSelf, cmdUpgrade } from '../js/maintain/update.mjs';
67
68
  import { cmdWeb } from '../js/web/server.mjs';
68
69
 
69
70
  /**
@@ -74,7 +75,7 @@ import { cmdWeb } from '../js/web/server.mjs';
74
75
  * `add_argument` calls, gated against the subparsers by NAME only. When
75
76
  * `_web_docs._cli_reference` stopped walking the parser and started reading the CLI table,
76
77
  * keeping that would have made THREE transcriptions of one parser — the parser, the file, and
77
- * here. So the argument surface comes from `js/cli.mjs`'s `cliSpec()`, derived from the same
78
+ * here. So the argument surface comes from `js/ui/cli.mjs`'s `cliSpec()`, derived from the same
78
79
  * file the docs page serves, and what is left in a row is the one thing a data file cannot
79
80
  * hold: the FUNCTION.
80
81
  *
@@ -150,14 +151,14 @@ const VERBS = {
150
151
  // Points at `cmdSyncSelf` rather than at `cmdUpgrade`, and that is not tidiness:
151
152
  // `sync-self` DROPS its `ref` before `upgrade` ever sees it, where `upgrade` both WARNS
152
153
  // about one and re-reads it as a theme — so the two verbs answer `sync-self cyberpunk`
153
- // and `sync-self v1.2.3` differently. See `js/update.mjs`'s `syncSelf` for both halves.
154
+ // and `sync-self v1.2.3` differently. See `js/maintain/update.mjs`'s `syncSelf` for both halves.
154
155
  fn: cmdSyncSelf,
155
156
  },
156
157
  bootstrap: {
157
158
  fn: cmdBootstrap,
158
159
  },
159
160
  // P7a. Both are DISPATCHERS whose off-TTY arm is the whole of what a cell can reach, and
160
- // `menu`'s on-TTY arm falls back rather than opening a panel — `js/menu.mjs`'s header
161
+ // `menu`'s on-TTY arm falls back rather than opening a panel — `js/ui/menu.mjs`'s header
161
162
  // argues both.
162
163
  menu: {
163
164
  fn: cmdMenu,
@@ -168,7 +169,7 @@ const VERBS = {
168
169
  // P7b, and it is the twenty-fifth and last. `cmd_tui`'s FIRST arm is `if not sys.stdin.
169
170
  // isatty()`, so off a TTY — which is every cell there is — the verb is one line and an
170
171
  // exit code, and that arm crosses byte for byte. The panel behind it is P7c's;
171
- // `js/tui.mjs`'s header argues why this entry falls back to `cmd_tui`'s own
172
+ // `js/ui/tui.mjs`'s header argues why this entry falls back to `cmd_tui`'s own
172
173
  // panel-unavailable line rather than inventing a second full-screen UI, and
173
174
  // `tests/test_tui_boundary.py` asserts that the arm it declares is genuinely unreachable
174
175
  // here rather than merely untested.
@@ -201,7 +202,7 @@ const VERBS = {
201
202
  };
202
203
 
203
204
  function die(code, msg) {
204
- // CRLF on Windows, for the same reason `js/hooks.mjs`'s funnels translate: argparse writes
205
+ // CRLF on Windows, for the same reason `js/hosts/hooks.mjs`'s funnels translate: argparse writes
205
206
  // this line through `sys.stderr`, which does.
206
207
  process.stderr.write(`geneseed: error: ${msg}${process.platform === 'win32' ? '\r\n' : '\n'}`);
207
208
  return code;
@@ -211,7 +212,7 @@ function die(code, msg) {
211
212
  * `argparse`'s surface for the verbs here, and only theirs: ordered positionals, some
212
213
  * optional, some with `choices`.
213
214
  *
214
- * `spec` comes from `js/cli.mjs`'s `cliSpec()` since P10c — derived from `js/cli-table.json`,
215
+ * `spec` comes from `js/ui/cli.mjs`'s `cliSpec()` since P10c — derived from `js/cli-table.json`,
215
216
  * which P2 made the owned document. This function's rules are unchanged; what
216
217
  * changed is that the table they run over is no longer written out by hand beside them. Every
217
218
  * argparse feature named below is a row of that derivation.
@@ -274,7 +275,7 @@ function parse(spec, argv) {
274
275
  // `type=int`. argparse's own wording, minus the usage block it prints around it — the
275
276
  // same rule the rest of this parser follows. The VALUE stays a string in `args`; the
276
277
  // command converts, and this is what guarantees the conversion cannot fail there.
277
- if ((spec.ints ?? []).includes(flag) && pyInt(value) === null) {
278
+ if ((spec.ints ?? []).includes(flag) && parseIntStrict(value) === null) {
278
279
  return { error: `argument ${flag}: invalid int value: '${value}'` };
279
280
  }
280
281
  args[dest] = value;
@@ -347,7 +348,7 @@ async function main(argv) {
347
348
  //
348
349
  // So a row would have meant amending two gates and recording a corpus half blind. It takes
349
350
  // the generator's own parser (see `parseDriverArgs`) because its flags are the generator's,
350
- // and it is on THIS binary — not on `bin/geneseed.mjs` — because it runs the doctor, which
351
+ // and it is on THIS binary — not on `bin/build-driver.mjs` — because it runs the doctor, which
351
352
  // starts a process the driver is banned from reaching.
352
353
  if (verb === 'validate') {
353
354
  try {
@@ -397,7 +398,7 @@ async function main(argv) {
397
398
  return await spec.fn(parsed.args);
398
399
  } catch (e) {
399
400
  // `e.exitCode` is the generator's existing marker for a DELIBERATE refusal that has
400
- // already explained itself on stderr — `js/emit.mjs:1289` reads the same flag, and it is
401
+ // already explained itself on stderr — `js/build/emit-claude.mjs` reads the same flag, and it is
401
402
  // `sys.exit(<message>)` on the Python side. Anything without it is a crash and keeps its
402
403
  // stack: Python prints a traceback for an unhandled exception, and turning one into a
403
404
  // tidy one-liner here would hide a bug behind a refusal's clothing.