geneseed 3.9.0 → 3.11.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 (219) hide show
  1. package/CHANGELOG.md +89 -1
  2. package/DESIGN.md +1 -1
  3. package/README.md +52 -71
  4. package/SHIPPED.md +3 -3
  5. package/adapters/bob/README.md +18 -2
  6. package/adapters/claude-code/README.md +17 -12
  7. package/adapters/openclaude/README.md +2 -2
  8. package/adapters/opencode/README.md +4 -4
  9. package/adapters/opencode/plugins/geneseed-context.js +78 -8
  10. package/bin/build-driver.mjs +2 -0
  11. package/bin/geneseed-cli.mjs +17 -0
  12. package/bin/geneseed-hook.mjs +3 -1
  13. package/docs/README.md +42 -0
  14. package/docs/_groups.json +18 -0
  15. package/docs/candidate-hosts.md +12 -5
  16. package/docs/concepts/agents.md +44 -0
  17. package/docs/concepts/collaboration.md +62 -0
  18. package/docs/concepts/exclusions.md +55 -0
  19. package/docs/concepts/footprint.md +52 -0
  20. package/docs/concepts/foreman-mode.md +69 -0
  21. package/docs/concepts/hooks.md +101 -0
  22. package/docs/concepts/hosts.md +55 -0
  23. package/docs/concepts/lsp.md +50 -0
  24. package/docs/{web → concepts}/memory.md +2 -2
  25. package/docs/{web → concepts}/notebook.md +2 -2
  26. package/docs/concepts/rules.md +52 -0
  27. package/docs/concepts/skills.md +49 -0
  28. package/docs/concepts/themes.md +50 -0
  29. package/docs/extending.md +27 -21
  30. package/docs/guides/autostart.md +70 -0
  31. package/docs/guides/choose-your-setup.md +120 -0
  32. package/docs/guides/headless.md +22 -0
  33. package/docs/guides/install.md +161 -0
  34. package/docs/guides/mcp.md +195 -0
  35. package/docs/guides/migrate.md +41 -0
  36. package/docs/guides/project-context.md +87 -0
  37. package/docs/guides/run-anywhere.md +45 -0
  38. package/docs/guides/uninstall.md +51 -0
  39. package/docs/guides/upgrade.md +65 -0
  40. package/docs/guides/verify.md +65 -0
  41. package/docs/guides/wiki.md +69 -0
  42. package/docs/guides/worktree.md +47 -0
  43. package/docs/reference/about.md +7 -0
  44. package/docs/reference/cli.md +7 -0
  45. package/docs/reference/environment.md +99 -0
  46. package/docs/reference/glossary.md +33 -0
  47. package/docs/reference/opencode-plugins.md +131 -0
  48. package/docs/reference/troubleshooting.md +350 -0
  49. package/docs/token-footprint.md +26 -21
  50. package/docs/understand/a-day.md +77 -0
  51. package/docs/understand/enforced-vs-asked.md +64 -0
  52. package/docs/understand/harness.md +36 -0
  53. package/docs/understand/on-your-machine.md +76 -0
  54. package/docs/web-ui.md +2 -2
  55. package/harness.config.json +1 -1
  56. package/js/README.md +7 -6
  57. package/js/build/driver.mjs +26 -95
  58. package/js/build/emit-claude.mjs +76 -81
  59. package/js/build/generate.mjs +12 -2
  60. package/js/build/render.mjs +4 -0
  61. package/js/build/source.mjs +6 -0
  62. package/js/build/stubs.mjs +18 -2
  63. package/js/cli-table.json +6 -6
  64. package/js/hosts/hooks.mjs +86 -54
  65. package/js/hosts/hosts.mjs +5 -21
  66. package/js/hosts/installs.mjs +4 -6
  67. package/js/hosts/mcp.mjs +4 -14
  68. package/js/hosts/native.mjs +27 -50
  69. package/js/hosts/opencode.mjs +1 -1
  70. package/js/hosts/settings.mjs +17 -79
  71. package/js/inspect/checks-authoring.mjs +9 -4
  72. package/js/inspect/checks-repo.mjs +45 -1
  73. package/js/inspect/diff.mjs +11 -3
  74. package/js/inspect/doctor.mjs +4 -4
  75. package/js/inspect/excludes.mjs +0 -4
  76. package/js/inspect/inventory.mjs +1 -1
  77. package/js/inspect/registry.mjs +20 -3
  78. package/js/inspect/status.mjs +11 -6
  79. package/js/lib/node-floor.mjs +30 -0
  80. package/js/maintain/migrate.mjs +5 -5
  81. package/js/maintain/setup.mjs +112 -26
  82. package/js/maintain/uninstall.mjs +8 -42
  83. package/js/maintain/update.mjs +4 -15
  84. package/js/ui/cli.mjs +2 -2
  85. package/js/web/actions.mjs +4 -5
  86. package/js/web/api.mjs +7 -4
  87. package/js/web/daemon.mjs +52 -0
  88. package/js/web/docs.mjs +149 -65
  89. package/js/web/server.mjs +5 -2
  90. package/package.json +6 -5
  91. package/registry.json +7 -0
  92. package/src/AGENT.md.tmpl +73 -52
  93. package/src/laws/universal.md +3 -0
  94. package/src/ontology/universal.md +1 -1
  95. package/src/postures/mentor.md +1 -0
  96. package/src/skills/daydream/SKILL.md +10 -3
  97. package/src/skills/daydream/VENDOR.md +10 -2
  98. package/src/skills/daydream/instructions.md +2 -2
  99. package/src/skills/explain-changes/SKILL.md +1 -1
  100. package/src/skills/explain-changes/VENDOR.md +1 -1
  101. package/src/skills/geneseed.md +1 -1
  102. package/src/skills/learn-mode.md +89 -0
  103. package/src/skills/pipeline.md +2 -2
  104. package/src/skills/quiz.md +2 -1
  105. package/src/skills/react-view-transitions/SKILL.md +1 -1
  106. package/src/skills/react-view-transitions/VENDOR.md +7 -2
  107. package/src/skills/teach.md +2 -1
  108. package/src/skills/token-report/SKILL.md +2 -3
  109. package/src/skills/token-report/VENDOR.md +2 -3
  110. package/src/skills/token-report/scripts/token_report.mjs +7 -121
  111. package/src/skills/workflow.md +1 -1
  112. package/themes/_TEMPLATE.json +1 -0
  113. package/themes/biker.json +1 -0
  114. package/themes/commentator.json +1 -0
  115. package/themes/cyberpunk.json +1 -0
  116. package/themes/gamer.json +1 -0
  117. package/themes/imperial.json +1 -0
  118. package/themes/joker.json +1 -0
  119. package/themes/marvin.json +1 -0
  120. package/themes/mean.json +1 -0
  121. package/themes/military.json +1 -0
  122. package/themes/neutral.json +1 -0
  123. package/themes/pirate.json +1 -0
  124. package/themes/sports.json +1 -0
  125. package/themes/verstappen.json +1 -0
  126. package/themes/wizard.json +1 -0
  127. package/web/dist/assets/Activity-C52LEb-2.js +1 -0
  128. package/web/dist/assets/ActivityDetail-CjNLRvXg.js +1 -0
  129. package/web/dist/assets/Dashboard-B6_sSCHi.js +2 -0
  130. package/web/dist/assets/Docs-Dj7BmCSA.js +5 -0
  131. package/web/dist/assets/{GreenhouseView-Bq1QmaGO.js → GreenhouseView-BALwjQsA.js} +1 -1
  132. package/web/dist/assets/Installs-BbffDQm3.js +2 -0
  133. package/web/dist/assets/JournalView-JpAQ8Hqe.js +1 -0
  134. package/web/dist/assets/Laws-DK7eCWSm.js +3 -0
  135. package/web/dist/assets/Library-CoamHelE.js +1 -0
  136. package/web/dist/assets/{Markdown-CkFyfgNf.js → Markdown-qjM2_ofS.js} +5 -5
  137. package/web/dist/assets/{OperatorHudView-B-HVBqfY.js → OperatorHudView-DVx1HlBx.js} +1 -1
  138. package/web/dist/assets/Personal-eaXb1zcj.js +1 -0
  139. package/web/dist/assets/{Tabs-_pjUFIEI.js → Tabs-BmGtt8E2.js} +1 -1
  140. package/web/dist/assets/api-Dc8f3ca-.js +1 -0
  141. package/web/dist/assets/drawable-Du_PZrQj.js +1 -0
  142. package/web/dist/assets/index-Bz_KMRDH.css +1 -0
  143. package/web/dist/assets/index-CNFH7o55.js +2 -0
  144. package/web/dist/index.html +3 -2
  145. package/QUICKSTART.md +0 -50
  146. package/SETUP.md +0 -1183
  147. package/adapters/copilot/README.md +0 -105
  148. package/docs/web/_groups.json +0 -45
  149. package/docs/web/about.md +0 -6
  150. package/docs/web/adapters-claude-code.md +0 -8
  151. package/docs/web/adapters-opencode-loads.md +0 -8
  152. package/docs/web/adapters-opencode-spec.md +0 -8
  153. package/docs/web/adapters-opencode.md +0 -8
  154. package/docs/web/agents.md +0 -8
  155. package/docs/web/authoring.md +0 -11
  156. package/docs/web/autostart.md +0 -9
  157. package/docs/web/claude-hooks.md +0 -37
  158. package/docs/web/cli.md +0 -6
  159. package/docs/web/collaboration.md +0 -27
  160. package/docs/web/configure-wiki.md +0 -9
  161. package/docs/web/design.md +0 -7
  162. package/docs/web/env-knobs.md +0 -9
  163. package/docs/web/exclusions.md +0 -27
  164. package/docs/web/first-session.md +0 -28
  165. package/docs/web/footprint.md +0 -38
  166. package/docs/web/foreman-mode.md +0 -33
  167. package/docs/web/glossary.md +0 -6
  168. package/docs/web/harnesses.md +0 -30
  169. package/docs/web/headless.md +0 -9
  170. package/docs/web/ingest-docs.md +0 -9
  171. package/docs/web/install-paths.md +0 -9
  172. package/docs/web/install-quick.md +0 -35
  173. package/docs/web/lsp-overview.md +0 -37
  174. package/docs/web/mcp-claude-code.md +0 -10
  175. package/docs/web/mcp-filesystem.md +0 -9
  176. package/docs/web/mcp-gitlab.md +0 -9
  177. package/docs/web/mcp-markitdown.md +0 -9
  178. package/docs/web/mcp-overview.md +0 -34
  179. package/docs/web/mcp-trouble.md +0 -9
  180. package/docs/web/mcp-verify.md +0 -9
  181. package/docs/web/migrate.md +0 -39
  182. package/docs/web/model.md +0 -20
  183. package/docs/web/pipelines.md +0 -24
  184. package/docs/web/plugin-activity.md +0 -23
  185. package/docs/web/plugin-context.md +0 -26
  186. package/docs/web/plugin-guard.md +0 -24
  187. package/docs/web/plugin-learn.md +0 -27
  188. package/docs/web/plugin-notify.md +0 -24
  189. package/docs/web/plugin-ponytail.md +0 -23
  190. package/docs/web/plugin-setup.md +0 -7
  191. package/docs/web/plugin-workflow.md +0 -25
  192. package/docs/web/plugins.md +0 -17
  193. package/docs/web/project-context.md +0 -9
  194. package/docs/web/rules.md +0 -30
  195. package/docs/web/run-anywhere.md +0 -9
  196. package/docs/web/self-improve.md +0 -8
  197. package/docs/web/setup-choices.md +0 -64
  198. package/docs/web/skills.md +0 -8
  199. package/docs/web/themes-author.md +0 -20
  200. package/docs/web/themes.md +0 -9
  201. package/docs/web/token-footprint.md +0 -7
  202. package/docs/web/trouble.md +0 -38
  203. package/docs/web/upgrade.md +0 -9
  204. package/docs/web/verify.md +0 -30
  205. package/docs/web/wiki.md +0 -7
  206. package/docs/web/worktree.md +0 -10
  207. package/docs/wiki.md +0 -43
  208. package/web/dist/assets/Activity-DAzV-qQj.js +0 -1
  209. package/web/dist/assets/ActivityDetail-CwEwMtKl.js +0 -1
  210. package/web/dist/assets/Dashboard-C2ELBW-X.js +0 -2
  211. package/web/dist/assets/Docs-iZrvQP8J.js +0 -2
  212. package/web/dist/assets/Installs-BiqR8xdO.js +0 -2
  213. package/web/dist/assets/JournalView-6wilG5Zd.js +0 -1
  214. package/web/dist/assets/Laws-D8YXD_XE.js +0 -3
  215. package/web/dist/assets/Library-DwuTZ2-L.js +0 -1
  216. package/web/dist/assets/Personal-BbUTmZQR.js +0 -1
  217. package/web/dist/assets/drawable-CoIulEH2.js +0 -1
  218. package/web/dist/assets/index-BMwSp8BP.js +0 -2
  219. package/web/dist/assets/index-CDPDk2vj.css +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
 
