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.
- package/CHANGELOG.md +71 -1
- package/DESIGN.md +13 -10
- package/QUICKSTART.md +1 -1
- package/README.md +31 -32
- package/SETUP.md +7 -42
- package/SHIPPED.md +4 -4
- package/adapters/bob/README.md +17 -1
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
- package/adapters/opencode/README.md +3 -3
- package/adapters/opencode/plugins/geneseed-context.js +78 -8
- package/adapters/opencode/plugins/geneseed-guard.js +9 -9
- package/adapters/opencode/plugins/geneseed-workflow.js +1 -1
- package/adapters/opencode/workflows/_runtime.js +2 -2
- package/adapters/opencode/workflows/dispatch.js +1 -1
- package/bin/geneseed-hook.mjs +1 -1
- package/docs/candidate-hosts.md +12 -5
- package/docs/extending.md +72 -40
- package/docs/token-footprint.md +26 -21
- package/docs/web/claude-hooks.md +1 -1
- package/docs/web/collaboration.md +1 -1
- package/docs/web/exclusions.md +0 -1
- package/docs/web/footprint.md +1 -1
- package/docs/web/harnesses.md +14 -14
- package/docs/web/install-quick.md +1 -1
- package/docs/web/plugin-guard.md +2 -2
- package/docs/web/rules.md +1 -1
- package/docs/web/themes-author.md +2 -2
- package/docs/web-ui.md +2 -2
- package/harness.config.json +1 -1
- package/js/README.md +8 -7
- package/js/build/driver.mjs +26 -95
- package/js/build/emit-claude.mjs +76 -81
- package/js/build/generate.mjs +0 -1
- package/js/build/render.mjs +10 -3
- package/js/build/source.mjs +105 -1
- package/js/build/stubs.mjs +25 -4
- package/js/cli-table.json +8 -8
- package/js/hosts/hooks.mjs +91 -59
- package/js/hosts/hosts.mjs +5 -21
- package/js/hosts/installs.mjs +4 -6
- package/js/hosts/mcp.mjs +3 -13
- package/js/hosts/native.mjs +28 -51
- package/js/hosts/opencode.mjs +1 -1
- package/js/hosts/settings.mjs +4 -78
- package/js/inspect/checks-authoring.mjs +153 -117
- package/js/inspect/checks-repo.mjs +45 -1
- package/js/inspect/diff.mjs +11 -3
- package/js/inspect/doctor.mjs +3 -3
- package/js/inspect/excludes.mjs +0 -4
- package/js/inspect/inventory.mjs +1 -2
- package/js/inspect/status.mjs +4 -4
- package/js/maintain/migrate.mjs +5 -5
- package/js/maintain/setup.mjs +3 -5
- package/js/maintain/uninstall.mjs +8 -42
- package/js/ui/tui.mjs +1 -1
- package/js/web/actions.mjs +4 -5
- package/js/web/api.mjs +10 -5
- package/package.json +1 -2
- package/registry.json +7 -0
- package/src/AGENT.md.tmpl +86 -65
- package/src/agents/_template.md +3 -3
- package/src/agents/advocate.md +3 -3
- package/src/agents/architect.md +3 -3
- package/src/agents/developer.md +4 -4
- package/src/agents/docs.md +3 -3
- package/src/agents/empiricist.md +4 -4
- package/src/agents/explorer.md +4 -4
- package/src/agents/framer.md +3 -3
- package/src/agents/historian.md +3 -3
- package/src/agents/operator.md +3 -3
- package/src/agents/pragmatist.md +3 -3
- package/src/agents/researcher.md +7 -7
- package/src/agents/reviewer.md +3 -3
- package/src/agents/security.md +3 -3
- package/src/agents/skeptic.md +3 -3
- package/src/agents/steward.md +3 -3
- package/src/agents/tester.md +3 -3
- package/src/agents/user-advocate.md +3 -3
- package/src/agents/visionary.md +3 -3
- package/src/doctrines/README.md +4 -3
- package/src/doctrines/comms.md +4 -4
- package/src/doctrines/craft.md +20 -20
- package/src/doctrines/ops.md +19 -19
- package/src/doctrines/process.md +20 -20
- package/src/doctrines/rigor.md +22 -22
- package/src/laws/universal.md +26 -44
- package/src/memory/README.md +3 -3
- package/src/modes/foreman.md +1 -1
- package/src/notebook/README.md +1 -1
- package/src/ontology/universal.md +8 -10
- package/src/postures/artisan.md +2 -2
- package/src/skills/_self-improvement.md +4 -4
- package/src/skills/_template.md +1 -1
- package/src/skills/brainstorm.md +3 -3
- package/src/skills/bruno-collection-generator.md +6 -6
- package/src/skills/bruno-test-writer.md +5 -5
- package/src/skills/ci-fix.md +4 -4
- package/src/skills/commit.md +11 -10
- package/src/skills/consolidate-memory.md +4 -4
- package/src/skills/council.md +2 -2
- package/src/skills/daydream/SKILL.md +10 -3
- package/src/skills/daydream/VENDOR.md +10 -2
- package/src/skills/daydream/instructions.md +2 -2
- package/src/skills/debug.md +4 -4
- package/src/skills/deps-audit.md +3 -3
- package/src/skills/develop.md +7 -7
- package/src/skills/document-project.md +6 -6
- package/src/skills/explain-changes/SKILL.md +69 -0
- package/src/skills/explain-changes/VENDOR.md +20 -0
- package/src/skills/explain-changes/scripts/render_changes.mjs +558 -0
- package/src/skills/forge-mcp.md +4 -4
- package/src/skills/fresh-eyes.md +4 -4
- package/src/skills/frontend-design.md +1 -1
- package/src/skills/geneseed.md +4 -4
- package/src/skills/git-archaeology.md +5 -5
- package/src/skills/git-rescue.md +7 -7
- package/src/skills/handoff.md +3 -3
- package/src/skills/ingest.md +5 -5
- package/src/skills/migrate.md +8 -8
- package/src/skills/parallel-agents.md +2 -2
- package/src/skills/pipeline.md +2 -2
- package/src/skills/plan.md +3 -3
- package/src/skills/ponytail.md +2 -2
- package/src/skills/profile.md +2 -2
- package/src/skills/prose.md +1 -1
- package/src/skills/prototype.md +1 -1
- package/src/skills/react-view-transitions/SKILL.md +1 -1
- package/src/skills/react-view-transitions/VENDOR.md +7 -2
- package/src/skills/refactor.md +3 -3
- package/src/skills/release.md +5 -5
- package/src/skills/repo-map.md +4 -4
- package/src/skills/research.md +9 -9
- package/src/skills/review-response.md +4 -4
- package/src/skills/rule.md +3 -3
- package/src/skills/security-audit.md +5 -5
- package/src/skills/ship.md +5 -5
- package/src/skills/skill-forge.md +4 -4
- package/src/skills/token-report/SKILL.md +2 -3
- package/src/skills/token-report/VENDOR.md +2 -3
- package/src/skills/token-report/scripts/token_report.mjs +7 -121
- package/src/skills/wiki.md +5 -5
- package/src/skills/workflow.md +3 -4
- package/src/skills/worktree.md +4 -4
- package/themes/_TEMPLATE.json +37 -39
- package/themes/biker.json +37 -39
- package/themes/commentator.json +37 -39
- package/themes/cyberpunk.json +37 -39
- package/themes/gamer.json +37 -39
- package/themes/imperial.json +37 -39
- package/themes/joker.json +37 -39
- package/themes/marvin.json +37 -39
- package/themes/mean.json +37 -39
- package/themes/military.json +37 -39
- package/themes/neutral.json +37 -39
- package/themes/pirate.json +37 -39
- package/themes/sports.json +37 -39
- package/themes/verstappen.json +37 -39
- package/themes/wizard.json +37 -39
- package/web/dist/assets/Activity-BjVp522q.js +1 -0
- package/web/dist/assets/ActivityDetail-8ep1bspW.js +1 -0
- package/web/dist/assets/Dashboard-OkC_PCH6.js +2 -0
- package/web/dist/assets/{Docs-DrJNDPwI.js → Docs-CKPWz00o.js} +2 -2
- package/web/dist/assets/FilterInput-CVyqmYWM.js +1 -0
- package/web/dist/assets/GreenhouseView-D0cbCJ5m.js +2 -0
- package/web/dist/assets/Installs-BtGGdoTt.js +2 -0
- package/web/dist/assets/JournalView-Ckl4dmzR.js +1 -0
- package/web/dist/assets/Laws-CLpdRRk8.js +3 -0
- package/web/dist/assets/Library-wPPuKQrI.js +1 -0
- package/web/dist/assets/{Markdown-BQAvN8xx.js → Markdown-CkFyfgNf.js} +14 -14
- package/web/dist/assets/OperatorHudView-CiAkAkQL.js +2 -0
- package/web/dist/assets/Personal-D8CnDo9v.js +1 -0
- package/web/dist/assets/Tabs-B4HNrrCs.js +1 -0
- package/web/dist/assets/drawable-BiwaOo6x.js +1 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/web/dist/assets/index-C1yil5j1.js +2 -0
- package/web/dist/assets/index-CDPDk2vj.css +1 -0
- package/web/dist/assets/{react-DbkvxWG5.js → react-B9Z_tXRV.js} +1 -1
- package/web/dist/assets/sections-GalMS0Zv.js +1 -0
- package/web/dist/assets/usePoll-O0sJEw1Y.js +1 -0
- package/web/dist/index.html +4 -4
- package/web/src/pages/Laws.jsx +616 -361
- package/adapters/copilot/README.md +0 -105
- package/web/dist/assets/Activity-CH-7cPIM.js +0 -1
- package/web/dist/assets/ActivityDetail-DZvz0xS5.js +0 -1
- package/web/dist/assets/CatalogRow-DftXpZHY.js +0 -1
- package/web/dist/assets/Diff-DvwdG0L9.js +0 -2
- package/web/dist/assets/Doctor-OTmwmYZg.js +0 -1
- package/web/dist/assets/FilterInput-BakJLvaV.js +0 -1
- package/web/dist/assets/GreenhouseView-XxrAdriX.js +0 -2
- package/web/dist/assets/Harness-C_LYNk5c.js +0 -1
- package/web/dist/assets/JournalView-BcuyeEaR.js +0 -1
- package/web/dist/assets/Laws-CWHrTKY4.js +0 -3
- package/web/dist/assets/Library-CA9IHghn.js +0 -1
- package/web/dist/assets/OperatorHudView-BMRY1Tt4.js +0 -2
- package/web/dist/assets/Profile-wceET1JY.js +0 -1
- package/web/dist/assets/Rules-BEXvY4Tx.js +0 -1
- package/web/dist/assets/Settings-DS2zF0Ez.js +0 -1
- package/web/dist/assets/Skills-DahlOJuI.js +0 -1
- package/web/dist/assets/StatusBadge-Cg6j-NJL.js +0 -1
- package/web/dist/assets/activity-iWaWVtsR.js +0 -1
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/drawable-BTHjTzF7.js +0 -1
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-AN5Emnrg.js +0 -2
- package/web/dist/assets/index-Ruh93A3A.css +0 -1
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/lawCats-Btjp3Hk_.js +0 -1
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +0 -1
package/js/hosts/hooks.mjs
CHANGED
|
@@ -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.
|
|
12
|
-
*
|
|
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'],
|
|
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
|
|
464
|
-
|
|
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
|
|
515
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
763
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/js/hosts/hosts.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
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,
|
|
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`/`.
|
|
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
|
|
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.
|
|
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
|
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -137,15 +137,14 @@ function themeFromAgent(agentMd) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
* `_harness_setup.CARRIERS` — the
|
|
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
|
|
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', '
|
|
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,
|
|
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', '
|
|
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
|
|
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.
|
package/js/hosts/native.mjs
CHANGED
|
@@ -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
|
|
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>/…`, `.
|
|
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
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* the
|
|
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
|
-
|
|
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
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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)
|
|
449
|
-
|
|
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
|
|
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);
|
package/js/hosts/opencode.mjs
CHANGED
|
@@ -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
|
|
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
|
),
|