geneseed 3.8.0 → 3.10.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 (229) hide show
  1. package/CHANGELOG.md +71 -1
  2. package/DESIGN.md +13 -10
  3. package/QUICKSTART.md +1 -1
  4. package/README.md +31 -32
  5. package/SETUP.md +7 -42
  6. package/SHIPPED.md +4 -4
  7. package/adapters/bob/README.md +17 -1
  8. package/adapters/openclaude/README.md +1 -1
  9. package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
  10. package/adapters/opencode/README.md +3 -3
  11. package/adapters/opencode/plugins/geneseed-context.js +78 -8
  12. package/adapters/opencode/plugins/geneseed-guard.js +9 -9
  13. package/adapters/opencode/plugins/geneseed-workflow.js +1 -1
  14. package/adapters/opencode/workflows/_runtime.js +2 -2
  15. package/adapters/opencode/workflows/dispatch.js +1 -1
  16. package/bin/geneseed-hook.mjs +1 -1
  17. package/docs/candidate-hosts.md +12 -5
  18. package/docs/extending.md +72 -40
  19. package/docs/token-footprint.md +26 -21
  20. package/docs/web/claude-hooks.md +1 -1
  21. package/docs/web/collaboration.md +1 -1
  22. package/docs/web/exclusions.md +0 -1
  23. package/docs/web/footprint.md +1 -1
  24. package/docs/web/harnesses.md +14 -14
  25. package/docs/web/install-quick.md +1 -1
  26. package/docs/web/plugin-guard.md +2 -2
  27. package/docs/web/rules.md +1 -1
  28. package/docs/web/themes-author.md +2 -2
  29. package/docs/web-ui.md +2 -2
  30. package/harness.config.json +1 -1
  31. package/js/README.md +8 -7
  32. package/js/build/driver.mjs +26 -95
  33. package/js/build/emit-claude.mjs +76 -81
  34. package/js/build/generate.mjs +0 -1
  35. package/js/build/render.mjs +10 -3
  36. package/js/build/source.mjs +105 -1
  37. package/js/build/stubs.mjs +25 -4
  38. package/js/cli-table.json +8 -8
  39. package/js/hosts/hooks.mjs +91 -59
  40. package/js/hosts/hosts.mjs +5 -21
  41. package/js/hosts/installs.mjs +4 -6
  42. package/js/hosts/mcp.mjs +3 -13
  43. package/js/hosts/native.mjs +28 -51
  44. package/js/hosts/opencode.mjs +1 -1
  45. package/js/hosts/settings.mjs +4 -78
  46. package/js/inspect/checks-authoring.mjs +153 -117
  47. package/js/inspect/checks-repo.mjs +45 -1
  48. package/js/inspect/diff.mjs +11 -3
  49. package/js/inspect/doctor.mjs +3 -3
  50. package/js/inspect/excludes.mjs +0 -4
  51. package/js/inspect/inventory.mjs +1 -2
  52. package/js/inspect/status.mjs +4 -4
  53. package/js/maintain/migrate.mjs +5 -5
  54. package/js/maintain/setup.mjs +3 -5
  55. package/js/maintain/uninstall.mjs +8 -42
  56. package/js/ui/tui.mjs +1 -1
  57. package/js/web/actions.mjs +4 -5
  58. package/js/web/api.mjs +10 -5
  59. package/package.json +1 -2
  60. package/registry.json +7 -0
  61. package/src/AGENT.md.tmpl +86 -65
  62. package/src/agents/_template.md +3 -3
  63. package/src/agents/advocate.md +3 -3
  64. package/src/agents/architect.md +3 -3
  65. package/src/agents/developer.md +4 -4
  66. package/src/agents/docs.md +3 -3
  67. package/src/agents/empiricist.md +4 -4
  68. package/src/agents/explorer.md +4 -4
  69. package/src/agents/framer.md +3 -3
  70. package/src/agents/historian.md +3 -3
  71. package/src/agents/operator.md +3 -3
  72. package/src/agents/pragmatist.md +3 -3
  73. package/src/agents/researcher.md +7 -7
  74. package/src/agents/reviewer.md +3 -3
  75. package/src/agents/security.md +3 -3
  76. package/src/agents/skeptic.md +3 -3
  77. package/src/agents/steward.md +3 -3
  78. package/src/agents/tester.md +3 -3
  79. package/src/agents/user-advocate.md +3 -3
  80. package/src/agents/visionary.md +3 -3
  81. package/src/doctrines/README.md +4 -3
  82. package/src/doctrines/comms.md +4 -4
  83. package/src/doctrines/craft.md +20 -20
  84. package/src/doctrines/ops.md +19 -19
  85. package/src/doctrines/process.md +20 -20
  86. package/src/doctrines/rigor.md +22 -22
  87. package/src/laws/universal.md +26 -44
  88. package/src/memory/README.md +3 -3
  89. package/src/modes/foreman.md +1 -1
  90. package/src/notebook/README.md +1 -1
  91. package/src/ontology/universal.md +8 -10
  92. package/src/postures/artisan.md +2 -2
  93. package/src/skills/_self-improvement.md +4 -4
  94. package/src/skills/_template.md +1 -1
  95. package/src/skills/brainstorm.md +3 -3
  96. package/src/skills/bruno-collection-generator.md +6 -6
  97. package/src/skills/bruno-test-writer.md +5 -5
  98. package/src/skills/ci-fix.md +4 -4
  99. package/src/skills/commit.md +11 -10
  100. package/src/skills/consolidate-memory.md +4 -4
  101. package/src/skills/council.md +2 -2
  102. package/src/skills/daydream/SKILL.md +10 -3
  103. package/src/skills/daydream/VENDOR.md +10 -2
  104. package/src/skills/daydream/instructions.md +2 -2
  105. package/src/skills/debug.md +4 -4
  106. package/src/skills/deps-audit.md +3 -3
  107. package/src/skills/develop.md +7 -7
  108. package/src/skills/document-project.md +6 -6
  109. package/src/skills/explain-changes/SKILL.md +69 -0
  110. package/src/skills/explain-changes/VENDOR.md +20 -0
  111. package/src/skills/explain-changes/scripts/render_changes.mjs +558 -0
  112. package/src/skills/forge-mcp.md +4 -4
  113. package/src/skills/fresh-eyes.md +4 -4
  114. package/src/skills/frontend-design.md +1 -1
  115. package/src/skills/geneseed.md +4 -4
  116. package/src/skills/git-archaeology.md +5 -5
  117. package/src/skills/git-rescue.md +7 -7
  118. package/src/skills/handoff.md +3 -3
  119. package/src/skills/ingest.md +5 -5
  120. package/src/skills/migrate.md +8 -8
  121. package/src/skills/parallel-agents.md +2 -2
  122. package/src/skills/pipeline.md +2 -2
  123. package/src/skills/plan.md +3 -3
  124. package/src/skills/ponytail.md +2 -2
  125. package/src/skills/profile.md +2 -2
  126. package/src/skills/prose.md +1 -1
  127. package/src/skills/prototype.md +1 -1
  128. package/src/skills/react-view-transitions/SKILL.md +1 -1
  129. package/src/skills/react-view-transitions/VENDOR.md +7 -2
  130. package/src/skills/refactor.md +3 -3
  131. package/src/skills/release.md +5 -5
  132. package/src/skills/repo-map.md +4 -4
  133. package/src/skills/research.md +9 -9
  134. package/src/skills/review-response.md +4 -4
  135. package/src/skills/rule.md +3 -3
  136. package/src/skills/security-audit.md +5 -5
  137. package/src/skills/ship.md +5 -5
  138. package/src/skills/skill-forge.md +4 -4
  139. package/src/skills/token-report/SKILL.md +2 -3
  140. package/src/skills/token-report/VENDOR.md +2 -3
  141. package/src/skills/token-report/scripts/token_report.mjs +7 -121
  142. package/src/skills/wiki.md +5 -5
  143. package/src/skills/workflow.md +3 -4
  144. package/src/skills/worktree.md +4 -4
  145. package/themes/_TEMPLATE.json +37 -39
  146. package/themes/biker.json +37 -39
  147. package/themes/commentator.json +37 -39
  148. package/themes/cyberpunk.json +37 -39
  149. package/themes/gamer.json +37 -39
  150. package/themes/imperial.json +37 -39
  151. package/themes/joker.json +37 -39
  152. package/themes/marvin.json +37 -39
  153. package/themes/mean.json +37 -39
  154. package/themes/military.json +37 -39
  155. package/themes/neutral.json +37 -39
  156. package/themes/pirate.json +37 -39
  157. package/themes/sports.json +37 -39
  158. package/themes/verstappen.json +37 -39
  159. package/themes/wizard.json +37 -39
  160. package/web/dist/assets/Activity-BjVp522q.js +1 -0
  161. package/web/dist/assets/ActivityDetail-8ep1bspW.js +1 -0
  162. package/web/dist/assets/Dashboard-OkC_PCH6.js +2 -0
  163. package/web/dist/assets/{Docs-DrJNDPwI.js → Docs-CKPWz00o.js} +2 -2
  164. package/web/dist/assets/FilterInput-CVyqmYWM.js +1 -0
  165. package/web/dist/assets/GreenhouseView-D0cbCJ5m.js +2 -0
  166. package/web/dist/assets/Installs-BtGGdoTt.js +2 -0
  167. package/web/dist/assets/JournalView-Ckl4dmzR.js +1 -0
  168. package/web/dist/assets/Laws-CLpdRRk8.js +3 -0
  169. package/web/dist/assets/Library-wPPuKQrI.js +1 -0
  170. package/web/dist/assets/{Markdown-BQAvN8xx.js → Markdown-CkFyfgNf.js} +14 -14
  171. package/web/dist/assets/OperatorHudView-CiAkAkQL.js +2 -0
  172. package/web/dist/assets/Personal-D8CnDo9v.js +1 -0
  173. package/web/dist/assets/Tabs-B4HNrrCs.js +1 -0
  174. package/web/dist/assets/drawable-BiwaOo6x.js +1 -0
  175. package/web/dist/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
  176. package/web/dist/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
  177. package/web/dist/assets/index-C1yil5j1.js +2 -0
  178. package/web/dist/assets/index-CDPDk2vj.css +1 -0
  179. package/web/dist/assets/{react-DbkvxWG5.js → react-B9Z_tXRV.js} +1 -1
  180. package/web/dist/assets/sections-GalMS0Zv.js +1 -0
  181. package/web/dist/assets/usePoll-O0sJEw1Y.js +1 -0
  182. package/web/dist/index.html +4 -4
  183. package/web/src/pages/Laws.jsx +616 -361
  184. package/adapters/copilot/README.md +0 -105
  185. package/web/dist/assets/Activity-CH-7cPIM.js +0 -1
  186. package/web/dist/assets/ActivityDetail-DZvz0xS5.js +0 -1
  187. package/web/dist/assets/CatalogRow-DftXpZHY.js +0 -1
  188. package/web/dist/assets/Diff-DvwdG0L9.js +0 -2
  189. package/web/dist/assets/Doctor-OTmwmYZg.js +0 -1
  190. package/web/dist/assets/FilterInput-BakJLvaV.js +0 -1
  191. package/web/dist/assets/GreenhouseView-XxrAdriX.js +0 -2
  192. package/web/dist/assets/Harness-C_LYNk5c.js +0 -1
  193. package/web/dist/assets/JournalView-BcuyeEaR.js +0 -1
  194. package/web/dist/assets/Laws-CWHrTKY4.js +0 -3
  195. package/web/dist/assets/Library-CA9IHghn.js +0 -1
  196. package/web/dist/assets/OperatorHudView-BMRY1Tt4.js +0 -2
  197. package/web/dist/assets/Profile-wceET1JY.js +0 -1
  198. package/web/dist/assets/Rules-BEXvY4Tx.js +0 -1
  199. package/web/dist/assets/Settings-DS2zF0Ez.js +0 -1
  200. package/web/dist/assets/Skills-DahlOJuI.js +0 -1
  201. package/web/dist/assets/StatusBadge-Cg6j-NJL.js +0 -1
  202. package/web/dist/assets/activity-iWaWVtsR.js +0 -1
  203. package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
  204. package/web/dist/assets/drawable-BTHjTzF7.js +0 -1
  205. package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
  206. package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
  207. package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
  208. package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  209. package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  210. package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  211. package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
  212. package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
  213. package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
  214. package/web/dist/assets/index-AN5Emnrg.js +0 -2
  215. package/web/dist/assets/index-Ruh93A3A.css +0 -1
  216. package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  217. package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  218. package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  219. package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  220. package/web/dist/assets/lawCats-Btjp3Hk_.js +0 -1
  221. package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
  222. package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
  223. package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
  224. package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
  225. package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  226. package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  227. package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  228. package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  229. package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +0 -1
