geneseed 3.2.0 → 3.4.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 +150 -0
- package/README.md +24 -14
- package/SETUP.md +38 -7
- package/SHIPPED.md +6 -5
- package/adapters/bob/README.md +40 -10
- package/adapters/claude-code/README.md +5 -0
- package/adapters/claude-code/settings.json +8 -1
- package/adapters/copilot/README.md +52 -25
- package/adapters/opencode/README.md +11 -3
- package/adapters/opencode/plugins/geneseed-guard.js +40 -6
- package/bin/build-driver.mjs +16 -10
- package/bin/geneseed-cli.mjs +55 -56
- package/bin/geneseed-hook.mjs +21 -14
- package/docs/candidate-hosts.md +74 -0
- package/docs/extending.md +10 -8
- package/docs/token-footprint.md +32 -21
- package/docs/web/agents.md +1 -1
- package/docs/web/first-session.md +1 -1
- package/docs/web/skills.md +1 -1
- package/docs/web-ui.md +5 -13
- package/harness.config.json +1 -1
- package/js/README.md +5 -6
- package/js/build/emit-claude.mjs +22 -7
- package/js/build/render.mjs +37 -19
- package/js/build/source.mjs +22 -2
- package/js/cli-table.json +84 -0
- package/js/hosts/hooks.mjs +240 -78
- package/js/hosts/hosts.mjs +56 -2
- package/js/hosts/link.mjs +1 -1
- package/js/hosts/mcp.mjs +3 -1
- package/js/hosts/native.mjs +86 -15
- package/js/hosts/opencode.mjs +14 -0
- package/js/hosts/settings.mjs +173 -5
- package/js/inspect/excludes.mjs +1 -1
- package/js/inspect/inventory.mjs +13 -8
- package/js/inspect/status.mjs +50 -2
- package/js/maintain/uninstall.mjs +38 -11
- package/js/maintain/update.mjs +2 -2
- package/js/web/actions.mjs +8 -8
- package/js/web/api.mjs +5 -10
- package/js/web/docs.mjs +9 -31
- package/js/web/handler.mjs +20 -30
- package/js/web/jobs.mjs +5 -26
- package/js/web/routes.mjs +18 -84
- package/js/web/server.mjs +3 -3
- package/package.json +1 -1
- package/registry.json +99 -78
- package/src/AGENT.md.tmpl +69 -28
- package/src/agents/architect.md +4 -1
- package/src/agents/explorer.md +3 -1
- package/src/agents/operator.md +13 -0
- package/src/agents/pragmatist.md +12 -0
- package/src/agents/researcher.md +72 -0
- package/src/agents/security.md +7 -1
- package/src/agents/skeptic.md +2 -1
- package/src/agents/tester.md +5 -1
- package/src/agents/user-advocate.md +13 -0
- package/src/doctrines/README.md +3 -3
- package/src/doctrines/rigor.md +14 -1
- package/src/laws/universal.md +98 -20
- package/src/ontology/universal.md +38 -13
- package/src/skills/_self-improvement.md +19 -0
- package/src/skills/_template.md +6 -13
- package/src/skills/brainstorm.md +37 -19
- package/src/skills/bruno-collection-generator.md +52 -0
- package/src/skills/bruno-test-writer.md +51 -0
- package/src/skills/ci-fix.md +49 -0
- package/src/skills/codebase-design.md +1 -12
- package/src/skills/commit.md +1 -12
- package/src/skills/consolidate-memory.md +50 -0
- package/src/skills/council.md +1 -12
- package/src/skills/debug.md +1 -12
- package/src/skills/deps-audit.md +46 -0
- package/src/skills/develop.md +38 -20
- package/src/skills/document-project.md +1 -12
- package/src/skills/domain-modeling.md +1 -12
- package/src/skills/forge-mcp.md +2 -13
- package/src/skills/fresh-eyes.md +1 -12
- package/src/skills/frontend-design.md +1 -12
- package/src/skills/geneseed-code-review.md +1 -12
- package/src/skills/geneseed.md +1 -12
- package/src/skills/git-archaeology.md +1 -12
- package/src/skills/git-rescue.md +1 -12
- package/src/skills/handoff.md +1 -12
- package/src/skills/herdr.md +2 -12
- package/src/skills/ingest.md +1 -12
- package/src/skills/migrate.md +1 -12
- package/src/skills/opencode-theme.md +2 -12
- package/src/skills/parallel-agents.md +1 -12
- package/src/skills/pipeline.md +2 -13
- package/src/skills/plan.md +1 -12
- package/src/skills/ponytail.md +1 -12
- package/src/skills/profile.md +1 -12
- package/src/skills/prose.md +1 -12
- package/src/skills/prototype.md +1 -12
- package/src/skills/quiz.md +43 -0
- package/src/skills/refactor.md +1 -12
- package/src/skills/release.md +1 -12
- package/src/skills/repo-map.md +1 -12
- package/src/skills/research.md +6 -14
- package/src/skills/review-response.md +1 -12
- package/src/skills/roast-me.md +1 -12
- package/src/skills/rule.md +1 -12
- package/src/skills/security-audit.md +51 -0
- package/src/skills/ship.md +1 -12
- package/src/skills/skill-forge.md +59 -0
- package/src/skills/teach.md +50 -0
- package/src/skills/tickets.md +1 -12
- package/src/skills/wayfinder.md +1 -12
- package/src/skills/wiki.md +1 -12
- package/src/skills/workflow.md +74 -26
- package/src/skills/worktree.md +52 -0
- package/themes/_TEMPLATE.json +12 -8
- package/themes/biker.json +12 -8
- package/themes/commentator.json +12 -8
- package/themes/cyberpunk.json +12 -8
- package/themes/gamer.json +12 -8
- package/themes/imperial.json +12 -8
- package/themes/joker.json +12 -8
- package/themes/marvin.json +12 -8
- package/themes/mean.json +12 -8
- package/themes/military.json +12 -8
- package/themes/neutral.json +12 -8
- package/themes/pirate.json +12 -8
- package/themes/sports.json +12 -8
- package/themes/verstappen.json +12 -8
- package/themes/wizard.json +12 -8
- package/web/dist/assets/Activity-DMGhhjQc.js +1 -0
- package/web/dist/assets/ActivityDetail-CeQcDA7e.js +1 -0
- package/web/dist/assets/{CatalogRow-DqS4e5XM.js → CatalogRow-CnSbDPyg.js} +1 -1
- package/web/dist/assets/{Diff-CIrGbiqm.js → Diff-D_ksMBD1.js} +1 -1
- package/web/dist/assets/{Doctor-CpI53oHY.js → Doctor-Brb3_oCT.js} +1 -1
- package/web/dist/assets/FilterInput-DbRWPhtT.js +1 -0
- package/web/dist/assets/GreenhouseView-F3VNFr73.js +2 -0
- package/web/dist/assets/{Harness-vRYs11kX.js → Harness-DJSwfYeF.js} +1 -1
- package/web/dist/assets/JournalView-BLRYRnB3.js +1 -0
- package/web/dist/assets/Laws-CKYns9qe.js +3 -0
- package/web/dist/assets/Library-BK0YloSv.js +1 -0
- package/web/dist/assets/{Markdown-kgpg9EQM.js → Markdown-BzTRzc7a.js} +1 -1
- package/web/dist/assets/OperatorHudView-D3OQSGp5.js +2 -0
- package/web/dist/assets/{Profile-CHr-PPf6.js → Profile-CPA2u_0g.js} +1 -1
- package/web/dist/assets/{Rules-CsC6hfqk.js → Rules-Lukel-Ru.js} +1 -1
- package/web/dist/assets/Skills-BKRLHH7m.js +1 -0
- package/web/dist/assets/{StatusBadge-gRrak0nO.js → StatusBadge-DI3weRKd.js} +1 -1
- package/web/dist/assets/{activity-B0-FXZCj.js → activity-C_H2Bx1P.js} +1 -1
- package/web/dist/assets/{index-Bx3I7cRW.js → index-B7_Wga23.js} +1 -1
- package/web/dist/assets/index-DzdQjvM1.css +1 -0
- package/web/dist/assets/{index-DJOhOwwR.js → index-N8dTrVro.js} +1 -1
- package/web/dist/assets/index-Py_IQjgK.js +10 -0
- package/web/dist/assets/jobBuckets-DfTUJUxR.js +1 -0
- package/web/dist/assets/lawCats-DUS9UTH4.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +82 -64
- package/js/web/graph.mjs +0 -0
- package/src/skills/clarify.md +0 -28
- package/src/skills/crash-course.md +0 -28
- package/src/skills/decode.md +0 -28
- package/src/skills/drill.md +0 -28
- package/src/skills/feynman.md +0 -26
- package/src/skills/gap-detector.md +0 -27
- package/src/skills/learning-path.md +0 -26
- package/src/skills/tdd.md +0 -31
- package/web/dist/assets/Activity-BTCG2gMl.js +0 -1
- package/web/dist/assets/ActivityDetail-julsmPDB.js +0 -1
- package/web/dist/assets/Graph-CS4tA7YS.js +0 -1
- package/web/dist/assets/Laws-BjbHJrJt.js +0 -3
- package/web/dist/assets/Library-CjC7BAWN.js +0 -1
- package/web/dist/assets/Skills-BvatJGfe.js +0 -1
- package/web/dist/assets/index-BnRWQbZb.js +0 -12
- package/web/dist/assets/index-D0f9tJlA.css +0 -1
- package/web/dist/assets/roman-DkYN0LX9.js +0 -1
|
@@ -57,7 +57,8 @@ your-repo/
|
|
|
57
57
|
- Read-only agents (their spec says *Read-only*) get an OpenCode `permission` block
|
|
58
58
|
— `edit: deny`, `webfetch: deny`, and `bash: deny`, so a read-only agent can't
|
|
59
59
|
mutate via the shell either. One that must run read-only commands (tests, linters,
|
|
60
|
-
scanners) declares `<!-- bash: allow -->` in its spec, which gates bash to `ask
|
|
60
|
+
scanners) declares `<!-- bash: allow -->` in its spec, which gates bash to `ask`;
|
|
61
|
+
`<!-- webfetch: allow -->` (the researcher) re-opens web fetch the same way
|
|
61
62
|
instead. The rest keep edit access.
|
|
62
63
|
- OpenCode invokes a subagent via the task tool, e.g. `subagent_type: "reviewer"`.
|
|
63
64
|
- **Skills are native, not slash commands** — model-invoked via the `skill` tool
|
|
@@ -357,7 +358,10 @@ behaviour** — nothing changes the machine's current agent/model unless you opt
|
|
|
357
358
|
{ "agents": { "reviewer": { "model": "anthropic/claude-haiku-4-5", "temperature": 0.1, "variant": "high", "steps": 20 } } }
|
|
358
359
|
```
|
|
359
360
|
Re-emit to apply. (A future TUI screen will edit this map.) Unlisted keys are omitted,
|
|
360
|
-
so the agent inherits OpenCode's defaults.
|
|
361
|
+
so the agent inherits OpenCode's defaults. The stub also carries an inert
|
|
362
|
+
`_recommended.cheap-tier` list — the read-and-summarise seats (explorer, researcher,
|
|
363
|
+
tester, and the council bench) worth routing to a cheaper model; copy the names you
|
|
364
|
+
want into `agents`, nothing applies by itself.
|
|
361
365
|
- **Agent colours.** Each capability agent is emitted with a `color:` set to an OpenCode
|
|
362
366
|
*named theme slot*, sourced from the active harness theme's `AGENT_COLORS` map
|
|
363
367
|
(`themes/_TEMPLATE.json`; the shipped default — architect=`primary`, reviewer=`warning`,
|
|
@@ -383,7 +387,11 @@ behaviour** — nothing changes the machine's current agent/model unless you opt
|
|
|
383
387
|
files (Law I), catastrophic shell like `rm -rf /` (Law IV), and any mutation under
|
|
384
388
|
a declared wiki's `protected` folders (AGENT.md §8, from `wiki.jsonc`); **warns** on
|
|
385
389
|
`.env` writes and force-push. `GENESEED_GUARD=off` disables it, `=warn` downgrades
|
|
386
|
-
blocks to warnings.
|
|
390
|
+
blocks to warnings. Every **block** appends one line to the install's
|
|
391
|
+
`notebook/gates.jsonl` (rule `law-1` / `law-4` / `process-1` / `wiki`, a timestamp,
|
|
392
|
+
never the path or command) — the same ledger the Claude-family hooks write, so
|
|
393
|
+
`geneseed status` counts what the boundary caught on every host. Warnings are not
|
|
394
|
+
recorded.
|
|
387
395
|
- **Invisible context injection** (the default). The context plugin delivers via
|
|
388
396
|
`experimental.chat.messages.transform`, so the PROJECT CONTEXT block never appears in
|
|
389
397
|
the conversation and survives compaction inherently; on a build without the hook it
|
|
@@ -60,6 +60,36 @@ const WARN_ONLY = MODE === "warn"
|
|
|
60
60
|
|
|
61
61
|
function log(msg) { console.error(`[geneseed-guard] ${msg}`) }
|
|
62
62
|
|
|
63
|
+
// ---- the gate ledger ------------------------------------------------------------
|
|
64
|
+
// The twin of `ledger()` in js/hosts/hooks.mjs: one JSON line per BLOCK into
|
|
65
|
+
// `<cfg>/notebook/gates.jsonl`, the rule and never the content, so `geneseed status` can
|
|
66
|
+
// count what the boundary actually caught on an OpenCode install too. Until this the
|
|
67
|
+
// recommended install was the one with no gate evidence at all. A warning is not recorded
|
|
68
|
+
// (nothing was refused) and neither is `GENESEED_GUARD=warn`. `notebook/` is never created
|
|
69
|
+
// here — an install without one (or a test importing this file) records nothing. Capped
|
|
70
|
+
// at the newest MAX_LEDGER_LINES like the hook's; never throws, never decides.
|
|
71
|
+
const MAX_LEDGER_LINES = 1000
|
|
72
|
+
async function ledger(rule) {
|
|
73
|
+
try {
|
|
74
|
+
const bases = []
|
|
75
|
+
if (process.env.GENESEED_HARNESS) bases.push(process.env.GENESEED_HARNESS)
|
|
76
|
+
bases.push(path.dirname(PLUGIN_DIR))
|
|
77
|
+
for (const base of bases) {
|
|
78
|
+
const dir = path.join(base, "notebook")
|
|
79
|
+
if (!(await isDir(dir))) continue
|
|
80
|
+
const file = path.join(dir, "gates.jsonl")
|
|
81
|
+
await fs.appendFile(file, `${JSON.stringify({
|
|
82
|
+
ts: new Date().toISOString(), verb: "guard", rule, cwd: process.cwd(),
|
|
83
|
+
})}\n`)
|
|
84
|
+
const lines = (await fs.readFile(file, "utf8")).split("\n").filter(Boolean)
|
|
85
|
+
if (lines.length > MAX_LEDGER_LINES) {
|
|
86
|
+
await fs.writeFile(file, `${lines.slice(-MAX_LEDGER_LINES).join("\n")}\n`)
|
|
87
|
+
}
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
} catch { /* the ledger never decides */ }
|
|
91
|
+
}
|
|
92
|
+
|
|
63
93
|
// Files the agent must not write (secrets / private keys) → BLOCK.
|
|
64
94
|
const SECRET_RE = [
|
|
65
95
|
/(^|\/)id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$/i,
|
|
@@ -119,6 +149,7 @@ const hasAny = (name, parts) => parts.some((s) => name.includes(s))
|
|
|
119
149
|
// per wiki. Mutating anything under one is denied. Same resolution chain as the
|
|
120
150
|
// context plugin: $GENESEED_WIKI -> $GENESEED_HARNESS/wiki.jsonc -> beside the install.
|
|
121
151
|
async function isFile(p) { try { return (await fs.stat(p)).isFile() } catch { return false } }
|
|
152
|
+
async function isDir(p) { try { return (await fs.stat(p)).isDirectory() } catch { return false } }
|
|
122
153
|
|
|
123
154
|
// wiki.jsonc is JSONC (the seeded stub carries a commented example): strip // and
|
|
124
155
|
// /* */ comments plus trailing commas before parsing — string-aware, so quoted
|
|
@@ -243,22 +274,25 @@ export const GeneseedGuard = async () => {
|
|
|
243
274
|
if (await sovereignBypass(process.cwd())) return
|
|
244
275
|
const tool = (input?.tool || input?.name || "").toLowerCase()
|
|
245
276
|
const args = output?.args || input?.args || {}
|
|
246
|
-
|
|
277
|
+
// `rule` is the ledger key, spelled as js/hosts/hooks.mjs spells it (`law-1`,
|
|
278
|
+
// `law-4`, `process-1`) plus `wiki`, so status counts one vocabulary across hosts.
|
|
279
|
+
const deny = async (why, rule) => {
|
|
247
280
|
if (WARN_ONLY) { log(`WARN (would block): ${why}`); return false }
|
|
248
281
|
log(`BLOCKED: ${why}`)
|
|
282
|
+
await ledger(rule)
|
|
249
283
|
throw new Error(`[geneseed-guard] blocked: ${why} — set GENESEED_GUARD=off to allow`)
|
|
250
284
|
}
|
|
251
285
|
try {
|
|
252
286
|
if (hasAny(tool, WRITE_TOOLS)) {
|
|
253
287
|
const p = pickPath(args)
|
|
254
|
-
if (p && SECRET_RE.some((re) => re.test(p))) { deny(`write to secret/key file ${p} (Law I)
|
|
288
|
+
if (p && SECRET_RE.some((re) => re.test(p))) { await deny(`write to secret/key file ${p} (Law I)`, "law-1"); return }
|
|
255
289
|
if (p && SECRET_WARN_RE.some((re) => re.test(p))) log(`WARN: writing ${p} — keep secrets out of tracked files (Law I)`)
|
|
256
290
|
const store = p && ruleStoreTarget(p)
|
|
257
291
|
if (store && !RULE_STORE_BUMPED.has(p)) {
|
|
258
292
|
RULE_STORE_BUMPED.add(p)
|
|
259
|
-
deny(`writing to ${store} — a standing rule, or a fact to remember? That ` +
|
|
293
|
+
await deny(`writing to ${store} — a standing rule, or a fact to remember? That ` +
|
|
260
294
|
`choice is the user's (Doctrine process 1). Settle it through the rule skill, ` +
|
|
261
|
-
`then re-issue this write
|
|
295
|
+
`then re-issue this write`, "process-1")
|
|
262
296
|
return
|
|
263
297
|
}
|
|
264
298
|
}
|
|
@@ -268,14 +302,14 @@ export const GeneseedGuard = async () => {
|
|
|
268
302
|
const abs = (path.isAbsolute(p) ? p : path.resolve(p)).replace(/\\/g, "/").toLowerCase()
|
|
269
303
|
const hit = (await protectedPrefixes()).find(
|
|
270
304
|
(x) => abs.startsWith(x.prefix) || abs === x.prefix.slice(0, -1))
|
|
271
|
-
if (hit) { deny(`mutation in protected wiki folder — ${hit.label} (AGENT.md §8)
|
|
305
|
+
if (hit) { await deny(`mutation in protected wiki folder — ${hit.label} (AGENT.md §8)`, "wiki"); return }
|
|
272
306
|
}
|
|
273
307
|
}
|
|
274
308
|
// NOT else-if: a compound tool name (e.g. exec_and_save) can match both
|
|
275
309
|
// classes, and the shell check must still run after the write check.
|
|
276
310
|
if (hasAny(tool, SHELL_TOOLS)) {
|
|
277
311
|
const c = pickCommand(args)
|
|
278
|
-
if (c && SHELL_BLOCK_RE.some((re) => re.test(c))) { deny(`catastrophic command (Law IV): ${c.slice(0, 80)}
|
|
312
|
+
if (c && SHELL_BLOCK_RE.some((re) => re.test(c))) { await deny(`catastrophic command (Law IV): ${c.slice(0, 80)}`, "law-4"); return }
|
|
279
313
|
if (c && SHELL_WARN_RE.some((re) => re.test(c))) log(`WARN: irreversible op — confirm intent (Law IV): ${c.slice(0, 80)}`)
|
|
280
314
|
}
|
|
281
315
|
} catch (err) {
|
package/bin/build-driver.mjs
CHANGED
|
@@ -42,7 +42,7 @@ import { parseArgs as nodeParseArgs } from 'node:util';
|
|
|
42
42
|
import { build, phaseLog } from '../js/build/bundle.mjs';
|
|
43
43
|
import { emitClaudeRender } from '../js/build/emit-claude.mjs';
|
|
44
44
|
import { emitOpencodeRender, emitOpencodeGlobalRender } from '../js/build/emit-opencode.mjs';
|
|
45
|
-
import { settingsIntegrityCheck } from '../js/hosts/settings.mjs';
|
|
45
|
+
import { copilotIntegrityCheck, settingsIntegrityCheck } from '../js/hosts/settings.mjs';
|
|
46
46
|
import { writeText, withPlatformNewlines, isFile } from '../js/lib/fs.mjs';
|
|
47
47
|
import { parseJson, jsonDumpsIndent } from '../js/lib/json.mjs';
|
|
48
48
|
// P5c moved these out of this file: `bin/geneseed-cli.mjs` needs the same four resolvers to
|
|
@@ -848,8 +848,8 @@ function emitClaudeCore(cfg, args, { cfgDir, claudeMd, scope, host, out, hookOpt
|
|
|
848
848
|
// VERIFY — re-read the settings file just written and match it against the claims just
|
|
849
849
|
// recorded, so a merge that silently did not stick (a commented file, a mid-flight
|
|
850
850
|
// external edit, a bug in the merge) is loud now instead of surfacing as hooks that
|
|
851
|
-
// quietly never fire.
|
|
852
|
-
//
|
|
851
|
+
// quietly never fire. Copilot's settings have their own shape and their own check; a
|
|
852
|
+
// Copilot PROJECT emit records no hooks and so checks nothing.
|
|
853
853
|
//
|
|
854
854
|
// On the Python driver this stage runs in Python AFTER a Node child did the wiring, which
|
|
855
855
|
// makes it a live cross-implementation check on every build. Here both halves are Node, so
|
|
@@ -860,15 +860,17 @@ function emitClaudeCore(cfg, args, { cfgDir, claudeMd, scope, host, out, hookOpt
|
|
|
860
860
|
// SILENT ON SUCCESS, which is its own coverage hazard: a clean emit prints nothing, so
|
|
861
861
|
// deleting this call is byte-identical in all 259 cells. `test_verify_reports_an_orphaned
|
|
862
862
|
// _geneseed_hook` plants the fault that makes it speak.
|
|
863
|
+
phaseLog('VERIFY');
|
|
863
864
|
if (!isCopilot) {
|
|
864
|
-
phaseLog('VERIFY');
|
|
865
865
|
settingsIntegrityCheck(
|
|
866
866
|
path.join(cfgDir, managed.settings_file || 'settings.json'), managed, 'present');
|
|
867
|
+
} else if (managed.copilot_hooks) {
|
|
868
|
+
copilotIntegrityCheck(path.join(cfgDir, 'settings.json'), managed.copilot_hooks, 'present');
|
|
867
869
|
}
|
|
868
870
|
return {
|
|
869
871
|
nAgents: stats.nAgents,
|
|
870
872
|
nSkills: stats.nSkills,
|
|
871
|
-
nHooks: (managed.settings_hooks || []).length,
|
|
873
|
+
nHooks: (managed.settings_hooks || managed.copilot_hooks || []).length,
|
|
872
874
|
memStatus,
|
|
873
875
|
nbStatus,
|
|
874
876
|
};
|
|
@@ -883,7 +885,8 @@ function emitCopilot(cfg, args, out) {
|
|
|
883
885
|
});
|
|
884
886
|
process.stdout.write(`[geneseed] copilot (folder) -> ${root}: AGENTS.md + .github/ `
|
|
885
887
|
+ `(${r.nAgents} agents (.agent.md), ${r.nSkills} skills), ${r.memStatus}, `
|
|
886
|
-
+ `${r.nbStatus}. No
|
|
888
|
+
+ `${r.nbStatus}. No hooks at project scope (Copilot reads hooks from `
|
|
889
|
+
+ '~/.copilot/settings.json only — the global emit wires them).\n');
|
|
887
890
|
return cfgDir;
|
|
888
891
|
}
|
|
889
892
|
|
|
@@ -891,14 +894,17 @@ function emitCopilot(cfg, args, out) {
|
|
|
891
894
|
function emitCopilotGlobal(cfg, args, out) {
|
|
892
895
|
const cfgDir = args.cfgDir ?? copilotConfigDir();
|
|
893
896
|
warnCopilotGlobalOverProject();
|
|
897
|
+
// The hook runner pair, as for Claude and Bob: the global emit is the one Copilot scope
|
|
898
|
+
// that wires hooks (`~/.copilot/settings.json`), and the shim it bakes is machine-wide.
|
|
899
|
+
const hookOpts = hookRunnerEntry();
|
|
894
900
|
const r = emitClaudeCore(cfg, args, {
|
|
895
901
|
cfgDir, claudeMd: path.join(cfgDir, 'copilot-instructions.md'),
|
|
896
|
-
scope: 'global', host: 'copilot', out,
|
|
902
|
+
scope: 'global', host: 'copilot', out, hookOpts,
|
|
897
903
|
});
|
|
898
904
|
process.stdout.write(`[geneseed] copilot-global -> ${cfgDir}: ${r.nAgents} agents (.agent.md), `
|
|
899
|
-
+ `${r.nSkills} skills, copilot-instructions.md, ${r.memStatus}, ${r.nbStatus}
|
|
900
|
-
+
|
|
901
|
-
+ "preamble's instructions
|
|
905
|
+
+ `${r.nSkills} skills, copilot-instructions.md, ${r.memStatus}, ${r.nbStatus}, `
|
|
906
|
+
+ `${r.nHooks} hooks (settings.json: sessionStart context, toolCall gate). Memory `
|
|
907
|
+
+ "write-back rides the preamble's instructions; MCP servers go in mcp-config.json.\n");
|
|
902
908
|
return cfgDir;
|
|
903
909
|
}
|
|
904
910
|
|
package/bin/geneseed-cli.mjs
CHANGED
|
@@ -41,32 +41,22 @@
|
|
|
41
41
|
* keeps the two tables from ever answering the same verb twice, since the shim bakes only one
|
|
42
42
|
* of them.
|
|
43
43
|
*
|
|
44
|
-
* P6h MADE THE DISPATCH ASYNCHRONOUS and put `js/web/` on this entry's import graph
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* P6h MADE THE DISPATCH ASYNCHRONOUS and put `js/web/` on this entry's import graph — LAZILY,
|
|
45
|
+
* since the slim pass: every command module loads through its verb's thunk, so the graph is
|
|
46
|
+
* reached per verb rather than per invocation. The modules that spawn are still declared argv
|
|
47
|
+
* by argv in `ALLOWED_SPAWNS` (`tests/unit/hook_cli.test.mjs`), whose closure walk follows
|
|
48
|
+
* the dynamic spelling too.
|
|
48
49
|
*/
|
|
49
|
-
|
|
50
|
+
// THE FOUR EAGER IMPORTS ARE THE PARSER'S OWN, AND NOTHING ELSE LOADS BEFORE DISPATCH.
|
|
51
|
+
// The twenty command modules used to be static imports, which made every invocation pay for
|
|
52
|
+
// all of them: `geneseed status` loaded the web daemon, the TUI, the updater and the whole
|
|
53
|
+
// build driver — 57 modules, ~1 MB, ~40-55 ms — before a byte of its own work. The verb
|
|
54
|
+
// table below now holds one LOADER per row (`() => import(…).then(m => m.cmdX)`), so a verb
|
|
55
|
+
// loads its slice of the tree and no other verb's. The spawn allow-list gate still sees the
|
|
56
|
+
// whole graph: `tests/unit/hook_cli.test.mjs`'s `importClosure` follows both import spellings.
|
|
50
57
|
import { cliSpec, printHelp } from '../js/ui/cli.mjs';
|
|
51
|
-
import { cmdDiff } from '../js/inspect/diff.mjs';
|
|
52
|
-
import { cmdDoctor } from '../js/inspect/doctor.mjs';
|
|
53
|
-
import { cmdValidate } from '../js/inspect/validate.mjs';
|
|
54
|
-
import { parseDriverArgs } from './build-driver.mjs';
|
|
55
|
-
import { cmdExclude } from '../js/inspect/excludes.mjs';
|
|
56
|
-
import { cmdBuild, cmdPrompt, cmdRebuildAll, cmdTheme } from '../js/build/generate.mjs';
|
|
57
|
-
import { cmdMigrate } from '../js/maintain/migrate.mjs';
|
|
58
58
|
import { parseIntStrict } from '../js/lib/text.mjs';
|
|
59
59
|
import { printErr } from '../js/lib/fs.mjs';
|
|
60
|
-
import { cmdLink, cmdUnlink } from '../js/hosts/link.mjs';
|
|
61
|
-
import { cmdMcp } from '../js/hosts/mcp.mjs';
|
|
62
|
-
import { cmdMemory } from '../js/maintain/memory.mjs';
|
|
63
|
-
import { cmdHome, cmdMenu } from '../js/ui/menu.mjs';
|
|
64
|
-
import { cmdTui } from '../js/ui/tui.mjs';
|
|
65
|
-
import { cmdSetup } from '../js/maintain/setup.mjs';
|
|
66
|
-
import { cmdStatus, cmdVersion } from '../js/inspect/status.mjs';
|
|
67
|
-
import { cmdUninstall } from '../js/maintain/uninstall.mjs';
|
|
68
|
-
import { cmdBootstrap, cmdSyncSelf, cmdUpgrade } from '../js/maintain/update.mjs';
|
|
69
|
-
import { cmdWeb } from '../js/web/server.mjs';
|
|
70
60
|
|
|
71
61
|
/**
|
|
72
62
|
* The verbs this entry answers — and, since P10c, NOTHING ELSE ABOUT THEM.
|
|
@@ -85,30 +75,30 @@ import { cmdWeb } from '../js/web/server.mjs';
|
|
|
85
75
|
* `main`'s refusal lists `Object.keys(VERBS)` IN THIS ORDER, which `harness_golden` cells
|
|
86
76
|
* assert verbatim — the order is the old table's and is not alphabetical.
|
|
87
77
|
*
|
|
88
|
-
* FLATTENED TO `verb: fn` since Task 5
|
|
89
|
-
* the
|
|
90
|
-
*
|
|
91
|
-
* `VERBS[verb]
|
|
92
|
-
* match on the KEY and the colon only; the
|
|
93
|
-
* order neither changed.
|
|
78
|
+
* FLATTENED TO `verb: fn` since Task 5, AND TO `verb: loader` since the lazy pass: every row
|
|
79
|
+
* is now a thunk that imports the command's module and resolves to its function, so the one
|
|
80
|
+
* field a row holds is still the one thing a data file cannot — where the FUNCTION lives.
|
|
81
|
+
* `main` awaits `VERBS[verb]()` after the parse succeeds; the scraping regexes in
|
|
82
|
+
* `tests/unit/{cli_table,hook_cli,docs}.test.mjs` match on the KEY and the colon only; the
|
|
83
|
+
* literal string `const VERBS = {` and the key order neither changed.
|
|
94
84
|
*/
|
|
95
85
|
const VERBS = {
|
|
96
|
-
exclude: cmdExclude,
|
|
97
|
-
status: cmdStatus,
|
|
98
|
-
version: cmdVersion,
|
|
99
|
-
build: cmdBuild,
|
|
100
|
-
prompt: cmdPrompt,
|
|
101
|
-
theme: cmdTheme,
|
|
102
|
-
diff: cmdDiff,
|
|
103
|
-
'rebuild-all': cmdRebuildAll,
|
|
104
|
-
migrate: cmdMigrate,
|
|
105
|
-
doctor: cmdDoctor,
|
|
106
|
-
uninstall: cmdUninstall,
|
|
107
|
-
link: cmdLink,
|
|
108
|
-
unlink: cmdUnlink,
|
|
109
|
-
setup: cmdSetup,
|
|
110
|
-
web: cmdWeb,
|
|
111
|
-
upgrade: cmdUpgrade,
|
|
86
|
+
exclude: () => import('../js/inspect/excludes.mjs').then((m) => m.cmdExclude),
|
|
87
|
+
status: () => import('../js/inspect/status.mjs').then((m) => m.cmdStatus),
|
|
88
|
+
version: () => import('../js/inspect/status.mjs').then((m) => m.cmdVersion),
|
|
89
|
+
build: () => import('../js/build/generate.mjs').then((m) => m.cmdBuild),
|
|
90
|
+
prompt: () => import('../js/build/generate.mjs').then((m) => m.cmdPrompt),
|
|
91
|
+
theme: () => import('../js/build/generate.mjs').then((m) => m.cmdTheme),
|
|
92
|
+
diff: () => import('../js/inspect/diff.mjs').then((m) => m.cmdDiff),
|
|
93
|
+
'rebuild-all': () => import('../js/build/generate.mjs').then((m) => m.cmdRebuildAll),
|
|
94
|
+
migrate: () => import('../js/maintain/migrate.mjs').then((m) => m.cmdMigrate),
|
|
95
|
+
doctor: () => import('../js/inspect/doctor.mjs').then((m) => m.cmdDoctor),
|
|
96
|
+
uninstall: () => import('../js/maintain/uninstall.mjs').then((m) => m.cmdUninstall),
|
|
97
|
+
link: () => import('../js/hosts/link.mjs').then((m) => m.cmdLink),
|
|
98
|
+
unlink: () => import('../js/hosts/link.mjs').then((m) => m.cmdUnlink),
|
|
99
|
+
setup: () => import('../js/maintain/setup.mjs').then((m) => m.cmdSetup),
|
|
100
|
+
web: () => import('../js/web/server.mjs').then((m) => m.cmdWeb),
|
|
101
|
+
upgrade: () => import('../js/maintain/update.mjs').then((m) => m.cmdUpgrade),
|
|
112
102
|
// `up.add_parser(..., aliases=["update"])`, reproduced in P8c as a ROW OF ITS OWN rather than
|
|
113
103
|
// as an `aliases` field on the row above — because a field would be a DECLARATION and the
|
|
114
104
|
// three matrix gates read this table as the DISPATCH (rule 7, and M23 is where it was
|
|
@@ -120,18 +110,18 @@ const VERBS = {
|
|
|
120
110
|
//
|
|
121
111
|
// It is NOT what the web console's `update` ACTION needs: that row's argv names `upgrade`,
|
|
122
112
|
// the subparser, and always did. The two were coupled only in a handoff's note.
|
|
123
|
-
update: cmdUpgrade,
|
|
113
|
+
update: () => import('../js/maintain/update.mjs').then((m) => m.cmdUpgrade),
|
|
124
114
|
// Points at `cmdSyncSelf` rather than at `cmdUpgrade`, and that is not tidiness:
|
|
125
115
|
// `sync-self` DROPS its `ref` before `upgrade` ever sees it, where `upgrade` both WARNS
|
|
126
116
|
// about one and re-reads it as a theme — so the two verbs answer `sync-self cyberpunk`
|
|
127
117
|
// and `sync-self v1.2.3` differently. See `js/maintain/update.mjs`'s `syncSelf` for both halves.
|
|
128
|
-
'sync-self': cmdSyncSelf,
|
|
129
|
-
bootstrap: cmdBootstrap,
|
|
118
|
+
'sync-self': () => import('../js/maintain/update.mjs').then((m) => m.cmdSyncSelf),
|
|
119
|
+
bootstrap: () => import('../js/maintain/update.mjs').then((m) => m.cmdBootstrap),
|
|
130
120
|
// P7a. Both are DISPATCHERS whose off-TTY arm is the whole of what a cell can reach, and
|
|
131
121
|
// `menu`'s on-TTY arm falls back rather than opening a panel — `js/ui/menu.mjs`'s header
|
|
132
122
|
// argues both.
|
|
133
|
-
menu: cmdMenu,
|
|
134
|
-
home: cmdHome,
|
|
123
|
+
menu: () => import('../js/ui/menu.mjs').then((m) => m.cmdMenu),
|
|
124
|
+
home: () => import('../js/ui/menu.mjs').then((m) => m.cmdHome),
|
|
135
125
|
// P7b, and it is the twenty-fifth and last. `cmd_tui`'s FIRST arm is `if not sys.stdin.
|
|
136
126
|
// isatty()`, so off a TTY — which is every cell there is — the verb is one line and an
|
|
137
127
|
// exit code, and that arm crosses byte for byte. The panel behind it is P7c's;
|
|
@@ -139,7 +129,7 @@ const VERBS = {
|
|
|
139
129
|
// panel-unavailable line rather than inventing a second full-screen UI, and
|
|
140
130
|
// `tests/test_tui_boundary.py` asserts that the arm it declares is genuinely unreachable
|
|
141
131
|
// here rather than merely untested.
|
|
142
|
-
tui: cmdTui,
|
|
132
|
+
tui: () => import('../js/ui/tui.mjs').then((m) => m.cmdTui),
|
|
143
133
|
// THE THREE THAT NEVER HAD A PYTHON ORIGINAL, and they are last for that reason rather
|
|
144
134
|
// than by alphabet: every row above is the twin of a subparser, and these three are not.
|
|
145
135
|
// The information behind them existed only behind the web console — the catalog endpoint,
|
|
@@ -154,9 +144,9 @@ const VERBS = {
|
|
|
154
144
|
// ABSOLUTE — `tests/snapshot/cli_help.test.mjs` splits the two populations by name and refuses a
|
|
155
145
|
// verb that falls into neither, and each has its own unit gate stating what it does rather
|
|
156
146
|
// than that it agrees.
|
|
157
|
-
catalog: cmdCatalog,
|
|
158
|
-
mcp: cmdMcp,
|
|
159
|
-
memory: cmdMemory,
|
|
147
|
+
catalog: () => import('../js/build/catalog.mjs').then((m) => m.cmdCatalog),
|
|
148
|
+
mcp: () => import('../js/hosts/mcp.mjs').then((m) => m.cmdMcp),
|
|
149
|
+
memory: () => import('../js/maintain/memory.mjs').then((m) => m.cmdMemory),
|
|
160
150
|
};
|
|
161
151
|
|
|
162
152
|
function die(code, msg) {
|
|
@@ -309,6 +299,11 @@ async function main(argv) {
|
|
|
309
299
|
// and it is on THIS binary — not on `bin/build-driver.mjs` — because it runs the doctor, which
|
|
310
300
|
// starts a process the driver is banned from reaching.
|
|
311
301
|
if (verb === 'validate') {
|
|
302
|
+
// Lazy for the same reason the table rows are — `parseDriverArgs` alone drags the whole
|
|
303
|
+
// generator driver in, and every OTHER verb was paying for it.
|
|
304
|
+
const [{ cmdValidate }, { parseDriverArgs }] = await Promise.all([
|
|
305
|
+
import('../js/inspect/validate.mjs'), import('./build-driver.mjs'),
|
|
306
|
+
]);
|
|
312
307
|
try {
|
|
313
308
|
return cmdValidate(parseDriverArgs(argv.slice(1)));
|
|
314
309
|
} catch (e) {
|
|
@@ -316,8 +311,8 @@ async function main(argv) {
|
|
|
316
311
|
throw e;
|
|
317
312
|
}
|
|
318
313
|
}
|
|
319
|
-
const
|
|
320
|
-
if (!
|
|
314
|
+
const load = VERBS[verb];
|
|
315
|
+
if (!load) {
|
|
321
316
|
// "and every other harness subcommand is still Python — run the interpreter against the
|
|
322
317
|
// old harness script" is GONE, and its removal is not cosmetic: with the four hook verbs
|
|
323
318
|
// named, the two entry points now cover every subcommand there is, so the clause was not
|
|
@@ -353,7 +348,11 @@ async function main(argv) {
|
|
|
353
348
|
// it `process.exitCode` would be assigned a PENDING PROMISE, which Node coerces to 0
|
|
354
349
|
// and prints nothing about. `web status` would then report "not running" and exit 0.
|
|
355
350
|
// Awaiting a number is a no-op for the ten synchronous verbs.
|
|
356
|
-
|
|
351
|
+
//
|
|
352
|
+
// The load happens HERE, after the parse succeeded: a bad argument line refuses in a
|
|
353
|
+
// few milliseconds without touching the command's module tree, which is also why the
|
|
354
|
+
// parse errors above cannot depend on anything a loader would have brought in.
|
|
355
|
+
return await (await load())(parsed.args);
|
|
357
356
|
} catch (e) {
|
|
358
357
|
// `e.exitCode` is the generator's existing marker for a DELIBERATE refusal that has
|
|
359
358
|
// already explained itself on stderr — `js/build/emit-claude.mjs` reads the same flag, and it is
|
package/bin/geneseed-hook.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* The hook entry point — a Node twin of `rituals/harness.py` for the
|
|
4
|
-
*
|
|
3
|
+
* The hook entry point — a Node twin of `rituals/harness.py` for the verbs the emitted
|
|
4
|
+
* `settings.json` actually invokes (four from the reference, plus `tool-gate` for Copilot).
|
|
5
5
|
*
|
|
6
6
|
* WHY IT IS A SECOND BINARY AND NOT A SUBCOMMAND OF `bin/build-driver.mjs`. Three reasons,
|
|
7
7
|
* and each alone would be enough:
|
|
@@ -37,16 +37,20 @@
|
|
|
37
37
|
* every Geneseed hook returns 0 and signals through stdout — so "did nothing" and "worked"
|
|
38
38
|
* are the same observation.
|
|
39
39
|
*/
|
|
40
|
-
import { cmdContext, cmdGitGate, cmdRuleGate, cmdLearn } from '../js/hosts/hooks.mjs';
|
|
41
|
-
import { printHelp } from '../js/ui/cli.mjs';
|
|
40
|
+
import { cmdContext, cmdGitGate, cmdRuleGate, cmdToolGate, cmdLearn } from '../js/hosts/hooks.mjs';
|
|
42
41
|
// Not a new module on the hot path: `js/hosts/hooks.mjs` above already imports
|
|
43
42
|
// `js/lib/fs.mjs`, so this edge names a file the process has already loaded.
|
|
44
43
|
import { printErr } from '../js/lib/fs.mjs';
|
|
45
44
|
|
|
45
|
+
// `--host` selects the verdict dialect (see js/hosts/hooks.mjs's header); the emitter bakes
|
|
46
|
+
// it into the command for every host that is not Claude Code, so the hook never has to guess
|
|
47
|
+
// its reader from the payload's shape.
|
|
48
|
+
const HOSTED = { '--root': 'root', '--host': 'host' };
|
|
46
49
|
const VERBS = {
|
|
47
|
-
context: { fn: cmdContext, flags:
|
|
48
|
-
'git-gate': { fn: cmdGitGate, flags:
|
|
49
|
-
'rule-gate': { fn: cmdRuleGate, flags:
|
|
50
|
+
context: { fn: cmdContext, flags: HOSTED },
|
|
51
|
+
'git-gate': { fn: cmdGitGate, flags: HOSTED },
|
|
52
|
+
'rule-gate': { fn: cmdRuleGate, flags: HOSTED },
|
|
53
|
+
'tool-gate': { fn: cmdToolGate, flags: HOSTED },
|
|
50
54
|
learn: {
|
|
51
55
|
fn: cmdLearn,
|
|
52
56
|
flags: { '--memory': 'memory' },
|
|
@@ -63,12 +67,12 @@ function die(code, msg) {
|
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/**
|
|
66
|
-
* `argparse`'s surface for these
|
|
70
|
+
* `argparse`'s surface for these verbs, and only theirs: `--flag VALUE`, the
|
|
67
71
|
* `--flag=VALUE` spelling it also accepts, store_true switches, and one optional
|
|
68
72
|
* positional.
|
|
69
73
|
*/
|
|
70
74
|
function parse(spec, argv) {
|
|
71
|
-
const args = { root: null, memory: null, consolidate: false, file: null };
|
|
75
|
+
const args = { root: null, host: null, memory: null, consolidate: false, file: null };
|
|
72
76
|
for (let i = 0; i < argv.length; i += 1) {
|
|
73
77
|
const tok = argv[i];
|
|
74
78
|
const eq = tok.indexOf('=');
|
|
@@ -92,7 +96,7 @@ function parse(spec, argv) {
|
|
|
92
96
|
return { args };
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
function main(argv) {
|
|
99
|
+
async function main(argv) {
|
|
96
100
|
// `geneseed status | head` closes stdout early. Python exits quietly through
|
|
97
101
|
// BrokenPipeError; the Node equivalent is an EPIPE that would otherwise be an unhandled
|
|
98
102
|
// 'error' event and a stack trace on a hook path.
|
|
@@ -111,16 +115,19 @@ function main(argv) {
|
|
|
111
115
|
// The COMMAND is not a table: `geneseed` is this package's `bin` entry for the CLI
|
|
112
116
|
// (package.json), it answers every non-hook verb, and it survives the deletion of the
|
|
113
117
|
// interpreter-plus-script invocation this line used to print.
|
|
114
|
-
return die(2, `invalid choice: '${verb}'. This entry point carries only the
|
|
118
|
+
return die(2, `invalid choice: '${verb}'. This entry point carries only the HOOK `
|
|
115
119
|
+ `verbs (${Object.keys(VERBS).join(', ')}); every other harness subcommand lives `
|
|
116
120
|
+ 'elsewhere — run `geneseed ' + verb + '`.');
|
|
117
121
|
}
|
|
118
122
|
// `<verb> --help`, for the same reason and by the same owner as `bin/geneseed-cli.mjs`:
|
|
119
123
|
// argparse holds `-h` at the parser, so this entry's `parse` calls it an unrecognized
|
|
120
124
|
// argument. The four hook verbs are four of the reference's 26 and had the same gap.
|
|
121
|
-
// `js/ui/cli.mjs`
|
|
122
|
-
//
|
|
125
|
+
// `js/ui/cli.mjs` is imported HERE, not at the top: a static import cost every tool call
|
|
126
|
+
// of every session a module parse (~3 ms of an ~12 ms controllable budget) for a branch
|
|
127
|
+
// only a human at a terminal ever takes. The hook path never awaits it, so `main` staying
|
|
128
|
+
// async-shaped costs the hot verbs nothing.
|
|
123
129
|
if (argv.slice(1).some((t) => t === '-h' || t === '--help')) {
|
|
130
|
+
const { printHelp } = await import('../js/ui/cli.mjs');
|
|
124
131
|
const rc = printHelp('geneseed-hook', verb);
|
|
125
132
|
if (rc !== null) return rc;
|
|
126
133
|
}
|
|
@@ -129,4 +136,4 @@ function main(argv) {
|
|
|
129
136
|
return spec.fn(parsed.args);
|
|
130
137
|
}
|
|
131
138
|
|
|
132
|
-
|
|
139
|
+
main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Candidate hosts — what Codex, Cursor and Gemini CLI actually offer
|
|
2
|
+
|
|
3
|
+
*2026-09-05. Item 6 of the host-gap review. A docs check, not a design: each tool's
|
|
4
|
+
own reference was read for the three surfaces Geneseed emits — an instructions
|
|
5
|
+
carrier, skills, and hooks — so that "add host X" can be costed without re-reading
|
|
6
|
+
the docs. Nothing here is implemented. Sources: the Codex CLI repo
|
|
7
|
+
(`codex-rs/hooks`, `codex-rs/core/src/agents_md.rs`), `cursor.com/docs/{rules,skills,hooks}`,
|
|
8
|
+
`geminicli.com/docs/{hooks,cli/gemini-md,cli/tutorials/skills-getting-started}`.*
|
|
9
|
+
|
|
10
|
+
## The finding that changes the cost
|
|
11
|
+
|
|
12
|
+
**`.agents/skills/` is a cross-tool convention.** Cursor loads skills from
|
|
13
|
+
`.agents/skills/` and `~/.agents/skills/` (and, for compatibility, from `.claude/skills/`
|
|
14
|
+
and `.codex/skills/`); Codex from `~/.codex/skills` (`$CODEX_HOME/skills`) and
|
|
15
|
+
`.agents/skills`; Gemini CLI from `.gemini/skills` **or the `.agents/skills` alias**. One
|
|
16
|
+
`skills/` emit into `.agents/` therefore reaches all three, byte-identical to what every
|
|
17
|
+
current host gets. Skills are the largest surface by file count and they cost nothing new.
|
|
18
|
+
|
|
19
|
+
## Per-host contracts
|
|
20
|
+
|
|
21
|
+
| Surface | Codex CLI | Cursor | Gemini CLI |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| **Instructions** | `AGENTS.md` at the repo root (and nested), plus a global `~/.codex/AGENTS.md` that no size cap touches | `AGENTS.md` at the root and in subdirectories, combined; also `.cursor/rules/*.mdc` | `GEMINI.md` by default; `context.fileName` in `settings.json` accepts a list, e.g. `["AGENTS.md", "GEMINI.md"]` |
|
|
24
|
+
| **Skills** | `~/.codex/skills`, `.agents/skills` — `SKILL.md` + frontmatter | `.cursor/skills`, `.agents/skills`, `~/.cursor/skills`, `~/.agents/skills`, plus `.claude/skills` and `.codex/skills` for compatibility | `.gemini/skills` or `.agents/skills`; `/skills list`, `/skills reload` |
|
|
25
|
+
| **Hook config** | `hooks.json` in a config layer's folder, or a `[hooks]` table in `config.toml` (same schema, capitalised event names) | `.cursor/hooks.json` (project) and `~/.cursor/hooks.json` (user); plugins may ship `hooks/hooks.json` | `settings.json` `hooks` object (project `.gemini/`, user `~/.gemini/`) |
|
|
26
|
+
| **Hook shape** | **Claude-shaped**: event → `[{matcher, hooks: [{type: "command", command, timeout}]}]`; 11 events incl. `PreToolUse`, `PostToolUse`, `PermissionRequest`, `PreCompact`, `PostCompact`, `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `SubagentStart`, `SubagentStop`, `Stop` | event → `[{command, matcher?}]`; events incl. `sessionStart`, `preToolUse`, `beforeShellExecution`, `beforeReadFile`, `afterFileEdit`, `stop`, `sessionEnd` | **Claude-shaped groups, Gemini names**: `SessionStart`, `BeforeTool`, `AfterTool`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `PreCompact`, `SessionEnd`; matchers are regexes over tool names |
|
|
27
|
+
| **Gate payload** | `tool_name` / `tool_input` (Claude's field names) | `tool_name` / `tool_input`, `cwd`, `tool_use_id` | `tool_name` / `tool_input`, `session_id`, `transcript_path`, `cwd`, `hook_event_name` |
|
|
28
|
+
| **Gate verdict** | `hookSpecificOutput.permissionDecision`: **`allow` / `deny` / `ask`** — the ask tier exists | `permission: "allow" \| "deny"` + `user_message` / `agent_message`; **no ask** | `decision: "deny"` + `reason`, or exit code 2 with stderr as the reason; `systemMessage` shows the user a line; **no ask** |
|
|
29
|
+
| **Context injection** | `SessionStart` → `additionalContext` (Claude form) | `sessionStart` → `additional_context` (snake_case) | `SessionStart` → `hookSpecificOutput.additionalContext`; plain stdout is not read |
|
|
30
|
+
| **Learn point** | `Stop` / `SubagentStop` / `PreCompact`, transcript on stdin as on Claude | `stop` (payload not checked for a transcript path) | `AfterAgent` (turn end) and `PreCompact`; `transcript_path` is in the base schema |
|
|
31
|
+
| **Tool names for matchers** | Claude's (`Bash`, `Write`, `Edit`) | `Shell`, `Read`, `Write`, MCP, `Task` (generic `preToolUse`) | `run_shell_command` (`command`), `write_file` (`file_path`, `content`), `replace` (`file_path`, `old_string`, `new_string`) |
|
|
32
|
+
| **Subagents** | `SubagentStart`/`SubagentStop` exist; custom agents not checked | Plugins carry `agents/` | Local and remote subagents (`/agents list`, `enable`, `disable`) |
|
|
33
|
+
|
|
34
|
+
## What each would cost, against what exists
|
|
35
|
+
|
|
36
|
+
- **Codex CLI — cheapest, and the only one with an ask tier.** Its hook schema is
|
|
37
|
+
Claude's with `hooks.json`/`config.toml` as the carrier, its payload field names are
|
|
38
|
+
Claude's, and `permissionDecision: "ask"` exists. The Claude engine with `host: "codex"`
|
|
39
|
+
covers instructions (`AGENTS.md`), skills (`.agents/skills`) and the four hook verbs
|
|
40
|
+
unchanged; the only new code is the carrier writer (a `hooks.json` merge or a TOML
|
|
41
|
+
table) and the config-dir resolver (`~/.codex`, `$CODEX_HOME`). Roughly the Copilot
|
|
42
|
+
port minus the dialect work.
|
|
43
|
+
|
|
44
|
+
- **Gemini CLI — Claude's groups, Gemini's names.** The hook groups are Claude's
|
|
45
|
+
`event → [{matcher, hooks}]` and the payload fields are Claude's, but the events are
|
|
46
|
+
`BeforeTool`/`AfterAgent` and the verdict is `decision: "deny"` or a `systemMessage`.
|
|
47
|
+
Bob Shell shares Gemini's *config* shape (`context.fileName`, `tools.allowed:
|
|
48
|
+
["run_shell_command(git)"]`), but Bob's documented *hook* protocol is its own —
|
|
49
|
+
Claude's event names, refusal by exit code 2; see `adapters/bob/README.md` — so the two
|
|
50
|
+
are not one dialect. Instructions need one settings key (`context.fileName` to include
|
|
51
|
+
`AGENTS.md`) or a `GEMINI.md` carrier; skills go to `.agents/skills`. No ask tier: Laws
|
|
52
|
+
I/IV deny, the consent rules become a `systemMessage`.
|
|
53
|
+
|
|
54
|
+
- **Cursor — a third verdict dialect.** `AGENTS.md` and `.agents/skills` are free; hooks
|
|
55
|
+
are `hooks.json` with a `{command}` list per event and a `permission: allow|deny`
|
|
56
|
+
verdict, `additional_context` in snake_case for `sessionStart`. Same block-or-warn stance
|
|
57
|
+
as Copilot, one more output spelling in `js/hosts/hooks.mjs`, one more carrier writer.
|
|
58
|
+
Cursor also reads `.claude/skills/`, so a Claude project install already gives a Cursor
|
|
59
|
+
user the skills.
|
|
60
|
+
|
|
61
|
+
## Recommendation
|
|
62
|
+
|
|
63
|
+
If a fifth host is wanted, Codex first: it reuses the most and it is the only candidate
|
|
64
|
+
where the gates can *ask* rather than block. Before any of them, a `--emit agents` for
|
|
65
|
+
the shared `.agents/skills/` folder would give Codex, Cursor and Gemini users the whole
|
|
66
|
+
skill catalogue for the price of one directory name, with no host-specific code at all.
|
|
67
|
+
A Gemini host would be the fourth verdict spelling in `js/hosts/hooks.mjs` (after Claude's
|
|
68
|
+
ask, Copilot's block, Bob's exit code 2), plus a resolver and a carrier.
|
|
69
|
+
|
|
70
|
+
## Not verified
|
|
71
|
+
|
|
72
|
+
None of the three CLIs is installed on the reviewing machine. Every row above is from the
|
|
73
|
+
tool's own documentation as indexed on 2026-09-05; the "learn point" column for Cursor and
|
|
74
|
+
the Codex custom-agent surface were not checked further than noted.
|
package/docs/extending.md
CHANGED
|
@@ -215,11 +215,12 @@ a defect.
|
|
|
215
215
|
|
|
216
216
|
Steps 1 and 9 of §2c, plus `LAW_META` if the Principle line moved. Two cautions:
|
|
217
217
|
|
|
218
|
-
- `--footprint` defaults to **lean**, and lean
|
|
219
|
-
(`
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
- `--footprint` defaults to **lean**, and a law's lean text is the authored `LEAN:else` half of
|
|
219
|
+
its block in `src/laws/universal.md` (`resolveLean` in `js/build/render.mjs`). Amend **both**
|
|
220
|
+
halves, or the amendment does not exist for most installs. A doctrine rule is different: it is
|
|
221
|
+
still machine-cut to its heading plus first sentence (`terseBlocks`), so a clause in its second
|
|
222
|
+
paragraph is invisible in the default build. The on-disk `laws/` and `ontology/` copies always
|
|
223
|
+
render at full, whatever the footprint.
|
|
223
224
|
|
|
224
225
|
### 2c — Adding an INVARIANT
|
|
225
226
|
|
|
@@ -292,7 +293,7 @@ emit leaves the prior install intact.
|
|
|
292
293
|
a blank line, then a `>` blockquote holding `{{DESC_<NAME>}}` — title, blockquote, nothing
|
|
293
294
|
between. `doctor` checks both the presence of the purpose line and that the blockquote is the
|
|
294
295
|
*first* block (`authoringProblems`, via `firstBlockquote` and `descBlockProblem`).
|
|
295
|
-
2. One row in the hand-authored table in `src/AGENT.md.tmpl` (agents
|
|
296
|
+
2. One row in the hand-authored table in `src/AGENT.md.tmpl` (the `## 3.` table for agents, the `## 4.` one for skills).
|
|
296
297
|
Gated both ways: a row with no file, and a file with no row.
|
|
297
298
|
3. `DESC_<NAME>` in **all fourteen** themes. Theme parity catches a missing one; nothing catches
|
|
298
299
|
an unstyled placeholder.
|
|
@@ -317,8 +318,9 @@ and **skip steps 2, 4, 6, 7**: the flat-spec globs only see `*.md` at the top le
|
|
|
317
318
|
Two traps worth knowing before you write the spec:
|
|
318
319
|
|
|
319
320
|
- **The word "Read-only" anywhere in an agent spec locks its emit.** It is a bare substring test
|
|
320
|
-
(`js/hosts/native.mjs:148`). The escape
|
|
321
|
-
re-opens bash on all three hosts
|
|
321
|
+
(`js/hosts/native.mjs:148`). The escape hatches are explicit markers: `<!-- bash: allow -->`
|
|
322
|
+
re-opens bash on all three hosts (OpenCode gates it to `ask`), and `<!-- webfetch: allow -->`
|
|
323
|
+
re-opens web fetch — the researcher is the one spec that carries it.
|
|
322
324
|
- **The console has a fourth, ungated copy of the skill taxonomy.** `SKILL_CATS` in
|
|
323
325
|
`web/src/pages/Skills.jsx:19` mirrors `SKILL_CLASS` with nothing comparing them (contrast
|
|
324
326
|
`LAW_META`, which *is* cross-checked). Inventing a new *category* means editing that file —
|