geneseed 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/DESIGN.md +31 -20
  3. package/README.md +2 -2
  4. package/SETUP.md +30 -3
  5. package/SHIPPED.md +1 -1
  6. package/bin/{geneseed.mjs → build-driver.mjs} +88 -42
  7. package/bin/geneseed-cli.mjs +33 -32
  8. package/bin/geneseed-hook.mjs +7 -7
  9. package/docs/design-history.md +40 -40
  10. package/docs/extending.md +113 -85
  11. package/docs/web/rules.md +2 -0
  12. package/docs/web/setup-choices.md +13 -1
  13. package/harness.config.json +1 -1
  14. package/js/README.md +203 -0
  15. package/js/build/bundle.mjs +231 -0
  16. package/js/{catalog.mjs → build/catalog.mjs} +7 -7
  17. package/js/build/emit-claude.mjs +287 -0
  18. package/js/build/emit-common.mjs +255 -0
  19. package/js/build/emit-opencode.mjs +214 -0
  20. package/js/{generate.mjs → build/generate.mjs} +45 -31
  21. package/js/{render.mjs → build/render.mjs} +72 -11
  22. package/js/{checkout.mjs → build/source.mjs} +52 -11
  23. package/js/build/stubs.mjs +256 -0
  24. package/js/{themes.mjs → build/themes.mjs} +24 -24
  25. package/js/build/version.mjs +155 -0
  26. package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
  27. package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
  28. package/js/{installs.mjs → hosts/installs.mjs} +68 -16
  29. package/js/{link.mjs → hosts/link.mjs} +29 -28
  30. package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
  31. package/js/{native.mjs → hosts/native.mjs} +24 -22
  32. package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
  33. package/js/{settings.mjs → hosts/settings.mjs} +94 -69
  34. package/js/inspect/checks-authoring.mjs +761 -0
  35. package/js/inspect/checks-build.mjs +231 -0
  36. package/js/inspect/checks-repo.mjs +308 -0
  37. package/js/{diff.mjs → inspect/diff.mjs} +34 -31
  38. package/js/inspect/doctor.mjs +286 -0
  39. package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
  40. package/js/{inventory.mjs → inspect/inventory.mjs} +34 -20
  41. package/js/{registry.mjs → inspect/registry.mjs} +12 -10
  42. package/js/inspect/scan.mjs +125 -0
  43. package/js/{status.mjs → inspect/status.mjs} +27 -25
  44. package/js/inspect/validate.mjs +199 -0
  45. package/js/lib/fs.mjs +37 -759
  46. package/js/lib/json.mjs +344 -0
  47. package/js/lib/paths.mjs +242 -0
  48. package/js/lib/proc.mjs +3 -3
  49. package/js/lib/text.mjs +181 -0
  50. package/js/lib/udiff.mjs +1 -1
  51. package/js/{memory.mjs → maintain/memory.mjs} +10 -10
  52. package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
  53. package/js/{setup.mjs → maintain/setup.mjs} +49 -37
  54. package/js/{uninstall.mjs → maintain/uninstall.mjs} +62 -58
  55. package/js/{update.mjs → maintain/update.mjs} +51 -51
  56. package/js/{anim.mjs → ui/anim.mjs} +30 -28
  57. package/js/{cli.mjs → ui/cli.mjs} +10 -10
  58. package/js/{menu.mjs → ui/menu.mjs} +10 -9
  59. package/js/{tui.mjs → ui/tui.mjs} +36 -34
  60. package/js/web/actions.mjs +124 -89
  61. package/js/web/activity.mjs +22 -21
  62. package/js/web/api.mjs +88 -58
  63. package/js/web/daemon.mjs +319 -0
  64. package/js/web/docs.mjs +24 -21
  65. package/js/web/graph.mjs +0 -0
  66. package/js/web/handler.mjs +442 -0
  67. package/js/web/jobs.mjs +29 -21
  68. package/js/web/routes.mjs +223 -0
  69. package/js/web/server.mjs +56 -987
  70. package/package.json +11 -3
  71. package/src/AGENT.md.tmpl +1 -1
  72. package/src/doctrines/README.md +1 -1
  73. package/themes/opencode/README.md +1 -1
  74. package/web/dist/assets/{About-_MnJcwb_.js → About-CPvIFZGh.js} +1 -1
  75. package/web/dist/assets/{Activity-DpjqO-FR.js → Activity-z0uXdS7J.js} +1 -1
  76. package/web/dist/assets/{ActivityDetail-Bvrjv8oW.js → ActivityDetail-CEj5IJpF.js} +1 -1
  77. package/web/dist/assets/{Diff-DZ0IehBV.js → Diff-BMR8fPuG.js} +1 -1
  78. package/web/dist/assets/{Doctor-cu4krooO.js → Doctor-Bz5qE4r9.js} +1 -1
  79. package/web/dist/assets/{Graph-DUOPY65j.js → Graph-BV6a6t2l.js} +1 -1
  80. package/web/dist/assets/{Harnesses-B5Osg-BF.js → Harnesses-C5Ag5PaQ.js} +1 -1
  81. package/web/dist/assets/Laws-BK07NKw9.js +3 -0
  82. package/web/dist/assets/{Library-8y69I3_z.js → Library-CiXN9sC3.js} +1 -1
  83. package/web/dist/assets/{Markdown-Ds5GS1OD.js → Markdown-DG6FjUz-.js} +1 -1
  84. package/web/dist/assets/{Profile-7Jxa7ZUX.js → Profile-B2oSvdxH.js} +1 -1
  85. package/web/dist/assets/{Rules-UZ0jWkjg.js → Rules-DobVPd80.js} +1 -1
  86. package/web/dist/assets/{Skills-Bgkl0xLd.js → Skills-DQPsvsn9.js} +1 -1
  87. package/web/dist/assets/{StatusBadge-Cx4caF_2.js → StatusBadge-BhJ3HhdI.js} +1 -1
  88. package/web/dist/assets/{Themes-De8az7Sv.js → Themes-BP38FBU9.js} +1 -1
  89. package/web/dist/assets/{activity-VDAdVQ3q.js → activity-D9mfv7pY.js} +1 -1
  90. package/web/dist/assets/index-BE226Gv6.css +1 -0
  91. package/web/dist/assets/{index-BKrjBRV4.js → index-BZ7cPLMI.js} +1 -1
  92. package/web/dist/assets/index-BgIs5eIa.js +12 -0
  93. package/web/dist/assets/{index-wNiNzFFQ.js → index-DGM5j2hB.js} +1 -1
  94. package/web/dist/index.html +2 -2
  95. package/web/src/pages/Laws.jsx +135 -69
  96. package/docs/declined.md +0 -200
  97. package/docs/limits.md +0 -329
  98. package/js/doctor.mjs +0 -1712
  99. package/js/emit.mjs +0 -1410
  100. package/web/dist/assets/Laws-D8DJgTBp.js +0 -3
  101. package/web/dist/assets/index-1pPSz7Kl.css +0 -1
  102. package/web/dist/assets/index-C-gb-q-r.js +0 -12