@@ -255,7 +254,18 @@ export function rebuildCommand(argv) {
255
254
  * choose — which is the same failure mode `_footprint_of_dir`'s WARN exists to prevent, at a
256
255
  * larger scale.
257
256
  */
258
- export function cmdRebuildAll() {
257
+ export async function cmdRebuildAll() {
258
+ const rc = rebuildAll();
259
+ // A console started before an upgrade keeps serving the old `index.html`; restart it — only
260
+ // when stale, see `bounceDaemonIfRunning`. Lazy: no other verb in this module needs js/web/.
261
+ const { bounceDaemonIfRunning } = await import('../web/daemon.mjs');
262
+ await bounceDaemonIfRunning(null, (m) => printOut(`${m}
263
+ `), { onlyIfStale: true });
264
+ return rc;
265
+ }
266
+
267
+ /** The rebuild itself, synchronous — `cmdRebuildAll` is it plus the console restart. */
268
+ export function rebuildAll() {
259
269
  const targets = installTargets().filter(([h, s, r]) => installState(r, h, s) === 'active');
260
270
  if (!targets.length) {
261
271
  printOut('[rebuild-all] no active installs detected.\n');
@@ -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.
@@ -168,6 +168,12 @@ export const CONFIG = path.join(ROOT, 'harness.config.json');
168
168
  export const THEMES = path.join(ROOT, 'themes');
169
169
  export const PLUGIN_SRC = path.join(ROOT, 'adapters', 'opencode', 'plugins');
170
170
  export const WORKFLOW_SRC = path.join(ROOT, 'adapters', 'opencode', 'workflows');
171
+ /**
172
+ * The user docs: one folder per kind of reading, all under `docs/` so GitHub renders the same
173
+ * files the console serves. A page's id is its basename, so ids must be unique across folders.
174
+ */
175
+ export const DOCS = path.join(ROOT, 'docs');
176
+ export const DOC_FOLDERS = ['understand', 'guides', 'concepts', 'reference'];
171
177
 
172
178
  /**
173
179
  * `_build_render.posture_names()` / `mode_names()` — discovered, never hardcoded, so a new
@@ -245,9 +245,25 @@ export const ensureBundleGitignore = stubWriter('.gitignore', BUNDLE_GITIGNORE);
245
245
  * right after the store dir is created, but never invent the dir themselves). */
246
246
  const storeIndexWriter = (file, header) => (dir) => { if (isDir(dir)) seed(path.join(dir, file), header); };
247
247
 
248
+ const MEMORY_INDEX = '# Memory Index\n';
249
+ const NOTEBOOK_INDEX = '# Notebook Index\n';
250
+
248
251
  /** `_build_render.ensure_memory_index` — only inside an EXISTING store dir. */
249
- export const ensureMemoryIndex = storeIndexWriter('MEMORY.md', '# Memory Index\n');
252
+ export const ensureMemoryIndex = storeIndexWriter('MEMORY.md', MEMORY_INDEX);
250
253
 
251
254
  /** `_build_render.ensure_notebook_index`. */
252
- 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
+ };
253
269
 
package/js/cli-table.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "prog": "harness",
2
+ "prog": "geneseed",
3
3
  "commands": [
4
4
  {
5
5
  "name": "bootstrap",
@@ -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,
@@ -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,
@@ -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,
@@ -8,11 +8,8 @@
8
8
  *
9
9
  * ONE VERB, TWO DIALECTS. `--host` names the host that will read the verdict. Claude Code
10
10
  * (the default) reads `hookSpecificOutput.permissionDecision: "ask"` and shows the user a
11
- * prompt. GitHub Copilot's `preToolUse` reads the same decision TOP-LEVEL —
12
- * `{"permissionDecision": "ask", "permissionDecisionReason": …}` — so `--host copilot` asks
13
- * exactly where Claude asks, in Copilot's envelope. `--host bob` is Bob's own
14
- * protocol: PreToolUse ignores stdout and refuses only on EXIT CODE 2, so the two Laws
15
- * exit 2 with the reason on stderr and the rest is a stderr line with exit 0; SessionStart
11
+ * prompt. `--host bob` is Bob's own protocol: PreToolUse ignores stdout and refuses only on
12
+ * EXIT CODE 2, so the two Laws exit 2 with the reason on stderr and the rest is a stderr line with exit 0; SessionStart
16
13
  * context is plain stdout, as on Claude. `--host openclaude` speaks Claude's dialect verbatim
17
14
  * (OpenClaude is a Claude Code fork); the flag only picks which root file counts as native. Since P5b they are also what the emitted hooks name: `bin/build-driver.mjs` bakes
18
15
  * `<node> <checkout>/bin/geneseed-hook.mjs` into the machine-wide shim, so an install this
@@ -47,6 +44,7 @@ import { existsSync, statSync, readFileSync, readdirSync, mkdirSync,
47
44
  from 'node:fs';
48
45
  import path from 'node:path';
49
46
  import { spawnSync } from 'node:child_process';
47
+ import { createHash } from 'node:crypto';
50
48
  import { readText, writeText, printOut, printErr, withDiscardableStderr } from '../lib/fs.mjs';
51
49
  import { jsonDumpsCompact } from '../lib/json.mjs';
52
50
  import { normcase, comparePaths, toPlatformPath } from '../lib/paths.mjs';
@@ -227,7 +225,7 @@ const EXCLUDE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'vendor',
227
225
  // OpenClaude's root file is AGENTS.md, or CLAUDE.md when AGENTS.md is absent — resolved
228
226
  // per repo in `discoverContext`, since which one it loads depends on what is on disk.
229
227
  const NATIVE_ROOT = {
230
- claude: ['CLAUDE.md'], bob: ['AGENTS.md'], copilot: ['AGENTS.md'],
228
+ claude: ['CLAUDE.md'], bob: ['AGENTS.md'],
231
229
  openclaude: ['AGENTS.md', 'CLAUDE.md'],
232
230
  };
233
231
 
@@ -237,6 +235,49 @@ const NATIVE_ROOT = {
237
235
  const EAGER_FILE_BYTES = 16 * 1024;
238
236
  const EAGER_TOTAL_BYTES = 48 * 1024;
239
237
 
238
+ // The files the root instruction file names for session start, relative to the HARNESS dir
239
+ // (`--root`: `.claude/`, `.bob/`, a global config dir) — not to the repo root discovery walks,
240
+ // which is why none of them reached a hooked host before 2026-10. Injected here so the read does
241
+ // not depend on the model remembering it. `context.json` is not in the list: it is a manifest,
242
+ // honoured by `resolveContextSets` (the harness dir is its last candidate). The OpenCode
243
+ // context plugin carries the same list.
244
+ const SESSION_FILES = ['user-rules.md', 'PROFILE.md', 'memory/MEMORY.md', 'anamnesis/MEMORY.md',
245
+ 'notebook/NOTEBOOK.md', 'wiki.jsonc'];
246
+ // SHA-256 of each seed body in `js/build/stubs.mjs` (`SESSION_SEEDS`), CRLF folded. A file still
247
+ // byte-identical to its seed says nothing, so it is skipped rather than injected as noise.
248
+ // Hashes, not an import: stubs.mjs pulls in the build's writers, and this module loads on every
249
+ // tool call. tests/unit/claude.test.mjs gates this set, and the plugin's copy, against the seeds.
250
+ const SEED_SHA256 = new Set([
251
+ '5c2e92fb1acde041e02d9ebf158460d8ff7a07cbc2b447859631604610130721', // user-rules.md
252
+ '29e012c3c4349ea62a9082cbd04bb25599a0ec280d62c33f6e10742809471816', // PROFILE.md
253
+ 'cccc917c34e6b990e821620bdb4739090151885ab71b6e85046f30a3b71fa5e8', // wiki.jsonc
254
+ '99c786049c260f6baf7aec68a5c0f59907861afe9b867da009440613bdddb907', // MEMORY.md
255
+ '9acb5c9d9cb5dac572104480f05b48cc144e676869bd60e86d8a15b1f6308184', // NOTEBOOK.md
256
+ ]);
257
+ export { SESSION_FILES, SEED_SHA256 };
258
+
259
+ /**
260
+ * The session files present under `hookRoot` and changed from their seed, as `{ rel, abs, text }`
261
+ * (text CRLF-folded, trailing newlines trimmed). `$GENESEED_WIKI` overrides the wiki declaration,
262
+ * as it does on OpenCode. A missing or unreadable file is simply absent — this is best-effort
263
+ * context, never a gate.
264
+ */
265
+ export function sessionFiles(hookRoot) {
266
+ const found = [];
267
+ for (const rel of SESSION_FILES) {
268
+ let abs = path.join(hookRoot, rel);
269
+ if (rel === 'wiki.jsonc' && process.env.GENESEED_WIKI && isFile(process.env.GENESEED_WIKI)) {
270
+ abs = process.env.GENESEED_WIKI;
271
+ }
272
+ if (!isFile(abs)) continue;
273
+ let text;
274
+ try { text = readFileSync(abs, 'utf8').replace(/\r\n/g, '\n'); } catch { continue; }
275
+ if (SEED_SHA256.has(createHash('sha256').update(text).digest('hex'))) continue;
276
+ found.push({ rel, abs, text: text.replace(/\n+$/, '') });
277
+ }
278
+ return found;
279
+ }
280
+
240
281
  const CLAUDE_MARKERS = ['.claude', '.bob', '.openclaude'];
241
282
  const GENESEED_MANIFEST = '.geneseed-manifest.json';
242
283
 
@@ -348,7 +389,7 @@ export function discoverContext(root, host = HOST) {
348
389
  * `recs` is a Map, not an object: JS reorders integer-like keys on a plain object, and
349
390
  * these keys are absolute paths whose iteration order becomes the printed order.
350
391
  */
351
- export function resolveContextSets(root) {
392
+ export function resolveContextSets(root, hookRoot = null) {
352
393
  let manifest = null;
353
394
  const env = process.env.GENESEED_CONTEXT;
354
395
  if (env && isFile(env)) {
@@ -356,8 +397,9 @@ export function resolveContextSets(root) {
356
397
  // way `str(Path(...))` folds them, or the source line differs by every slash in it.
357
398
  manifest = toPlatformPath(env);
358
399
  } else {
400
+ // The harness dir last: a repo's own manifest outranks the one seeded beside the install.
359
401
  for (const cand of [path.join(root, '.harness', 'context.json'),
360
- path.join(root, 'context.json')]) {
402
+ path.join(root, 'context.json'), ...(hookRoot ? [path.join(hookRoot, 'context.json')] : [])]) {
361
403
  if (isFile(cand)) { manifest = cand; break; }
362
404
  }
363
405
  }
@@ -460,18 +502,40 @@ export function cmdContext(args) {
460
502
  if (hookRoot && sovereignBypass(hookRoot)) return 0;
461
503
  if (hookRoot && !process.env.GENESEED_STACK_GLOBAL
462
504
  && globalHookStandingDown(hookRoot, root)) return 0;
463
- const [eager, lazy, source] = resolveContextSets(root);
464
- if (!eager.length && !lazy.length) {
505
+ const session = hookRoot ? sessionFiles(hookRoot) : [];
506
+ // A session file discovery would also pick up (a `user-rules.md` at the repo root of an
507
+ // install whose harness dir IS the repo root) is injected once, in the session block.
508
+ const seen = new Set(session.map((s) => normcase(path.resolve(s.abs))));
509
+ const fresh = (e) => !seen.has(normcase(path.resolve(path.isAbsolute(e.path)
510
+ ? e.path : path.join(root, e.path))));
511
+ let [eager, lazy, source] = resolveContextSets(root, hookRoot);
512
+ eager = eager.filter(fresh);
513
+ lazy = lazy.filter(fresh);
514
+ if (!session.length && !eager.length && !lazy.length) {
465
515
  err(`[context] nothing to load for ${root} `
466
516
  + '(no docs discovered, no manifest entries).\n');
467
517
  return 0;
468
518
  }
469
519
 
470
- const lines = [
471
- `=== PROJECT CONTEXT \u2014 binding for this repo (via ${source}) ===`,
472
- '',
473
- ];
520
+ const lines = [];
474
521
  let spent = 0;
522
+ if (session.length) {
523
+ lines.push('=== SESSION FILES \u2014 your harness files, injected at session start '
524
+ + '(untouched seeds skipped) ===', '');
525
+ for (const s of session) {
526
+ let text = s.text;
527
+ if (text.length > EAGER_FILE_BYTES) {
528
+ const nl = text.lastIndexOf('\n', EAGER_FILE_BYTES);
529
+ text = `${text.slice(0, nl > 0 ? nl : EAGER_FILE_BYTES)}\n`
530
+ + `[context] truncated at ${EAGER_FILE_BYTES / 1024} KB \u2014 read ${s.abs} on demand`;
531
+ }
532
+ spent += text.length;
533
+ lines.push(`----- ${s.rel} -----`, text, '');
534
+ }
535
+ }
536
+ if (eager.length || lazy.length) {
537
+ lines.push(`=== PROJECT CONTEXT \u2014 binding for this repo (via ${source}) ===`, '');
538
+ }
475
539
  const demoted = [];
476
540
  for (const entry of eager) {
477
541
  const p = entry.path === undefined ? '' : entry.path;
@@ -511,12 +575,8 @@ export function cmdContext(args) {
511
575
  lines.push('');
512
576
  }
513
577
 
514
- // Claude Code takes a SessionStart hook's plain stdout as context. Copilot's `sessionStart`
515
- // wants a JSON document and reads `additionalContext` out of it; plain text there would be
516
- // parsed as nothing and silently dropped.
517
- // Bob reads a SessionStart hook's stdout as context exactly as Claude does.
518
- const text = `${lines.join('\n')}\n`;
519
- out(HOST === 'copilot' ? `${jsonDumpsCompact({ additionalContext: text })}\n` : text);
578
+ // Claude Code takes a SessionStart hook's plain stdout as context; Bob reads it the same way.
579
+ out(`${lines.join('\n')}\n`);
520
580
  return 0;
521
581
  }
522
582
 
@@ -559,16 +619,6 @@ function askDecision(reason) {
559
619
  let HOST = 'claude';
560
620
  const setHost = (args) => { HOST = (args && args.host) || 'claude'; };
561
621
 
562
- /**
563
- * Copilot's `preToolUse` verdict: the same `ask` Claude reads, but top-level rather than under
564
- * `hookSpecificOutput` (docs.github.com/en/copilot/reference/hooks-configuration). An older
565
- * emit wired a `toolCall` event answered with `{"block": true}`; Copilot no longer has that
566
- * event, so those gates never ran.
567
- */
568
- function copilotAskDecision(reason) {
569
- return `${jsonDumpsCompact({ permissionDecision: 'ask', permissionDecisionReason: reason })}\n`;
570
- }
571
-
572
622
  /**
573
623
  * Bob's PreToolUse protocol: stdout is ignored and EXIT CODE 2 is the one refusal. The
574
624
  * single exception to "every gate exits 0" in this file, confined to `--host bob` — on
@@ -636,7 +686,7 @@ function ask(args, verb, rule, reason) {
636
686
  return BOB_DENY_EXIT;
637
687
  }
638
688
  ledger(args && args.root, verb, rule);
639
- out(HOST === 'copilot' ? copilotAskDecision(reason) : askDecision(reason));
689
+ out(askDecision(reason));
640
690
  return 0;
641
691
  }
642
692
 
@@ -663,12 +713,6 @@ export function guardGate(fn, verb = 'gate') {
663
713
 
664
714
  /**
665
715
  * The hook payload on stdin, or null for a well-formed absence. Not-JSON THROWS.
666
- *
667
- * Copilot spells the same payload in camelCase — `toolName`/`toolArgs` where Claude sends
668
- * `tool_name`/`tool_input` — and `toolArgs` may arrive as a JSON STRING rather than an object.
669
- * Both are folded to Claude's shape so every gate reads one. (`toolInput` and the `after`
670
- * phase are the retired `toolCall` event's spelling, kept so an install not yet re-emitted
671
- * still reads right.) A `toolArgs` string that is not JSON throws: fail closed.
672
716
  */
673
717
  function readPayload() {
674
718
  const raw = readStdin();
@@ -679,16 +723,7 @@ function readPayload() {
679
723
  } catch {
680
724
  throw new Error('the hook payload on stdin is not JSON');
681
725
  }
682
- if (!payload || typeof payload !== 'object') return null;
683
- if (payload.phase === 'after') return null;
684
- if (payload.tool_input === undefined) {
685
- let ta = payload.toolArgs !== undefined ? payload.toolArgs : payload.toolInput;
686
- if (typeof ta === 'string') {
687
- try { ta = JSON.parse(ta); } catch { throw new Error('the payload\'s toolArgs is not JSON'); }
688
- }
689
- if (ta !== undefined) payload.tool_input = ta;
690
- }
691
- return payload;
726
+ return payload && typeof payload === 'object' ? payload : null;
692
727
  }
693
728
 
694
729
  function gitGate(args) {
@@ -759,9 +794,8 @@ function ruleDecide(args, payload) {
759
794
  const ti = (payload && payload.tool_input) || {};
760
795
  const p = ti.file_path || ti.path || '';
761
796
  if (typeof p !== 'string' || !p) return 0;
762
- // Write carries `content`, Edit carries `new_string` (Copilot: `file_text`, `new_str`);
763
- // any of them can plant a credential.
764
- const body = [ti.content, ti.new_string, ti.file_text, ti.new_str]
797
+ // Write carries `content`, Edit carries `new_string`; either can plant a credential.
798
+ const body = [ti.content, ti.new_string]
765
799
  .find((v) => typeof v === 'string') || '';
766
800
  if (body && !DOTENV_RE.test(p) && SECRET_RE.test(body)) {
767
801
  return ask(args, 'rule-gate', 'law-1', `Geneseed (Sealed Secrets) — ${p} would carry a `
@@ -781,8 +815,7 @@ export const cmdRuleGate = guardGate(ruleGate, 'rule-gate');
781
815
  // ======================================================================================
782
816
 
783
817
  /**
784
- * Copilot's and Bob's PreToolUse entries carry no matcher over documented tool names, so
785
- * neither can name `git-gate` for Bash and `rule-gate` for Write the way Claude's
818
+ * Bob's PreToolUse entry carries no matcher over documented tool names, so it cannot name `git-gate` for Bash and `rule-gate` for Write the way Claude's
786
819
  * `PreToolUse` does. This verb is the two fused, dispatched on the payload rather
787
820
  * than on a matcher: a `command` field is a shell call and gets the git checks, a path
788
821
  * field is a write and gets the rule checks. Nothing here decides anything the two named
@@ -926,8 +959,7 @@ export function readNotes(raw) {
926
959
  if (s[0] === '{') {
927
960
  let payload;
928
961
  try { payload = JSON.parse(s); } catch { return raw; }
929
- // Copilot's agentStop/preCompact spell it `transcriptPath`.
930
- const tp = payload && (payload.transcript_path || payload.transcriptPath);
962
+ const tp = payload && payload.transcript_path;
931
963
  if (tp) return flattenTranscript(tp);
932
964
  return raw;
933
965
  }