geneseed 3.13.0 → 3.14.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 (93) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/SHIPPED.md +1 -1
  3. package/adapters/opencode/plugins/geneseed-learn.js +10 -1
  4. package/bin/geneseed-cli.mjs +10 -14
  5. package/bin/geneseed-hook.mjs +22 -15
  6. package/docs/extending.md +5 -5
  7. package/harness.config.json +1 -1
  8. package/js/README.md +19 -10
  9. package/js/build/args.mjs +374 -0
  10. package/js/build/driver.mjs +137 -553
  11. package/js/build/emit-claude.mjs +2 -4
  12. package/js/build/generate.mjs +6 -3
  13. package/js/build/render.mjs +5 -9
  14. package/js/build/source.mjs +3 -11
  15. package/js/hosts/hooks-context.mjs +431 -0
  16. package/js/hosts/hooks-learn.mjs +275 -0
  17. package/js/hosts/hooks-prims.mjs +108 -0
  18. package/js/hosts/hooks.mjs +50 -977
  19. package/js/hosts/hosts.mjs +12 -4
  20. package/js/hosts/installs.mjs +5 -6
  21. package/js/hosts/mcp.mjs +12 -48
  22. package/js/hosts/memory-files.mjs +181 -0
  23. package/js/hosts/settings.mjs +135 -414
  24. package/js/hosts/shim.mjs +373 -0
  25. package/js/inspect/checks-authoring.mjs +21 -828
  26. package/js/inspect/checks-build.mjs +4 -4
  27. package/js/inspect/checks-constitution.mjs +493 -0
  28. package/js/inspect/checks-counts.mjs +242 -0
  29. package/js/inspect/checks-repo.mjs +17 -8
  30. package/js/inspect/diff.mjs +4 -1
  31. package/js/inspect/doctor.mjs +2 -2
  32. package/js/inspect/inventory.mjs +4 -4
  33. package/js/inspect/scan.mjs +1 -1
  34. package/js/inspect/status.mjs +1 -1
  35. package/js/inspect/validate.mjs +48 -35
  36. package/js/loop/catalog.mjs +5 -3
  37. package/js/loop/cli.mjs +26 -12
  38. package/js/loop/graph.mjs +1 -1
  39. package/js/loop/registry.mjs +10 -15
  40. package/js/loop/score.mjs +1 -1
  41. package/js/loop/state.mjs +35 -11
  42. package/js/maintain/memory.mjs +1 -1
  43. package/js/maintain/migrate.mjs +18 -30
  44. package/js/maintain/uninstall.mjs +47 -88
  45. package/js/ui/cli.mjs +12 -12
  46. package/js/web/actions.mjs +29 -19
  47. package/js/web/api.mjs +21 -21
  48. package/js/web/docs.mjs +29 -72
  49. package/package.json +1 -1
  50. package/web/dist/assets/Activity-CjRqZnr-.js +1 -0
  51. package/web/dist/assets/ActivityDetail-OQTxKBj2.js +1 -0
  52. package/web/dist/assets/Dashboard-J-xCe_Gk.js +2 -0
  53. package/web/dist/assets/Docs-CvOZ8akL.js +5 -0
  54. package/web/dist/assets/FilterInput-CVyqmYWM.js +1 -0
  55. package/web/dist/assets/GreenhouseView-6KDjX8da.js +2 -0
  56. package/web/dist/assets/Installs-MWuhNjSm.js +2 -0
  57. package/web/dist/assets/JournalView-C8MrOz2c.js +1 -0
  58. package/web/dist/assets/Laws-Du0-Zyp2.js +3 -0
  59. package/web/dist/assets/Library-DOkO0TtB.js +1 -0
  60. package/web/dist/assets/Loops-Dv_aLByJ.js +2 -0
  61. package/web/dist/assets/{Markdown-CdTX0kLe.js → Markdown-X2a3qvOh.js} +11 -11
  62. package/web/dist/assets/OperatorHudView-BPP1dtLz.js +2 -0
  63. package/web/dist/assets/Personal-26VPYkK0.js +1 -0
  64. package/web/dist/assets/Tabs-BzKuFskD.js +1 -0
  65. package/web/dist/assets/activity-Dr7ZMBI3.js +1 -0
  66. package/web/dist/assets/{drawable-C_x5xncq.js → drawable-zVVDbsyw.js} +1 -1
  67. package/web/dist/assets/index-Ctp-4poP.css +1 -0
  68. package/web/dist/assets/index-KHt3wE_K.js +2 -0
  69. package/web/dist/assets/{react-Bbzhmq4v.js → react-B9Z_tXRV.js} +1 -1
  70. package/web/dist/assets/sections-GalMS0Zv.js +1 -0
  71. package/web/dist/assets/usePoll-DD8VjtSg.js +1 -0
  72. package/web/dist/index.html +4 -5
  73. package/web/src/pages/Laws.jsx +0 -1
  74. package/web/dist/assets/Activity-Ca2Aruu7.js +0 -1
  75. package/web/dist/assets/ActivityDetail-Bo7v4Z_r.js +0 -1
  76. package/web/dist/assets/Dashboard-BuMq46aT.js +0 -2
  77. package/web/dist/assets/Docs-BzccCllG.js +0 -5
  78. package/web/dist/assets/FilterInput-CBcJBQPI.js +0 -1
  79. package/web/dist/assets/GreenhouseView-BCIKcnGG.js +0 -2
  80. package/web/dist/assets/Installs-DMFJvq7s.js +0 -2
  81. package/web/dist/assets/JournalView-o1rmd1_1.js +0 -1
  82. package/web/dist/assets/Laws-BANxq_I_.js +0 -3
  83. package/web/dist/assets/Library-DW4x0fbh.js +0 -1
  84. package/web/dist/assets/Loops-DEBibbBP.js +0 -2
  85. package/web/dist/assets/OperatorHudView-CLD0fYaA.js +0 -2
  86. package/web/dist/assets/Personal-6uMJvyBG.js +0 -1
  87. package/web/dist/assets/Tabs-BubnyZhF.js +0 -1
  88. package/web/dist/assets/activity-CObqyxkL.js +0 -1
  89. package/web/dist/assets/index-CMPgezFm.js +0 -2
  90. package/web/dist/assets/index-DMsT4Aa6.css +0 -1
  91. package/web/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
  92. package/web/dist/assets/sections-Dg23Hel5.js +0 -1
  93. package/web/dist/assets/usePoll-BOqeunzF.js +0 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,45 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [3.14.0] — 2026-10-04