@@ -19,7 +19,7 @@ something you can open. The same goes for the `tests/test_*.py` gates named thro
19
19
  were real, they were green, and they are gone — their Node successors are named in
20
20
  `tests/ported.json`. Where a passage says "now" it means *now, at that phase*.
21
21
 
22
- This annex is deliberately **not** de-Pythoned. Rewriting `_build_render.py` to `js/render.mjs`
22
+ This annex is deliberately **not** de-Pythoned. Rewriting `_build_render.py` to `js/build/render.mjs`
23
23
  throughout would destroy the one thing it documents — that there were two implementations, and
24
24
  what it cost to keep them byte-identical. What was wrong was filing it under a header that
25
25
  promised a spec someone should read before changing structure. That spec is now
@@ -31,7 +31,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
31
31
  ---
32
32
 
33
33
  ## ⚙️ Generator contract — the port narrative
34
- - **The render core has a Node twin: [`js/render.mjs`](js/render.mjs).** It is a
34
+ - **The render core has a Node twin: [`js/build/render.mjs`](js/build/render.mjs).** It is a
35
35
  translation of `_build_render.py`'s pure pipeline, byte-identical by test rather than by
36
36
  intent — `tests/test_render_parity.py` renders both over every theme × footprint ×
37
37
  catalog × laws-prefix × posture × mode and compares the written trees byte for byte,
@@ -39,7 +39,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
39
39
  sort. `js/lib/fs.mjs` holds the primitives where the two runtimes disagree in silence;
40
40
  `Path.write_text`'s `\n` → `os.linesep` translation is the load-bearing one. Zero
41
41
  dependencies on that side too.
