geneseed 3.4.0 → 3.5.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 (164) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/DESIGN.md +11 -14
  3. package/QUICKSTART.md +3 -1
  4. package/README.md +51 -42
  5. package/SETUP.md +25 -27
  6. package/SHIPPED.md +26 -32
  7. package/adapters/bob/README.md +12 -9
  8. package/adapters/copilot/README.md +26 -25
  9. package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
  10. package/adapters/opencode/README.md +2 -2
  11. package/adapters/opencode/plugins/geneseed-context.js +5 -3
  12. package/adapters/opencode/plugins/geneseed-workflow.js +30 -3
  13. package/adapters/opencode/workflows/_runtime.js +97 -7
  14. package/bin/build-driver.mjs +5 -1248
  15. package/bin/geneseed-cli.mjs +11 -6
  16. package/bin/geneseed-hook.mjs +4 -2
  17. package/docs/candidate-hosts.md +1 -1
  18. package/docs/design-history.md +37 -37
  19. package/docs/extending.md +45 -24
  20. package/docs/token-footprint.md +52 -26
  21. package/docs/web/claude-hooks.md +3 -2
  22. package/docs/web/collaboration.md +2 -2
  23. package/docs/web/first-session.md +1 -1
  24. package/docs/web/footprint.md +2 -2
  25. package/docs/web/harnesses.md +1 -1
  26. package/docs/web/model.md +1 -1
  27. package/docs/web/plugin-workflow.md +1 -0
  28. package/docs/web/rules.md +3 -3
  29. package/docs/web/setup-choices.md +2 -2
  30. package/docs/web/themes-author.md +1 -1
  31. package/docs/web-ui.md +18 -10
  32. package/harness.config.json +1 -1
  33. package/js/README.md +6 -5
  34. package/js/build/driver.mjs +1191 -0
  35. package/js/build/emit-claude.mjs +17 -10
  36. package/js/build/emit-common.mjs +4 -4
  37. package/js/build/generate.mjs +1 -1
  38. package/js/build/render.mjs +48 -44
  39. package/js/build/stubs.mjs +1 -1
  40. package/js/build/themes.mjs +4 -4
  41. package/js/build/version.mjs +12 -0
  42. package/js/cli-table.json +21 -6
  43. package/js/hosts/hooks.mjs +89 -74
  44. package/js/hosts/hosts.mjs +19 -2
  45. package/js/hosts/mcp.mjs +14 -20
  46. package/js/hosts/native.mjs +23 -17
  47. package/js/hosts/settings.mjs +52 -104
  48. package/js/inspect/checks-authoring.mjs +117 -3
  49. package/js/inspect/diff.mjs +1 -1
  50. package/js/inspect/doctor.mjs +1 -1
  51. package/js/inspect/excludes.mjs +1 -1
  52. package/js/inspect/scan.mjs +3 -14
  53. package/js/inspect/status.mjs +2 -2
  54. package/js/inspect/validate.mjs +1 -1
  55. package/js/lib/fs.mjs +23 -8
  56. package/js/lib/json.mjs +7 -1
  57. package/js/lib/proc.mjs +6 -4
  58. package/js/maintain/migrate.mjs +1 -1
  59. package/js/maintain/setup.mjs +1 -1
  60. package/js/maintain/uninstall.mjs +17 -15
  61. package/js/ui/cli.mjs +19 -0
  62. package/js/ui/menu.mjs +1 -1
  63. package/js/web/actions.mjs +4 -4
  64. package/js/web/activity.mjs +4 -4
  65. package/js/web/api.mjs +14 -11
  66. package/js/web/daemon.mjs +1 -1
  67. package/js/web/docs.mjs +12 -12
  68. package/js/web/handler.mjs +12 -5
  69. package/js/web/jobs.mjs +1 -1
  70. package/package.json +1 -1
  71. package/src/AGENT.md.tmpl +69 -36
  72. package/src/agents/developer.md +2 -2
  73. package/src/agents/reviewer.md +5 -2
  74. package/src/doctrines/README.md +26 -3
  75. package/src/doctrines/craft.md +78 -5
  76. package/src/doctrines/ops.md +62 -0
  77. package/src/doctrines/process.md +91 -4
  78. package/src/doctrines/rigor.md +40 -0
  79. package/src/laws/universal.md +27 -21
  80. package/src/memory/README.md +4 -2
  81. package/src/modes/foreman.md +5 -5
  82. package/src/notebook/README.md +2 -1
  83. package/src/ontology/universal.md +71 -67
  84. package/src/postures/peer.md +3 -0
  85. package/src/skills/bruno-collection-generator.md +1 -1
  86. package/src/skills/bruno-test-writer.md +2 -2
  87. package/src/skills/daydream/LICENSE +21 -0
  88. package/src/skills/daydream/VENDOR.md +3 -1
  89. package/src/skills/geneseed-code-review.md +6 -2
  90. package/src/skills/parallel-agents.md +3 -3
  91. package/src/skills/pipeline.md +4 -4
  92. package/src/skills/react-view-transitions/VENDOR.md +7 -1
  93. package/src/skills/workflow.md +12 -4
  94. package/themes/_TEMPLATE.json +6 -4
  95. package/themes/biker.json +5 -3
  96. package/themes/commentator.json +5 -3
  97. package/themes/cyberpunk.json +5 -3
  98. package/themes/gamer.json +5 -3
  99. package/themes/imperial.json +5 -3
  100. package/themes/joker.json +5 -3
  101. package/themes/marvin.json +5 -3
  102. package/themes/mean.json +5 -3
  103. package/themes/military.json +5 -3
  104. package/themes/neutral.json +6 -4
  105. package/themes/opencode/README.md +2 -2
  106. package/themes/pirate.json +5 -3
  107. package/themes/sports.json +5 -3
  108. package/themes/verstappen.json +5 -3
  109. package/themes/wizard.json +5 -3
  110. package/web/dist/assets/Activity-Flgf4kHy.js +1 -0
  111. package/web/dist/assets/ActivityDetail-DIpr85nh.js +1 -0
  112. package/web/dist/assets/CatalogRow-CjyLjaLb.js +1 -0
  113. package/web/dist/assets/Diff-Deisdha8.js +2 -0
  114. package/web/dist/assets/Docs-DGUdY4l4.js +2 -0
  115. package/web/dist/assets/Doctor-F74lJ69P.js +1 -0
  116. package/web/dist/assets/FilterInput-BakJLvaV.js +1 -0
  117. package/web/dist/assets/GreenhouseView-D072lBiD.js +2 -0
  118. package/web/dist/assets/Harness-DNRP2Va7.js +1 -0
  119. package/web/dist/assets/JournalView-DWFqZ0Iw.js +1 -0
  120. package/web/dist/assets/Laws-BN6g3HU7.js +3 -0
  121. package/web/dist/assets/Library-CDOv4emM.js +1 -0
  122. package/web/dist/assets/Markdown-BQAvN8xx.js +38 -0
  123. package/web/dist/assets/OperatorHudView-CwNIUVX8.js +2 -0
  124. package/web/dist/assets/Profile-CpP9BTne.js +1 -0
  125. package/web/dist/assets/Rules-DFXsUCB7.js +1 -0
  126. package/web/dist/assets/Settings-BqZVUtJ5.js +1 -0
  127. package/web/dist/assets/Skills-Cu6kBgfF.js +1 -0
  128. package/web/dist/assets/StatusBadge-Cg6j-NJL.js +1 -0
  129. package/web/dist/assets/activity-iWaWVtsR.js +1 -0
  130. package/web/dist/assets/drawable-BgfrEBer.js +1 -0
  131. package/web/dist/assets/index-CAqrWhKl.js +2 -0
  132. package/web/dist/assets/index-Ruh93A3A.css +1 -0
  133. package/web/dist/assets/jobBuckets-Dbq8ZlWW.js +1 -0
  134. package/web/dist/assets/lawCats-BLgxl151.js +1 -0
  135. package/web/dist/assets/react-DbkvxWG5.js +8 -0
  136. package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +1 -0
  137. package/web/dist/index.html +4 -3
  138. package/web/src/pages/Laws.jsx +22 -8
  139. package/src/skills/react-view-transitions/README.md +0 -42
  140. package/web/dist/assets/Activity-DMGhhjQc.js +0 -1
  141. package/web/dist/assets/ActivityDetail-CeQcDA7e.js +0 -1
  142. package/web/dist/assets/CatalogRow-CnSbDPyg.js +0 -1
  143. package/web/dist/assets/Diff-D_ksMBD1.js +0 -5
  144. package/web/dist/assets/Doctor-Brb3_oCT.js +0 -1
  145. package/web/dist/assets/FilterInput-DbRWPhtT.js +0 -1
  146. package/web/dist/assets/GreenhouseView-F3VNFr73.js +0 -2
  147. package/web/dist/assets/Harness-DJSwfYeF.js +0 -1
  148. package/web/dist/assets/JournalView-BLRYRnB3.js +0 -1
  149. package/web/dist/assets/Laws-CKYns9qe.js +0 -3
  150. package/web/dist/assets/Library-BK0YloSv.js +0 -1
  151. package/web/dist/assets/Markdown-BzTRzc7a.js +0 -46
  152. package/web/dist/assets/OperatorHudView-D3OQSGp5.js +0 -2
  153. package/web/dist/assets/Profile-CPA2u_0g.js +0 -1
  154. package/web/dist/assets/Rules-Lukel-Ru.js +0 -1
  155. package/web/dist/assets/Skills-BKRLHH7m.js +0 -1
  156. package/web/dist/assets/StatusBadge-DI3weRKd.js +0 -1
  157. package/web/dist/assets/activity-C_H2Bx1P.js +0 -1
  158. package/web/dist/assets/index-B7_Wga23.js +0 -2
  159. package/web/dist/assets/index-DzdQjvM1.css +0 -1
  160. package/web/dist/assets/index-N8dTrVro.js +0 -1
  161. package/web/dist/assets/index-Py_IQjgK.js +0 -10
  162. package/web/dist/assets/jobBuckets-DfTUJUxR.js +0 -1
  163. package/web/dist/assets/lawCats-DUS9UTH4.js +0 -1
  164. package/web/dist/assets/react-DOpiTn5v.js +0 -32