12
+
13
+ **Upgrading:** re-emit (`geneseed rebuild-all`) so installed hooks pick up the gate fixes and the
14
+ per-verb hook modules; then `geneseed web restart`.
15
+
16
+ ### Security
17
+ - **Sealed Secrets gate** — `sk-ant-` keys containing `_` (base64url, most real keys) no longer
18
+ slip past; `MultiEdit` (`edits[].new_string`) and `NotebookEdit` (`notebook_path`/`new_source`)
19
+ are now scanned like `Write`/`Edit`.
20
+ - **Destructive-git gate** — catches `git push -f`, `git push origin +main`, `git reset -q --hard`
21
+ and `git clean -d -f` (flags anywhere after the verb); `LAW_IV_BASH` mirrors it.
22
+ - **Memory names** — a model-written memory `name:` that is not a plain slug is refused instead
23
+ of becoming a path outside the memory dir (Claude `learn` hook and the OpenCode learn plugin).
24
+ - **Uninstall** — manifest paths are contained to the install dir before unlinking (as deactivate
25
+ already did); empty-directory pruning stops at the base.
26
+
27
+ ### Fixed
28
+ - `--exclude-rules` is carried through every re-render: `geneseed diff` no longer reports drift on
29
+ an install with excluded rules, console Restore and `geneseed migrate` no longer re-admit them,
30
+ and `validate` honours every render axis. Unknown ids in `harness.config.json` now warn and are
31
+ dropped instead of breaking every later rebuild.
32
+ - Claude `settings.json` that is not a JSON object, or unreadable, is refused instead of
33
+ overwritten; every JSON config write is atomic.
34
+ - Loop: a malformed `record --card` is refused before it can wedge the loop; `check` resolves
35
+ bricks from the git root; `next` no longer rewrites `LOOP.md`; a clean tree is recognised.
36
+ - Console: the Docs host selector follows the deployed install instead of sticking to OpenCode;
37
+ the About page shows the installed and source fingerprints; the outline strips inline markdown.
38
+ - `theme --transparent-only` names the file it wrote; `validate --help` prints its own usage.
39
+ - Doctor's module-map check keys by folder, so twin module names can no longer hide a missing row.
40
+ - `learn` reports on stderr when it cannot start the model CLI.
41
+
42
+ ### Changed
43
+ - The hook binary loads one verb's module per call (`hooks.mjs` gates, `hooks-context.mjs`,
44
+ `hooks-learn.mjs`): gate cold import ~16 → ~10 ms, on every tool call. The memory-file helpers
45
+ live in the no-spawn `memory-files.mjs`, so the CLI no longer reaches `child_process`.
46
+ - Internal splits: `js/hosts/shim.mjs`, `js/build/args.mjs`, `checks-constitution.mjs`,
47
+ `checks-counts.mjs`; one JSON-object reader and one `CLAUDE_STYLE` host list; dead code and
48
+ Python-era comments removed.
49
+
11
50
  ## [3.13.0] — 2026-10-04