42
- - **The host-native layer has one too: [`js/native.mjs`](js/native.mjs)** — the point where
42
+ - **The host-native layer has one too: [`js/hosts/native.mjs`](js/hosts/native.mjs)** — the point where
43
43
  RENDER stops being a pure function of `src/`. Its output depends on your
44
44
  `agent-overrides.json`, on which files already exist in the target (claim-on-create never
45
45
  overwrites a file Geneseed did not write), and on which of the three host dialects is
@@ -49,7 +49,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
49
49
  items and compares the written tree byte for byte, the returned ownership list *in order*,
50
50
  and the warning stream — and asserts the Node side printed nothing at all on stdout,
51
51
  because the emitted hook gates signal their verdict there and return 0 on every path.
52
- - **The OpenCode extras are ported too: [`js/opencode.mjs`](js/opencode.mjs)** — the
52
+ - **The OpenCode extras are ported too: [`js/hosts/opencode.mjs`](js/hosts/opencode.mjs)** — the
53
53
  branded and curated colour themes, the opt-in primary agent and slash-command layer, the
54
54
  always-on `/ponytail` switch, the verbatim plugin/workflow copies, and the
55
55
  `agent-overrides.json` stub with its staleness notice. Half of it sits behind
@@ -61,13 +61,13 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
61
61
  streams** — the staleness notice goes to stdout where every native-layer warning goes to
62
62
  stderr, an asymmetry inherited from the Python rather than corrected.
63
63
  - **The seam is now a real process boundary: `build.py` spawns Node once per emit.**
64
- [`js/emit.mjs`](js/emit.mjs) writes the bundle (`build`) and the OpenCode layer's RENDER
64
+ [`js/build/emit.mjs`](js/build/emit.mjs) writes the bundle (`build`) and the OpenCode layer's RENDER
65
65
  stage; Python keeps WIRE, PRUNE, MANIFEST and VERIFY, and drives — so doctor, web deploy,
66
66
  setup and rebuild-all keep calling `build.emit_*` in-process with no call site changed.
67
67
  Node falls back to Python when `node` is missing, silently and by design: the whole claim
68
68
  is that the two are indistinguishable. `GENESEED_NO_JS=1` forces the Python path.
69
69
  **The child's stdout carries the protocol document and nothing else — structurally.**
70
- `js/emit.mjs` replaces `process.stdout.write` and `process.stderr.write` with buffers for
70
+ `js/build/emit.mjs` replaces `process.stdout.write` and `process.stderr.write` with buffers for
71
71
  the whole run and restores them only to emit that document, so a stray `console.log`
72
72
  cannot corrupt the handoff; it lands in the payload, Python re-prints it, and the byte
73
73
  comparison fails. Python re-emits both buffers through its own `print`, which is what
@@ -93,7 +93,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
93
93
  pre-switch vocabulary by design — and a cell whose carve-out excuses nothing fails too.
94
94
  - **All nine emits cross that seam** — and this bullet said so for two phases while eight
95
95
  did. `emit_opencode_global` spawned Node **zero** times: its render half was inline
96
- Python, `js/emit.mjs` offered no job kind for it, and `tests/test_emit_boundary.py` had no
96
+ Python, `js/build/emit.mjs` offered no job kind for it, and `tests/test_emit_boundary.py` had no
97
97
  cell for it either, which is why nothing contradicted the sentence.
98
98
  `tests/test_seam_coverage.py` measures the spawn count of every mode, fails if the table
99
99
  drifts, and **refuses a mode that crosses without a boundary cell** — so the count above
@@ -121,7 +121,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
121
121
  (with the bundle in a subfolder, so `--out` and the target are not the same directory), a
122
122
  `.claude/` the user got to first, and every write-once file edited *between* two emits —
123
123
  the only way a byte comparison can tell "kept your store" from "re-seeded it".
124
- - **The wiring layer has a Node twin as well: [`js/settings.mjs`](js/settings.mjs)** — the
124
+ - **The wiring layer has a Node twin as well: [`js/hosts/settings.mjs`](js/hosts/settings.mjs)** — the
125
125
  JSONC reader, the `opencode.json` and `settings.json` merges, the hook shim, the
126
126
  managed-block machinery and the settings integrity check. It was proven before it was
127
127
  wired, the way every piece before it was; the emit now drives it. It was the last unit to
@@ -228,7 +228,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
228
228
  six at once. Four of them write `settings.json` hooks, so they reach `hookPrefix`, which