@@ -11,7 +11,7 @@ import { loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
11
11
  import { ensureAgentOverridesStub } from '../hosts/opencode.mjs';
12
12
  import {
13
13
  managedBlockRemove, managedBlockWrite, mergeClaudeSettings,
14
- unwireClaudeExcludes, unwireClaudeSettings, unwireCopilotSettings, wireClaudeExcludes,
14
+ unwireClaudeExcludes, unwireClaudeSettings, wireClaudeExcludes,
15
15
  } from '../hosts/settings.mjs';
16
16
  import { writeText } from '../lib/fs.mjs';
17
17
  import { isTruthy } from '../lib/json.mjs';
@@ -21,7 +21,8 @@ import {
21
21
  } from './emit-common.mjs';
22
22
  import { renderAll, renderFile } from './render.mjs';
23
23
  import {
24
- BOB_RULES_STUB, ensureExcludesStub, ensureMemoryIndex, ensureNotebookIndex, ensureProfileStub,
24
+ BOB_RULES_STUB, ensureContextStub, ensureExcludesStub, ensureMemoryIndex, ensureNotebookIndex,
25
+ ensureProfileStub,
25
26
  ensureRulesStub, ensureWikiStub,
26
27
  } from './stubs.mjs';
27
28
  import { writeVersion } from './version.mjs';
@@ -36,7 +37,7 @@ import { existsSync, mkdirSync } from 'node:fs';
36
37
  * `claudeWire` below for the CLAUDE.md managed-block merge and the settings file. PRUNE,
37
38
  * MANIFEST and VERIFY are the driver's, in `bin/build-driver.mjs`.
38
39
  *
39
- * Six emits route through here — claude, bob and copilot at both scopes — so this one
40
+ * Six emits route through here — claude, bob and openclaude at both scopes — so this one
40
41
  * job is what puts two thirds of the matrix on the seam.
41
42
  *
42
43
  * `claudeMdText` is the payload's one item that is not a file this process wrote. WIRE
@@ -76,9 +77,11 @@ export function emitClaudeRender(cfg, job) {
76
77
  // `themed_rel` and `lstripNewlines` are, so the two verdicts stay distinguishable.
77
78
  if (!prefix) return tmplItem.text;
78
79
  const ptheme = { ...theme };
79
- for (const tok of ['DIR_LAWS', 'DIR_AGENTS', 'DIR_SKILLS', 'DIR_MEMORY', 'DIR_NOTEBOOK']) {
80
+ for (const tok of ['DIR_LAWS', 'DIR_AGENTS', 'DIR_SKILLS', 'DIR_MEMORY', 'DIR_NOTEBOOK',
81
+ 'DIR_DOCTRINES']) {
80
82
  ptheme[tok] = prefix + (ptheme[tok] ?? tok.split('_').slice(1).join('_').toLowerCase());
81
83
  }
84
+ ptheme.AT_HARNESS = prefix;
82
85
  // Same catalogue decision as the renderAll above — a re-render that forgot it would
83
86
  // quietly put the stripped tables back.
84
87
  return renderFile(cfg, tmplItem.src, ptheme, footprint, '', new Set(), nativeCatalog);
@@ -87,7 +90,6 @@ export function emitClaudeRender(cfg, job) {
87
90
  const owned = [];
88
91
  const agentText = items.find((i) => i.rel === 'AGENT.md' && i.text !== null)?.text ?? null;
89
92
  const isBob = host === 'bob';
90
- const isCopilot = host === 'copilot';
91
93
 
92
94
  // Bob's always-injected channel is the rules folder. At GLOBAL scope rules/geneseed.md
93
95
  // IS the preamble (a global ~/.bob/AGENTS.md is not auto-loaded), and its pointers need
@@ -102,19 +104,30 @@ export function emitClaudeRender(cfg, job) {
102
104
  }
103
105
 
104
106
  ensureAgentOverridesStub(cfg, cfgDir);
105
- // Bob's agents/skills use the Claude dialect verbatim; Copilot has its own frontmatter.
107
+ // Bob gives the model a skill's text but not its directory, so a folder skill's
108
+ // `<this-skill-directory>` is resolved here. Per repo it is relative to the workspace root,
109
+ // where Bob runs commands, so a committed `.bob/` stays valid on a teammate's checkout;
110
+ // globally there is no workspace to be relative to, so it is absolute.
111
+ const skillDirOf = !isBob ? null
112
+ : scope === 'project' ? (name) => relPosix(path.dirname(cfgDir), path.join(cfgDir, 'skills', name))
113
+ : (name) => path.join(cfgDir, 'skills', name);
114
+ // Bob's and OpenClaude's agents/skills use the Claude dialect verbatim.
106
115
  // `manifestExisted` is deliberately not passed: the Python does not pass it either, so
107
116
  // the pre-manifest header line is unreachable from this emit on both sides.
108
117
  const { nAgents, nSkills, written } = writeNativeLayer(
109
118
  items, path.join(cfgDir, 'agents'), path.join(cfgDir, 'skills'),
110
119
  loadAgentOverrides(cfgDir),
111
- { host: isCopilot ? 'copilot' : 'claude', oldOwned, cfg: cfgDir, src: cfg.src });
120
+ { host: 'claude', oldOwned, cfg: cfgDir, src: cfg.src, skillDirOf });
112
121
  for (const p of written) owned.push(relPosix(cfgDir, p));
113
122
 
114
123
  const memStatus = globalMemory(cfgDir, items, out, cfg.src);
115
124
  ensureMemoryIndex(path.join(cfgDir, 'memory'));
116
125
  const nbStatus = globalNotebook(cfgDir, items, out, cfg.src);
117
126
  ensureNotebookIndex(path.join(cfgDir, 'notebook'));
127
+ // context.json too: the root names it as the project's own manifest, and until 2026-10 this
128
+ // emit never seeded one — the bundle and OpenCode did — so §9 pointed at a file that was not
129
+ // there. The context hook reads it from this dir (`resolveContextSets`' last candidate).
130
+ ensureContextStub(cfgDir);
118
131
  ensureWikiStub(cfgDir);
119
132
  ensureRulesStub(cfgDir);
120
133
  ensureProfileStub(cfgDir);
@@ -128,7 +141,7 @@ export function emitClaudeRender(cfg, job) {
128
141
  const gi = path.join(cfgDir, '.gitignore');
129
142
  const giLines = (host === 'claude' || host === 'openclaude' ? ['settings.local.json']
130
143
  : host === 'bob' ? ['settings.json'] : [])
131
- .concat(['wiki.jsonc', 'agent-overrides.json']);
144
+ .concat(['context.json', 'wiki.jsonc', 'agent-overrides.json']);
132
145
  if (!existsSync(gi)) {
133
146
  writeText(gi, `${giLines.join('\n')}\n`);
134
147
  owned.push('.gitignore');
@@ -194,7 +207,6 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
194
207
  const old = oldManaged && typeof oldManaged === 'object' && !Array.isArray(oldManaged)
195
208
  ? oldManaged : {};
196
209
  const isBob = host === 'bob';
197
- const isCopilot = host === 'copilot';
198
210
  const managed = {};
199
211
 
200
212
  // CLAUDE.md — Claude auto-loads it by location; merge as a delimited block so any user
@@ -228,80 +240,63 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
228
240
  // settings.local.json — the personal, untracked file — never the team-shared
229
241
  // settings.json, which would hand every teammate failing hooks pointing at this machine's
230
242
  // node and this machine's checkout. (Bob documents no local variant, so it keeps
231
- // settings.json.) Copilot's settings are a different shape entirely (one command per
232
- // event, no matcher groups) and take the `else` branch below; its PROJECT scope wires
233
- // nothing, because the CLI documents hooks in `~/.copilot/settings.json` only and a
234
- // machine-absolute hook committed into `.github/` would fail on every teammate's machine.
235
- if (!isCopilot) {
236
- // Bob GLOBAL: its hooks doc puts the global file at `~/.bob/settings/settings.json`
237
- // (nested), where an older emit wrote `~/.bob/settings.json`; the `oldSf !==
238
- // settingsName` branch below unwires the old file, so a re-emit migrates. Bob's project
239
- // file stays `.bob/settings.json`.
240
- const settingsName = scope === 'project' && !isBob ? 'settings.local.json'
241
- : isBob && scope === 'global' ? path.join('settings', 'settings.json') : 'settings.json';
242
- const settingsPath = path.join(cfgDir, settingsName);
243
- mkdirSync(path.dirname(settingsPath), { recursive: true });
244
- managed.settings_file = settingsName;
245
- // Migration: an older install wired hooks/excludes into a different file — unwire the
246
- // recorded claims there, or they linger (and run) forever.
247
- const oldSf = get(old, 'settings_file') || 'settings.json';
248
- if (oldSf !== settingsName) {
249
- unwireClaudeSettings(path.join(cfgDir, oldSf), get(old, 'settings_hooks') || []);
250
- unwireClaudeExcludes(path.join(cfgDir, oldSf), get(old, 'settings_excludes') || []);
251
- }
252
- // The merge prunes recorded groups that are no longer canonical and returns the
253
- // COMPLETE current claim set — store it as-is; unioning with prior would resurrect the
254
- // stale claims.
255
- // `doctrines` decides whether the git-gate group is part of the canonical claim set. It
256
- // travels from `cfg` rather than being re-read off the deployment: this is the emit that
257
- // DECIDES the install's packs, so the marker on disk is still the previous build's.
258
- const [, managedHooks] = mergeClaudeSettings(
259
- settingsPath, scope, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
260
- doctrines, excludeRules, host, cfgDir,
261
- );
262
- managed.settings_hooks = managedHooks;
243
+ // settings.json.)
244
+ // Bob GLOBAL: its hooks doc puts the global file at `~/.bob/settings/settings.json`
245
+ // (nested), where an older emit wrote `~/.bob/settings.json`; the `oldSf !==
246
+ // settingsName` branch below unwires the old file, so a re-emit migrates. Bob's project
247
+ // file stays `.bob/settings.json`.
248
+ const settingsName = scope === 'project' && !isBob ? 'settings.local.json'
249
+ : isBob && scope === 'global' ? path.join('settings', 'settings.json') : 'settings.json';
250
+ const settingsPath = path.join(cfgDir, settingsName);
251
+ mkdirSync(path.dirname(settingsPath), { recursive: true });
252
+ managed.settings_file = settingsName;
253
+ // Migration: an older install wired hooks/excludes into a different file — unwire the
254
+ // recorded claims there, or they linger (and run) forever.
255
+ const oldSf = get(old, 'settings_file') || 'settings.json';
256
+ if (oldSf !== settingsName) {
257
+ unwireClaudeSettings(path.join(cfgDir, oldSf), get(old, 'settings_hooks') || []);
258
+ unwireClaudeExcludes(path.join(cfgDir, oldSf), get(old, 'settings_excludes') || []);
259
+ }
260
+ // The merge prunes recorded groups that are no longer canonical and returns the
261
+ // COMPLETE current claim set — store it as-is; unioning with prior would resurrect the
262
+ // stale claims.
263
+ // `doctrines` decides whether the git-gate group is part of the canonical claim set. It
264
+ // travels from `cfg` rather than being re-read off the deployment: this is the emit that
265
+ // DECIDES the install's packs, so the marker on disk is still the previous build's.
266
+ const [, managedHooks] = mergeClaudeSettings(
267
+ settingsPath, scope, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
268
+ doctrines, excludeRules, host, cfgDir,
269
+ );
270
+ managed.settings_hooks = managedHooks;
263
271
 
264
- // Project-bypasses-global (Claude only): a PROJECT install suppresses the GLOBAL
265
- // ~/.claude/CLAUDE.md while cwd is this repo, via Claude's native claudeMdExcludes.
266
- // Written only when this run actually emitted the project's own preamble (never
267
- // suppress with no replacement); GENESEED_STACK_GLOBAL=1 opts out, and a re-emit with
268
- // it set strips a prior exclude. Bob never gets one: its bypass is the same-named
269
- // workspace rules file.
270
- const priorRaw = get(old, 'settings_excludes');
271
- const priorExcl = Array.isArray(priorRaw) ? priorRaw : [];
272
- if (scope === 'project' && hasAgentText && preambleExclude) {
273
- const wantExcl = [preambleExclude];
274
- if (process.env.GENESEED_STACK_GLOBAL) {
275
- unwireClaudeExcludes(settingsPath, wantExcl);
276
- managed.settings_excludes = [];
277
- } else {
278
- const addedExcl = wireClaudeExcludes(settingsPath, wantExcl);
279
- // Claim only what Geneseed itself wired (prior + newly added) — folding `wantExcl`
280
- // in unconditionally would claim a user's own pre-existing exclude, and uninstall
281
- // would then strip it. `sorted(set(a) | set(b))` — Python sorts strings by code
282
- // point and so does the default `Array.sort`, which is why no comparator is passed.
283
- managed.settings_excludes = [...new Set([...priorExcl, ...addedExcl])].sort();
284
- }
285
- } else if (priorExcl.length && isBob) {
286
- // Self-heal older Bob installs: earlier versions wrote the global AGENTS.md into
287
- // claudeMdExcludes here. The key is Claude-only and its Bob semantics are unknown, so
288
- // a re-emit removes it instead of carrying it forward.
289
- unwireClaudeExcludes(settingsPath, priorExcl);
290
- } else if (priorExcl.length) {
291
- managed.settings_excludes = priorExcl;
272
+ // Project-bypasses-global (Claude only): a PROJECT install suppresses the GLOBAL
273
+ // ~/.claude/CLAUDE.md while cwd is this repo, via Claude's native claudeMdExcludes.
274
+ // Written only when this run actually emitted the project's own preamble (never
275
+ // suppress with no replacement); GENESEED_STACK_GLOBAL=1 opts out, and a re-emit with
276
+ // it set strips a prior exclude. Bob never gets one: its bypass is the same-named
277
+ // workspace rules file.
278
+ const priorRaw = get(old, 'settings_excludes');
279
+ const priorExcl = Array.isArray(priorRaw) ? priorRaw : [];
280
+ if (scope === 'project' && hasAgentText && preambleExclude) {
281
+ const wantExcl = [preambleExclude];
282
+ if (process.env.GENESEED_STACK_GLOBAL) {
283
+ unwireClaudeExcludes(settingsPath, wantExcl);
284
+ managed.settings_excludes = [];
285
+ } else {
286
+ const addedExcl = wireClaudeExcludes(settingsPath, wantExcl);
287
+ // Claim only what Geneseed itself wired (prior + newly added) — folding `wantExcl`
288
+ // in unconditionally would claim a user's own pre-existing exclude, and uninstall
289
+ // would then strip it. `sorted(set(a) | set(b))` — Python sorts strings by code
290
+ // point and so does the default `Array.sort`, which is why no comparator is passed.
291
+ managed.settings_excludes = [...new Set([...priorExcl, ...addedExcl])].sort();
292
292
  }
293
- } else if (scope === 'global') {
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.
298
- const settingsPath = path.join(cfgDir, 'settings.json');
299
- managed.settings_file = 'settings.json';
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;
293
+ } else if (priorExcl.length && isBob) {
294
+ // Self-heal older Bob installs: earlier versions wrote the global AGENTS.md into
295
+ // claudeMdExcludes here. The key is Claude-only and its Bob semantics are unknown, so
296
+ // a re-emit removes it instead of carrying it forward.
297
+ unwireClaudeExcludes(settingsPath, priorExcl);
298
+ } else if (priorExcl.length) {
299
+ managed.settings_excludes = priorExcl;
305
300
  }
306
301
  return managed;
307
302
  }
@@ -173,7 +173,6 @@ export const DEFAULT_EMIT = new Map([
173
173
  ['opencode global', 'opencode-global'], ['opencode project', 'opencode'],
174
174
  ['claude global', 'claude-global'], ['claude project', 'claude'],
175
175
  ['bob global', 'bob-global'], ['bob project', 'bob'],
176
- ['copilot global', 'copilot-global'], ['copilot project', 'copilot'],
177
176
  ['openclaude global', 'openclaude-global'], ['openclaude project', 'openclaude'],
178
177
  ]);
179
178
 
@@ -34,7 +34,7 @@ import { readText } from '../lib/fs.mjs';
34
34
  import { parseJson } from '../lib/json.mjs';
35
35
  import { normcase, comparePaths } from '../lib/paths.mjs';
36
36
  // `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
37
- import { PACK_ORDER, readPackText } from './source.mjs';
37
+ import { PACK_ORDER, readPackText, resolveRuleIds } from './source.mjs';
38
38
 
39
39
  /** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
40
40
  export const STRUCTURE = {
@@ -42,6 +42,10 @@ export const STRUCTURE = {
42
42
  SCRIPT: 'Script', SCRIPTS: 'Scripts',
43
43
  DIR_LAWS: 'laws', DIR_AGENTS: 'agents', DIR_SKILLS: 'skills', DIR_MEMORY: 'memory',
44
44
  DIR_NOTEBOOK: 'notebook', DIR_ONTOLOGY: 'ontology', DIR_DOCTRINES: 'doctrines',
45
+ // Where the harness's own files sit, as seen from the root instruction file: empty when they
46
+ // are its siblings (`files`, OpenCode), `.claude/`-style when the root sits a level above them —
47
+ // set per emit by `prefixedAgentText` in js/build/emit-claude.mjs, like the DIR_* prefixes.
48
+ AT_HARNESS: '',
45
49
  // Ontology section names. Theme-INDEPENDENT on purpose: a citation reads
46
50
  // `({{ONTOLOGY}}: {{ONT_TELOS}})` — token on both sides, so the heading and every
47
51
  // reference to it move together and a rename can never desync them.
@@ -216,7 +220,8 @@ function readSource(cfg, p) {
216
220
  let seen = SRC_TEXT.get(cfg);
217
221
  if (!seen) SRC_TEXT.set(cfg, (seen = new Map()));
218
222
  let text = seen.get(p);
219
- if (text === undefined) seen.set(p, (text = readText(p)));
223
+ // Rule ids resolve here, once per file, so every stage after sees the addressed form.
224
+ if (text === undefined) seen.set(p, (text = resolveRuleIds(readText(p), cfg.src)));
220
225
  return text;
221
226
  }
222
227
 
@@ -250,7 +255,9 @@ export function renderFile(cfg, filePath, theme, footprint = 'full', lawsPrefix
250
255
  function registerBody(cfg, theme, dir, selected, fallback) {
251
256
  for (const name of [selected, fallback]) {
252
257
  const p = path.join(cfg.src, dir, `${name}.md`);
253
- if (existsSync(p) && statSync(p).isFile()) return substitute(readText(p), theme).trim();
258
+ if (existsSync(p) && statSync(p).isFile()) {
259
+ return substitute(resolveRuleIds(readText(p), cfg.src), theme).trim();
260
+ }
254
261
  }
255
262
  return '';
256
263
  }
@@ -37,7 +37,7 @@ import path from 'node:path';
37
37
  * on raw or folded text, so one behaviour serves both.
38
38
  */
39
39
  const _packTexts = new Map();
40
- export function readPackText(file) {
40
+ export function readRawText(file) {
41
41
  const key = path.resolve(file);
42
42
  const mtime = statSync(file).mtimeMs;
43
43
  const hit = _packTexts.get(key);
@@ -47,6 +47,110 @@ export function readPackText(file) {
47
47
  return text;
48
48
  }
49
49
 
50
+ /**
51
+ * A doctrine pack as the render sees it: ids resolved, so the heading reads
52
+ * `### {{DOCTRINE}} process 5 — {{DOC_CONSENT_BEFORE_PUSH}}` again. Every reader that keys on the
53
+ * `<pack> <n>` address (`knownRuleIds`, the exclusion filter, the survives gate) goes through
54
+ * here and keeps working unchanged — the address is computed, only the source stopped typing it.
55
+ */
56
+ export function readPackText(file) {
57
+ return resolveRuleIds(readRawText(file), path.dirname(path.dirname(file)));
58
+ }
59
+
60
+ /**
61
+ * STABLE RULE IDS. Every law and doctrine rule is DECLARED by its heading —
62
+ * `### {{LAW:verify-before-asserting}} Verify Before Asserting` — and CITED by its id —
63
+ * `{{LAW:verify-before-asserting}}`. The id is permanent; the number is not stored anywhere. It
64
+ * is the rule's position (Roman in `laws/universal.md`, Arabic within its pack), computed here
65
+ * at render time, so removing or reordering a rule renumbers every heading after it and rewires
66
+ * nothing: no citation ever named the number.
67
+ *
68
+ * WHY CITATIONS RENDER AS THE NAME, NOT THE NUMBER. A citation is the one place a number would
69
+ * leak into text that outlives the build — an agent reads "Law III", writes "Law III" into its
70
+ * memory, and the memory is wrong the day the canon moves. The rendered harness models what it
71
+ * asks of the agent: a rule is cited by its principle, in English, identical in every voice. The
72
+ * number survives only where it is structural — the rule's own heading.
73
+ *
74
+ * Retired ids live in `RETIRED_RULE_IDS` so the doctor can refuse their reuse: an id that meant
75
+ * one thing in someone's notes must never come back meaning another.
76
+ */
77
+ const DECL_RE = /^### \{\{(LAW|DOCTRINE):([a-z0-9]+(?:-[a-z0-9]+)*)\}\}[ \t]+(\S[^\n]*?)[ \t]*$/gm;
78
+ const CITE_RE = /\{\{(LAW|DOCTRINE):([a-z0-9-]+)\}\}/g;
79
+ export const RULE_ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
80
+
81
+ /**
82
+ * Ids that no longer name a live rule, and must never be declared again. `liveAs` names the one
83
+ * tier that may still declare it — `external-gate` was Law IX before it became a rigor rule, so
84
+ * the doctrine keeps it and `{{LAW:external-gate}}` is refused; `null` means gone for good.
85
+ */
86
+ export const RETIRED_RULE_IDS = {
87
+ 'external-gate': { was: 'Law IX', liveAs: 'DOCTRINE', note: 'moved to the rigor pack' },
88
+ 'absence-is-a-claim': { was: 'Law XI', liveAs: null, note: 'folded into verify-before-asserting' },
89
+ };
90
+
91
+ const ROMAN = [[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], [100, 'C'], [90, 'XC'],
92
+ [50, 'L'], [40, 'XL'], [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']];
93
+ export function toRoman(n) {
94
+ let out = '';
95
+ for (const [v, r] of ROMAN) while (n >= v) { out += r; n -= v; }
96
+ return out;
97
+ }
98
+
99
+ /** `sealed-secrets` -> `LEX_SEALED_SECRETS`; a doctrine rule's title key is `DOC_<ID>`. */
100
+ export const titleKey = (kind, id) =>
101
+ `${kind === 'LAW' ? 'LEX' : 'DOC'}_${id.toUpperCase().replaceAll('-', '_')}`;
102
+
103
+ /** The files that may declare rules, in canon order: `[kind, pack|null, file]`. */
104
+ export function canonFiles(src = SRC) {
105
+ return [['LAW', null, path.join(src, 'laws', 'universal.md')],
106
+ ...PACK_ORDER.map((p) => ['DOCTRINE', p, path.join(src, 'doctrines', `${p}.md`)])];
107
+ }
108
+
109
+ const _canons = new Map();
110
+ /**
111
+ * Every declared rule, in render order: `{ kind, pack, id, name, n, label }`, where `label` is
112
+ * the address the heading renders (`III`, `process 5`). Memoised on the raw texts' identity —
113
+ * `readRawText` hands back the same string until a file's mtime moves.
114
+ */
115
+ export function ruleCanon(src = SRC) {
116
+ const files = canonFiles(src);
117
+ const texts = files.map(([, , f]) => { try { return readRawText(f); } catch { return ''; } });
118
+ const key = path.resolve(src);
119
+ const hit = _canons.get(key);
120
+ if (hit && hit.texts.every((t, i) => t === texts[i])) return hit.canon;
121
+ const rules = [];
122
+ files.forEach(([kind, pack], i) => {
123
+ let n = 0;
124
+ for (const m of texts[i].matchAll(DECL_RE)) {
125
+ if (m[1] !== kind) continue; // a wrong-tier declaration is the doctor's to name
126
+ n += 1;
127
+ rules.push({ kind, pack, id: m[2], name: m[3], n, label: kind === 'LAW' ? toRoman(n) : `${pack} ${n}` });
128
+ }
129
+ });
130
+ const byId = new Map();
131
+ for (const r of rules) if (!byId.has(r.id)) byId.set(r.id, r);
132
+ const canon = { rules, byId };
133
+ _canons.set(key, { texts, canon });
134
+ return canon;
135
+ }
136
+
137
+ /**
138
+ * Resolve every declaration and citation in `text` against the canon under `src`. An unknown id
139
+ * is left exactly as written — visible in the output, and named by the doctor — rather than
140
+ * guessed at.
141
+ */
142
+ export function resolveRuleIds(text, src = SRC) {
143
+ if (!text.includes('{{LAW:') && !text.includes('{{DOCTRINE:')) return text;
144
+ const { byId } = ruleCanon(src);
145
+ const known = (kind, id) => { const r = byId.get(id); return r && r.kind === kind ? r : null; };
146
+ return text
147
+ .replace(DECL_RE, (whole, kind, id) => {
148
+ const r = known(kind, id);
149
+ return r ? `### {{${kind}}} ${r.label} — {{${titleKey(kind, id)}}}` : whole;
150
+ })
151
+ .replace(CITE_RE, (whole, kind, id) => { const r = known(kind, id); return r ? `*${r.name}*` : whole; });
152
+ }
153
+
50
154
  /**
51
155
  * `_build_core.ROOT` — the checkout, from this file's own location (`js/..`).
52
156
  *
@@ -137,8 +137,13 @@ the project's own. Follow the root \`AGENTS.md\`.
137
137
  /** `_build_render.PROFILE_FILE`. */
138
138
  const PROFILE_FILE = 'PROFILE.md';
139
139
 
140
- /** `_build_render.PROFILE_STUB`. */
141
- const PROFILE_STUB = `\
140
+ /**
141
+ * `_build_render.PROFILE_STUB`.
142
+ *
143
+ * Exported for the web console: `/api/profile` answers `seeded: true` while PROFILE.md is
144
+ * still byte-for-byte this text, which is how the Overview knows to ask for it to be filled in.
145
+ */
146
+ export const PROFILE_STUB = `\
142
147
  # Your profile
143
148
 
144
149
  Who you are and how you like to work — so the agent can meet you where you are
@@ -240,9 +245,25 @@ export const ensureBundleGitignore = stubWriter('.gitignore', BUNDLE_GITIGNORE);
240
245
  * right after the store dir is created, but never invent the dir themselves). */
241
246
  const storeIndexWriter = (file, header) => (dir) => { if (isDir(dir)) seed(path.join(dir, file), header); };
242
247
 
248
+ const MEMORY_INDEX = '# Memory Index\n';
249
+ const NOTEBOOK_INDEX = '# Notebook Index\n';
250
+
243
251
  /** `_build_render.ensure_memory_index` — only inside an EXISTING store dir. */
244
- export const ensureMemoryIndex = storeIndexWriter('MEMORY.md', '# Memory Index\n');
252
+ export const ensureMemoryIndex = storeIndexWriter('MEMORY.md', MEMORY_INDEX);
245
253
 
246
254
  /** `_build_render.ensure_notebook_index`. */
247
- export const ensureNotebookIndex = storeIndexWriter('NOTEBOOK.md', '# Notebook Index\n');
255
+ export const ensureNotebookIndex = storeIndexWriter('NOTEBOOK.md', NOTEBOOK_INDEX);
256
+
257
+ /**
258
+ * The seeds of the files the session-start injection reads (`SESSION_FILES` in
259
+ * `js/hosts/hooks.mjs`, and its twin in the OpenCode context plugin). Both carry the SHA-256
260
+ * of these bodies rather than importing them — the hook path pays for every import, and the
261
+ * plugin is deployed on its own — so an untouched seed is skipped instead of injected as
262
+ * noise. `tests/unit/claude.test.mjs` gates both hash lists against this map: edit a stub and
263
+ * the two copies must move with it.
264
+ */
265
+ export const SESSION_SEEDS = {
266
+ 'user-rules.md': RULES_STUB, 'PROFILE.md': PROFILE_STUB, 'wiki.jsonc': WIKI_STUB,
267
+ 'MEMORY.md': MEMORY_INDEX, 'NOTEBOOK.md': NOTEBOOK_INDEX,
268
+ };
248
269
 
package/js/cli-table.json CHANGED
@@ -297,7 +297,7 @@
297
297
  ],
298
298
  "dest": "host",
299
299
  "metavar": null,
300
- "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",
300
+ "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
301
301
  "choices": null,
302
302
  "default": null,
303
303
  "required": false,
@@ -488,7 +488,7 @@
488
488
  },
489
489
  {
490
490
  "name": "git-gate",
491
- "help": "PreToolUse hook: ASK before a history-discarding git act (Law IV) and before every git commit/push (process 5 backstop)",
491
+ "help": "PreToolUse hook: ASK before a history-discarding git act (Deletion Is Deliberate) and before every git commit/push (Consent Before Push backstop)",
492
492
  "description": "",
493
493
  "positionals": [],
494
494
  "options": [
@@ -513,7 +513,7 @@
513
513
  ],
514
514
  "dest": "host",
515
515
  "metavar": null,
516
- "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",
516
+ "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
517
517
  "choices": null,
518
518
  "default": null,
519
519
  "required": false,
@@ -528,7 +528,7 @@
528
528
  ],
529
529
  "dest": "no_consent",
530
530
  "metavar": null,
531
- "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",
531
+ "help": "skip the Consent Before Push commit/push ask (set by the emit when the process pack or that rule is off); Deletion Is Deliberate still asks",
532
532
  "choices": null,
533
533
  "default": false,
534
534
  "required": false,
@@ -766,7 +766,7 @@
766
766
  },
767
767
  {
768
768
  "name": "rule-gate",
769
- "help": "PreToolUse hook: force an ASK before any write to user-rules.md or memory (process 1 backstop)",
769
+ "help": "PreToolUse hook: force an ASK before any write to user-rules.md or memory (Persist Insight backstop)",
770
770
  "description": "",
771
771
  "positionals": [],
772
772
  "options": [
@@ -791,7 +791,7 @@
791
791
  ],
792
792
  "dest": "host",
793
793
  "metavar": null,
794
- "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",
794
+ "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
795
795
  "choices": null,
796
796
  "default": null,
797
797
  "required": false,
@@ -975,7 +975,7 @@
975
975
  },
976
976
  {
977
977
  "name": "tool-gate",
978
- "help": "PreToolUse hook for hosts with no tool matcher (Copilot, Bob): git-gate and rule-gate fused behind one command, dispatched on the payload",
978
+ "help": "PreToolUse hook for hosts with no tool matcher (Bob): git-gate and rule-gate fused behind one command, dispatched on the payload",
979
979
  "description": "",
980
980
  "positionals": [],
981
981
  "options": [
@@ -1000,7 +1000,7 @@
1000
1000
  ],
1001
1001
  "dest": "host",
1002
1002
  "metavar": null,
1003
- "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",
1003
+ "help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
1004
1004
  "choices": null,
1005
1005
  "default": null,
1006
1006
  "required": false,