geneseed 3.9.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 (76) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/QUICKSTART.md +1 -1
  3. package/README.md +27 -28
  4. package/SETUP.md +7 -42
  5. package/SHIPPED.md +2 -2
  6. package/adapters/bob/README.md +17 -1
  7. package/adapters/openclaude/README.md +1 -1
  8. package/adapters/opencode/plugins/geneseed-context.js +78 -8
  9. package/bin/geneseed-hook.mjs +1 -1
  10. package/docs/candidate-hosts.md +12 -5
  11. package/docs/token-footprint.md +26 -21
  12. package/docs/web/claude-hooks.md +1 -1
  13. package/docs/web/collaboration.md +1 -1
  14. package/docs/web/exclusions.md +0 -1
  15. package/docs/web/footprint.md +1 -1
  16. package/docs/web/harnesses.md +13 -13
  17. package/docs/web/install-quick.md +1 -1
  18. package/docs/web-ui.md +1 -1
  19. package/harness.config.json +1 -1
  20. package/js/README.md +4 -4
  21. package/js/build/driver.mjs +26 -95
  22. package/js/build/emit-claude.mjs +76 -81
  23. package/js/build/generate.mjs +0 -1
  24. package/js/build/render.mjs +4 -0
  25. package/js/build/stubs.mjs +18 -2
  26. package/js/cli-table.json +5 -5
  27. package/js/hosts/hooks.mjs +86 -54
  28. package/js/hosts/hosts.mjs +5 -21
  29. package/js/hosts/installs.mjs +4 -6
  30. package/js/hosts/mcp.mjs +3 -13
  31. package/js/hosts/native.mjs +27 -50
  32. package/js/hosts/opencode.mjs +1 -1
  33. package/js/hosts/settings.mjs +4 -78
  34. package/js/inspect/checks-authoring.mjs +4 -1
  35. package/js/inspect/checks-repo.mjs +45 -1
  36. package/js/inspect/diff.mjs +11 -3
  37. package/js/inspect/doctor.mjs +3 -3
  38. package/js/inspect/excludes.mjs +0 -4
  39. package/js/inspect/status.mjs +4 -4
  40. package/js/maintain/migrate.mjs +5 -5
  41. package/js/maintain/setup.mjs +3 -5
  42. package/js/maintain/uninstall.mjs +8 -42
  43. package/js/web/actions.mjs +4 -5
  44. package/js/web/api.mjs +4 -4
  45. package/package.json +1 -2
  46. package/src/AGENT.md.tmpl +69 -52
  47. package/src/laws/universal.md +3 -0
  48. package/src/ontology/universal.md +1 -1
  49. package/src/skills/daydream/SKILL.md +10 -3
  50. package/src/skills/daydream/VENDOR.md +10 -2
  51. package/src/skills/daydream/instructions.md +2 -2
  52. package/src/skills/explain-changes/SKILL.md +1 -1
  53. package/src/skills/explain-changes/VENDOR.md +1 -1
  54. package/src/skills/geneseed.md +1 -1
  55. package/src/skills/react-view-transitions/SKILL.md +1 -1
  56. package/src/skills/react-view-transitions/VENDOR.md +7 -2
  57. package/src/skills/token-report/SKILL.md +2 -3
  58. package/src/skills/token-report/VENDOR.md +2 -3
  59. package/src/skills/token-report/scripts/token_report.mjs +7 -121
  60. package/src/skills/workflow.md +1 -1
  61. package/web/dist/assets/{Activity-DAzV-qQj.js → Activity-BjVp522q.js} +1 -1
  62. package/web/dist/assets/{ActivityDetail-CwEwMtKl.js → ActivityDetail-8ep1bspW.js} +1 -1
  63. package/web/dist/assets/{Dashboard-C2ELBW-X.js → Dashboard-OkC_PCH6.js} +2 -2
  64. package/web/dist/assets/{Docs-iZrvQP8J.js → Docs-CKPWz00o.js} +1 -1
  65. package/web/dist/assets/{GreenhouseView-Bq1QmaGO.js → GreenhouseView-D0cbCJ5m.js} +1 -1
  66. package/web/dist/assets/{Installs-BiqR8xdO.js → Installs-BtGGdoTt.js} +1 -1
  67. package/web/dist/assets/{JournalView-6wilG5Zd.js → JournalView-Ckl4dmzR.js} +1 -1
  68. package/web/dist/assets/{Laws-D8YXD_XE.js → Laws-CLpdRRk8.js} +1 -1
  69. package/web/dist/assets/{Library-DwuTZ2-L.js → Library-wPPuKQrI.js} +1 -1
  70. package/web/dist/assets/{OperatorHudView-B-HVBqfY.js → OperatorHudView-CiAkAkQL.js} +1 -1
  71. package/web/dist/assets/{Personal-BbUTmZQR.js → Personal-D8CnDo9v.js} +1 -1
  72. package/web/dist/assets/{Tabs-_pjUFIEI.js → Tabs-B4HNrrCs.js} +1 -1
  73. package/web/dist/assets/{drawable-CoIulEH2.js → drawable-BiwaOo6x.js} +1 -1
  74. package/web/dist/assets/{index-BMwSp8BP.js → index-C1yil5j1.js} +2 -2
  75. package/web/dist/index.html +1 -1
  76. package/adapters/copilot/README.md +0 -105