229
229
  needs the two machine-absolute values `_hook_runner_entry()` returns — `sys.executable`
230
230
  and the harness entry point. **A Node driver has no `sys.executable`**: the hooks it would
231
- be wiring are Python and `process.execPath` is node. `js/emit.mjs` gates the whole
231
+ be wiring are Python and `process.execPath` is node. `js/build/emit.mjs` gates the whole
232
232
  settings path behind `!isCopilot` and skips VERIFY for the same host, so the Copilot pair
233
233
  rides the shared engine without touching either — which buys the entire driver body
234
234
  (manifest-with-`managed`, prune, atomic write) while leaving the interpreter question
@@ -259,7 +259,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
259
259
  `statSync` on the reparse point raises `EACCES`, so the plain spelling already skips the
260
260
  thing the launcher needed a subprocess to detect. (End of the superseded passage.)
261
261
  **VERIFY did not need porting; it needed calling.** `settingsIntegrityCheck` has been
262
- complete in `js/settings.mjs` since P3a with no production caller, so the "largest unported
262
+ complete in `js/hosts/settings.mjs` since P3a with no production caller, so the "largest unported
263
263
  unit left" was one call site. What changed with it is what it proves: on the Python driver
264
264
  the stage runs in Python *after* a Node child did the wiring, making it a live
265
265
  cross-implementation check on every build; with both halves in Node that property is gone
@@ -299,7 +299,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
299
299
  location. Two `cfg` keys are deliberately **absent**, and their absence is load-bearing:
300
300
  `js_cfg()` always sends `structure` and `capabilityLinkRe` because the Python originals are
301
301
  module names tests mutate, but this driver has no Python module to mutate — so
302
- `js/render.mjs`'s `cfg.structure ?? STRUCTURE` and `js/emit.mjs`'s `capabilityLinkRe`
302
+ `js/build/render.mjs`'s `cfg.structure ?? STRUCTURE` and `js/build/emit.mjs`'s `capabilityLinkRe`
303
303
  fallback take their right-hand branches for the first time since they were written. They
304
304
  were unreachable, not untested. A second driver is what reached them.
305
305
  **A byte gate cannot tell two implementations from one**, so the driver owes a process
@@ -336,7 +336,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
336
336
  - **The runtime port starts with the four HOOK verbs, and with a gate rather than with
337
337
  code.** `context`, `git-gate`, `rule-gate` and `learn` are the four commands an emitted
338
338
  `settings.json` actually invokes, and they now have a Node twin:
339
- [`js/hooks.mjs`](js/hooks.mjs) behind [`bin/geneseed-hook.mjs`](bin/geneseed-hook.mjs), a
339
+ [`js/hosts/hooks.mjs`](js/hosts/hooks.mjs) behind [`bin/geneseed-hook.mjs`](bin/geneseed-hook.mjs), a
340
340
  **second binary** rather than a subcommand of the generator driver — `learn` must spawn
341
341
  whatever `$GENESEED_LLM` names, and `bin/geneseed.mjs` is under a hard `child_process`
342
342
  ban that is half the proof it is not a passthrough. The hook entry loads none of the
@@ -404,20 +404,20 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
404
404
  `harness_golden.py` reads stdout through a universal-newline decoder, so the difference
405
405
  was invisible to the entire matrix — the same shape as the shim's exclusion from
406
406
  `golden.py`. Harmless while nothing baked the Node entry; the bytes a real host reads once
407
- something does. `js/hooks.mjs`'s two output funnels translate, and one gate reads raw
407
+ something does. `js/hosts/hooks.mjs`'s two output funnels translate, and one gate reads raw
408
408
  bytes.
409
409
  The acceptance harness is what kept the port from being dead code before any of this — it
410
410
  executes `bin/geneseed-hook.mjs` as a real process against the Python one, which is the
411
- lesson from a stage that sat complete and uncalled in `js/settings.mjs` for two phases and
411
+ lesson from a stage that sat complete and uncalled in `js/hosts/settings.mjs` for two phases and
412
412
  was then briefed as unported.
413
413
  **Adding a module that legitimately spawns opened a door the driver's own gate cannot see
414
414
  through**: `test_the_driver_imports_no_child_process_module` greps `bin/geneseed.mjs`'s
415
- source, and one `import` of `js/hooks.mjs` would put `child_process` in the driver's
415
+ source, and one `import` of `js/hosts/hooks.mjs` would put `child_process` in the driver's
416
416
  process with that source still clean. `tests/test_hook_cli_parity.py` walks the driver's
