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
@@ -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) {
@@ -700,14 +735,14 @@ function gitDecide(args, payload) {
700
735
  const command = ((payload && payload.tool_input) || {}).command;
701
736
  if (typeof command !== 'string') return 0;
702
737
  if (DESTRUCTIVE_GIT_RE.test(command)) {
703
- return ask(args, 'git-gate', 'law-4', 'Geneseed Law IV \u2014 a history-rewriting or '
738
+ return ask(args, 'git-gate', 'law-4', 'Geneseed (Deletion Is Deliberate) \u2014 a history-rewriting or '
704
739
  + 'discarding git act needs confirmation bound to this specific command');
705
740
  }
706
741
  // `--no-consent`: the process pack (or process 5 alone) is off, so the commit/push ask has
707
742
  // no rule behind it — but Law IV above is universal and has already run.
708
743
  if (args.noConsent || !GIT_GATE_RE.test(command)) return 0;
709
- return ask(args, 'git-gate', 'process-5', 'Geneseed Doctrine process 5 \u2014 every git '
710
- + 'commit/push needs explicit approval');
744
+ return ask(args, 'git-gate', 'process-5', 'Geneseed (Consent Before Push) \u2014 every git '
745
+ + 'commit/push needs explicit approval; to see the change first, use the explain-changes skill');
711
746
  }
712
747
  export const cmdGitGate = guardGate(gitGate, 'git-gate');
713
748
 
@@ -759,18 +794,17 @@ 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
- return ask(args, 'rule-gate', 'law-1', `Geneseed Law I — ${p} would carry a `
801
+ return ask(args, 'rule-gate', 'law-1', `Geneseed (Sealed Secrets) — ${p} would carry a `
768
802
  + 'credential-shaped string. Secrets live in .env or a secret manager, never in a '
769
803
  + 'tracked file.');
770
804
  }
771
805
  const target = ruleGateTarget(p, args.root);
772
806
  if (!target) return 0;
773
- return ask(args, 'rule-gate', 'process-1', `Geneseed Doctrine process 1 \u2014 writing to `
807
+ return ask(args, 'rule-gate', 'process-1', `Geneseed (Persist Insight) \u2014 writing to `
774
808
  + `${target}: a standing rule, or a fact to remember? That choice is the user's. Run the `
775
809
  + 'rule skill first.');
776
810
  }
@@ -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.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A faithful translation of `_build_emit._write_native_layer` and the frontmatter
5
5
  * builders it dispatches to. Capability specs become host-native subagents and skills:
6
- * one dialect per host (OpenCode / Claude Code / Copilot), vendored skill folders copied
6
+ * one dialect per host (OpenCode / Claude Code), vendored skill folders copied
7
7
  * through verbatim, authoring templates shipped flat.
8
8
  *
9
9
  * WHY THIS IS THE HARD HALF. `js/build/render.mjs` is a pure function of `src/` and `themes/`;
@@ -33,8 +33,11 @@ import path from 'node:path';
33
33
  import { writeText, copyFile, readText } from '../lib/fs.mjs';
34
34
  import { jsonDumps, parseJson, formatValue, formatRepr, isTruthy, isDict } from '../lib/json.mjs';
35
35
 
36
+ /** What a folder skill writes where it needs its own directory — see `writeNativeLayer`. */
37
+ export const SKILL_DIR_PLACEHOLDER = '<this-skill-directory>';
38
+
36
39
  /** Mirrors `_build_core.VENDORED_SKILL_DIRS`. */
37
- export const VENDORED_SKILL_DIRS = new Set(['react-view-transitions', 'daydream', 'token-report']);
40
+ export const VENDORED_SKILL_DIRS = new Set(['react-view-transitions', 'daydream', 'token-report', 'explain-changes']);
38
41
 
39
42
  /**
40
43
  * `PurePath(rel).parts` — split on EITHER separator, empties dropped.
@@ -58,7 +61,7 @@ export function isVendoredPath(rel) {
58
61
  * `build._validate_is_vendored` — the same question, tolerant of `skills` at ANY depth.
59
62
  *
60
63
  * The per-repo native layers nest one level deeper than a `files`/opencode-global bundle
61
- * (`.claude/skills/<name>/…`, `.bob/skills/<name>/…`, `.github/skills/<name>/…`), so
64
+ * (`.claude/skills/<name>/…`, `.bob/skills/<name>/…`, `.openclaude/skills/<name>/…`), so
62
65
  * `doctor`'s scan of those trees needs the loose form or every vendored folder's own
63
66
  * upstream cross-links read as dead links. Never the last segment — `parts[:-1]`, because a
64
67
  * FILE named `skills` is not a directory of them.
@@ -79,8 +82,6 @@ const AGENT_COLORS = {
79
82
  const VALID_AGENT_COLOR_SLOTS = new Set(
80
83
  ['primary', 'secondary', 'accent', 'success', 'warning', 'error', 'info']);
81
84
 
82
- /** `_build_emit._SIBLING_AGENT_LINK_RE` — a bare same-dir agent filename in a link. */
83
- const SIBLING_AGENT_LINK_RE = /\]\(([A-Za-z0-9_-]+)\.md\)/g;
84
85
 
85
86
  /**
86
87
  * `_build_emit._strip_skill_body_links`'s pattern: every RELATIVE markdown link to a
@@ -296,51 +297,23 @@ const BASH_MARKER = '<!-- bash: allow -->';
296
297
  const WEBFETCH_MARKER = '<!-- webfetch: allow -->';
297
298
 
298
299
  /**
299
- * The readonly-tool line `claudeAgentFrontmatter`/`copilotAgentFrontmatter` each emit — a
300
- * DENYLIST for Claude, an ALLOWLIST for Copilot — differing only in the key, the base list,
301
- * the gated entries the markers toggle, and whether the value is bracketed. Each `gated`
302
- * row is `[tool, marker, listedWhenMarked]`: `false` for Claude (the tool is denied UNLESS
303
- * the marker allows it) and `true` for Copilot (the tool is allowed ONLY IF the marker is
304
- * present) — the same marker, read in the opposite sense a denylist and an allowlist
305
- * require. Row order is emit order: WebFetch before Bash keeps the unmarked Claude line
306
- * byte-identical to what it was before the webfetch marker existed.
300
+ * `_build_emit._claude_agent_frontmatter`. A read-only spec gets a `disallowedTools` DENYLIST;
301
+ * each opt-in marker takes its tool back off the list. Order is emit order: WebFetch before
302
+ * Bash keeps the unmarked line byte-identical to what it was before the webfetch marker existed.
307
303
  */
308
- const READONLY_TOOLS = {
309
- claude: {
310
- key: 'disallowedTools', tools: ['Write', 'Edit', 'NotebookEdit'], brackets: false,
311
- gated: [['WebFetch', WEBFETCH_MARKER, false], ['Bash', BASH_MARKER, false]],
312
- },
313
- copilot: {
314
- key: 'tools', tools: ['read', 'search', 'todo', 'agent'], brackets: true,
315
- gated: [['fetch', WEBFETCH_MARKER, true], ['execute', BASH_MARKER, true]],
316
- },
317
- };
318
-
319
- /** `_build_emit._claude_agent_frontmatter` / `._copilot_agent_frontmatter`. */
320
- function claudeCopilotAgentFrontmatter(host, stem, text, overrides) {
304
+ function claudeAgentFrontmatter(stem, text, overrides) {
321
305
  const fm = [`name: ${stem}`, `description: ${jsonDumps(descOf(text))}`];
322
306
  const ov = agentOverride(overrides, stem);
323
307
  if (isTruthy(ov.model)) fm.push(`model: ${formatValue(ov.model)}`);
324
308
  if (isReadonly(text)) {
325
- const { key, tools, brackets, gated } = READONLY_TOOLS[host];
326
- const list = [...tools];
327
- for (const [tool, marker, listedWhenMarked] of gated) {
328
- if (text.includes(marker) === listedWhenMarked) list.push(tool);
329
- }
330
- fm.push(`${key}: ${brackets ? `[${list.join(', ')}]` : list.join(', ')}`);
309
+ const list = ['Write', 'Edit', 'NotebookEdit'];
310
+ if (!text.includes(WEBFETCH_MARKER)) list.push('WebFetch');
311
+ if (!text.includes(BASH_MARKER)) list.push('Bash');
312
+ fm.push(`disallowedTools: ${list.join(', ')}`);
331
313
  }
332
314
  return fm;
333
315
  }
334
316
 
335
- function claudeAgentFrontmatter(stem, text, overrides) {
336
- return claudeCopilotAgentFrontmatter('claude', stem, text, overrides);
337
- }
338
-
339
- /** `tools:` is an ALLOWLIST, not a denylist. */
340
- function copilotAgentFrontmatter(stem, text, overrides) {
341
- return claudeCopilotAgentFrontmatter('copilot', stem, text, overrides);
342
- }
343
-
344
317
  /** `_build_emit._opencode_agent_frontmatter`. */
345
318
  function opencodeAgentFrontmatter(stem, text, overrides, theme = null) {
346
319
  const fm = [`description: ${jsonDumps(descOf(text))}`, 'mode: subagent'];
@@ -387,6 +360,14 @@ function relPosix(base, target) {
387
360
  * source root it is relativised against (Python reads `_build_core.SRC` directly, which
388
361
  * is the module-level single owner; here it travels explicitly, as in `js/build/render.mjs`).
389
362
  *
363
+ * `opts.skillDirOf(name)` — set for a host that hands the model a skill's TEXT but not where
364
+ * the skill lives (Bob). A folder skill names its scripts and prompts
365
+ * `<this-skill-directory>/…`; Claude Code resolves that because it announces each skill's
366
+ * base directory on load, while on Bob the model got the text with a hole in it and went
367
+ * hunting through the filesystem. With the option, every rendered `.md` of a vendored folder
368
+ * has the placeholder replaced by the answer. Only vendored folders carry the placeholder — a
369
+ * flat skill has no siblings to point at.
370
+ *
390
371
  * Returns `{ nAgents, nSkills, written }` — Python's 3-tuple, with `written` as absolute
391
372
  * paths in write order.
392
373
  *
@@ -400,7 +381,7 @@ function relPosix(base, target) {
400
381
  */
401
382
  export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null, {
402
383
  host = 'opencode', oldOwned = null, cfg = null, manifestExisted = true,
403
- theme = null, src,
384
+ theme = null, src, skillDirOf = null,
404
385
  } = {}) {
405
386
  const ov = overrides || {};
406
387
  const oldSet = oldOwned !== null && oldOwned !== undefined ? new Set(oldOwned) : null;
@@ -445,8 +426,10 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
445
426
  mkdirSync(path.dirname(dest), { recursive: true });
446
427
  // The same vendored folder mixes both writers: a rendered `.md` goes through
447
428
  // writeText (and picks up CRLF on Windows), a binary through copy2 (and keeps LF).
448
- if (text !== null) writeText(dest, text);
449
- else copyFile(source, dest);
429
+ if (text !== null) {
430
+ writeText(dest, skillDirOf === null ? text
431
+ : text.replaceAll(SKILL_DIR_PLACEHOLDER, skillDirOf(sparts[1])));
432
+ } else copyFile(source, dest);
450
433
  written.push(dest);
451
434
  continue;
452
435
  }
@@ -480,12 +463,6 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
480
463
  if (host === 'claude') {
481
464
  fm = claudeAgentFrontmatter(stem, text, ov);
482
465
  dest = path.join(agentsDir, `${stem}.md`);
483
- } else if (host === 'copilot') {
484
- fm = copilotAgentFrontmatter(stem, text, ov);
485
- dest = path.join(agentsDir, `${stem}.agent.md`);
486
- // The filename change drags the links with it: a sibling `](skeptic.md)` would
487
- // point at a file this dialect never writes.
488
- body = body.replace(SIBLING_AGENT_LINK_RE, ']($1.agent.md)');
489
466
  } else {
490
467
  fm = opencodeAgentFrontmatter(stem, text, ov, theme);
491
468
  dest = path.join(agentsDir, `${stem}.md`);
@@ -494,7 +471,7 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
494
471
  } else {
495
472
  // Skills are BYTE-IDENTICAL across hosts: name + description, body link-stripped.
496
473
  // The user-only key is emitted for every host too — Claude Code and Bob honour it,
497
- // OpenCode and Copilot ignore an unknown key — so the identity holds.
474
+ // OpenCode ignores an unknown key — so the identity holds.
498
475
  fm = [`name: ${stem}`, `description: ${jsonDumps(skillDescription(text))}`];
499
476
  if (isUserInvokedOnly(text)) fm.push('disable-model-invocation: true');
500
477
  body = stripSkillBodyLinks(body);
@@ -84,7 +84,7 @@ const AGENT_OVERRIDES_STUB = {
84
84
  _recommended: {
85
85
  _comment: (
86
86
  'Cheap-tier seats: copy the ones you want into "agents" with your host\'s cheap '
87
- + 'model id (OpenCode: "anthropic/claude-haiku-4-5"; Claude Code / Copilot: "haiku"). '
87
+ + 'model id (OpenCode: "anthropic/claude-haiku-4-5"; Claude Code: "haiku"). '
88
88
  + 'Read-and-summarise work — a council convenes several at once, so this is where '
89
89
  + 'the savings compound. Nothing applies until you copy it.'
90
90
  ),