geneseed 3.1.3 → 3.3.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 (150) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +24 -14
  3. package/SETUP.md +38 -7
  4. package/SHIPPED.md +6 -5
  5. package/adapters/bob/README.md +40 -10
  6. package/adapters/claude-code/README.md +5 -0
  7. package/adapters/claude-code/settings.json +8 -1
  8. package/adapters/copilot/README.md +50 -24
  9. package/adapters/opencode/README.md +5 -1
  10. package/adapters/opencode/plugins/geneseed-guard.js +40 -6
  11. package/bin/build-driver.mjs +143 -89
  12. package/bin/geneseed-cli.mjs +63 -106
  13. package/bin/geneseed-hook.mjs +26 -16
  14. package/docs/candidate-hosts.md +74 -0
  15. package/docs/extending.md +6 -5
  16. package/docs/token-footprint.md +32 -21
  17. package/docs/web-ui.md +5 -13
  18. package/harness.config.json +1 -1
  19. package/js/README.md +5 -15
  20. package/js/build/emit-claude.mjs +26 -12
  21. package/js/build/emit-common.mjs +63 -114
  22. package/js/build/emit-opencode.mjs +74 -66
  23. package/js/build/generate.mjs +4 -6
  24. package/js/build/render.mjs +50 -32
  25. package/js/build/source.mjs +29 -8
  26. package/js/build/stubs.mjs +25 -33
  27. package/js/build/themes.mjs +1 -1
  28. package/js/build/version.mjs +1 -3
  29. package/js/cli-table.json +84 -0
  30. package/js/hosts/hooks.mjs +243 -81
  31. package/js/hosts/hosts.mjs +57 -5
  32. package/js/hosts/installs.mjs +35 -28
  33. package/js/hosts/link.mjs +2 -2
  34. package/js/hosts/mcp.mjs +3 -1
  35. package/js/hosts/native.mjs +54 -19
  36. package/js/hosts/opencode.mjs +39 -28
  37. package/js/hosts/settings.mjs +206 -115
  38. package/js/inspect/checks-authoring.mjs +5 -4
  39. package/js/inspect/checks-build.mjs +1 -1
  40. package/js/inspect/diff.mjs +1 -3
  41. package/js/inspect/doctor.mjs +59 -44
  42. package/js/inspect/excludes.mjs +5 -11
  43. package/js/inspect/inventory.mjs +38 -56
  44. package/js/inspect/registry.mjs +4 -7
  45. package/js/inspect/scan.mjs +8 -15
  46. package/js/inspect/status.mjs +51 -3
  47. package/js/lib/fs.mjs +34 -0
  48. package/js/lib/json.mjs +28 -1
  49. package/js/lib/paths.mjs +0 -10
  50. package/js/lib/udiff.mjs +11 -16
  51. package/js/maintain/memory.mjs +2 -4
  52. package/js/maintain/migrate.mjs +3 -3
  53. package/js/maintain/uninstall.mjs +127 -88
  54. package/js/maintain/update.mjs +4 -4
  55. package/js/ui/anim.mjs +1 -1
  56. package/js/ui/menu.mjs +1 -1
  57. package/js/ui/tui.mjs +3 -20
  58. package/js/web/actions.mjs +230 -217
  59. package/js/web/activity.mjs +71 -89
  60. package/js/web/api.mjs +130 -226
  61. package/js/web/daemon.mjs +92 -110
  62. package/js/web/docs.mjs +59 -126
  63. package/js/web/handler.mjs +94 -123
  64. package/js/web/jobs.mjs +116 -178
  65. package/js/web/routes.mjs +47 -192
  66. package/js/web/server.mjs +47 -82
  67. package/package.json +1 -1
  68. package/registry.json +14 -0
  69. package/src/AGENT.md.tmpl +55 -16
  70. package/src/agents/explorer.md +2 -1
  71. package/src/doctrines/README.md +3 -3
  72. package/src/doctrines/process.md +4 -2
  73. package/src/doctrines/rigor.md +13 -0
  74. package/src/laws/universal.md +133 -43
  75. package/src/ontology/universal.md +38 -13
  76. package/src/skills/_template.md +5 -1
  77. package/src/skills/bruno-collection-generator.md +63 -0
  78. package/src/skills/bruno-test-writer.md +62 -0
  79. package/src/skills/clarify.md +1 -1
  80. package/src/skills/crash-course.md +1 -0
  81. package/src/skills/decode.md +1 -0
  82. package/src/skills/drill.md +1 -0
  83. package/src/skills/feynman.md +1 -0
  84. package/src/skills/forge-mcp.md +2 -1
  85. package/src/skills/fresh-eyes.md +1 -1
  86. package/src/skills/herdr.md +1 -0
  87. package/src/skills/ingest.md +2 -1
  88. package/src/skills/learning-path.md +1 -0
  89. package/src/skills/opencode-theme.md +1 -0
  90. package/src/skills/parallel-agents.md +1 -1
  91. package/src/skills/ponytail.md +2 -2
  92. package/src/skills/research.md +4 -2
  93. package/themes/_TEMPLATE.json +5 -0
  94. package/themes/biker.json +5 -0
  95. package/themes/commentator.json +5 -0
  96. package/themes/cyberpunk.json +5 -0
  97. package/themes/gamer.json +5 -0
  98. package/themes/imperial.json +5 -0
  99. package/themes/joker.json +5 -0
  100. package/themes/marvin.json +5 -0
  101. package/themes/mean.json +5 -0
  102. package/themes/military.json +5 -0
  103. package/themes/neutral.json +5 -0
  104. package/themes/pirate.json +5 -0
  105. package/themes/sports.json +5 -0
  106. package/themes/verstappen.json +5 -0
  107. package/themes/wizard.json +5 -0
  108. package/web/dist/assets/Activity-BfVcLrda.js +1 -0
  109. package/web/dist/assets/ActivityDetail-CQXccGPl.js +1 -0
  110. package/web/dist/assets/CatalogRow-DALEQImZ.js +1 -0
  111. package/web/dist/assets/Diff-BIjxXx46.js +5 -0
  112. package/web/dist/assets/Doctor-CJLbgsrw.js +1 -0
  113. package/web/dist/assets/FilterInput-CkUCk_lY.js +1 -0
  114. package/web/dist/assets/GreenhouseView-DfC5IILJ.js +2 -0
  115. package/web/dist/assets/Harness-Dm5CCU-v.js +1 -0
  116. package/web/dist/assets/JournalView-Cpu2eI4s.js +1 -0
  117. package/web/dist/assets/Laws-DDDVFuQz.js +3 -0
  118. package/web/dist/assets/Library-Dao3T9na.js +1 -0
  119. package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-C__nuWX5.js} +15 -15
  120. package/web/dist/assets/OperatorHudView-DEpCtAjP.js +2 -0
  121. package/web/dist/assets/Profile-BlXtUlh2.js +1 -0
  122. package/web/dist/assets/{Rules-CfsV1EVy.js → Rules-DIre6gxR.js} +1 -1
  123. package/web/dist/assets/Skills-rY13ZPhM.js +1 -0
  124. package/web/dist/assets/{StatusBadge-C9bZOEZk.js → StatusBadge-wW_RS_my.js} +1 -1
  125. package/web/dist/assets/{activity-Cc0n2jwx.js → activity-DpYPKNrA.js} +1 -1
  126. package/web/dist/assets/index-BEZvPvpF.js +1 -0
  127. package/web/dist/assets/index-DNQE5ydy.js +10 -0
  128. package/web/dist/assets/index-DzdQjvM1.css +1 -0
  129. package/web/dist/assets/index-PmQGRS1X.js +2 -0
  130. package/web/dist/assets/jobBuckets-DfTUJUxR.js +1 -0
  131. package/web/dist/assets/lawCats-DUS9UTH4.js +1 -0
  132. package/web/dist/index.html +2 -2
  133. package/web/src/pages/Laws.jsx +119 -130
  134. package/js/web/graph.mjs +0 -0
  135. package/web/dist/assets/Activity-B49fE_ug.js +0 -1
  136. package/web/dist/assets/ActivityDetail-abt7hzD2.js +0 -1
  137. package/web/dist/assets/Diff-D3_bVkih.js +0 -5
  138. package/web/dist/assets/Doctor-hXp7srsx.js +0 -1
  139. package/web/dist/assets/Graph-DvYMtlvl.js +0 -1
  140. package/web/dist/assets/Harness-BIWei7N5.js +0 -1
  141. package/web/dist/assets/Laws-CxmnRdwt.js +0 -3
  142. package/web/dist/assets/Library-MjbOkHIW.js +0 -1
  143. package/web/dist/assets/Markdown-Dx_w0faR.js +0 -1
  144. package/web/dist/assets/Profile-BRFvv2gr.js +0 -1
  145. package/web/dist/assets/Skills-DArA6IHS.js +0 -1
  146. package/web/dist/assets/index-Bx3oxZ1o.css +0 -1
  147. package/web/dist/assets/index-DC3-GlCY.js +0 -1
  148. package/web/dist/assets/index-DP035wPa.js +0 -12
  149. package/web/dist/assets/index-dHi-7M-F.js +0 -2
  150. package/web/dist/assets/roman-DkYN0LX9.js +0 -1