417
417
  transitive relative imports instead — the same assertion, one level out, added because of
418
418
  this phase.
419
419
  - **`status` and `version` cross, and the phase's method is a CORPUS beside the matrix.**
420
- [`js/status.mjs`](js/status.mjs) answers both from `bin/geneseed-cli.mjs`. Two structural
420
+ [`js/inspect/status.mjs`](js/inspect/status.mjs) answers both from `bin/geneseed-cli.mjs`. Two structural
421
421
  things came with them.
422
422
  **A closure walk counts what the callee COMPUTES; the caller consumes a fraction of it.**
423
423
  An `ast` walk puts `status` at 527 LOC across 7 modules — `_tui_inventory` returns every
@@ -448,7 +448,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
448
448
  a candidate ordered *after* the unfenceable `ROOT/"Harness"`), and `_accent_for`'s
449
449
  fallback — plus `pyLen`/`pyLjust`, because a reproduction of a language primitive needs a
450
450
  corpus rather than a cell.
451
- **And the corpus found a live defect in shipped code.** `js/hooks.mjs` carried its own
451
+ **And the corpus found a live defect in shipped code.** `js/hosts/hooks.mjs` carried its own
452
452
  `str(Path(p))` as `pyStrPath`, built on `path.normalize`; P5c had already found that
453
453
  `normalize` collapses `a/../b` where `PurePath` keeps it, fixed `js/lib/fs.mjs`'s
454
454
  `pyPathStr`, and gated it with a 25-path corpus — which never reached the twin, because a
@@ -459,7 +459,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
459
459
  now has one owner for the primitive instead of two.
460
460
  - **The first NON-hook verb crosses, and it needed a third binary rather than a row in an
461
461
  existing table.** `harness exclude add|remove|list` — the sovereign-repo exclusions —
462
- is now [`js/excludes.mjs`](js/excludes.mjs) behind
462
+ is now [`js/inspect/excludes.mjs`](js/inspect/excludes.mjs) behind
463
463
  [`bin/geneseed-cli.mjs`](bin/geneseed-cli.mjs). The *reader* had been ported since P5a
464
464
  (`sovereignBypass` runs on every hook call); this is the writer, which maintains
465
465
  `excludes.json` across every global install and wires each host's native per-repo
@@ -467,8 +467,8 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
467
467
  **Where to put it was the whole question, and two of the three answers cost something
468
468
  specific.** Growing `bin/geneseed-hook.mjs` would have meant relaxing
469
469
  `test_the_entry_carries_exactly_the_verbs_the_emitter_wires` from equality to a subset
470
- relation — the gate the machine-wide shim now rests on — *and* loading `js/settings.mjs`
471
- plus `js/emit.mjs`, ~2,100 lines, on every `PreToolUse` call, against a spec that says the
470
+ relation — the gate the machine-wide shim now rests on — *and* loading `js/hosts/settings.mjs`
471
+ plus `js/build/emit.mjs`, ~2,100 lines, on every `PreToolUse` call, against a spec that says the
472
472
  shim must exec a minimal entry. Putting it in `bin/geneseed.mjs` would have crossed two
473
473
  different Python programs: that file is `build.py`'s `main()`, parses generator flags with
474
474
  a partition asserted over them, and carries a `child_process` ban that `web` and `upgrade`
@@ -492,7 +492,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
492
492
  atomic (temp + rename) on both sides, so a concurrent pair loses an edit rather than
493
493
  tearing the file.
494
494
  - **`build`, `prompt` and `theme` cross — the generator's own CLI face —** as
495
- [`js/generate.mjs`](js/generate.mjs). **Ten of the 24 subcommands are now Node.** Three
495
+ [`js/build/generate.mjs`](js/build/generate.mjs). **Ten of the 24 subcommands are now Node.** Three
496
496
  things are worth recording, and the first is about measurement rather than code.
497
497
  **A closure walk cannot see through a `subprocess`, and `build` is nothing but one.**
498
498
  `cmd_build` measures three lines because the work is `run([sys.executable, BUILD])`. Its
@@ -536,9 +536,9 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
536
536
  interpreter using the discovery the shim flip deleted; and a shim naming
537
537
  `bin/geneseed-cli.mjs` instead would answer three of the 24 verbs. The npm `bin` map is
538
538
  what replaces them, so they belong to the publish phase and not to this one.