@@ -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
 
@@ -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.
@@ -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
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The five host config dirs, and the two path primitives they are built on.
2
+ * The four host config dirs, and the two path primitives they are built on.
3
3
  *
4
4
  * EXTRACTED IN P5c, and the reason is arithmetic rather than taste. `bin/build-driver.mjs`
5
5
  * owned these; `bin/geneseed-cli.mjs` needs the same four to find a global install, and a
@@ -172,20 +172,6 @@ export function opencodeConfigDir() {
172
172
  return resolvePath(path.join(base, 'opencode'));
173
173
  }
174
174
 
175
- /**
176
- * `_build_core._copilot_config_dir` — `~/.copilot`, relocatable via `$COPILOT_CONFIG_DIR`.
177
- *
178
- * Geneseed's own knob, mirroring `$BOB_CONFIG_DIR`: Copilot documents no such variable, but
179
- * tests, doctor and locked-down setups still need to re-point the target. Like every
180
- * relocation variable it is CLEARED by `golden.cell_env`, so no cell can observe whether
181
- * a driver honours it — see `test_the_relocation_var_moves_the_global_target`.
182
- */
183
- export function copilotConfigDir() {
184
- const env = process.env.COPILOT_CONFIG_DIR;
185
- if (env) return resolvePath(env);
186
- return resolvePath(path.join(os.homedir(), '.copilot'));
187
- }
188
-
189
175
  /**
190
176
  * `_build_core._claude_config_dir` — `~/.claude`, and there is NO env branch BY DESIGN.
191
177
  *
@@ -223,7 +209,7 @@ export function openclaudeConfigDir() {
223
209
 
224
210
  /**
225
211
  * `_build_global.HOSTS`, reduced to the two columns a non-emitting caller needs, and IN ITS
226
- * ORDER — opencode, claude, bob, copilot, openclaude.
212
+ * ORDER — opencode, claude, bob, openclaude.
227
213
  *
228
214
  * An array rather than an object because the order is observable output, not an
229
215
  * implementation detail: `harness exclude add` walks it and prints one message per host, so
@@ -233,21 +219,20 @@ export function openclaudeConfigDir() {
233
219
  *
234
220
  * `projectMarker` joined in P5f. `_install_targets` asks "does this cwd carry a project
235
221
  * install of host H", and `_claude_cfg` asks "which subdir holds a project install's
236
- * manifest" — both are the same `.opencode`/`.claude`/`.bob`/`.github` value the Python reads
222
+ * manifest" — both are the same `.opencode`/`.claude`/`.bob`/`.openclaude` value the Python reads
237
223
  * out of `build.HOSTS[host]["project_marker"]`, so it belongs beside the config dir rather
238
224
  * than in a second table in `js/hosts/installs.mjs`.
239
225
  *
240
226
  * `agentFile` joined in P5h, and for one caller: `cmd_uninstall` names the managed block's
241
227
  * carrier in its "removes:" preamble (`the CLAUDE.md managed block`, `the AGENTS.md managed
242
228
  * block`). It is a column and not a literal in the message for the reason every other column
243
- * here is one — Bob and Copilot both answer `AGENTS.md` while Claude answers `CLAUDE.md`, and
229
+ * here is one — Bob answers `AGENTS.md` while Claude answers `CLAUDE.md`, and
244
230
  * a host added later must not need the message edited to stay true.
245
231
  */