@@ -41,31 +41,22 @@
41
41
  * keeps the two tables from ever answering the same verb twice, since the shim bakes only one
42
42
  * of them.
43
43
  *
44
- * P6h MADE THE DISPATCH ASYNCHRONOUS and put `js/web/` on this entry's import graph. Both
45
- * are noted where they happen (`main`'s `await`, and the `web` row below); the consequence
46
- * for the file as a whole is that its transitive imports now reach two modules that spawn,
47
- * which `_ALLOWED_SPAWNS` in `tests/test_hook_cli_parity.py` declares argv by argv.
44
+ * P6h MADE THE DISPATCH ASYNCHRONOUS and put `js/web/` on this entry's import graph — LAZILY,
45
+ * since the slim pass: every command module loads through its verb's thunk, so the graph is
46
+ * reached per verb rather than per invocation. The modules that spawn are still declared argv
47
+ * by argv in `ALLOWED_SPAWNS` (`tests/unit/hook_cli.test.mjs`), whose closure walk follows
48
+ * the dynamic spelling too.
48
49
  */
49
- import { cmdCatalog } from '../js/build/catalog.mjs';
50
+ // THE FOUR EAGER IMPORTS ARE THE PARSER'S OWN, AND NOTHING ELSE LOADS BEFORE DISPATCH.
51
+ // The twenty command modules used to be static imports, which made every invocation pay for
52
+ // all of them: `geneseed status` loaded the web daemon, the TUI, the updater and the whole
53
+ // build driver — 57 modules, ~1 MB, ~40-55 ms — before a byte of its own work. The verb
54
+ // table below now holds one LOADER per row (`() => import(…).then(m => m.cmdX)`), so a verb
55
+ // loads its slice of the tree and no other verb's. The spawn allow-list gate still sees the
56
+ // whole graph: `tests/unit/hook_cli.test.mjs`'s `importClosure` follows both import spellings.
50
57
  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
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';
68
- import { cmdWeb } from '../js/web/server.mjs';
59
+ import { printErr } from '../js/lib/fs.mjs';
69
60
 
70
61
  /**
71
62
  * The verbs this entry answers — and, since P10c, NOTHING ELSE ABOUT THEM.
@@ -83,56 +74,31 @@ import { cmdWeb } from '../js/web/server.mjs';
83
74
  * `tests/test_hook_cli_parity.py` scrapes `const VERBS = {` for its three matrix gates, and
84
75
  * `main`'s refusal lists `Object.keys(VERBS)` IN THIS ORDER, which `harness_golden` cells
85
76
  * assert verbatim — the order is the old table's and is not alphabetical.
77
+ *
78
+ * FLATTENED TO `verb: fn` since Task 5, AND TO `verb: loader` since the lazy pass: every row
79
+ * is now a thunk that imports the command's module and resolves to its function, so the one
80
+ * field a row holds is still the one thing a data file cannot — where the FUNCTION lives.
81
+ * `main` awaits `VERBS[verb]()` after the parse succeeds; the scraping regexes in
82
+ * `tests/unit/{cli_table,hook_cli,docs}.test.mjs` match on the KEY and the colon only; the
83
+ * literal string `const VERBS = {` and the key order neither changed.
86
84
  */
87
85
  const VERBS = {
88
- exclude: {
89
- fn: cmdExclude,
90
- },
91
- status: {
92
- fn: cmdStatus,
93
- },
94
- version: {
95
- fn: cmdVersion,
96
- },
97
- build: {
98
- fn: cmdBuild,
99
- },
100
- prompt: {
101
- fn: cmdPrompt,
102
- },
103
- theme: {
104
- fn: cmdTheme,
105
- },
106
- diff: {
107
- fn: cmdDiff,
108
- },
109
- 'rebuild-all': {
110
- fn: cmdRebuildAll,
111
- },
112
- migrate: {
113
- fn: cmdMigrate,
114
- },
115
- doctor: {
116
- fn: cmdDoctor,
117
- },
118
- uninstall: {
119
- fn: cmdUninstall,
120
- },
121
- link: {
122
- fn: cmdLink,
123
- },
124
- unlink: {
125
- fn: cmdUnlink,
126
- },
127
- setup: {
128
- fn: cmdSetup,
129
- },
130
- web: {
131
- fn: cmdWeb,
132
- },
133
- upgrade: {
134
- fn: cmdUpgrade,
135
- },
86
+ exclude: () => import('../js/inspect/excludes.mjs').then((m) => m.cmdExclude),
87
+ status: () => import('../js/inspect/status.mjs').then((m) => m.cmdStatus),
88
+ version: () => import('../js/inspect/status.mjs').then((m) => m.cmdVersion),
89
+ build: () => import('../js/build/generate.mjs').then((m) => m.cmdBuild),
90
+ prompt: () => import('../js/build/generate.mjs').then((m) => m.cmdPrompt),
91
+ theme: () => import('../js/build/generate.mjs').then((m) => m.cmdTheme),
92
+ diff: () => import('../js/inspect/diff.mjs').then((m) => m.cmdDiff),
93
+ 'rebuild-all': () => import('../js/build/generate.mjs').then((m) => m.cmdRebuildAll),
94
+ migrate: () => import('../js/maintain/migrate.mjs').then((m) => m.cmdMigrate),
95
+ doctor: () => import('../js/inspect/doctor.mjs').then((m) => m.cmdDoctor),
96
+ uninstall: () => import('../js/maintain/uninstall.mjs').then((m) => m.cmdUninstall),
97
+ link: () => import('../js/hosts/link.mjs').then((m) => m.cmdLink),
98
+ unlink: () => import('../js/hosts/link.mjs').then((m) => m.cmdUnlink),
99
+ setup: () => import('../js/maintain/setup.mjs').then((m) => m.cmdSetup),
100
+ web: () => import('../js/web/server.mjs').then((m) => m.cmdWeb),
101
+ upgrade: () => import('../js/maintain/update.mjs').then((m) => m.cmdUpgrade),
136
102
  // `up.add_parser(..., aliases=["update"])`, reproduced in P8c as a ROW OF ITS OWN rather than
137
103
  // as an `aliases` field on the row above — because a field would be a DECLARATION and the
138
104
  // three matrix gates read this table as the DISPATCH (rule 7, and M23 is where it was
@@ -144,28 +110,18 @@ const VERBS = {
144
110
  //
145
111
  // It is NOT what the web console's `update` ACTION needs: that row's argv names `upgrade`,
146
112
  // the subparser, and always did. The two were coupled only in a handoff's note.
147
- update: {
148
- fn: cmdUpgrade,
149
- },
150
- 'sync-self': {
151
- // Points at `cmdSyncSelf` rather than at `cmdUpgrade`, and that is not tidiness:
152
- // `sync-self` DROPS its `ref` before `upgrade` ever sees it, where `upgrade` both WARNS
153
- // about one and re-reads it as a theme — so the two verbs answer `sync-self cyberpunk`
154
- // and `sync-self v1.2.3` differently. See `js/maintain/update.mjs`'s `syncSelf` for both halves.
155
- fn: cmdSyncSelf,
156
- },
157
- bootstrap: {
158
- fn: cmdBootstrap,
159
- },
113
+ update: () => import('../js/maintain/update.mjs').then((m) => m.cmdUpgrade),
114
+ // Points at `cmdSyncSelf` rather than at `cmdUpgrade`, and that is not tidiness:
115
+ // `sync-self` DROPS its `ref` before `upgrade` ever sees it, where `upgrade` both WARNS
116
+ // about one and re-reads it as a theme — so the two verbs answer `sync-self cyberpunk`
117
+ // and `sync-self v1.2.3` differently. See `js/maintain/update.mjs`'s `syncSelf` for both halves.
118
+ 'sync-self': () => import('../js/maintain/update.mjs').then((m) => m.cmdSyncSelf),
119
+ bootstrap: () => import('../js/maintain/update.mjs').then((m) => m.cmdBootstrap),
160
120
  // P7a. Both are DISPATCHERS whose off-TTY arm is the whole of what a cell can reach, and
161
121
  // `menu`'s on-TTY arm falls back rather than opening a panel — `js/ui/menu.mjs`'s header
162
122
  // argues both.
163
- menu: {
164
- fn: cmdMenu,
165
- },
166
- home: {
167
- fn: cmdHome,
168
- },
123
+ menu: () => import('../js/ui/menu.mjs').then((m) => m.cmdMenu),
124
+ home: () => import('../js/ui/menu.mjs').then((m) => m.cmdHome),
169
125
  // P7b, and it is the twenty-fifth and last. `cmd_tui`'s FIRST arm is `if not sys.stdin.
170
126
  // isatty()`, so off a TTY — which is every cell there is — the verb is one line and an
171
127
  // exit code, and that arm crosses byte for byte. The panel behind it is P7c's;
@@ -173,9 +129,7 @@ const VERBS = {
173
129
  // panel-unavailable line rather than inventing a second full-screen UI, and
174
130
  // `tests/test_tui_boundary.py` asserts that the arm it declares is genuinely unreachable
175
131
  // here rather than merely untested.
176
- tui: {
177
- fn: cmdTui,
178
- },
132
+ tui: () => import('../js/ui/tui.mjs').then((m) => m.cmdTui),
179
133
  // THE THREE THAT NEVER HAD A PYTHON ORIGINAL, and they are last for that reason rather
180
134
  // than by alphabet: every row above is the twin of a subparser, and these three are not.
181
135
  // The information behind them existed only behind the web console — the catalog endpoint,
@@ -190,21 +144,15 @@ const VERBS = {
190
144
  // ABSOLUTE — `tests/snapshot/cli_help.test.mjs` splits the two populations by name and refuses a
191
145
  // verb that falls into neither, and each has its own unit gate stating what it does rather
192
146
  // than that it agrees.
193
- catalog: {
194
- fn: cmdCatalog,
195
- },
196
- mcp: {
197
- fn: cmdMcp,
198
- },
199
- memory: {
200
- fn: cmdMemory,
201
- },
147
+ catalog: () => import('../js/build/catalog.mjs').then((m) => m.cmdCatalog),
148
+ mcp: () => import('../js/hosts/mcp.mjs').then((m) => m.cmdMcp),
149
+ memory: () => import('../js/maintain/memory.mjs').then((m) => m.cmdMemory),
202
150
  };
203
151
 
204
152
  function die(code, msg) {
205
153
  // CRLF on Windows, for the same reason `js/hosts/hooks.mjs`'s funnels translate: argparse writes
206
- // this line through `sys.stderr`, which does.
207
- process.stderr.write(`geneseed: error: ${msg}${process.platform === 'win32' ? '\r\n' : '\n'}`);
154
+ // this line through `sys.stderr`, which does. `printErr` owns the `\n`-to-`os.linesep` rule.
155
+ printErr(`geneseed: error: ${msg}\n`);
208
156
  return code;
209
157
  }
210
158
 
@@ -351,6 +299,11 @@ async function main(argv) {
351
299
  // and it is on THIS binary — not on `bin/build-driver.mjs` — because it runs the doctor, which
352
300
  // starts a process the driver is banned from reaching.
353
301
  if (verb === 'validate') {
302
+ // Lazy for the same reason the table rows are — `parseDriverArgs` alone drags the whole
303
+ // generator driver in, and every OTHER verb was paying for it.
304
+ const [{ cmdValidate }, { parseDriverArgs }] = await Promise.all([
305
+ import('../js/inspect/validate.mjs'), import('./build-driver.mjs'),
306
+ ]);
354
307
  try {
355
308
  return cmdValidate(parseDriverArgs(argv.slice(1)));
356
309
  } catch (e) {
@@ -358,8 +311,8 @@ async function main(argv) {
358
311
  throw e;
359
312
  }
360
313
  }
361
- const spec = VERBS[verb];
362
- if (!spec) {
314
+ const load = VERBS[verb];
315
+ if (!load) {
363
316
  // "and every other harness subcommand is still Python — run the interpreter against the
364
317
  // old harness script" is GONE, and its removal is not cosmetic: with the four hook verbs
365
318
  // named, the two entry points now cover every subcommand there is, so the clause was not
@@ -395,7 +348,11 @@ async function main(argv) {
395
348
  // it `process.exitCode` would be assigned a PENDING PROMISE, which Node coerces to 0
396
349
  // and prints nothing about. `web status` would then report "not running" and exit 0.
397
350
  // Awaiting a number is a no-op for the ten synchronous verbs.
398
- return await spec.fn(parsed.args);
351
+ //
352
+ // The load happens HERE, after the parse succeeded: a bad argument line refuses in a
353
+ // few milliseconds without touching the command's module tree, which is also why the
354
+ // parse errors above cannot depend on anything a loader would have brought in.
355
+ return await (await load())(parsed.args);
399
356
  } catch (e) {
400
357
  // `e.exitCode` is the generator's existing marker for a DELIBERATE refusal that has
401
358
  // already explained itself on stderr — `js/build/emit-claude.mjs` reads the same flag, and it is
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * The hook entry point — a Node twin of `rituals/harness.py` for the four verbs the
4
- * emitted `settings.json` actually invokes.
3
+ * The hook entry point — a Node twin of `rituals/harness.py` for the verbs the emitted
4
+ * `settings.json` actually invokes (four from the reference, plus `tool-gate` for Copilot).
5
5
  *
6
6
  * WHY IT IS A SECOND BINARY AND NOT A SUBCOMMAND OF `bin/build-driver.mjs`. Three reasons,
7
7
  * and each alone would be enough:
@@ -37,13 +37,20 @@
37
37
  * every Geneseed hook returns 0 and signals through stdout — so "did nothing" and "worked"
38
38
  * are the same observation.
39
39
  */
40
- import { cmdContext, cmdGitGate, cmdRuleGate, cmdLearn } from '../js/hosts/hooks.mjs';
41
- import { printHelp } from '../js/ui/cli.mjs';
40
+ import { cmdContext, cmdGitGate, cmdRuleGate, cmdToolGate, cmdLearn } from '../js/hosts/hooks.mjs';
41
+ // Not a new module on the hot path: `js/hosts/hooks.mjs` above already imports
42
+ // `js/lib/fs.mjs`, so this edge names a file the process has already loaded.
43
+ import { printErr } from '../js/lib/fs.mjs';
42
44
 
45
+ // `--host` selects the verdict dialect (see js/hosts/hooks.mjs's header); the emitter bakes
46
+ // it into the command for every host that is not Claude Code, so the hook never has to guess
47
+ // its reader from the payload's shape.
48
+ const HOSTED = { '--root': 'root', '--host': 'host' };
43
49
  const VERBS = {
44
- context: { fn: cmdContext, flags: { '--root': 'root' } },
45
- 'git-gate': { fn: cmdGitGate, flags: { '--root': 'root' } },
46
- 'rule-gate': { fn: cmdRuleGate, flags: { '--root': 'root' } },
50
+ context: { fn: cmdContext, flags: HOSTED },
51
+ 'git-gate': { fn: cmdGitGate, flags: HOSTED },
52
+ 'rule-gate': { fn: cmdRuleGate, flags: HOSTED },
53
+ 'tool-gate': { fn: cmdToolGate, flags: HOSTED },
47
54
  learn: {
48
55
  fn: cmdLearn,
49
56
  flags: { '--memory': 'memory' },
@@ -54,18 +61,18 @@ const VERBS = {
54
61
 
55
62
  function die(code, msg) {
56
63
  // CRLF on Windows, for the same reason `js/hosts/hooks.mjs`'s funnels translate: argparse
57
- // writes this line through `sys.stderr`, which does.
58
- process.stderr.write(`geneseed-hook: error: ${msg}${process.platform === 'win32' ? '\r\n' : '\n'}`);
64
+ // writes this line through `sys.stderr`, which does. `printErr` owns the `\n`-to-`os.linesep` rule.
65
+ printErr(`geneseed-hook: error: ${msg}\n`);
59
66
  return code;
60
67
  }
61
68
 
62
69
  /**
63
- * `argparse`'s surface for these four verbs, and only theirs: `--flag VALUE`, the
70
+ * `argparse`'s surface for these verbs, and only theirs: `--flag VALUE`, the
64
71
  * `--flag=VALUE` spelling it also accepts, store_true switches, and one optional
65
72
  * positional.
66
73
  */
67
74
  function parse(spec, argv) {
68
- const args = { root: null, memory: null, consolidate: false, file: null };
75
+ const args = { root: null, host: null, memory: null, consolidate: false, file: null };
69
76
  for (let i = 0; i < argv.length; i += 1) {
70
77
  const tok = argv[i];
71
78
  const eq = tok.indexOf('=');
@@ -89,7 +96,7 @@ function parse(spec, argv) {
89
96
  return { args };
90
97
  }
91
98
 
92
- function main(argv) {
99
+ async function main(argv) {
93
100
  // `geneseed status | head` closes stdout early. Python exits quietly through
94
101
  // BrokenPipeError; the Node equivalent is an EPIPE that would otherwise be an unhandled
95
102
  // 'error' event and a stack trace on a hook path.
@@ -108,16 +115,19 @@ function main(argv) {
108
115
  // The COMMAND is not a table: `geneseed` is this package's `bin` entry for the CLI
109
116
  // (package.json), it answers every non-hook verb, and it survives the deletion of the
110
117
  // interpreter-plus-script invocation this line used to print.
111
- return die(2, `invalid choice: '${verb}'. This entry point carries only the four HOOK `
118
+ return die(2, `invalid choice: '${verb}'. This entry point carries only the HOOK `
112
119
  + `verbs (${Object.keys(VERBS).join(', ')}); every other harness subcommand lives `
113
120
  + 'elsewhere — run `geneseed ' + verb + '`.');
114
121
  }
115
122
  // `<verb> --help`, for the same reason and by the same owner as `bin/geneseed-cli.mjs`:
116
123
  // argparse holds `-h` at the parser, so this entry's `parse` calls it an unrecognized
117
124
  // argument. The four hook verbs are four of the reference's 26 and had the same gap.
118
- // `js/ui/cli.mjs` reads the CLI table lazily, inside its functions, so importing it costs this
119
- // entry a module parse and no file read on the hook path.
125
+ // `js/ui/cli.mjs` is imported HERE, not at the top: a static import cost every tool call
126
+ // of every session a module parse (~3 ms of an ~12 ms controllable budget) for a branch
127
+ // only a human at a terminal ever takes. The hook path never awaits it, so `main` staying
128
+ // async-shaped costs the hot verbs nothing.
120
129
  if (argv.slice(1).some((t) => t === '-h' || t === '--help')) {
130
+ const { printHelp } = await import('../js/ui/cli.mjs');
121
131
  const rc = printHelp('geneseed-hook', verb);
122
132
  if (rc !== null) return rc;
123
133
  }
@@ -126,4 +136,4 @@ function main(argv) {
126
136
  return spec.fn(parsed.args);
127
137
  }
128
138
 
129
- process.exitCode = main(process.argv.slice(2));
139
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
@@ -0,0 +1,74 @@
1
+ # Candidate hosts — what Codex, Cursor and Gemini CLI actually offer
2
+
3
+ *2026-09-05. Item 6 of the host-gap review. A docs check, not a design: each tool's
4
+ own reference was read for the three surfaces Geneseed emits — an instructions
5
+ carrier, skills, and hooks — so that "add host X" can be costed without re-reading
6
+ the docs. Nothing here is implemented. Sources: the Codex CLI repo
7
+ (`codex-rs/hooks`, `codex-rs/core/src/agents_md.rs`), `cursor.com/docs/{rules,skills,hooks}`,
8
+ `geminicli.com/docs/{hooks,cli/gemini-md,cli/tutorials/skills-getting-started}`.*
9
+
10
+ ## The finding that changes the cost
11
+
12
+ **`.agents/skills/` is a cross-tool convention.** Cursor loads skills from
13
+ `.agents/skills/` and `~/.agents/skills/` (and, for compatibility, from `.claude/skills/`
14
+ and `.codex/skills/`); Codex from `~/.codex/skills` (`$CODEX_HOME/skills`) and
15
+ `.agents/skills`; Gemini CLI from `.gemini/skills` **or the `.agents/skills` alias**. One
16
+ `skills/` emit into `.agents/` therefore reaches all three, byte-identical to what every
17
+ current host gets. Skills are the largest surface by file count and they cost nothing new.
18
+
19
+ ## Per-host contracts
20
+
21
+ | Surface | Codex CLI | Cursor | Gemini CLI |
22
+ | --- | --- | --- | --- |
23
+ | **Instructions** | `AGENTS.md` at the repo root (and nested), plus a global `~/.codex/AGENTS.md` that no size cap touches | `AGENTS.md` at the root and in subdirectories, combined; also `.cursor/rules/*.mdc` | `GEMINI.md` by default; `context.fileName` in `settings.json` accepts a list, e.g. `["AGENTS.md", "GEMINI.md"]` |
24
+ | **Skills** | `~/.codex/skills`, `.agents/skills` — `SKILL.md` + frontmatter | `.cursor/skills`, `.agents/skills`, `~/.cursor/skills`, `~/.agents/skills`, plus `.claude/skills` and `.codex/skills` for compatibility | `.gemini/skills` or `.agents/skills`; `/skills list`, `/skills reload` |
25
+ | **Hook config** | `hooks.json` in a config layer's folder, or a `[hooks]` table in `config.toml` (same schema, capitalised event names) | `.cursor/hooks.json` (project) and `~/.cursor/hooks.json` (user); plugins may ship `hooks/hooks.json` | `settings.json` `hooks` object (project `.gemini/`, user `~/.gemini/`) |
26
+ | **Hook shape** | **Claude-shaped**: event → `[{matcher, hooks: [{type: "command", command, timeout}]}]`; 11 events incl. `PreToolUse`, `PostToolUse`, `PermissionRequest`, `PreCompact`, `PostCompact`, `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `SubagentStart`, `SubagentStop`, `Stop` | event → `[{command, matcher?}]`; events incl. `sessionStart`, `preToolUse`, `beforeShellExecution`, `beforeReadFile`, `afterFileEdit`, `stop`, `sessionEnd` | **Claude-shaped groups, Gemini names**: `SessionStart`, `BeforeTool`, `AfterTool`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `PreCompact`, `SessionEnd`; matchers are regexes over tool names |
27
+ | **Gate payload** | `tool_name` / `tool_input` (Claude's field names) | `tool_name` / `tool_input`, `cwd`, `tool_use_id` | `tool_name` / `tool_input`, `session_id`, `transcript_path`, `cwd`, `hook_event_name` |
28
+ | **Gate verdict** | `hookSpecificOutput.permissionDecision`: **`allow` / `deny` / `ask`** — the ask tier exists | `permission: "allow" \| "deny"` + `user_message` / `agent_message`; **no ask** | `decision: "deny"` + `reason`, or exit code 2 with stderr as the reason; `systemMessage` shows the user a line; **no ask** |
29
+ | **Context injection** | `SessionStart` → `additionalContext` (Claude form) | `sessionStart` → `additional_context` (snake_case) | `SessionStart` → `hookSpecificOutput.additionalContext`; plain stdout is not read |
30
+ | **Learn point** | `Stop` / `SubagentStop` / `PreCompact`, transcript on stdin as on Claude | `stop` (payload not checked for a transcript path) | `AfterAgent` (turn end) and `PreCompact`; `transcript_path` is in the base schema |
31
+ | **Tool names for matchers** | Claude's (`Bash`, `Write`, `Edit`) | `Shell`, `Read`, `Write`, MCP, `Task` (generic `preToolUse`) | `run_shell_command` (`command`), `write_file` (`file_path`, `content`), `replace` (`file_path`, `old_string`, `new_string`) |
32
+ | **Subagents** | `SubagentStart`/`SubagentStop` exist; custom agents not checked | Plugins carry `agents/` | Local and remote subagents (`/agents list`, `enable`, `disable`) |
33
+
34
+ ## What each would cost, against what exists
35
+
36
+ - **Codex CLI — cheapest, and the only one with an ask tier.** Its hook schema is
37
+ Claude's with `hooks.json`/`config.toml` as the carrier, its payload field names are
38
+ Claude's, and `permissionDecision: "ask"` exists. The Claude engine with `host: "codex"`
39
+ covers instructions (`AGENTS.md`), skills (`.agents/skills`) and the four hook verbs
40
+ unchanged; the only new code is the carrier writer (a `hooks.json` merge or a TOML
41
+ table) and the config-dir resolver (`~/.codex`, `$CODEX_HOME`). Roughly the Copilot
42
+ port minus the dialect work.
43
+
44
+ - **Gemini CLI — Claude's groups, Gemini's names.** The hook groups are Claude's
45
+ `event → [{matcher, hooks}]` and the payload fields are Claude's, but the events are
46
+ `BeforeTool`/`AfterAgent` and the verdict is `decision: "deny"` or a `systemMessage`.
47
+ Bob Shell shares Gemini's *config* shape (`context.fileName`, `tools.allowed:
48
+ ["run_shell_command(git)"]`), but Bob's documented *hook* protocol is its own —
49
+ Claude's event names, refusal by exit code 2; see `adapters/bob/README.md` — so the two
50
+ are not one dialect. Instructions need one settings key (`context.fileName` to include
51
+ `AGENTS.md`) or a `GEMINI.md` carrier; skills go to `.agents/skills`. No ask tier: Laws
52
+ I/IV deny, the consent rules become a `systemMessage`.
53
+
54
+ - **Cursor — a third verdict dialect.** `AGENTS.md` and `.agents/skills` are free; hooks
55
+ are `hooks.json` with a `{command}` list per event and a `permission: allow|deny`
56
+ verdict, `additional_context` in snake_case for `sessionStart`. Same block-or-warn stance
57
+ as Copilot, one more output spelling in `js/hosts/hooks.mjs`, one more carrier writer.
58
+ Cursor also reads `.claude/skills/`, so a Claude project install already gives a Cursor
59
+ user the skills.
60
+
61
+ ## Recommendation
62
+
63
+ If a fifth host is wanted, Codex first: it reuses the most and it is the only candidate
64
+ where the gates can *ask* rather than block. Before any of them, a `--emit agents` for
65
+ the shared `.agents/skills/` folder would give Codex, Cursor and Gemini users the whole
66
+ skill catalogue for the price of one directory name, with no host-specific code at all.
67
+ A Gemini host would be the fourth verdict spelling in `js/hosts/hooks.mjs` (after Claude's
68
+ ask, Copilot's block, Bob's exit code 2), plus a resolver and a carrier.
69
+
70
+ ## Not verified
71
+
72
+ None of the three CLIs is installed on the reviewing machine. Every row above is from the
73
+ tool's own documentation as indexed on 2026-09-05; the "learn point" column for Cursor and
74
+ the Codex custom-agent surface were not checked further than noted.
package/docs/extending.md CHANGED
@@ -215,11 +215,12 @@ a defect.
215
215
 
216
216
  Steps 1 and 9 of §2c, plus `LAW_META` if the Principle line moved. Two cautions:
217
217
 
218
- - `--footprint` defaults to **lean**, and lean keeps only the heading plus the first sentence
219
- (`terseBlocks` in `js/build/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.
218
+ - `--footprint` defaults to **lean**, and a law's lean text is the authored `LEAN:else` half of
219
+ its block in `src/laws/universal.md` (`resolveLean` in `js/build/render.mjs`). Amend **both**
220
+ halves, or the amendment does not exist for most installs. A doctrine rule is different: it is
221
+ still machine-cut to its heading plus first sentence (`terseBlocks`), so a clause in its second
222
+ paragraph is invisible in the default build. The on-disk `laws/` and `ontology/` copies always
223
+ render at full, whatever the footprint.
223
224
 
224
225
  ### 2c — Adding an INVARIANT
225
226
 
@@ -4,10 +4,11 @@ What a deployed harness costs in context-window tokens, per host. Token counts a
4
4
  chars/4 estimates and shift a little with theme and version.
5
5
 
6
6
  **Treat every number below as a floor, not a reading.** They were measured on a
7
- `--footprint full` build of the neutral theme when the tree carried 41 skills and 17
8
- agents; it carries more skills than that now, and nothing re-measures this page
9
- automatically. For a live figure on your own install, ask the agent for the
10
- `token-report` skill — that is what it is for.
7
+ `--footprint lean` build (the default) of the neutral theme when the tree carried
8
+ 47 skills and 17 agents, and nothing re-measures this page automatically. For a
9
+ live figure on your own install, ask the agent for the `token-report` skill —
10
+ that is what it is for. A `--footprint full` build runs roughly 5k tokens
11
+ heavier on the root file.
11
12
 
12
13
  ## Always-on cost per session
13
14
 
@@ -15,40 +16,50 @@ The context every host injects at session start, before you type anything:
15
16
 
16
17
  | Component | Claude Code | OpenCode | Bob | Copilot |
17
18
  |---|---|---|---|---|
18
- | Root instruction file | ~10.7k (`CLAUDE.md`) | ~10.6k (`AGENT.md`) | ~10.7k (`AGENTS.md`) | ~10.7k (`AGENTS.md`) |
19
- | Skill metadata (name + description) | ~1.6k | ~1.6k | ~1.6k | ~1.6k |
20
- | Agent metadata | ~0.45k | ~0.55k | ~0.45k | ~0.45k |
19
+ | Root instruction file | ~6.4k (`CLAUDE.md`) | ~6.4k (`AGENT.md`) | ~8.3k (`AGENTS.md`) | ~8.3k (`AGENTS.md`) |
20
+ | Skill metadata (name + description) | ~1.7k | ~1.7k | ~1.7k | ~1.7k |
21
+ | Agent metadata | ~0.5k | ~0.55k | ~0.5k | ~0.5k |
21
22
  | Eager memory/notebook injection | ~1.2k (SessionStart hook) | ~1.2k (context plugin) | ~1.2k (SessionStart hook) | — (no hooks; read on demand) |
22
- | **Total** | **~14k** | **~14k** | **~14k** | **~12.8k** |
23
+ | **Total** | **~9.8k** | **~9.9k** | **~11.7k** | **~10.5k** |
23
24
 
24
- The emits are at parity by design: ~14k tokens, about 7% of a 200k
25
- window (Copilot runs slightly lighter because it has no hook mechanism — the
26
- memory/notebook indexes load when the agent reads them, not eagerly). The
25
+ The emits are at parity by design: ~10–12k tokens, about 5–6% of a 200k
26
+ window. Bob and Copilot carry the §3/§4 catalogue tables inline because their
27
+ hosts expose no native skill/agent inventory; Claude Code and OpenCode ship a
28
+ pointer to the host's own inventory instead, ~1.9k tokens lighter. Copilot also
29
+ runs without eager injection — it has no hook mechanism, so the
30
+ memory/notebook indexes load when the agent reads them, not eagerly. The
27
31
  eager-injection path is budget-capped identically everywhere
28
32
  (16 KB per file, 48 KB total ≈ 12k tokens ceiling), so growing Memory degrades
29
33
  every host the same way instead of one silently falling behind.
30
34
 
31
35
  ## Where the tokens go
32
36
 
33
- - **The root file is ~76% of the bill**, and the constitution inside it is the
37
+ - **The root file is ~65% of the bill**, and the constitution inside it is the
34
38
  bulk of that — no longer one section but three: the Ontology, the nine Rules,
35
39
  and the doctrine packs this install built in. **Two** levers move it, not one.
36
- The [footprint dial](#/docs/footprint) (`lean`, the default, keeps each Rule
37
- and each doctrine rule as its heading plus first line, and ships the Ontology
38
- *whole*; `full` inlines every rationale). And the
40
+ The [footprint dial](#/docs/footprint) (`lean`, the default, swaps each Rule,
41
+ the Ontology and the longest prose sections — the preamble, §6 Notebook, §8
42
+ Wiki — for their hand-written condensations, and cuts each doctrine rule to its
43
+ heading plus first line; `full` inlines every rationale). And the
39
44
  [doctrine packs](#/docs/setup-choices) — `geneseed-build --doctrines craft,rigor`
40
45
  drops whole blocks of §2 at build time, which removes far more than the
41
46
  footprint dial trims. Anything else is noise.
42
47
 
43
- The three-tier split grew the root file: measured on `neutral`, `AGENT.md`
44
- went from 31,457 bytes to 36,882 at `lean` (+17.2%) and 53,378 to 54,810 at
45
- `full` (+2.7%). The lean growth is the larger one *by design* — the Ontology
46
- is prose, and truncating four flowing sections to four orphan sentences would
47
- have shipped a worldview nobody could read.
48
+ The lean condensations are authored, not generated. An earlier design shipped
49
+ the Ontology *whole* under `lean` on the grounds that truncating four flowing
50
+ sections to four orphan sentences would ship a worldview nobody could read —
51
+ the `LEAN:` blocks answer that by hand-writing the short form instead
52
+ (measured on `neutral`, the root file at `lean` went from 29,966 bytes to
53
+ 25,614 with them).
48
54
  - **Skill bodies (~53k) and agent bodies (~10k) are lazy** on every host —
49
55
  loaded only when invoked. A typical skill costs ≤3k per invocation; the
50
56
  heaviest (`react-view-transitions`, ~17.5k with its reference files) loads
51
57
  its references progressively.
58
+ - **User-only skills leave the catalogue.** A skill whose source carries
59
+ `<!-- invocation: user -->` renders `disable-model-invocation: true`; Claude
60
+ Code and Bob then drop its description from the always-on skill metadata and
61
+ only `/name` opens it. Seven skills carry it today (the five teaching drills,
62
+ `herdr`, `opencode-theme`), roughly a seventh of the ~1.7k skill-metadata line.
52
63
  - Plugin JavaScript (OpenCode, ~26k on disk) runs in the host runtime and
53
64
  never enters the context window.
54
65
 
@@ -73,7 +84,7 @@ token counter also includes, none of which Geneseed controls:
73
84
  per-repo install, a personal `~/.claude/CLAUDE.md`, or MCP servers each add
74
85
  their own share.
75
86
 
76
- So a ~34k session on a work repo is normal arithmetic: ~14k harness +
87
+ So a ~30k session on a work repo is normal arithmetic: ~10k harness +
77
88
  ~10k repo docs + ~5–10k host overhead. Set `GENESEED_DEBUG=1` (OpenCode) to
78
89
  log exactly which files the context plugin injected, or run the SessionStart
79
90
  command by hand to see the hook's payload.
package/docs/web-ui.md CHANGED
@@ -51,7 +51,7 @@ The left rail mirrors the harness's own shape:
51
51
  | Group | Views |
52
52
  | --- | --- |
53
53
  | *(ungrouped)* | Dashboard |
54
- | **📖 Codex** | Constitution · Rules · Profile · Skills · Agents · Library · Docs · Graph |
54
+ | **📖 Codex** | Constitution · Rules · Profile · Skills · Agents · Library · Docs |
55
55
  | **🩺 Care** | Activity · Changes · Doctor |
56
56
  | **🎨 Setup** | Harness · Settings |
57
57
 
@@ -72,9 +72,10 @@ streams the output of background jobs and keeps their history across reloads.
72
72
  ### 🧬 Harness
73
73
 
74
74
  - **Dashboard** (`#/`) — a live readout of what's deployed. Three lenses: **Status**
75
- (readiness ring, a KPI band, a genome grid, recent activity), **Lineage** (a mini
76
- cross-link graph), and **Operator** (a searchable table of every deployed capability).
77
- Headlines reflect the active theme's voice.
75
+ (readiness ring, a KPI band, a genome grid, recent activity), **Lineage** (the
76
+ source → render → deployed timeline and the genome-by-volume strand chart), and
77
+ **Operator** (a searchable table of every deployed capability). Headlines reflect the
78
+ active theme's voice.
78
79
 
79
80
  The KPI band reads **Agents · Skills · Invariants · Local edits**. That third tile is
80
81
  labelled *Invariants* and not *Constitution* on purpose: its value is the invariant count
@@ -107,15 +108,6 @@ streams the output of background jobs and keeps their history across reloads.
107
108
  **Promote to rule** control turns a recurring lesson into a trial rule in
108
109
  `user-rules.md` — provenance recorded, the source fact deleted so it isn't loaded
109
110
  twice — landing you on the Rules page.
110
- - **Graph** (`#/graph`) — the cross-link constellation across agents, skills and the
111
- **invariants only**: every `[[wikilink]]` between agents/skills, plus every `Rule N`
112
- mention that lands on a real invariant. ⚠ The reference pattern matches the tier noun
113
- followed by a **Roman numeral** — `Rule`, `Law`, or whatever the active theme calls one,
114
- which is how `Dictate III` still resolves under `imperial`. A doctrine address is
115
- `<pack> <n>` and matches nothing, so **doctrine cross-links are not drawn**; the graph is
116
- a view of the invariant backbone, not of the whole constitution. Hover to isolate a
117
- neighbourhood, scroll to zoom, drag to pan, search to highlight; orphans dim out. Click a
118
- node to open its spec.
119
111
 
120
112
  ### 📚 Learn
121
113
 
@@ -2,5 +2,5 @@
2
2
  "_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
3
3
  "theme": "neutral",
4
4
  "product": "Geneseed",
5
- "version": "3.1.3"
5
+ "version": "3.3.0"
6
6
  }
package/js/README.md CHANGED
@@ -13,7 +13,7 @@ a module that does not exist, is a test failure. `geneseed doctor` reports the s
13
13
 
14
14
  ```
15
15
  bin/geneseed-cli.mjs the `geneseed` CLI — 20+ verbs, may NOT spawn
16
- bin/geneseed-hook.mjs the hook entry — 4 verbs, loads on EVERY tool call (~14 ms)
16
+ bin/geneseed-hook.mjs the hook entry — 5 verbs, loads on EVERY tool call (~14 ms)
17
17
  bin/build-driver.mjs `geneseed-build` — the generator's flags and emit targets
18
18
 
19
19
  js/build/ src/ + themes/ → rendered text → a bundle → a per-host install
@@ -66,7 +66,7 @@ user co-owns, and the hook verbs those emitted configs then run.
66
66
  | module | owns |
67
67
  |---|---|
68
68
  | `hosts.mjs` | The four host config dirs (opencode/claude/bob/copilot), plus `resolvePath`/`expanduser` |
69
- | `hooks.mjs` | The verbs the emitted hooks run every session: `context`, `git-gate`, `rule-gate`, `learn` |
69
+ | `hooks.mjs` | The verbs the emitted hooks run every session: `context`, `git-gate`, `rule-gate`, `tool-gate` (the two gates fused, for Copilot), `learn`. `--host` picks the verdict dialect |
70
70
  | `settings.mjs` | Merges into user-owned `settings.json`/`opencode.json`; owns the hook shim and the managed blocks |
71
71
  | `native.mjs` | Capability specs → host-native subagents and skills. The impure half of the emit |
72
72
  | `opencode.mjs` | OpenCode-only extras: colour themes, primary agent, `/commands`, plugins, overrides stub |
@@ -168,13 +168,12 @@ the deployed harness as JSON.
168
168
  | `daemon.mjs` | Detached web start/stop/status/restart, the `.geneseed-web.json` record, and `openUrl` |
169
169
  | `jobs.mjs` | `JobManager` and the action→argv table. The only module here that spawns job children |
170
170
  | `docs.mjs` | The Docs tab: the `docs/web/` page registry, `?harness=` filtering, the five page kinds |
171
- | `graph.mjs` | `/api/graph` alone — the agent/skill/law cross-link matrix built from rendered bodies |
172
171
  | `activity.mjs` | The `activity/` session files: pid-liveness prune, list, detail, and the on/off flag |
173
172
 
174
173
  **Before editing:** `makeHandler(state, jm, token, dist, holder)`'s arity is load-bearing — three
175
- test call sites construct one directly and drive it without a socket. Four of `routes.mjs`'s
176
- exported sets are deliberately EMPTY and must stay declared: each is half of a partition test.
177
- `graph.mjs` contains a literal NUL byte, so `grep`/`ripgrep` treat it as binary and skip it.
174
+ test call sites construct one directly and drive it without a socket. `routes.mjs`'s tables
175
+ and inline lists are held against a surface list written out in
176
+ `tests/unit/web_server.test.mjs` — growing or shrinking the API edits both, in one commit.
178
177
 
179
178
  ---
180
179
 
@@ -192,12 +191,3 @@ exported sets are deliberately EMPTY and must stay declared: each is half of a p
192
191
  | a console doc page | `docs/web/<id>.md` — the stem is the id | `docs/extending.md` §4c. Only `kind: "concept"` gets `{N_*}` substitution |
193
192
  | an OpenCode plugin | `adapters/opencode/plugins/geneseed-<name>.js` | `docs/extending.md` §4a — the `geneseed-` prefix is mechanically load-bearing |
194
193
  | to change a `js/lib/` primitive | `tests/fixtures/pure_probe.mjs` **first** | the docblock says which Node default it deliberately departs from — change that sentence too, or the next reader restores the default |
195
-
196
- ---
197
-
198
- ## Known debris
199
-
200
- * **`js/hosts/settings.mjs` carries a private second copy of Python's whitespace class**, duplicating
201
- `WHITESPACE` in `js/lib/text.mjs`. `text.mjs` is the owner.
202
- * **`web/src/pages/Skills.jsx`'s `SKILL_CATS` is a fourth, ungated copy of the skill taxonomy** —
203
- nothing compares it to `SKILL_CLASS`, and an unknown category renders as Build with no warning.