539
- - **`diff` and `rebuild-all` cross** as [`js/diff.mjs`](js/diff.mjs) and `cmdRebuildAll` in
540
- [`js/generate.mjs`](js/generate.mjs), with the install detectors extracted to
541
- [`js/installs.mjs`](js/installs.mjs) and the registry to [`js/registry.mjs`](js/registry.mjs).
539
+ - **`diff` and `rebuild-all` cross** as [`js/inspect/diff.mjs`](js/inspect/diff.mjs) and `cmdRebuildAll` in
540
+ [`js/build/generate.mjs`](js/build/generate.mjs), with the install detectors extracted to
541
+ [`js/hosts/installs.mjs`](js/hosts/installs.mjs) and the registry to [`js/inspect/registry.mjs`](js/inspect/registry.mjs).
542
542
  **Twelve of the 24 subcommands are now Node.**
543
543
  **A closure walk cannot see through the standard library either.** An `ast` walk puts
544
544
  `diff` at 252 LOC of marker and manifest reading, and the verb's *entire* user-visible
@@ -582,7 +582,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
582
582
  does not exist yet: copy the checkout into the sandbox, plant one fault, and run *both*
583
583
  binaries from the copy so their two `ROOT`s move together. That is the next phase's first
584
584
  design decision rather than a corner of this one.
585
- - **`doctor` crosses** as [`js/doctor.mjs`](js/doctor.mjs), and the fixture came first.
585
+ - **`doctor` crosses** as [`js/inspect/doctor.mjs`](js/inspect/doctor.mjs), and the fixture came first.
586
586
  **Thirteen of the 24 subcommands are now Node.**
587
587
  **The copy-the-checkout fixture, measured rather than feared.** `_copy_checkout` in
588
588
  [`tests/helpers/cli_golden.mjs`](tests/helpers/cli_golden.mjs) gives each cell its own copy of the
@@ -599,7 +599,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
599
599
  against what it wrote) is the only honest source of a bundle a fresh render *agrees* with.
600
600
  **A `git ls-files` fixture cannot see the file you are writing.** The first draft copied the
601
601
  *tracked* set, so every cell reported the candidate dying on `ERR_MODULE_NOT_FOUND`:
602
- `js/doctor.mjs` was untracked until it was committed. The fixture mirrors the working tree
602
+ `js/inspect/doctor.mjs` was untracked until it was committed. The fixture mirrors the working tree
603
603
  now, ignored paths excluded, which is also what keeps `node_modules/` out of 27 copies.
604
604
  **The `child_process` ban became an allow-list, on the condition its own docstring named.**
605
605
  `_authoring_problems` runs `node --check` over the OpenCode plugins and there is no
@@ -621,7 +621,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
621
621
  indistinguishable), and one of the mutations is aimed at the FIXTURE: with `_copy_checkout`
622
622
  planting nothing, a planted-fault cell does not go red, it goes *vacuous* — which is the
623
623
  absolute half of a cell doing its job one layer further out than it ever has.
624
- - **`uninstall` crosses** as [`js/uninstall.mjs`](js/uninstall.mjs), and it is the first verb
624
+ - **`uninstall` crosses** as [`js/maintain/uninstall.mjs`](js/maintain/uninstall.mjs), and it is the first verb
625
625
  in the port that **deletes**. **Fourteen of the 24 subcommands are now Node**; of the ten
626
626
  left, `setup`/`menu`/`home` are P5's and the rest belong to P6–P10.
627
627
  **A deletion needs the gate to prove what SURVIVED.** Every cell before this one asserts
@@ -660,7 +660,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
660
660
  question about the corpus of inputs before it is a question about the code. The control had to
661
661
  be re-chosen too; the first one shared mechanism with half the mutations and reported four
662
662
  over-reaches that were nothing of the kind.
663
- - **`setup` crosses** as [`js/setup.mjs`](js/setup.mjs), and it is the first **interactive**
663
+ - **`setup` crosses** as [`js/maintain/setup.mjs`](js/maintain/setup.mjs), and it is the first **interactive**
664
664
  verb in the port. **Fifteen of the 24 subcommands are now Node**, and P5 is done: the nine
665
665
  left belong to P6–P10.
666
666
  **`cmd_setup` is a dispatcher, and that is the phase's first finding.** It is 21 lines: a
@@ -694,7 +694,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
694
694
  The menu's three fallback arms — an in-range index, a *parsed* out-of-range one, and an