12
51
 
13
52
  **Upgrading:** re-emit (`geneseed rebuild-all`) so the installed `loop` skill knows the new
package/SHIPPED.md CHANGED
@@ -20,7 +20,7 @@ historical references, 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.13.0**.
23
+ > string. Current label: **3.14.0**.
24
24
 
25
25
  ## Capabilities
26
26
 
@@ -251,6 +251,11 @@ function parseFrontmatter(chunk) {
251
251
  return fm
252
252
  }
253
253
 
254
+ // A memory `name:` becomes a FILENAME and the model writes it, so it is a plain slug or
255
+ // nothing — no separator, no `..`, no drive. The same rule as MEMORY_SLUG_RE in
256
+ // js/hosts/hooks.mjs, inlined because this file ships standalone.
257
+ const MEMORY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/
258
+
254
259
  async function writeMemories(output, memDir, existing) {
255
260
  const written = []
256
261
  const indexLines = []
@@ -259,7 +264,7 @@ async function writeMemories(output, memDir, existing) {
259
264
  if (!chunk || chunk.toUpperCase() === "NOTHING") continue
260
265
  const fm = parseFrontmatter(chunk)
261
266
  const name = (fm.name || "").trim()
262
- if (!name || existing.has(name)) continue
267
+ if (!MEMORY_SLUG_RE.test(name) || existing.has(name)) continue
263
268
  await fs.writeFile(path.join(memDir, `${name}.md`), chunk.replace(/\n+$/, "") + "\n", "utf8")
264
269
  existing.add(name)
265
270
  written.push(name)
@@ -393,4 +398,8 @@ export const GeneseedLearn = async ({ client }) => {
393
398
  }
394
399
  }
395
400
 
401
+ // OpenCode treats every export as a plugin and rejects non-functions; hang the test
402
+ // helpers off the factory instead — reachable via import, invisible to the loader.
403
+ Object.assign(GeneseedLearn, { writeMemories })
404
+
396
405
  export default GeneseedLearn
@@ -58,7 +58,7 @@
58
58
  import '../js/lib/node-floor.mjs';
59
59
  import { cliSpec, printHelp, printVerbList } from '../js/ui/cli.mjs';
60
60
  import { parseIntStrict } from '../js/lib/text.mjs';
61
- import { printErr } from '../js/lib/fs.mjs';
61
+ import { printErr, printOut } from '../js/lib/fs.mjs';
62
62
 
63
63
  /**
64
64
  * The verbs this entry answers — and, since P10c, NOTHING ELSE ABOUT THEM.
@@ -302,7 +302,7 @@ async function main(argv) {
302
302
  // that binary is never reached, and its import graph is paid on every tool call.
303
303
  if (process.env.GENESEED_NO_SHIM_CHECK !== '1' && verb !== 'help'
304
304
  && !argv.some((t) => t === '-h' || t === '--help')) {
305
- const { shimDead } = await import('../js/hosts/settings.mjs');
305
+ const { shimDead } = await import('../js/hosts/shim.mjs');
306
306
  const dead = shimDead();
307
307
  if (dead.length) {
308
308
  printErr(`[geneseed] ⚠ hooks are off on this machine: the hook shim points at `
@@ -311,25 +311,21 @@ async function main(argv) {
311
311
  }
312
312
  }
313
313
  // `validate` — the generator's `--validate-only`, and the one verb this entry answers that is NOT
314
- // in the table above. It is dispatched here, before the lookup, for a reason that is a
315
- // property of the gates rather than a preference:
316
- //
317
- // * `VERBS` is asserted to be the twin of `rituals/harness.py`'s SUBPARSERS
318
- // (`test_every_entry_verb_is_a_real_harness_subcommand`), and `--validate-only` is a
319
- // GENERATOR flag — `harness.py` has never had a `validate` subcommand to be a twin of;
320
- // * `test_the_matrix_covers_every_verb_it_claims` demands a recorded cell group per table
321
- // row, and a cell has an `lf` half this host cannot produce.
322
- //
323
- // So a row would have meant amending two gates and recording a corpus half blind. It takes
324
- // the generator's own parser (see `parseDriverArgs`) because its flags are the generator's,
314
+ // in the table above. It is dispatched here, before the lookup, because its flags are the
315
+ // GENERATOR's, not a table row's: it takes the generator's own parser (see `parseDriverArgs`),
325
316
  // and it is on THIS binary — not on `bin/build-driver.mjs` — because it runs the doctor, which
326
317
  // starts a process the driver is banned from reaching.
327
318
  if (verb === 'validate') {
328
319
  // Lazy for the same reason the table rows are — `parseDriverArgs` alone drags the whole
329
320
  // generator driver in, and every OTHER verb was paying for it.
330
- const [{ cmdValidate }, { parseDriverArgs }] = await Promise.all([
321
+ const [{ cmdValidate, VALIDATE_USAGE }, { parseDriverArgs }] = await Promise.all([
331
322
  import('../js/inspect/validate.mjs'), import('../js/build/driver.mjs'),
332
323
  ]);
324
+ // Before the borrowed parser, whose own `-h` prints the GENERATOR's usage.
325
+ if (argv.slice(1).some((t) => t === '-h' || t === '--help')) {
326
+ printOut(VALIDATE_USAGE);
327
+ return 0;
328
+ }
333
329
  try {
334
330
  return cmdValidate(parseDriverArgs(argv.slice(1)));
335
331
  } catch (e) {
@@ -27,9 +27,10 @@
27
27
  * component — so the last driver to emit anything on a machine owns every install's hooks,
28
28
  * including installs the other driver wrote. That is safe exactly while the two entry
29
29
  * points answer the same verbs identically, which is not a hope:
30
- * `test_the_entry_carries_exactly_the_verbs_the_emitter_wires` pins the verb set to the
31
- * hooks the emitter wires, and `tests/harness_golden.py` runs this binary as a process
32
- * against the Python one on stdout, stderr, exit code and every file written.
30
+ * `tests/unit/hook_cli.test.mjs`'s "the hook entry carries exactly the verbs the emitter wires"
31
+ * pins the verb set to the hooks the emitter wires, and `node tests/golden.mjs --cli` runs
32
+ * this binary as a process with absolute assertions on stdout, stderr, exit code and every file
33
+ * written.
33
34
  *
34
35
  * The verb set is deliberately SMALL and refuses the rest by name. `harness.py` has 24
35
36
  * subparsers (25 invocable names — `update` is an alias of `upgrade`); a hook entry that
@@ -39,22 +40,29 @@
39
40
  */
40
41
  // FIRST, before anything it could fail to run: an old Node gets one sentence, not a stack trace.
41
42
  import '../js/lib/node-floor.mjs';
42
- import { cmdContext, cmdGitGate, cmdRuleGate, cmdToolGate, cmdLearn } from '../js/hosts/hooks.mjs';
43
- // Not a new module on the hot path: `js/hosts/hooks.mjs` above already imports
44
- // `js/lib/fs.mjs`, so this edge names a file the process has already loaded.
43
+ // Not a new module on the hot path: every verb's module imports `js/lib/fs.mjs` too, so this
44
+ // edge only loads early a file the process would load anyway.
45
45
  import { printErr } from '../js/lib/fs.mjs';
46
46
 
47
47
  // `--host` selects the verdict dialect (see js/hosts/hooks.mjs's header); the emitter bakes
48
48
  // it into the command for every host that is not Claude Code, so the hook never has to guess
49
49
  // its reader from the payload's shape.
50
+ //
51
+ // `load` is a DYNAMIC import per verb, so a process parses only its own verb's module: the gates
52
+ // run on every tool call and must not pay for `context`'s discovery or `learn`'s spawn. The
53
+ // specifiers stay literal strings — `tests/unit/hook_cli.test.mjs`'s import walk reads them.
50
54
  const HOSTED = { '--root': 'root', '--host': 'host' };
55
+ const gates = () => import('../js/hosts/hooks.mjs');
51
56
  const VERBS = {
52
- context: { fn: cmdContext, flags: HOSTED },
53
- 'git-gate': { fn: cmdGitGate, flags: HOSTED, switches: { '--no-consent': 'noConsent' } },
54
- 'rule-gate': { fn: cmdRuleGate, flags: HOSTED },
55
- 'tool-gate': { fn: cmdToolGate, flags: HOSTED },
57
+ context: { load: () => import('../js/hosts/hooks-context.mjs'), fn: 'cmdContext', flags: HOSTED },
58
+ 'git-gate': {
59
+ load: gates, fn: 'cmdGitGate', flags: HOSTED, switches: { '--no-consent': 'noConsent' },
60
+ },
61
+ 'rule-gate': { load: gates, fn: 'cmdRuleGate', flags: HOSTED },
62
+ 'tool-gate': { load: gates, fn: 'cmdToolGate', flags: HOSTED },
56
63
  learn: {
57
- fn: cmdLearn,
64
+ load: () => import('../js/hosts/hooks-learn.mjs'),
65
+ fn: 'cmdLearn',
58
66
  flags: { '--memory': 'memory' },
59
67
  switches: { '--consolidate': 'consolidate' },
60
68
  positional: 'file',
@@ -62,7 +70,7 @@ const VERBS = {
62
70
  };
63
71
 
64
72
  function die(code, msg) {
65
- // CRLF on Windows, for the same reason `js/hosts/hooks.mjs`'s funnels translate: argparse
73
+ // CRLF on Windows, for the same reason the hook verbs' funnels translate: argparse
66
74
  // writes this line through `sys.stderr`, which does. `printErr` owns the `\n`-to-`os.linesep` rule.
67
75
  printErr(`geneseed-hook: error: ${msg}\n`);
68
76
  return code;
@@ -128,8 +136,7 @@ async function main(argv) {
128
136
  // argument. The four hook verbs are four of the reference's 26 and had the same gap.
129
137
  // `js/ui/cli.mjs` is imported HERE, not at the top: a static import cost every tool call
130
138
  // of every session a module parse (~3 ms of an ~12 ms controllable budget) for a branch
131
- // only a human at a terminal ever takes. The hook path never awaits it, so `main` staying
132
- // async-shaped costs the hot verbs nothing.
139
+ // only a human at a terminal ever takes.
133
140
  if (argv.slice(1).some((t) => t === '-h' || t === '--help')) {
134
141
  const { printHelp } = await import('../js/ui/cli.mjs');
135
142
  const rc = printHelp('geneseed-hook', verb);
@@ -137,7 +144,7 @@ async function main(argv) {
137
144
  }
138
145
  const parsed = parse(spec, argv.slice(1));
139
146
  if (parsed.error) return die(2, parsed.error);
140
- return spec.fn(parsed.args);
147
+ return (await spec.load())[spec.fn](parsed.args);
141
148
  }
142
149
 
143
150
  main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
package/docs/extending.md CHANGED
@@ -336,7 +336,7 @@ render — no citation moves, because none names a number. Then:
336
336
  3. Its `LEX_<ID>`/`DOC_<ID>` key in all fifteen theme files (the equality arm names each).
337
337
  4. The number-keyed tables, which the doctor checks against the canon: `LAW_CLASS`
338
338
  (`js/inspect/inventory.mjs`), `LAW_META`/`DOCTRINE_META` (their id pins fail on every shifted
339
- row), and — only if a hook-gated rule moved — `HOOK_PINNED` in `checks-authoring.mjs` names the
339
+ row), and — only if a hook-gated rule moved — `HOOK_PINNED` in `checks-constitution.mjs` names the
340
340
  ledger keys, `CONSENT_RULE` and `HOOK_GATED` to move with it. `--exclude-rules` addresses are
341
341
  `<pack>.<n>` too, and an install that excluded a shifted doctrine rule excludes its neighbour
342
342
  after a re-emit: prefer appending in a pack, and say so in the CHANGELOG when you cannot.
@@ -391,9 +391,10 @@ Two traps worth knowing before you write the spec:
391
391
  re-opens bash on all three hosts (OpenCode gates it to `ask`), and `<!-- webfetch: allow -->`
392
392
  re-opens web fetch — the researcher is the one spec that carries it.
393
393
  - **The console has a fourth, ungated copy of the skill taxonomy.** `SKILL_CATS` in
394
- `web/src/pages/Skills.jsx:19` mirrors `SKILL_CLASS` with nothing comparing them (contrast
394
+ `web/src/lib/lawCats.js` mirrors `SKILL_CLASS` with nothing comparing them (contrast
395
395
  `LAW_META`, which *is* cross-checked). Inventing a new *category* means editing that file —
396
- which drags in the `web/dist` rebuild — and an unknown category renders as Build with no warning.
396
+ which drags in the `web/dist` rebuild — and an unknown category lands under Personal in the
397
+ Library (`splitSkills` in `web/src/pages/Library.jsx`) with no warning.
397
398
 
398
399
 
399
400
  ---
@@ -462,8 +463,7 @@ throws). The same file is what GitHub shows, so write for both
462
463
  passage for several hosts lists them, `<!--harness:claude,openclaude-->`, labelled
463
464
  `*(Claude Code and OpenClaude only)*` (names in the marker's order, joined by *and*).
464
465
  - No `{N_*}` token in `understand/` or `guides/` — GitHub would show it raw.
465
- - `kind: "markdown"` pulls from a repo file via `source:`, traversal-guarded, optionally sliced to
466
- one section with `anchor:` + `slice: true`.
466
+ - `kind: "markdown"` pulls from a repo file via `source:`, traversal-guarded.
467
467
 
468
468
  Inline `<!-- harness:claude -->` blocks strip per host and **fail open** on an unbalanced marker —
469
469
  the page renders, wrong, with nothing red.
@@ -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.13.0"
5
+ "version": "3.14.0"
6
6
  }
package/js/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `js/` — the module map
2
2
 
3
- Sixty-one modules in eight folders. This page is the address book: **what each one owns**, and
3
+ Sixty-six modules in eight folders. This page is the address book: **what each one owns**, and
4
4
  **which file to open first** for a given task. It is not documentation of behaviour — every module
5
5
  has a docblock for that, and the docblock is the thing to read before editing.
6
6
 
@@ -46,7 +46,8 @@ as a per-host install at project or global scope.
46
46
  | `emit-common.mjs` | Constants, tree walkers, and the three writers BOTH global emits share. Decides no host |
47
47
  | `stubs.mjs` | Write-once seed files: context, wiki, rules, excludes, profile, `.gitignore`, memory/notebook indexes |
48
48
  | `version.mjs` | The release marker: fingerprint the sources, read/compare/write an install's version, warn on downgrade |
49
- | `driver.mjs` | The generator driver: `--emit`/`--theme`/`--footprint` flag parsing, the nine emit targets, per-host emit orchestration, the prune and the manifest. `bin/build-driver.mjs` is only its entry |
49
+ | `driver.mjs` | The generator driver: the nine emit targets, per-host emit orchestration, the prune and the manifest. `bin/build-driver.mjs` is only its entry |
50
+ | `args.mjs` | The generator's flag surface: `harness.config.json` defaults, the `--emit`/`--theme`/`--footprint` parse tables and choices, `-h`, and `die` (a throw, so `rebuild-all` survives one bad install) |
50
51
  | `generate.mjs` | CLI verbs `build`, `prompt`, `theme`, `rebuild-all` — the thin face over `driver.mjs` |
51
52
  | `catalog.mjs` | `geneseed catalog` — prints the shipped roster. Classifies nothing itself |
52
53
  | `themes.mjs` | Maintainer-only `--sync-themes`: inserts `_TEMPLATE.json`'s missing keys into the committed themes |
@@ -67,19 +68,25 @@ user co-owns, and the hook verbs those emitted configs then run.
67
68
 
68
69
  | module | owns |
69
70
  |---|---|
70
- | `hosts.mjs` | The four host config dirs (opencode/claude/bob/openclaude), plus `resolvePath`/`expanduser` |
71
- | `hooks.mjs` | The verbs the emitted hooks run every session: `context`, `git-gate`, `rule-gate`, `tool-gate` (the two gates fused, for Bob), `learn`. `--host` picks the verdict dialect |
71
+ | `hosts.mjs` | The four host config dirs (opencode/claude/bob/openclaude), their `family` (`CLAUDE_STYLE`), plus `resolvePath`/`expanduser` |
72
+ | `hooks.mjs` | The gate verbs, run on every tool call: `git-gate`, `rule-gate`, `tool-gate` (the two gates fused, for Bob), plus `ask`/the gate ledger/`guardGate`. `--host` picks the verdict dialect |
73
+ | `hooks-context.mjs` | The `context` verb — SessionStart: session files plus the repo's context docs |
74
+ | `hooks-learn.mjs` | The `learn` verb — Stop/SubagentStop/PreCompact distiller; the hook path's ONE spawn (`$GENESEED_LLM`) |
75
+ | `hooks-prims.mjs` | The Python path/text primitives the hook verbs share (`splitLines`, `readStdin`, a never-throwing `isFile`) |
76
+ | `memory-files.mjs` | The memory store's file format — `frontmatter`, `MEMORY.md` index writes, agent lessons, consolidate. No spawn: the CLI and web console import it |
72
77
  | `gitref.mjs` | fs/path-only: `gitRootOf`, `gitDirOf`, `currentBranch`, `loopLaunched` — read straight off `.git`, never spawn git. Shared by `hooks.mjs`'s git-gate and `js/loop/cli.mjs`'s commit-message writer |
73
- | `settings.mjs` | Merges into user-owned `settings.json`/`opencode.json`; owns the hook shim and the managed blocks |
78
+ | `settings.mjs` | Merges into user-owned `settings.json`/`opencode.json` (one `loadJsonObject` reader for all of them) and the managed blocks |
79
+ | `shim.mjs` | The machine-wide hook shim: its path, body, liveness (`shimDead`, read on every CLI run), `hookPrefix`/`hookRunnerEntry`. Kept cheap to import |
74
80
  | `native.mjs` | Capability specs → host-native subagents and skills. The impure half of the emit |
75
81
  | `opencode.mjs` | OpenCode-only extras: colour themes, primary agent, `/commands`, plugins, overrides stub |
76
82
  | `installs.mjs` | Detects deployed installs and reads back their theme, footprint, posture, mode and loop trust preset |
77
83
  | `mcp.mjs` | MCP server presets, the per-host `mcpServers` config read/write, and toggle state |
78
84
  | `link.mjs` | `geneseed link`/`unlink` — the PATH shim, and the only Windows USER-Path registry edit |
79
85
 
80
- **Before editing:** `hooks.mjs` **is the hook path** — the verdict travels as JSON on stdout and
86
+ **Before editing:** `hooks*.mjs` **are the hook path** — the verdict travels as JSON on stdout and
81
87
  every arm exits 0, so one stray printed byte turns a blocking gate silently permissive, and anything
82
- imported here is paid on every tool call. `hosts.mjs` is shared by the generator, the CLI and the
88
+ `hooks.mjs` or `hooks-prims.mjs` imports is paid on every tool call. `bin/geneseed-hook.mjs` loads
89
+ one verb's module per process, so keep context and learn code out of the gate module. `hosts.mjs` is shared by the generator, the CLI and the
83
90
  hook path, so it must stay free of `child_process`. `installs.mjs` reads through `readMaybe`, which
84
91
  folds CRLF — a bare `readFileSync` here is a bug.
85
92
 
@@ -92,8 +99,10 @@ Everything that inspects an already-built or already-deployed harness and report
92
99
  | `doctor.mjs` | The `geneseed doctor` **driver** only: picks themes, runs the checks, prints the verdict |
93
100
  | `checks-build.mjs` | Gates over what the build PRODUCED: links, tokens, theme parity, OpenCode colours, bundle drift |
94
101
  | `checks-repo.mjs` | Gates over the repo's own records: `registry.json`, committed secrets, the hook shim, vendored pins |
95
- | `checks-authoring.mjs` | Self-consistency: law/doctrine metadata, constitution numbering, the counts README and the web quote |
96
- | `scan.mjs` | The shared walk/sort/strip/test primitives all three `checks-*` use. Owns no judgement itself |
102
+ | `checks-authoring.mjs` | `authoringProblems`, the aggregate self-consistency gate: spec purpose lines, the learn-prompt literal, `node --check` over the plugins (the doctor's one spawn), then the groups below |
103
+ | `checks-constitution.mjs` | The constitution: rule declarations and citations, LEAN blocks, `HOOK_PINNED`, theme vocabulary, the console's `LAW_META`/`DOCTRINE_META` |
104
+ | `checks-counts.mjs` | The hand-written counts: AGENT.md tables, README badges and prose, `SHIPPED.md`, held against `src/` |
105
+ | `scan.mjs` | The shared walk/sort/strip/test primitives every `checks-*` uses. Owns no judgement itself |
97
106
  | `validate.mjs` | The `--validate-only` verb: renders a sandbox, scans it for tokens and dead links, own exit contract |
98
107
  | `inventory.mjs` | The one catalog walk and its taxonomy tables, read by status, the TUI and the web console |
99
108
  | `status.mjs` | `geneseed status` and `version` — the panel data, counts, accent colour, version verdict |
@@ -220,7 +229,7 @@ and inline lists are held against a surface list written out in
220
229
  | a doctor check | `js/inspect/checks-{build,repo,authoring}.mjs` | register with `ran(...)` in `doctor.mjs`, then a PLANTED FAULT in `tests/unit/harness.test.mjs` |
221
230
  | a web endpoint | GET → `js/web/api.mjs`; POST → `js/web/actions.mjs` | declare it in `js/web/routes.mjs`; the partition test probes the real handler |
222
231
  | a CLI verb | `js/cli-table.json` — the table IS the parser | the `VERBS` row in `bin/geneseed-cli.mjs`, the row count in `cli_table.test.mjs`, `NATIVE` in **two** test files |
223
- | a hook verb | `js/hosts/hooks.mjs` | `claudeHookGroups` in `settings.mjs`, `VERBS` in `bin/geneseed-hook.mjs`. Keep the import graph tiny |
232
+ | a hook verb | `js/hosts/hooks.mjs` (a gate) or its own `js/hosts/hooks-<verb>.mjs` | `claudeHookGroups` in `settings.mjs`, the `VERBS` row (with its `load`) in `bin/geneseed-hook.mjs`. Keep the import graph tiny |
224
233
  | a console doc page | `docs/<understand|guides|concepts|reference>/<id>.md` — the stem is the id | `docs/extending.md` §4c. Only `kind: "concept"` gets `{N_*}` substitution |
225
234
  | an OpenCode plugin | `adapters/opencode/plugins/geneseed-<name>.js` | `docs/extending.md` §4a — the `geneseed-` prefix is mechanically load-bearing |
226
235
  | 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 |