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
@@ -47,7 +47,7 @@ import {
47
47
  } from 'node:fs';
48
48
  import path from 'node:path';
49
49
 
50
- import { hookRunnerEntry } from '../../bin/build-driver.mjs';
50
+ import { hookRunnerEntry } from '../build/driver.mjs';
51
51
  import { confirm } from './setup.mjs';
52
52
  import {
53
53
  claudeCfg, claudeReadManifest, doctrinesOfDir, emitHostScopeOf, excludedRulesOfDir, installKind,
@@ -60,7 +60,7 @@ import {
60
60
  import { mcpCommented } from '../hosts/mcp.mjs';
61
61
  import {
62
62
  copilotIntegrityCheck, managedBlockRead, managedBlockRemove, managedBlockWrite,
63
- mergeClaudeSettings, mergeCopilotSettings, opencodeTarget, readJsonc,
63
+ mergeClaudeSettings, opencodeTarget, readJsonc,
64
64
  settingsIntegrityCheck, wireClaudeExcludes, unwireClaudeExcludes, unwireClaudeSettings,
65
65
  unwireCopilotSettings,
66
66
  } from '../hosts/settings.mjs';
@@ -347,9 +347,8 @@ export function uninstallGlobal(target, archiveMemory, host = 'opencode') {
347
347
  /**
348
348
  * `_harness_mcp._mcp_load`, reduced to the OpenCode arm.
349
349
  *
350
- * The Claude arm (`.mcp.json`, `~/.claude.json`, parsed with STRICT `json.loads` because the
351
- * comment stripper's trailing-comma pass is not string-aware) belongs to the MCP catalog and
352
- * the TUI, and neither has crossed. `installAgentEntry` is this port's only caller and passes
350
+ * The Claude arm (`.mcp.json`, `~/.claude.json`, parsed STRICTLY — see `mcpLoad` in
351
+ * js/hosts/mcp.mjs for why) lives with the MCP catalog. `installAgentEntry` is this port's only caller and passes
353
352
  * no host, so porting the branch would ship an unreachable arm with no cell and no partition
354
353
  * to be part of — the criterion this port decides keep-vs-delete by.
355
354
  */
@@ -645,7 +644,7 @@ function printSurvivingProjectInventory(removedRoot) {
645
644
  function printOtherHostHits(root, removedHost) {
646
645
  for (const spec of HOSTS) {
647
646
  if (spec.host !== removedHost && projectQualifies(root, spec.host)) {
648
- printOut(`[uninstall] also found ${spec.host}:project here — run \`harness `
647
+ printOut(`[uninstall] also found ${spec.host}:project here — run \`geneseed `
649
648
  + 'uninstall` again to remove it.\n');
650
649
  }
651
650
  }
@@ -1058,15 +1057,18 @@ function cleanHostStash(cfg, host = 'claude') {
1058
1057
  function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
1059
1058
  const data = claudeReadManifest(cfg);
1060
1059
  const managed = managedOf(data);
1061
- // A Copilot install: its own merge, its own claim key, and none of the pack/exclude axes
1062
- // below — the Copilot gate is not pack-conditional (see `copilotHooks`). Without this
1063
- // branch a Copilot reactivate would write CLAUDE-shaped hook groups into
1064
- // ~/.copilot/settings.json, which Copilot cannot read.
1060
+ // A LEGACY Copilot install (single-slot `copilot_hooks`, the dead `toolCall` event):
1061
+ // reactivating it re-wires the CURRENT Copilot shape — event → array, recorded under
1062
+ // `settings_hooks` — rather than restoring hooks Copilot no longer reads. None of the
1063
+ // pack/exclude axes below apply: the Copilot gate is not pack-conditional.
1065
1064
  if (Array.isArray(managed.copilot_hooks)) {
1066
- const [, claims] = mergeCopilotSettings(settingsFile(cfg, managed), managed.copilot_hooks,
1067
- hookRunnerEntry());
1068
- if (!deepEquals(claims, managed.copilot_hooks) && Object.keys(data).length > 0) {
1069
- managed.copilot_hooks = claims;
1065
+ const sf = settingsFile(cfg, managed);
1066
+ unwireCopilotSettings(sf, managed.copilot_hooks);
1067
+ const [, claims] = mergeClaudeSettings(sf, 'global', null, hookRunnerEntry(), null, [],
1068
+ 'copilot', cfg);
1069
+ if (Object.keys(data).length > 0) {
1070
+ delete managed.copilot_hooks;
1071
+ managed.settings_hooks = claims;
1070
1072
  data.managed = managed;
1071
1073
  const tmp = path.join(cfg, `${GLOBAL_MANIFEST}.tmp`);
1072
1074
  try {
@@ -1098,7 +1100,7 @@ function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
1098
1100
  : excludedRulesOfDir(cfg);
1099
1101
  // `host` reaches the group builder: a Bob reactivate must re-wire Gemini-named groups.
1100
1102
  const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed), 'global',
1101
- managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded, host);
1103
+ managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded, host, cfg);
1102
1104
  // `and data` — a manifest that did not parse is not one to write back.
1103
1105
  if (!deepEquals(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
1104
1106
  managed.settings_hooks = claims;
package/js/ui/cli.mjs CHANGED
@@ -441,6 +441,25 @@ export function cliCommand(verb) {
441
441
  * CRLF ON WINDOWS for `die`'s reason: argparse writes through a Python text stream, which
442
442
  * translates, and this text lands in the same terminals and the same pipes as that one.
443
443
  */
444
+ /**
445
+ * `geneseed --help` — every verb with its one-line help, from the same table the per-verb help
446
+ * reads. `verbs` is the entry's own dispatch list, so the listing can name nothing the entry
447
+ * would refuse; a verb the table does not describe still appears, with no help line.
448
+ */
449
+ export function printVerbList(prog, verbs, extra = {}) {
450
+ const helpOf = new Map([...Object.entries(extra),
451
+ ...cliReference().commands.filter((c) => c.help).map((c) => [c.name, c.help])]);
452
+ const names = [...verbs].sort();
453
+ const pad = Math.max(...names.map((n) => n.length)) + 2;
454
+ const lines = [`usage: ${prog} <command> [options]`, '',
455
+ `A bare \`${prog}\` runs \`home\`. \`${prog} <command> --help\` describes one command.`, '',
456
+ 'commands:'];
457
+ for (const n of names) lines.push(` ${n.padEnd(pad)}${helpOf.get(n) || ''}`.trimEnd());
458
+ const text = `${lines.join('\n')}\n`;
459
+ process.stdout.write(process.platform === 'win32' ? text.replaceAll('\n', '\r\n') : text);
460
+ return 0;
461
+ }
462
+
444
463
  export function printHelp(prog, verb) {
445
464
  const cmd = cliCommand(verb);
446
465
  if (cmd === null) return null;
package/js/ui/menu.mjs CHANGED
@@ -45,7 +45,7 @@ import { startDaemon } from '../web/daemon.mjs';
45
45
  function menuHelp() {
46
46
  printOut('Geneseed — no interactive menu here. Get started with: geneseed setup\n');
47
47
  printOut('Other commands: bootstrap · update · build · doctor · diff · tui · web\n');
48
- printOut('On a VT-capable terminal, a bare `./geneseed` opens the interactive menu of these.\n');
48
+ printOut('`geneseed --help` lists every command.\n');
49
49
  return 0;
50
50
  }
51
51
 
@@ -32,7 +32,7 @@ import {
32
32
  import os from 'node:os';
33
33
  import path from 'node:path';
34
34
 
35
- import { emitGlobalInto } from '../../bin/build-driver.mjs';
35
+ import { emitGlobalInto } from '../build/driver.mjs';
36
36
  import {
37
37
  ROOT, discoverNames, knownRuleIds, PACK_ORDER,
38
38
  } from '../build/source.mjs';
@@ -127,7 +127,7 @@ function bodyExcludeRules(body) {
127
127
  const splitWords = (s) => s.split(new RegExp(`[${WHITESPACE}]+`)).filter(Boolean);
128
128
 
129
129
  /** Today's date, LOCAL (not UTC) — this is a user-facing date, not a wire timestamp. */
130
- export function todayIso(d = new Date()) {
130
+ function todayIso(d = new Date()) {
131
131
  const p2 = (n) => String(n).padStart(2, '0');
132
132
  return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
133
133
  }
@@ -278,13 +278,13 @@ const rulesPath = (state) => path.join(state.target, RULES_FILE);
278
278
  * `max(ids) + 1`. Widening this to match would only move the gap one line down, to whatever
279
279
  * parses the captured digits back into a number.
280
280
  */
281
- export const RULE_HEAD_RE = new RegExp(
281
+ const RULE_HEAD_RE = new RegExp(
282
282
  `^##[${WHITESPACE}]+R(\\d+)[${WHITESPACE}]*[—–-]+[${WHITESPACE}]*`
283
283
  + `([^${WHITESPACE}].*?)[${WHITESPACE}]*$`,
284
284
  );
285
285
 
286
286
  /** Advisory only; nothing blocks past it. */
287
- export const RULES_BUDGET = { max_rules: 15, max_tokens: 1500 };
287
+ const RULES_BUDGET = { max_rules: 15, max_tokens: 1500 };
288
288
 
289
289
  const META_RE = new RegExp(`^\\((.+)\\)[${WHITESPACE}]*$`);
290
290
 
@@ -48,7 +48,7 @@ import { NotFound, stemOf } from './api.mjs';
48
48
  * platform where the probe cannot tell. Generous on purpose: streaming bumps `updated_at`
49
49
  * constantly, so only a genuinely abandoned (process-still-alive) session ages out.
50
50
  */
51
- export const ACTIVITY_STALE_SECONDS = 1800;
51
+ const ACTIVITY_STALE_SECONDS = 1800;
52
52
 
53
53
  const nowSeconds = () => Date.now() / 1000;
54
54
 
@@ -66,7 +66,7 @@ const activityFlag = (state) => path.join(state.target, '.geneseed-activity');
66
66
  * default every user who has never toggled is in. An undecodable flag file is not treated
67
67
  * specially: Node substitutes U+FFFD and reads on rather than raising.
68
68
  */
69
- export function activityEnabled(state) {
69
+ function activityEnabled(state) {
70
70
  let raw;
71
71
  try {
72
72
  raw = readText(activityFlag(state));
@@ -97,7 +97,7 @@ function toIntOr(raw) {
97
97
  }
98
98
 
99
99
  /** Best-effort liveness for the writer process. See this file's header. */
100
- export function pidAlive(raw) {
100
+ function pidAlive(raw) {
101
101
  const pid = toIntOr(raw);
102
102
  if (pid === null || pid <= 0) return false;
103
103
  try {
@@ -161,7 +161,7 @@ function isLive(entry, now) {
161
161
  * missing dir is `[]`, a garbage file is skipped (and kept — only a snapshot that PARSED and
162
162
  * failed the liveness test is unlinked), newest first.
163
163
  */
164
- export function activityEntries(state) {
164
+ function activityEntries(state) {
165
165
  const d = activityDir(state);
166
166
  if (!isDir(d)) return [];
167
167
  const now = nowSeconds();
package/js/web/api.mjs CHANGED
@@ -201,7 +201,7 @@ export function specDesc(fm, body) {
201
201
  * plumbing rather than prose, which is what makes a deployed entry the same shape as a
202
202
  * source-rendered one and every consumer indifferent to the origin.
203
203
  */
204
- export function specEntries(root, nested) {
204
+ function specEntries(root, nested) {
205
205
  const out = [];
206
206
  if (!isDir(root)) return out;
207
207
  let names;
@@ -238,7 +238,7 @@ export function specEntries(root, nested) {
238
238
  * the walk. So the catalogue is always whole, and which packs this INSTALL built in comes from
239
239
  * its own carrier via `doctrinesForBuild`, which resolves "no marker" to every pack.
240
240
  */
241
- export function deployedInventory(state) {
241
+ function deployedInventory(state) {
242
242
  const render = tuiInventory(state.theme, doctrinesForBuild(state.target),
243
243
  excludedRulesOfDir(state.target));
244
244
  const registry = loadRegistry();
@@ -259,7 +259,7 @@ export function deployedInventory(state) {
259
259
  }
260
260
 
261
261
  /** The deployed record when there is one, else the source render. */
262
- export function inventoryFor(state) {
262
+ function inventoryFor(state) {
263
263
  return deployed(state) ? deployedInventory(state) : tuiInventory(state.theme);
264
264
  }
265
265
 
@@ -305,7 +305,7 @@ export function memoryItems(state) {
305
305
  });
306
306
  }
307
307
 
308
- export function notebookItems(state) {
308
+ function notebookItems(state) {
309
309
  const d = notebookDir(state);
310
310
  if (!isDir(d)) return [];
311
311
  return globMd(d).map((n) => ({
@@ -319,7 +319,7 @@ const CONFIG_META = {
319
319
  'wiki.jsonc': ['Wiki manifest', 'your machine-wide knowledge base(s)'],
320
320
  };
321
321
 
322
- export function configItems(state) {
322
+ function configItems(state) {
323
323
  const out = [];
324
324
  for (const fname of ['context.json', 'wiki.jsonc']) {
325
325
  const p = path.join(state.target, fname);
@@ -331,7 +331,7 @@ export function configItems(state) {
331
331
  return out;
332
332
  }
333
333
 
334
- export const WIKI_FILE_CAP = 5000;
334
+ const WIKI_FILE_CAP = 5000;
335
335
 
336
336
  /**
337
337
  * `resolvePath` for a path that came out of a HAND-MAINTAINED manifest — `null`, never a throw.
@@ -419,7 +419,10 @@ export function wikiItems(state) {
419
419
  // blank every OTHER vault in it.
420
420
  const root = wikiPath(String(dget(w, 'path', null) || ''));
421
421
  if (!root || !isDir(root)) continue;
422
- const entries = (dget(w, 'entries', null) || [])
422
+ // A hand-edited `entries` that is not a list (`{}`, a string) is a malformed vault, not a
423
+ // crash: `/api/status` counts these, and one bad manifest must not fail the dashboard.
424
+ const rawEntries = dget(w, 'entries', null);
425
+ const entries = (Array.isArray(rawEntries) ? rawEntries : [])
423
426
  .filter((e) => e && typeof e === 'object' && !Array.isArray(e));
424
427
  const excludes = entries.filter((e) => dget(e, 'load', null) === 'exclude')
425
428
  .map((e) => String(dget(e, 'path', null) || '').replace(/^\/+|\/+$/g, '').replace(/\\/g, '/'));
@@ -450,7 +453,7 @@ export function wikiItems(state) {
450
453
  }
451
454
 
452
455
  /** One page by `<wiki>:<relpath>`, never outside the vault. */
453
- export function apiWikiItem(state, name) {
456
+ function apiWikiItem(state, name) {
454
457
  const at = name.indexOf(':');
455
458
  const wname = at < 0 ? name : name.slice(0, at);
456
459
  const rel = (at < 0 ? '' : name.slice(at + 1))
@@ -497,7 +500,7 @@ function flatName(name) {
497
500
  }
498
501
  }
499
502
 
500
- export const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
503
+ const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
501
504
 
502
505
  /**
503
506
  * `[[name]]` matched against known agent and skill names.
@@ -746,9 +749,9 @@ export function apiInstalls(state) {
746
749
 
747
750
  /** Beside the deployed AGENT.md. */
748
751
  export const PROFILE_FILE = 'PROFILE.md';
749
- export const profilePath = (state) => path.join(state.target, PROFILE_FILE);
752
+ const profilePath = (state) => path.join(state.target, PROFILE_FILE);
750
753
 
751
- export function apiProfile(state) {
754
+ function apiProfile(state) {
752
755
  const p = profilePath(state);
753
756
  if (!isFile(p)) return { exists: false, path: p, text: '', fingerprint: '' };
754
757
  const text = readMaybe(p) ?? '';
package/js/web/daemon.mjs CHANGED
@@ -83,7 +83,7 @@ export async function probe(url, timeout = 1.5) {
83
83
  }
84
84
 
85
85
  /** The token-carrying POST `geneseed web stop` and the in-page Stop button both send. */
86
- export async function postShutdown(url, token, timeout = 3.0) {
86
+ async function postShutdown(url, token, timeout = 3.0) {
87
87
  try {
88
88
  const res = await fetch(`${url}/api/shutdown`, {
89
89
  method: 'POST',
package/js/web/docs.mjs CHANGED
@@ -24,7 +24,7 @@ import { readJsonMaybe, readMaybe } from '../hosts/installs.mjs';
24
24
  import { resolvePath } from '../hosts/hosts.mjs';
25
25
  import { parseJson } from '../lib/json.mjs';
26
26
  import { isFile } from '../lib/fs.mjs';
27
- import { normcase } from '../lib/paths.mjs';
27
+ import { normcase, within } from '../lib/paths.mjs';
28
28
  import { WHITESPACE, stripWhitespace } from '../lib/text.mjs';
29
29
  import { statusData } from '../inspect/status.mjs';
30
30
  import { originDisplay } from '../maintain/update.mjs';
@@ -39,7 +39,7 @@ const DOC_DIR = path.join(ROOT, 'docs', 'web');
39
39
  * covers every key these pages use and parses with the standard library. A value that is
40
40
  * not valid JSON is kept as the raw string, which is how `title: Something` works.
41
41
  */
42
- export function docFrontmatter(text) {
42
+ function docFrontmatter(text) {
43
43
  const marker = '---\n';
44
44
  if (!text.startsWith(marker)) return [{}, text];
45
45
  const rest = text.slice(marker.length);
@@ -109,8 +109,13 @@ export function docGroups() {
109
109
  */
110
110
  const GLOSSARY_KEYS = [
111
111
  // The three tiers, in constitutional order and before the entities — a reader who does not
112
- // know what an Ontology is here cannot read the rest of the glossary's first rows.
113
- ['Ontology', 'ONTOLOGY', 'the always-on worldview under the rules — telos, evidence, '
112
+ // know what an Ethos is here cannot read the rest of the glossary's first rows.
113
+ //
114
+ // `Ethos (Ontology)` for the same reason as `Rule (Law)` below: the neutral theme renamed
115
+ // what a reader SEES, while the address the reader will meet in `src/ontology/`, in the
116
+ // `ONTOLOGY` theme key and in every identifier did not move. The parenthesis is the bridge
117
+ // between the two, and the glossary is the one page that owes a reader that bridge.
118
+ ['Ethos (Ontology)', 'ONTOLOGY', 'the always-on worldview under the rules — telos, evidence, '
114
119
  + 'decisions, conduct; it holds the Pact'],
115
120
  ['Rule (Law)', 'LAW', 'one of the nine always-on invariants: what is never done'],
116
121
  ['Rules (Laws)', 'LAWS', 'the body of always-on invariants'],
@@ -126,8 +131,9 @@ const GLOSSARY_KEYS = [
126
131
  ['Wiki', 'WIKI', 'the machine-wide knowledge base'],
127
132
  // Still a token, still themed, but it names a CONCEPT INSIDE the ontology's Telos now
128
133
  // rather than a peer of the Rules — `src/postures/peer.md` cites it and the section that
129
- // used to carry it is the Ontology.
130
- ['Pact', 'PACT', "the two-way collaboration contract, stated in the Ontology's Telos"],
134
+ // used to carry it is the Ethos.
135
+ ['Pact', 'PACT', 'the two-way collaboration contract, stated in Telos, the first Ethos '
136
+ + 'section'],
131
137
  ['Posture', null, 'the relationship register the agent works in '
132
138
  + '(peer, mentor, expert, assistant, artisan)'],
133
139
  ['Mode', null, 'how work gets executed — direct (the agent works every '
@@ -357,12 +363,6 @@ function readDocSource(rel) {
357
363
  return readMaybe(target) ?? '';
358
364
  }
359
365
 
360
- function within(child, parent) {
361
- const c = normcase(child).split(/[\\/]/);
362
- const p = normcase(parent).split(/[\\/]/);
363
- return p.length <= c.length && p.every((seg, i) => c[i] === seg);
364
- }
365
-
366
366
  /** The deployed theme's words beside the neutral ones. */
367
367
  function glossary(state) {
368
368
  const load = (theme) => {
@@ -62,6 +62,13 @@ export function makeHandler(state, jm, token, dist, holder = null) {
62
62
  // syscall against the readFileSync + gzipSync it saves. `gz` is the gzipped body,
63
63
  // computed once on the first gzip-accepting request instead of per request.
64
64
  const staticCache = new Map();
65
+ // `dist` RESOLVED ONCE, because every path compared against it below is resolved too.
66
+ // `resolvePath` is `realpathSync.native`, so a checkout reached by any path that is not its
67
+ // canonical one — a symlink, a Windows junction, a `subst` or mapped drive, an 8.3 short name,
68
+ // a lowercase drive letter — made `fp.startsWith(dist + sep)` false for EVERY asset. Each one
69
+ // then fell back to index.html at `text/html`, the browser refused the module script, and the
70
+ // page stayed blank with nothing logged on either side.
71
+ const distReal = resolvePath(dist);
65
72
 
66
73
  function sendBytes(res, body, ctype, code = 200, extra = null, acceptEncoding = '', gzStore = null) {
67
74
  const headers = { ...(extra || {}) };
@@ -86,12 +93,12 @@ export function makeHandler(state, jm, token, dist, holder = null) {
86
93
 
87
94
  function serveStatic(res, path, acceptEncoding) {
88
95
  const rel = (path === '/' || path === '') ? 'index.html' : path.replace(/^\/+/, '');
89
- const index = resolvePath(join(dist, 'index.html'));
90
- let fp = resolvePath(join(dist, rel));
96
+ const index = join(distReal, 'index.html');
97
+ let fp = resolvePath(join(distReal, rel));
91
98
  // STRICTLY under `dist`: `fp === dist` itself must also take the fallback below, which
92
- // is why this checks `fp !== dist` and not just the prefix.
93
- if (!(fp.startsWith(dist + sep) && fp !== dist) && fp !== index) fp = join(dist, 'index.html');
94
- if (!isFile(fp)) fp = join(dist, 'index.html');
99
+ // is why this checks `fp !== distReal` and not just the prefix.
100
+ if (!(fp.startsWith(distReal + sep) && fp !== distReal) && fp !== index) fp = index;
101
+ if (!isFile(fp)) fp = index;
95
102
  if (!isFile(fp)) {
96
103
  return sendJson(res, { error: 'web/dist missing — run the UI build' }, 500, acceptEncoding);
97
104
  }
package/js/web/jobs.mjs CHANGED
@@ -98,7 +98,7 @@ import { jsonDumpsCompact, parseJson, isTruthy } from '../lib/json.mjs';
98
98
  * NEVER THROWS: it runs from `cancel()` and from the crash path in `_run`'s `catch`, and an
99
99
  * exception in either would leave the child alive with the manager believing it dead.
100
100
  */
101
- export function killJobTree(child) {
101
+ function killJobTree(child) {
102
102
  try {
103
103
  if (process.platform === 'win32') {
104
104
  // The one machine primitive with no Node equivalent — see this module's header.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
3
3
  "name": "geneseed",
4
- "version": "3.4.0",
4
+ "version": "3.5.0",
5
5
  "description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
6
6
  "keywords": [
7
7
  "ai",
package/src/AGENT.md.tmpl CHANGED
@@ -19,12 +19,11 @@ name, or reference it from the tool's config. Where a tool supports sub-agents,
19
19
  the {{AGENTS}} below are dispatched as real sub-agents; where it does not, treat
20
20
  each as a *persona* the single agent adopts for that slice of work.
21
21
  <!-- LEAN:else -->
22
- It is tool-agnostic: any assistant that reads a root instructions file can be
23
- pointed here. Where the tool supports sub-agents, the {{AGENTS}} below are
24
- dispatched as real sub-agents; where it does not, treat each as a *persona* the
25
- single agent adopts for that slice of work.
22
+ Tool-agnostic: where the host has no sub-agents, each {{AGENT}} below is a
23
+ *persona* the single agent adopts for that slice of work.
26
24
  <!-- LEAN:end -->
27
25
 
26
+ <!-- LEAN:begin -->
28
27
  **Readiness sigil — required.** Before your first reply, complete the startup
29
28
  ritual: read this file and its {{LAWS}}, then **load the project context** — if a
30
29
  `context.json` sits beside this file, read it and load its `eager` entries now
@@ -32,6 +31,11 @@ ritual: read this file and its {{LAWS}}, then **load the project context** — i
32
31
  exact line below, before any other text. Do not paraphrase it and do not skip it —
33
32
  a first reply that omits the sigil signals the harness was not read, and emitting
34
33
  it certifies you have loaded the {{LAWS}} *and* the project context.
34
+ <!-- LEAN:else -->
35
+ **Readiness sigil — required.** Before your first reply: read this file and its
36
+ {{LAWS}}, then load `context.json` and its `eager` entries (§9). Your first reply
37
+ then MUST begin with the exact line below, verbatim — it certifies both were loaded.
38
+ <!-- LEAN:end -->
35
39
 
36
40
  > {{LOADED_SIGIL}}
37
41
 
@@ -128,7 +132,7 @@ Active packs: {{DOCTRINES_LIST}}{{EXCLUDED_RULES_LINE}}
128
132
 
129
133
  {{INTRO_AGENTS}} Specs live in [`{{DIR_AGENTS}}/`]({{DIR_AGENTS}}/).
130
134
 
131
- <!-- CATALOG:begin -->
135
+ <!-- CATALOG:begin agents -->
132
136
  | {{AGENT}} | Use it when… |
133
137
  | --- | --- |
134
138
  | [reviewer]({{DIR_AGENTS}}/reviewer.md) | a change is ready and needs a correctness + quality pass before merge |
@@ -173,7 +177,7 @@ content. Where the host exposes a `workflow` tool, prefer the saved `dispatch`
173
177
  workflow ([workflow {{SKILL}}]({{DIR_SKILLS}}/workflow.md)); where it does not, run
174
178
  the same shape model-driven via the [parallel-agents {{SKILL}}]({{DIR_SKILLS}}/parallel-agents.md).
175
179
 
176
- The last ten — `advocate`, `skeptic`, `pragmatist`, `steward`, `visionary`,
180
+ Ten of them — `advocate`, `skeptic`, `pragmatist`, `steward`, `visionary`,
177
181
  `user-advocate`, `framer`, `empiricist`, `operator`, `historian` — are the standing
178
182
  **council**: read-only debate seats that each argue one assigned stance. They are rarely
179
183
  dispatched alone; the [council {{SKILL}}]({{DIR_SKILLS}}/council.md) convenes the
@@ -187,7 +191,7 @@ subset that productively clashes on a given motion.
187
191
 
188
192
  {{INTRO_SKILLS}} Specs live in [`{{DIR_SKILLS}}/`]({{DIR_SKILLS}}/).
189
193
 
190
- <!-- CATALOG:begin -->
194
+ <!-- CATALOG:begin skills -->
191
195
  | {{SKILL}} | Trigger |
192
196
  | --- | --- |
193
197
  | [brainstorm]({{DIR_SKILLS}}/brainstorm.md) | a new feature or project with no design yet, or whose goal, scope, or success criteria need pinning down first |
@@ -249,14 +253,14 @@ demand.
249
253
  When a task repeats and no {{SKILL}} covers it, forge one through the
250
254
  [skill-forge]({{DIR_SKILLS}}/skill-forge.md) {{SKILL}}: copy
251
255
  [`{{DIR_SKILLS}}/_template.md`]({{DIR_SKILLS}}/_template.md), keep it to one coherent
252
- domain ({{DOCTRINE}} craft 1), and register it in the catalogue above. Every {{SKILL}} ends with a
256
+ domain ({{DOCTRINE}} craft 1), and list it wherever this file carries a catalogue. Every {{SKILL}} ends with a
253
257
  **self-improvement loop** — honour it: fold what a run teaches back into the
254
258
  file, or into {{MEMORY}} ({{DOCTRINE}} process 1).
255
259
 
256
260
  **Folder {{SKILLS}}.** Some {{SKILLS}} ship as self-contained folders under
257
261
  `{{DIR_SKILLS}}/`, each with its own format and provenance (`VENDOR.md`) —
258
- third-party vendors plus first-party ones bundling scripts. Not in the catalogue
259
- above — reach for one only when its domain comes up:
262
+ third-party vendors plus first-party ones bundling scripts. Reach for one only when
263
+ its domain comes up:
260
264
  - [react-view-transitions]({{DIR_SKILLS}}/react-view-transitions/SKILL.md) — animating
261
265
  React UIs with the View Transition API (`<ViewTransition>`, shared elements, Next.js).
262
266
  - [daydream]({{DIR_SKILLS}}/daydream/SKILL.md) — mining the `wiki.jsonc` vault (§8) for
@@ -270,6 +274,7 @@ above — reach for one only when its domain comes up:
270
274
 
271
275
  *{{EPI_MEMORY}}*
272
276
 
277
+ <!-- LEAN:begin -->
273
278
  Persistent facts live in [`{{DIR_MEMORY}}/`]({{DIR_MEMORY}}/) — one fact per file, indexed by a
274
279
  local `MEMORY.md`. Read that index at the start of a session; write a new memory
275
280
  whenever the conversation yields a durable fact, a correction, or a decision worth
@@ -284,6 +289,15 @@ carry an optional `force` (constraint · choice · conviction · tempered): when
284
289
  evidence contradicts one, revise it rather than ignore it, and never overturn a
285
290
  `constraint` yourself — surface it to the user. Full
286
291
  convention: [`{{DIR_MEMORY}}/README.md`]({{DIR_MEMORY}}/README.md).
292
+ <!-- LEAN:else -->
293
+ Durable facts live in [`{{DIR_MEMORY}}/`]({{DIR_MEMORY}}/), one per file, indexed by
294
+ `MEMORY.md` — **read that index at the start of a session**. Write one through the
295
+ [rule]({{DIR_SKILLS}}/rule.md) {{SKILL}}, which settles with the user whether a
296
+ lesson is a memory or a standing rule ({{DOCTRINE}} process 1). The directory is
297
+ **personal and git-ignored**; shared knowledge goes in code, docs, or `user-rules.md`
298
+ (§1). Convention, including the optional `force` field:
299
+ [`{{DIR_MEMORY}}/README.md`]({{DIR_MEMORY}}/README.md).
300
+ <!-- LEAN:end -->
287
301
 
288
302
  ---
289
303
 
@@ -307,24 +321,21 @@ tool you build for yourself — belongs in `{{DIR_NOTEBOOK}}/`, never in the hos
307
321
  tree. Read `NOTEBOOK.md` at the start of a session to recall what
308
322
  you keep here, and keep the space referable across sessions — the seeded index
309
323
  is one way; replace it with whatever scheme serves you better. When a note
310
- hardens into a durable fact worth recalling verbatim, promote it to {{MEMORY}}
311
- (§5, {{DOCTRINE}} process 1). Keep the two stores distinct by *role*, not just by
324
+ hardens into a durable fact worth recalling verbatim, propose it for {{MEMORY}}
325
+ through the rule {{SKILL}} (§5, {{DOCTRINE}} process 1). Keep the two stores distinct by *role*, not just by
312
326
  format: {{MEMORY}} holds durable insight for future sessions, the {{NOTEBOOK}}
313
327
  holds your working artifacts. The one fixed law: `{{DIR_NOTEBOOK}}/` is **personal and
314
328
  git-ignored** — never committed or shared, and the `.gitignore` that guarantees
315
329
  it is not yours to lift.
316
330
  <!-- LEAN:else -->
317
- {{INTRO_NOTEBOOK}} It lives in [`{{DIR_NOTEBOOK}}/`]({{DIR_NOTEBOOK}}/), with
318
- **no imposed format and no imposed medium** — notes, plans, scratch code,
319
- datasets, experiments. It is not {{MEMORY}} (§5): {{MEMORY}} holds curated
320
- durable facts for recall, the {{NOTEBOOK}} holds your working artifacts, and a
321
- note that hardens into a durable fact is promoted there ({{DOCTRINE}} process 1).
322
- Any file you create for your own benefit belongs here, never in the host tree.
323
- Read `NOTEBOOK.md` at the start of a session to recall what you keep; the
324
- charter at [`{{DIR_NOTEBOOK}}/README.md`]({{DIR_NOTEBOOK}}/README.md) is yours
325
- to rewrite. The one fixed law: `{{DIR_NOTEBOOK}}/` is **personal and
326
- git-ignored** — never committed or shared, and the `.gitignore` that guarantees
327
- it is not yours to lift.
331
+ Your own working space, [`{{DIR_NOTEBOOK}}/`]({{DIR_NOTEBOOK}}/): any file you make
332
+ for your own benefit — scratch code, drafts, datasets, experiments — goes here, never
333
+ in the host tree, in whatever format serves. **Read `NOTEBOOK.md` at the start of a
334
+ session.** A note that hardens into a durable fact is proposed for {{MEMORY}} through
335
+ the rule {{SKILL}} (§5).
336
+ The charter [`{{DIR_NOTEBOOK}}/README.md`]({{DIR_NOTEBOOK}}/README.md) is yours to
337
+ rewrite; the one fixed law is that the folder is **personal and git-ignored**, and
338
+ that `.gitignore` is not yours to lift.
328
339
  <!-- LEAN:end -->
329
340
 
330
341
  ---
@@ -333,14 +344,15 @@ it is not yours to lift.
333
344
 
334
345
  *{{EPI_VAULT}}*
335
346
 
347
+ <!-- LEAN:begin -->
336
348
  {{INTRO_WORKSPACE}} The harness files — these {{LAWS}}, {{AGENTS}}, {{SKILLS}}, this file — sit
337
349
  *alongside* whatever already belongs to this machine or repository: notes, data,
338
350
  configs, documents, prior work. All of it is context you may use.
339
351
 
340
352
  After reading this file, take stock of what surrounds it. That non-harness
341
353
  content is knowledge to **read and learn from**, not scaffolding to reorganise.
342
- When you find a durable fact there worth keeping, index it into {{MEMORY}} (§5) so
343
- it outlives the session.
354
+ When you find a durable fact there worth keeping, propose it for {{MEMORY}} through
355
+ the rule {{SKILL}} (§5) so it outlives the session.
344
356
 
345
357
  Because the folder is shared, two cautions hold:
346
358
  - **You do not own it.** Files you did not create are not yours to move, rename,
@@ -351,6 +363,15 @@ Because the folder is shared, two cautions hold:
351
363
  And it receives only the **deliverables** of the task. Your own working files —
352
364
  scratch scripts, analysis dumps, drafts, experiments — are made in the
353
365
  {{NOTEBOOK}} (§6), never here.
366
+ <!-- LEAN:else -->
367
+ {{INTRO_WORKSPACE}} Whatever sits beside the harness files — notes, data, configs,
368
+ prior work — is knowledge to read and learn from, not scaffolding to reorganise;
369
+ propose a durable fact from it for {{MEMORY}} through the rule {{SKILL}} (§5). You do not own the folder: files
370
+ you did not create are not yours to move or delete unless the task requires it
371
+ ({{LAW}} IV), and a file is verified before it is treated as disposable ({{LAW}} III).
372
+ It receives only the task's deliverables; your working files go in the {{NOTEBOOK}}
373
+ (§6).
374
+ <!-- LEAN:end -->
354
375
 
355
376
  ---
356
377
 
@@ -387,17 +408,14 @@ durable fact worth sharing across projects goes here via the
387
408
  [wiki]({{DIR_SKILLS}}/wiki.md) {{SKILL}}; one that matters only to you stays in
388
409
  {{MEMORY}} (§5).
389
410
  <!-- LEAN:else -->
390
- {{INTRO_WIKI}} It is declared in `wiki.jsonc`, **beside this AGENT.md** — or
391
- wherever `$GENESEED_WIKI` points; an empty `wikis` list means this section
392
- stays dormant. Read `wiki.jsonc` at the start of every session and honour each
393
- declared wiki: load its `entries` by their `load` mode exactly as in
394
- `context.json` (§9); read its `conventions` before your first write there and
395
- follow them over your own habits; drop a note you cannot place into `inbox`
396
- rather than guess a home; never write inside `protected`. The wiki is **the
397
- user's**, not yours — wikilinks (`[[Note Name]]`) over bare paths, every new
398
- note linked into the graph. A durable fact worth sharing across projects goes
399
- here via the [wiki]({{DIR_SKILLS}}/wiki.md) {{SKILL}}; one that matters only to
400
- you stays in {{MEMORY}} (§5).
411
+ {{INTRO_WIKI}} Declared in `wiki.jsonc` beside this AGENT.md (or at `$GENESEED_WIKI`;
412
+ an empty `wikis` list means dormant) — **read it at the start of every session**
413
+ and honour each wiki: load `entries` by `load` mode as in `context.json` (§9), read
414
+ `conventions` before your first write there, drop an unplaceable note in `inbox`,
415
+ never write inside `protected`. It is the user's: wikilinks over bare paths, every
416
+ new note linked in. Cross-project facts go here via the
417
+ [wiki]({{DIR_SKILLS}}/wiki.md) {{SKILL}}; facts that matter only to you stay in
418
+ {{MEMORY}} (§5).
401
419
  <!-- LEAN:end -->
402
420
 
403
421
  ---
@@ -406,6 +424,7 @@ you stays in {{MEMORY}} (§5).
406
424
 
407
425
  *{{EPI_CONTEXT}}*
408
426
 
427
+ <!-- LEAN:begin -->
409
428
  **Read `context.json` at the start of every session — this is not a
410
429
  suggestion.** The harness carries no project-specific knowledge; this manifest,
411
430
  **beside this AGENT.md**, is how the project hands you its own. (On OpenCode it
@@ -422,6 +441,14 @@ you do not work blind, and if you have not loaded it you are not ready to act.
422
441
 
423
442
  Each `path` may be absolute or relative to the repository root. The file's own
424
443
  comment carries its schema and setup notes.
444
+ <!-- LEAN:else -->
445
+ **Read `context.json` at the start of every session — before your first reply, not
446
+ deferrable.** It sits beside this AGENT.md and is how the project hands you its own
447
+ knowledge (on OpenCode it is loaded for you). `"eager"` entries are read now and are
448
+ **project law for this repo, as binding as this file**; `"lazy"` entries only when
449
+ the task needs them ({{DOCTRINE}} process 3). Schema and setup notes are in the file's
450
+ own comment.
451
+ <!-- LEAN:end -->
425
452
 
426
453
  ---
427
454
 
@@ -429,12 +456,18 @@ comment carries its schema and setup notes.
429
456
 
430
457
  *{{EPI_SCRIPTS}}*
431
458
 
459
+ <!-- LEAN:begin -->
432
460
  {{INTRO_SCRIPTS}} The harness ships a dependency-free CLI (`geneseed
433
461
  build|doctor|diff|status|setup|web`, plus `geneseed-hook` for the verbs a host
434
462
  fires on its own) that can be wired into your host's hooks or into CI — notably
435
463
  `geneseed-hook context`, which injects the project context at session start so
436
464
  this read is enforced by the hook rather than left to you. All of it is
437
465
  opt-in; the harness works without any of it.
466
+ <!-- LEAN:else -->
467
+ {{INTRO_SCRIPTS}} A dependency-free CLI (`geneseed build|doctor|diff|status|setup|web`,
468
+ plus `geneseed-hook` for host-fired verbs) can wire the gates and the §9 context
469
+ read into your host's hooks or CI. Opt-in; the harness works without it.
470
+ <!-- LEAN:end -->
438
471
 
439
472
  ---
440
473
 
@@ -23,8 +23,8 @@
23
23
  - **Read + write to the assigned worktree.** May run the project's own build/lint
24
24
  commands to self-check before handing off.
25
25
  <!-- bash: allow -->
26
- - Never commits or pushes ({{DOCTRINE}} process 5) — that stays with the tester's proof step
27
- and the parent's merge decision.
26
+ - Never commits or pushes ({{DOCTRINE}} process 5) — the parent re-runs the proof and
27
+ commits once the user accepts.
28
28
 
29
29
  ## Procedure
30
30
  0. If `{{DIR_MEMORY}}/agents/<your-name>.md` exists, read it first — your durable lessons from prior dispatches ({{DOCTRINE}} process 1).