695
695
  unparseable answer matched against the option keys — are told apart by nothing else.
696
696
  **The `child_process` allow-list became a table.** `_lsp_prereqs` runs `java -version` to
697
- see whether OpenCode's `jdtls` has a JVM, so `js/setup.mjs` is the second module on this
697
+ see whether OpenCode's `jdtls` has a JVM, so `js/maintain/setup.mjs` is the second module on this
698
698
  entry allowed to spawn. The gate now carries the module, the binding, the call count and
699
699
  the literal argv for each one, cross-checked against the source — the port's rule that the
700
700
  second instance of anything stops being a special case.
@@ -764,7 +764,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
764
764
  `excludesSnapshot()` unmodified; `api_installs` is a row per `installTargets()` entry
765
765
  through five detectors P5d and P5f had already ported. That is the same discovery every
766
766
  P5 sub-phase made, one layer up.
767
- **One parameter had to grow: `doctorCollect({groups})`.** `js/doctor.mjs` already carried
767
+ **One parameter had to grow: `doctorCollect({groups})`.** `js/inspect/doctor.mjs` already carried
768
768
  a `ran(check, label, probs)` whose label went nowhere, with a docblock saying it stayed
769
769
  because it is the one place each check is named. `/api/doctor` renders one card per
770
770
  check, so this is the phase that fills it. `on_progress=` is still absent and still P7's.
@@ -797,7 +797,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
797
797
  path inside a JSON string, where the backslashes made `\U` an invalid escape — the
798
798
  reference then swallowed the parse error and returned the empty stub, which reads exactly
799
799
  like a working exclusion list. And the first parity run found a real port bug in
800
- `js/installs.mjs`: `readMaybe` was a bare `readFileSync` where `Path.read_text` opens in
800
+ `js/hosts/installs.mjs`: `readMaybe` was a bare `readFileSync` where `Path.read_text` opens in
801
801
  TEXT mode and folds `\r\n` to `\n`. Every caller until now read a single-line marker and
802
802
  trimmed it, so five phases never noticed; `/api/profile` is the first consumer that hands
803
803
  the whole decoded text back out, and it failed on both the text and the sha256 of it.
@@ -822,11 +822,11 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
822
822
  - **The catalog crosses, and the taxonomy it needed was already ported.**
823
823
  `/api/catalog/<section>`, `/api/item/<type>/<name>` and the wiki reader answer from
824
824
  [`js/web/api.mjs`](js/web/api.mjs), over a new
825
- [`js/inventory.mjs`](js/inventory.mjs) — `_harness_tui.py`'s catalog half. **Still
825
+ [`js/inspect/inventory.mjs`](js/inspect/inventory.mjs) — `_harness_tui.py`'s catalog half. **Still
826
826
  fifteen subcommands; still no call site until P6h.**
827
- **The measurement is the finding.** `js/status.mjs` had said since P5d that "the ~111
827
+ **The measurement is the finding.** `js/inspect/status.mjs` had said since P5d that "the ~111
828
828
  lines of TUI taxonomy" were P7's and not ported. `LAW_CLASS`, `SKILL_CLASS`,
829
- `LAW_CLASSES` and `ENTITY_STATUSES` had in fact crossed in P5g, inside `js/doctor.mjs`,
829
+ `LAW_CLASSES` and `ENTITY_STATUSES` had in fact crossed in P5g, inside `js/inspect/doctor.mjs`,
830
830
  because doctor's authoring gates are what validate them. So P6c moved the four to the
831
831
  module that mirrors where Python keeps them and had doctor import them back, rather than
832
832
  writing a second copy — a copy of a value under test stops being the value under test the
@@ -912,7 +912,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
912
912
  **Eighteen mutations, eighteen fire, no survivors** — two of them visible only in the file
913
913
  snapshot, because the response body is byte-identical with the unlink removed.
914
914
  - **The web console starts WRITING, and the argv-in-a-response-body trap had a false
915
- premise.** [`js/web/actions.mjs`](js/web/actions.mjs) and [`js/mcp.mjs`](js/mcp.mjs) answer
915
+ premise.** [`js/web/actions.mjs`](js/web/actions.mjs) and [`js/hosts/mcp.mjs`](js/hosts/mcp.mjs) answer
916
916
  the eight mutating POSTs that own their own path plus the `/api/rules` and `/api/mcp` GETs
917
917
  that had to cross with them — **27 of 29 web paths, still fifteen subcommands.** The plan