@@ -10,8 +10,8 @@ import { VERSION_MARKER } from '../hosts/hosts.mjs';
10
10
  import { loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
11
11
  import { ensureAgentOverridesStub } from '../hosts/opencode.mjs';
12
12
  import {
13
- managedBlockRemove, managedBlockWrite, mergeClaudeSettings, mergeCopilotSettings,
14
- unwireClaudeExcludes, unwireClaudeSettings, wireClaudeExcludes,
13
+ managedBlockRemove, managedBlockWrite, mergeClaudeSettings,
14
+ unwireClaudeExcludes, unwireClaudeSettings, unwireCopilotSettings, wireClaudeExcludes,
15
15
  } from '../hosts/settings.mjs';
16
16
  import { writeText } from '../lib/fs.mjs';
17
17
  import { isTruthy } from '../lib/json.mjs';
@@ -121,10 +121,13 @@ export function emitClaudeRender(cfg, job) {
121
121
  ensureExcludesStub(cfgDir);
122
122
 
123
123
  // Project hygiene, claim-on-create: an existing (possibly user-authored) .gitignore is
124
- // never rewritten, but one we created stays owned across re-emits.
124
+ // never rewritten, but one we created stays owned across re-emits. The hook file is
125
+ // machine-specific (it names this user's hook shim): Claude's is the personal
126
+ // settings.local.json; Bob documents no local variant, so its settings.json is ignored.
125
127
  if (scope === 'project') {
126
128
  const gi = path.join(cfgDir, '.gitignore');
127
- const giLines = (host === 'claude' ? ['settings.local.json'] : [])
129
+ const giLines = (host === 'claude' ? ['settings.local.json']
130
+ : host === 'bob' ? ['settings.json'] : [])
128
131
  .concat(['wiki.jsonc', 'agent-overrides.json']);
129
132
  if (!existsSync(gi)) {
130
133
  writeText(gi, `${giLines.join('\n')}\n`);
@@ -254,7 +257,7 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
254
257
  // DECIDES the install's packs, so the marker on disk is still the previous build's.
255
258
  const [, managedHooks] = mergeClaudeSettings(
256
259
  settingsPath, scope, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
257
- doctrines, excludeRules, host,
260
+ doctrines, excludeRules, host, cfgDir,
258
261
  );
259
262
  managed.settings_hooks = managedHooks;
260
263
 
@@ -288,13 +291,17 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
288
291
  managed.settings_excludes = priorExcl;
289
292
  }
290
293
  } else if (scope === 'global') {
291
- // `copilot_hooks`, not `settings_hooks`: a distinct key so the Claude-shaped unwire and
292
- // integrity code, which walks `settings_hooks` as event→group arrays, never sees a
293
- // Copilot record it would misread.
294
+ // Copilot's hooks are event → array now, so the Claude merge (append/prune by deep
295
+ // equality) is the Copilot merge too, recorded under `settings_hooks`. An install from
296
+ // before that recorded single-slot `copilot_hooks` (the dead `toolCall` event): unwire
297
+ // those claims first, or they linger in the file forever.
294
298
  const settingsPath = path.join(cfgDir, 'settings.json');
295
299
  managed.settings_file = 'settings.json';
296
- const [, claims] = mergeCopilotSettings(settingsPath, get(old, 'copilot_hooks'), hookOpts);
297
- managed.copilot_hooks = claims;
300
+ const legacy = get(old, 'copilot_hooks');
301
+ if (Array.isArray(legacy) && legacy.length) unwireCopilotSettings(settingsPath, legacy);
302
+ const [, managedHooks] = mergeClaudeSettings(settingsPath, scope,
303
+ Array.isArray(legacy) ? null : get(old, 'settings_hooks'), hookOpts, null, [], host, cfgDir);
304
+ managed.settings_hooks = managedHooks;
298
305
  }
299
306
  return managed;
300
307
  }
@@ -2,12 +2,12 @@
2
2
  * What every emit shares — the constants that name what this tool owns, the small readers it
3
3
  * walks a tree with, and the three writers both GLOBAL emits call.
4
4
  *
5
- * Split out of `emit.mjs` because each of `globalMemory`, `globalNotebook`, `shipLeanLaws`
5
+ * Split out of the old `emit.mjs` because each of `globalMemory`, `globalNotebook`, `shipLeanLaws`
6
6
  * and `stripCapabilityLinks` has two callers in two different host emits. A helper with two
7
7
  * hosts belongs to neither, and putting it in one of them is how a file grows to 1400 lines.
8
8
  *
9
9
  * Nothing here decides WHICH host is being emitted. `emit-opencode.mjs` and
10
- * `emit-claude.mjs` do that; `emit.mjs` renders the bundle they read from.
10
+ * `emit-claude.mjs` do that; `bundle.mjs` renders the bundle they read from.
11
11
  */
12
12
  import path from 'node:path';
13
13
  import { copyFile, writeText, isFile, isDir } from '../lib/fs.mjs';
@@ -176,8 +176,8 @@ export function globalNotebook(cfgDir, items, legacy, srcRoot) {
176
176
  *
177
177
  * NO LONGER LEAN-ONLY, and the name is kept because the lean case is still the whole reason
178
178
  * it exists. `doctrines` ships at BOTH footprints, because the two footprints lose different
179
- * things: lean loses text (every rule is truncated to its first sentence, so the full text has
180
- * to be on disk), while FULL loses whole packs (only the active ones are inlined, so the
179
+ * things: lean loses text (each rule inlines its authored brief, so the full text has to be on
180
+ * disk), while FULL loses whole packs (only the active ones are inlined, so the
181
181
  * inactive ones have to be on disk). AGENT.md tells the agent it may read a rule in a pack
182
182
  * that is not active, and `src/doctrines/README.md` rests cross-pack citations on exactly
183
183
  * that — a claim the emit has to make true, not merely the `files` bundle.
@@ -32,7 +32,7 @@
32
32
  import { existsSync, mkdirSync, statSync } from 'node:fs';
33
33
  import path from 'node:path';
34
34
 
35
- import { main as driverMain } from '../../bin/build-driver.mjs';
35
+ import { main as driverMain } from './driver.mjs';
36
36
  import { makeCfg, PACK_ORDER } from './source.mjs';
37
37
  import { opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
38
38
  import {
@@ -67,11 +67,15 @@ const TEXT_SUFFIXES = new Set(['.md', '.tmpl', '.json', '.txt', '.yml', '.yaml']
67
67
  // silently never substitutes. A leading digit stays illegal, so `{{1}}` is still not a token.
68
68
  const TOKEN_RE = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g;
69
69
  const INCLUDE_RE = /^[ \t]*<!--[ \t]*INCLUDE:[ \t]*(?<path>[^ \t]+)[ \t]*-->[ \t]*$/gm;
70
+ // Each block names its kind (`agents` | `skills`) so a host that catalogues one natively and
71
+ // not the other — Bob — keeps exactly the table it still needs.
70
72
  const CATALOG_BLOCK_RE =
71
- /[ \t]*<!-- CATALOG:begin -->\n(?<table>[\s\S]*?)[ \t]*<!-- CATALOG:else -->\n(?<pointer>[\s\S]*?)[ \t]*<!-- CATALOG:end -->\n/g;
73
+ /[ \t]*<!-- CATALOG:begin (?<kind>agents|skills) -->\n(?<table>[\s\S]*?)[ \t]*<!-- CATALOG:else -->\n(?<pointer>[\s\S]*?)[ \t]*<!-- CATALOG:end -->\n/g;
72
74
  // The footprint twin of CATALOG. Before it existed, `lean` could only reach the two
73
75
  // include-driven corpora (`laws/universal.md` and the doctrine packs, both truncated
74
- // mechanically to heading + first sentence); every other section of the root template was
76
+ // mechanically to heading + first sentence — neither is, now: the laws, the ontology and every
77
+ // doctrine rule reach lean through an authored half of this block); every other section of the
78
+ // root template was
75
79
  // byte-identical across footprints, which is how ~1.5k tokens of §-level prose rode into
76
80
  // every lean install. A LEAN block ships a HAND-WRITTEN condensation instead of a
77
81
  // truncation — the objection `docs/token-footprint.md` raised against terse-ing the
@@ -154,10 +158,16 @@ export function substitute(text, theme) {
154
158
 
155
159
  /** `_build_render._resolve_catalogs`. */
156
160
  function resolveCatalogs(text, nativeCatalog) {
157
- return text.replace(CATALOG_BLOCK_RE, (_m, table, pointer) => (nativeCatalog ? pointer : table));
161
+ // `true`/`false` still mean "both kinds", for the plain-bundle callers that pass a boolean.
162
+ const nat = nativeCatalog === true ? { skills: true, agents: true } : (nativeCatalog || {});
163
+ return text.replace(CATALOG_BLOCK_RE, (_m, kind, table, pointer) => (nat[kind] ? pointer : table));
158
164
  }
159
165
 
160
- /** LEAN blocks — full text or the hand-written condensation, by footprint. */
166
+ /**
167
+ * LEAN blocks — full text or the hand-written condensation, by footprint. Laws, ontology,
168
+ * doctrine rules: every tier of the constitution authors its own lean form, and this is the
169
+ * one function that chooses between the halves.
170
+ */
161
171
  function resolveLean(text, footprint) {
162
172
  return text.replace(LEAN_BLOCK_RE, (_m, full, lean) => (footprint === 'lean' ? lean : full));
163
173
  }
@@ -182,36 +192,6 @@ function splitAtLawHeadings(text) {
182
192
  return out;
183
193
  }
184
194
 
185
- /**
186
- * Heading + first sentence, per `### ` block. DOCTRINES ONLY, now.
187
- *
188
- * The laws used to go through this too, and it made a law's lean footprint a function of its
189
- * punctuation: Law II shipped as one 36-character sentence and lost its stop-and-ask
190
- * mechanism, while a law written as one colon-chained run-on shipped nearly whole. The laws
191
- * now carry authored LEAN blocks in `src/laws/universal.md`, the way the ontology already did,
192
- * and `resolveLean` picks the half. Doctrine rules stay machine-cut: there are ~23 of them
193
- * across four packs, and a pack is a practice catalogue whose first sentence is written to
194
- * stand alone.
195
- */
196
- function terseBlocks(text) {
197
- const blocks = splitAtLawHeadings(text);
198
- const out = [blocks[0].trimEnd()];
199
- for (const b of blocks.slice(1)) {
200
- // Python's `splitlines()` also breaks on VT, FF, FS-RS, NEL, LINE SEPARATOR and
201
- // PARAGRAPH SEPARATOR, where `split('\n')` does not. The law corpus is plain
202
- // markdown and readText has already folded CRLF and CR to LF, so the sets coincide.
203
- // (Spelling those out by name rather than by literal is not fussiness: the first
204
- // draft of this comment contained the actual LINE/PARAGRAPH SEPARATOR characters,
205
- // which are JS line terminators, and they ended the comment mid-sentence.)
206
- const lines = b.split('\n');
207
- const heading = lines[0].trimEnd();
208
- const body = lines.slice(1).map((l) => l.trim()).join(' ').trim();
209
- const m = /^([\s\S]+?[.!?])(?:\s|$)/.exec(body);
210
- out.push(`${heading}\n${m ? m[1].trim() : body}`);
211
- }
212
- return out.join('\n\n');
213
- }
214
-
215
195
  /** The pointer under §1 at lean. The laws' lean text itself is authored in LEAN blocks. */
216
196
  export function lawsPointer(theme, lawsPrefix = '') {
217
197
  const law = theme.LAW ?? 'Law';
@@ -222,11 +202,29 @@ export function lawsPointer(theme, lawsPrefix = '') {
222
202
  + 'secrets, deletion, git history, scope, or untrusted content.';
223
203
  }
224
204
 
205
+ /**
206
+ * SOURCE TEXT, READ ONCE PER `cfg`. One emit renders the same partials over and over — the
207
+ * self-improvement footer is included by ~50 specs — and `doctor --all` repeats that per theme:
208
+ * 91% of its file reads were repeats. Keyed on the `cfg` OBJECT, not the process: the web
209
+ * daemon renders in-process for days, and `makeCfg()` is called per operation, so each request
210
+ * sees today's source. Only files under `cfg.src` — the one tree nothing in a render writes to.
211
+ */
212
+ const SRC_TEXT = new WeakMap();
213
+ function readSource(cfg, p) {
214
+ const src = cfg && cfg.src ? normcase(path.resolve(cfg.src)) + path.sep : null;
215
+ if (!src || !normcase(p).startsWith(src)) return readText(p);
216
+ let seen = SRC_TEXT.get(cfg);
217
+ if (!seen) SRC_TEXT.set(cfg, (seen = new Map()));
218
+ let text = seen.get(p);
219
+ if (text === undefined) seen.set(p, (text = readText(p)));
220
+ return text;
221
+ }
222
+
225
223
  /** `_build_render.render_file`. */
226
224
  export function renderFile(cfg, filePath, theme, footprint = 'full', lawsPrefix = '',
227
225
  visiting = new Set(), nativeCatalog = false) {
228
226
  const here = path.resolve(filePath);
229
- let text = readText(filePath);
227
+ let text = readSource(cfg, here);
230
228
 
231
229
  text = text.replace(INCLUDE_RE, (_whole, rel) => {
232
230
  const target = path.resolve(cfg.src, rel);
@@ -318,10 +316,10 @@ function activeDoctrines(cfg) {
318
316
  * `registerBody`'s single-select one.
319
317
  *
320
318
  * `registerBody` reads ONE file because a posture and a mode are scalars; a doctrine set is
321
- * 0-4 files, so this loops instead of wrapping it. Under `lean` each pack gets the same
322
- * heading + first-sentence treatment the invariants get, and the whole section — not each
323
- * pack — closes with one pointer at the full catalogue, which ships in every bundle whether
324
- * or not a pack is active.
319
+ * 0-4 files, so this loops instead of wrapping it. Under `lean` each rule ships the authored
320
+ * `LEAN:else` half of its block — `resolveLean`, the same picker the invariants go through,
321
+ * the first-sentence cut gone — and the whole section, not each pack, closes with one pointer
322
+ * at the full catalogue, which ships in every bundle whether or not a pack is active.
325
323
  */
326
324
  /** `### {{DOCTRINE}} <pack> <n> —` in the UNRENDERED source, which is where the address is. */
327
325
  const SRC_RULE_HEADING_RE = /^### \{\{DOCTRINE\}\} ([a-z]+) (\d+)\b/;
@@ -356,8 +354,11 @@ function doctrinesBody(cfg, theme, active, footprint, lawsPrefix) {
356
354
  const parts = active.map((name) => {
357
355
  const raw = dropExcludedRules(readPackText(path.join(cfg.src, 'doctrines', `${name}.md`)),
358
356
  name, excluded);
359
- const body = substitute(raw, theme).trim();
360
- return footprint === 'lean' ? terseBlocks(body) : body;
357
+ // The same picker `renderFile` runs over every other file: the authored half first, the
358
+ // voice second. THE ORDER IS LOAD-BEARING — `resolveLean` reads HTML markers, and
359
+ // substituting first would hand it prose in fourteen voices. The first-sentence cut is gone;
360
+ // a rule's lean text is written in `src/doctrines/<pack>.md`, not measured out of its prose.
361
+ return substitute(resolveLean(raw, footprint), theme).trim();
361
362
  });
362
363
  if (footprint === 'lean') {
363
364
  const doctrine = theme.DOCTRINE ?? 'Doctrine';
@@ -454,10 +455,13 @@ export function renderAll(cfg, themeName, {
454
455
  for (const file of sortedSourceFiles(cfg.src)) {
455
456
  const rel = path.relative(cfg.src, file);
456
457
  const outRel = destRel(themedRel(rel, theme)).split(path.sep).join('/'); // as_posix()
457
- // The on-disk `laws/` and `ontology/` are the "complete, binding text" the lean pointer
458
- // promises. They render at full whatever the footprint; only what AGENT.md INLINES leans.
458
+ // The on-disk `laws/`, `ontology/` and `doctrines/` are the "complete text" the two lean
459
+ // pointers promise — §1's and the Doctrines section's. They render at full whatever the
460
+ // footprint; only what AGENT.md INLINES leans. `doctrines/` joined them when its rules got
461
+ // authored LEAN blocks: before that a pack file carried no markers and shipped whole by
462
+ // accident, which is a different thing from shipping whole by contract.
459
463
  // Keyed on the SOURCE rel, not the themed one, so a DIR_* rename cannot un-exempt them.
460
- const fp = /^(?:laws|ontology)[\\/]/.test(rel) ? 'full' : footprint;
464
+ const fp = /^(?:laws|ontology|doctrines)[\\/]/.test(rel) ? 'full' : footprint;
461
465
  items.push(TEXT_SUFFIXES.has(suffixOf(path.basename(file)))
462
466
  ? { rel: outRel, text: renderFile(cfg, file, theme, fp, lawsPrefix, new Set(), nativeCatalog), src: file }
463
467
  : { rel: outRel, text: null, src: file });
@@ -114,7 +114,7 @@ export const EXCLUDES_FILE = 'excludes.json';
114
114
  */
115
115
  export const EXCLUDES_STUB = `\
116
116
  {
117
- "_comment": "Folders where this global Geneseed install goes dormant (hooks silent, preamble suppressed). Managed by \`harness exclude add|remove|list\`; safe to edit by hand. Paths are absolute.",
117
+ "_comment": "Folders where this global Geneseed install goes dormant (hooks silent, preamble suppressed). Managed by \`geneseed exclude add|remove|list\`; safe to edit by hand. Paths are absolute.",
118
118
  "excludes": []
119
119
  }
120
120
  `;
@@ -14,10 +14,10 @@
14
14
  * `_argv` emits neither flag (argued in situ at `bin/build-driver.mjs`'s dispatch), so the
15
15
  * acceptance matrix that gates all nine emits is structurally blind to both, and the driver
16
16
  * refused them rather than crossing for exactly that reason. The gate is
17
- * `tests/unit/maintainer_tools.test.mjs`, replaying the frozen recording in
18
- * `tests/__snapshots__/sync_themes.json`: a corpus of hand-made theme directories, comparing
19
- * the printed output, the return value and the resulting FILE BYTES against what the reference
20
- * answered. Nothing can re-record it, so a change here that moves a byte is a product change.
17
+ * `tests/unit/maintainer_tools.test.mjs`, over the written-out cases in
18
+ * `tests/fixtures/sync_themes_cases.json`: hand-made theme directories, each with the output,
19
+ * exit status and FILE BYTES it must produce. A change that moves a byte changes a case and
20
+ * the sentence explaining it, in the same commit.
21
21
  *
22
22
  * WHY THIS MODULE IS NOT `js/inspect/doctor.mjs`. `--sync-themes` has no doctor dependency, so it
23
23
  * stays on the generator driver where its flag lives — and the driver is under a transitive
@@ -28,6 +28,18 @@ import { readFileSync } from 'node:fs';
28
28
  * in the output; reproducing the wrong one changes the fingerprint of every install.
29
29
  */
30
30
  export function sourceFingerprint(cfg) {
31
+ // Once per `cfg` — the source cannot change under one operation, and every emit (84 of them
32
+ // in `doctor --all`) hashed the whole tree again. Per cfg OBJECT, not per process, for the
33
+ // same reason as render.mjs's source cache: the web daemon is long-lived.
34
+ if (FINGERPRINT.has(cfg)) return FINGERPRINT.get(cfg);
35
+ const fp = hashSource(cfg);
36
+ FINGERPRINT.set(cfg, fp);
37
+ return fp;
38
+ }
39
+
40
+ const FINGERPRINT = new WeakMap();
41
+
42
+ function hashSource(cfg) {
31
43
  const h = createHash('sha256');
32
44
  const files = [];
33
45
  for (const r of [cfg.src, cfg.themes, cfg.pluginSrc, cfg.workflowSrc]) {
package/js/cli-table.json CHANGED
@@ -162,7 +162,7 @@
162
162
  ],
163
163
  "dest": "host",
164
164
  "metavar": null,
165
- "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'copilot' has no ask tier, so Laws I/IV block and everything else is a stderr warning",
165
+ "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) and 'copilot' ask, each in its own envelope; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
166
166
  "choices": null,
167
167
  "default": null,
168
168
  "required": false,
@@ -353,7 +353,7 @@
353
353
  },
354
354
  {
355
355
  "name": "git-gate",
356
- "help": "PreToolUse hook: force an ASK before every git commit/push (process 5 backstop)",
356
+ "help": "PreToolUse hook: ASK before a history-discarding git act (Law IV) and before every git commit/push (process 5 backstop)",
357
357
  "description": "",
358
358
  "positionals": [],
359
359
  "options": [
@@ -378,7 +378,7 @@
378
378
  ],
379
379
  "dest": "host",
380
380
  "metavar": null,
381
- "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'copilot' has no ask tier, so Laws I/IV block and everything else is a stderr warning",
381
+ "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) and 'copilot' ask, each in its own envelope; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
382
382
  "choices": null,
383
383
  "default": null,
384
384
  "required": false,
@@ -386,6 +386,21 @@
386
386
  "is_flag": false,
387
387
  "type": null,
388
388
  "hidden": false
389
+ },
390
+ {
391
+ "names": [
392
+ "--no-consent"
393
+ ],
394
+ "dest": "no_consent",
395
+ "metavar": null,
396
+ "help": "skip the process-5 commit/push ask (set by the emit when the process pack or process 5 is off); Law IV still asks",
397
+ "choices": null,
398
+ "default": false,
399
+ "required": false,
400
+ "nargs": "0",
401
+ "is_flag": true,
402
+ "type": null,
403
+ "hidden": false
389
404
  }
390
405
  ],
391
406
  "mutex": []
@@ -641,7 +656,7 @@
641
656
  ],
642
657
  "dest": "host",
643
658
  "metavar": null,
644
- "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'copilot' has no ask tier, so Laws I/IV block and everything else is a stderr warning",
659
+ "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) and 'copilot' ask, each in its own envelope; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
645
660
  "choices": null,
646
661
  "default": null,
647
662
  "required": false,
@@ -811,7 +826,7 @@
811
826
  },
812
827
  {
813
828
  "name": "tool-gate",
814
- "help": "toolCall hook (Copilot): git-gate and rule-gate fused behind one command, dispatched on the payload",
829
+ "help": "PreToolUse hook for hosts with no tool matcher (Copilot, Bob): git-gate and rule-gate fused behind one command, dispatched on the payload",
815
830
  "description": "",
816
831
  "positionals": [],
817
832
  "options": [
@@ -836,7 +851,7 @@
836
851
  ],
837
852
  "dest": "host",
838
853
  "metavar": null,
839
- "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'copilot' has no ask tier, so Laws I/IV block and everything else is a stderr warning",
854
+ "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) and 'copilot' ask, each in its own envelope; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
840
855
  "choices": null,
841
856
  "default": null,
842
857
  "required": false,