246
232
  export const HOSTS = [
247
233
  { host: 'opencode', configDir: opencodeConfigDir, projectMarker: '.opencode', agentFile: 'AGENT.md' },
248
234
  { host: 'claude', configDir: claudeConfigDir, projectMarker: '.claude', agentFile: 'CLAUDE.md' },
249
235
  { host: 'bob', configDir: bobConfigDir, projectMarker: '.bob', agentFile: 'AGENTS.md' },
250
- { host: 'copilot', configDir: copilotConfigDir, projectMarker: '.github', agentFile: 'AGENTS.md' },
251
236
  { host: 'openclaude', configDir: openclaudeConfigDir, projectMarker: '.openclaude', agentFile: 'CLAUDE.md' },
252
237
  ];
253
238
 
@@ -271,14 +256,13 @@ export const HOSTS = [
271
256
  * single boolean was wrong both ways: `false` shipped the §4 Skills table on top of a
272
257
  * catalogue Bob already had (~1.7k tokens twice), `true` would have stripped the §3 Agents
273
258
  * table that is Bob's ONLY agent catalogue. Each CATALOG block in AGENT.md names its kind and
274
- * resolves against its own flag. Copilot stays unverified and keeps both tables. Mirrored in
259
+ * resolves against its own flag. Mirrored in
275
260
  * `bin/build-driver.mjs` (which cannot import this module's spawning neighbours).
276
261
  */
277
262
  const NATIVE_CATALOG = {
278
263
  opencode: { skills: true, agents: true },
279
264
  claude: { skills: true, agents: true },
280
265
  bob: { skills: true, agents: false },
281
- copilot: { skills: false, agents: false },
282
266
  openclaude: { skills: true, agents: true },
283
267
  };
284
268
 
@@ -137,15 +137,14 @@ function themeFromAgent(agentMd) {
137
137
  }
138
138
 
139
139
  /**
140
- * `_harness_setup.CARRIERS` — the five instruction carriers, in the Python's order.
140
+ * `_harness_setup.CARRIERS` — the four instruction carriers, in the Python's order.
141
141
  *
142
142
  * A shared table rather than three copies of the same list: `_theme_of_dir`,
143
143
  * `_posture_of_dir` and `_mode_of_dir` each walk it, and the order is observable (the first
144
144
  * carrier that answers wins). `rules/geneseed.md` is spelled with `path.join` at each use
145
145
  * because the Python writes `d / "rules" / "geneseed.md"` for that one entry.
146
146
  */
147
- const CARRIERS = ['AGENT.md', 'CLAUDE.md', path.join('rules', 'geneseed.md'),
148
- 'copilot-instructions.md', 'AGENTS.md'];
147
+ const CARRIERS = ['AGENT.md', 'CLAUDE.md', path.join('rules', 'geneseed.md'), 'AGENTS.md'];
149
148
 
150
149
  /**
151
150
  * The scan `themeOfDir`, `leadOfDir`, `doctrinesOfDir` and `excludedRulesOfDir` each wrote
@@ -392,7 +391,6 @@ export const EMIT_HOST_SCOPE = new Map([
392
391
  ['opencode', ['opencode', 'project']], ['opencode-global', ['opencode', 'global']],
393
392
  ['claude', ['claude', 'project']], ['claude-global', ['claude', 'global']],
394
393
  ['bob', ['bob', 'project']], ['bob-global', ['bob', 'global']],
395
- ['copilot', ['copilot', 'project']], ['copilot-global', ['copilot', 'global']],
396
394
  ['openclaude', ['openclaude', 'project']], ['openclaude-global', ['openclaude', 'global']],
397
395
  ]);
398
396
 
@@ -410,7 +408,7 @@ export const DISABLED_STASH = '.geneseed-disabled';
410
408
  * `_harness_mcp._claude_cfg` — where a Claude-STYLE install keeps its manifest.
411
409
  *
412
410
  * The config dir itself for a global install; `<repo>/<project_marker>` for a project one.
413
- * Host-aware so Bob and Copilot ride the whole Claude lifecycle with only the subdir
411
+ * Host-aware so Bob and OpenClaude ride the whole Claude lifecycle with only the subdir
414
412
  * differing, and the marker comes from `HOSTS` rather than a literal for that reason.
415
413
  */
416
414
  export function claudeCfg(root, scope, host = 'claude') {
@@ -435,7 +433,7 @@ export function installKind(root) {
435
433
 
436
434
  /** `_harness_mcp._install_state` — 'active' | 'disabled' | 'absent'. */
437
435
  export function installState(root, host = 'opencode', scope = 'global') {
438
- if (['claude', 'bob', 'copilot', 'openclaude'].includes(host)) return claudeState(root, scope, host);
436
+ if (['claude', 'bob', 'openclaude'].includes(host)) return claudeState(root, scope, host);
439
437
  if (isDir(path.join(root, DISABLED_STASH))) return 'disabled';
440
438
  return installKind(root) !== null ? 'active' : 'absent';
441
439
  }
package/js/hosts/mcp.mjs CHANGED
@@ -32,7 +32,7 @@ import { existsSync, mkdirSync, renameSync } from 'node:fs';
32
32
  import os from 'node:os';
33
33
  import path from 'node:path';
34
34
 
35
- import { bobConfigDir, copilotConfigDir, resolvePath } from './hosts.mjs';
35
+ import { bobConfigDir, resolvePath } from './hosts.mjs';
36
36
  import { installState, installTargets } from './installs.mjs';
37
37
  import { printOut, printErr, readText, writeText } from '../lib/fs.mjs';
38
38
  import { isDict, jsonDumpsIndent, parseJson } from '../lib/json.mjs';
@@ -44,7 +44,7 @@ import { opencodeTarget, readJsonc } from './settings.mjs';
44
44
  const dget = (obj, key, dflt) => (isDict(obj) && Object.hasOwn(obj, key) ? obj[key] : dflt);
45
45
 
46
46
  /** The hosts whose MCP config is strict JSON under `mcpServers`, with no `enabled` flag. */
47
- const FLAGLESS = ['claude', 'bob', 'copilot', 'openclaude'];
47
+ const FLAGLESS = ['claude', 'bob', 'openclaude'];
48
48
 
49
49
  /**
50
50
  * `_harness_mcp._MCP_PRESETS` — the ready-to-wire servers the MCP screen can toggle.
@@ -105,8 +105,7 @@ const serversOf = (config, host) => {
105
105
  * `_mcp_preset_block` — the block to WRITE for a preset, shaped for `host`.
106
106
  *
107
107
  * OpenCode takes it verbatim. The flagless three split the command list into `command`
108
- * (head) + `args` (tail) and rename `environment` to `env`; Copilot additionally requires
109
- * `type` and a `tools` allowlist.
108
+ * (head) + `args` (tail) and rename `environment` to `env`.
110
109
  */
111
110
  export function mcpPresetBlock(name, host = 'opencode') {
112
111
  const block = { ...MCP_PRESETS[name].block };
@@ -115,10 +114,6 @@ export function mcpPresetBlock(name, host = 'opencode') {
115
114
  const out = { command: cmd.length ? cmd[0] : '', args: cmd.slice(1) };
116
115
  const env = block.environment;
117
116
  if (env) out.env = { ...env };
118
- if (host === 'copilot') {
119
- out.type = 'local';
120
- out.tools = ['*'];
121
- }
122
117
  return out;
123
118
  }
124
119
 
@@ -243,11 +238,6 @@ export function mcpConfigFor(host, scope, root) {
243
238
  return scope === 'project' ? path.join(root, '.bob', 'mcp.json')
244
239
  : path.join(bobConfigDir(), 'settings', 'mcp.json');
245
240
  }
246
- if (host === 'copilot') {
247
- // The Copilot CLI reads MCP servers from ~/.copilot/mcp-config.json only — no per-repo
248
- // file is documented, so a project install carries no MCP wiring.
249
- return scope === 'global' ? path.join(copilotConfigDir(), 'mcp-config.json') : null;
250
- }
251
241
  if (host === 'openclaude') {
252
242
  // Claude's `.mcp.json` per repo; globally `.openclaude.json` beside the home dir, or
253
243
  // INSIDE `$OPENCLAUDE_CONFIG_DIR` when set — OpenClaude's own `getGlobalClaudeFile` rule.