918
918
  called `api_install_cmd`'s `{"cmd": [sys.executable, "build.py", …]}` "a Python argv, as
@@ -1009,7 +1009,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
1009
1009
  every non-interactive run; the reference's `input()` raises there. The byte reader now
1010
1010
  reports that case as null, gated by a stdin-seeded corpus.
1011
1011
  - **The install TOGGLE crosses and the web console is whole — 29 of 29 paths ANSWER, still
1012
- sixteen subcommands.** [`js/uninstall.mjs`](js/uninstall.mjs) gains the reversible half it was
1012
+ sixteen subcommands.** [`js/maintain/uninstall.mjs`](js/maintain/uninstall.mjs) gains the reversible half it was
1013
1013
  written expecting: deactivate MOVES every owned artifact into a sibling stash and drops the
1014
1014
  wiring, reactivate moves the same bytes back, and the stash directory's PRESENCE is the
1015
1015
  disabled flag — no recorded state that could drift from the filesystem. `/api/pick-folder` is
@@ -1038,7 +1038,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
1038
1038
  default the hook shim's runner and entry, so the Claude reactivate answered with that refusal
1039
1039
  on its first Node run instead of quietly wiring hooks at an invented path.
1040
1040
  - **`upgrade` crosses — seventeen of 24 — and the phase's real work was a fixture that could
1041
- WRITE.** [`js/update.mjs`](js/update.mjs) is `rituals/_update.py`'s upgrade half: the
1041
+ WRITE.** [`js/maintain/update.mjs`](js/maintain/update.mjs) is `rituals/_update.py`'s upgrade half: the
1042
1042
  preflight, the streamed fetch, the four upstream classifications, the fast-forward, the
1043
1043
  doctor gate with its exact rollback, and the two rebuilds. Every other verb in this port
1044
1044
  READS the checkout; this one rewrites it, so its cells needed a git repository they could
@@ -1049,7 +1049,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
1049
1049
  the verb**. `pullAndValidate` fast-forwards and then has to ask the PULLED source whether it
1050
1050
  is sound, and `rebuildBundle` has to render with the PULLED generator — an in-process
1051
1051
  `cmdDoctor` would validate the modules this process loaded before the merge, pass, and then
1052
- render the old source while reporting an upgrade. `js/generate.mjs`'s `cmdBuild` calls the
1052
+ render the old source while reporting an upgrade. `js/build/generate.mjs`'s `cmdBuild` calls the
1053
1053
  driver in-process and is right to; the discriminator is whether anything changed underneath.
1054
1054
  **"No cell reaches the network" is enforced by git, not by the fixture.** A fixture that
1055
1055
  merely points `origin` somewhere safe is a claim about the fixture. `GIT_ALLOW_PROTOCOL=file`
@@ -1249,7 +1249,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
1249
1249
  **The panel itself is declared, and the declaration is asserted from both directions**: the
1250
1250
  reference is shown drawing one, and the port is shown emitting no alt-screen escape, no
1251
1251
  cursor hide and no section header — plus a structural check that there is nothing in
1252
- `js/tui.mjs` that *could* draw one. A declaration that only says "we did not" is a claim
1252
+ `js/ui/tui.mjs` that *could* draw one. A declaration that only says "we did not" is a claim
1253
1253
  about one run; this says there is no screen to reach.
1254
1254
  The phase's own worst defect was a fixture, not a port: a scripted key list feeding a
1255
1255
  BLOCKING reader deadlocked the suite the moment it ran dry — and it did not hang when the
@@ -1275,7 +1275,7 @@ pointer whatever the prose around it says. Dead **prose paths** are left as writ
1275
1275
  `dict.get` against the JavaScript prototype chain, `str.center`'s `marg & width & 1` term,
1276
1276
  Python's non-negative modulo, floor division into a repeat count that *throws* where Python
1277
1277
  empties, strict `int()` for `COLUMNS`, and `max(default=0)` against `Math.max()`. It went
1278
- into a new module rather than `js/tui.mjs`, so that module's "contains no escape sequence"
1278
+ into a new module rather than `js/ui/tui.mjs`, so that module's "contains no escape sequence"
1279
1279
  assertion — the thing that makes the panel's declaration a measurement — survived untouched.
1280
1280
  **The empty-inventory crash P7b recorded is fixed**: an inventory with no agents, skills or
1281
1281
  laws crashed the panel on its first frame, and the guard went at the call site rather than in