geneseed 3.9.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -1
- package/DESIGN.md +1 -1
- package/README.md +52 -71
- package/SHIPPED.md +3 -3
- package/adapters/bob/README.md +18 -2
- package/adapters/claude-code/README.md +17 -12
- package/adapters/openclaude/README.md +2 -2
- package/adapters/opencode/README.md +4 -4
- package/adapters/opencode/plugins/geneseed-context.js +78 -8
- package/bin/build-driver.mjs +2 -0
- package/bin/geneseed-cli.mjs +17 -0
- package/bin/geneseed-hook.mjs +3 -1
- package/docs/README.md +42 -0
- package/docs/_groups.json +18 -0
- package/docs/candidate-hosts.md +12 -5
- package/docs/concepts/agents.md +44 -0
- package/docs/concepts/collaboration.md +62 -0
- package/docs/concepts/exclusions.md +55 -0
- package/docs/concepts/footprint.md +52 -0
- package/docs/concepts/foreman-mode.md +69 -0
- package/docs/concepts/hooks.md +101 -0
- package/docs/concepts/hosts.md +55 -0
- package/docs/concepts/lsp.md +50 -0
- package/docs/{web → concepts}/memory.md +2 -2
- package/docs/{web → concepts}/notebook.md +2 -2
- package/docs/concepts/rules.md +52 -0
- package/docs/concepts/skills.md +49 -0
- package/docs/concepts/themes.md +50 -0
- package/docs/extending.md +27 -21
- package/docs/guides/autostart.md +70 -0
- package/docs/guides/choose-your-setup.md +120 -0
- package/docs/guides/headless.md +22 -0
- package/docs/guides/install.md +161 -0
- package/docs/guides/mcp.md +195 -0
- package/docs/guides/migrate.md +41 -0
- package/docs/guides/project-context.md +87 -0
- package/docs/guides/run-anywhere.md +45 -0
- package/docs/guides/uninstall.md +51 -0
- package/docs/guides/upgrade.md +65 -0
- package/docs/guides/verify.md +65 -0
- package/docs/guides/wiki.md +69 -0
- package/docs/guides/worktree.md +47 -0
- package/docs/reference/about.md +7 -0
- package/docs/reference/cli.md +7 -0
- package/docs/reference/environment.md +99 -0
- package/docs/reference/glossary.md +33 -0
- package/docs/reference/opencode-plugins.md +131 -0
- package/docs/reference/troubleshooting.md +350 -0
- package/docs/token-footprint.md +26 -21
- package/docs/understand/a-day.md +77 -0
- package/docs/understand/enforced-vs-asked.md +64 -0
- package/docs/understand/harness.md +36 -0
- package/docs/understand/on-your-machine.md +76 -0
- package/docs/web-ui.md +2 -2
- package/harness.config.json +1 -1
- package/js/README.md +7 -6
- package/js/build/driver.mjs +26 -95
- package/js/build/emit-claude.mjs +76 -81
- package/js/build/generate.mjs +12 -2
- package/js/build/render.mjs +4 -0
- package/js/build/source.mjs +6 -0
- package/js/build/stubs.mjs +18 -2
- package/js/cli-table.json +6 -6
- package/js/hosts/hooks.mjs +86 -54
- package/js/hosts/hosts.mjs +5 -21
- package/js/hosts/installs.mjs +4 -6
- package/js/hosts/mcp.mjs +4 -14
- package/js/hosts/native.mjs +27 -50
- package/js/hosts/opencode.mjs +1 -1
- package/js/hosts/settings.mjs +17 -79
- package/js/inspect/checks-authoring.mjs +9 -4
- package/js/inspect/checks-repo.mjs +45 -1
- package/js/inspect/diff.mjs +11 -3
- package/js/inspect/doctor.mjs +4 -4
- package/js/inspect/excludes.mjs +0 -4
- package/js/inspect/inventory.mjs +1 -1
- package/js/inspect/registry.mjs +20 -3
- package/js/inspect/status.mjs +11 -6
- package/js/lib/node-floor.mjs +30 -0
- package/js/maintain/migrate.mjs +5 -5
- package/js/maintain/setup.mjs +112 -26
- package/js/maintain/uninstall.mjs +8 -42
- package/js/maintain/update.mjs +4 -15
- package/js/ui/cli.mjs +2 -2
- package/js/web/actions.mjs +4 -5
- package/js/web/api.mjs +7 -4
- package/js/web/daemon.mjs +52 -0
- package/js/web/docs.mjs +149 -65
- package/js/web/server.mjs +5 -2
- package/package.json +6 -5
- package/registry.json +7 -0
- package/src/AGENT.md.tmpl +73 -52
- package/src/laws/universal.md +3 -0
- package/src/ontology/universal.md +1 -1
- package/src/postures/mentor.md +1 -0
- 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/explain-changes/SKILL.md +1 -1
- package/src/skills/explain-changes/VENDOR.md +1 -1
- package/src/skills/geneseed.md +1 -1
- package/src/skills/learn-mode.md +89 -0
- package/src/skills/pipeline.md +2 -2
- package/src/skills/quiz.md +2 -1
- package/src/skills/react-view-transitions/SKILL.md +1 -1
- package/src/skills/react-view-transitions/VENDOR.md +7 -2
- package/src/skills/teach.md +2 -1
- 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/workflow.md +1 -1
- package/themes/_TEMPLATE.json +1 -0
- package/themes/biker.json +1 -0
- package/themes/commentator.json +1 -0
- package/themes/cyberpunk.json +1 -0
- package/themes/gamer.json +1 -0
- package/themes/imperial.json +1 -0
- package/themes/joker.json +1 -0
- package/themes/marvin.json +1 -0
- package/themes/mean.json +1 -0
- package/themes/military.json +1 -0
- package/themes/neutral.json +1 -0
- package/themes/pirate.json +1 -0
- package/themes/sports.json +1 -0
- package/themes/verstappen.json +1 -0
- package/themes/wizard.json +1 -0
- package/web/dist/assets/Activity-C52LEb-2.js +1 -0
- package/web/dist/assets/ActivityDetail-CjNLRvXg.js +1 -0
- package/web/dist/assets/Dashboard-B6_sSCHi.js +2 -0
- package/web/dist/assets/Docs-Dj7BmCSA.js +5 -0
- package/web/dist/assets/{GreenhouseView-Bq1QmaGO.js → GreenhouseView-BALwjQsA.js} +1 -1
- package/web/dist/assets/Installs-BbffDQm3.js +2 -0
- package/web/dist/assets/JournalView-JpAQ8Hqe.js +1 -0
- package/web/dist/assets/Laws-DK7eCWSm.js +3 -0
- package/web/dist/assets/Library-CoamHelE.js +1 -0
- package/web/dist/assets/{Markdown-CkFyfgNf.js → Markdown-qjM2_ofS.js} +5 -5
- package/web/dist/assets/{OperatorHudView-B-HVBqfY.js → OperatorHudView-DVx1HlBx.js} +1 -1
- package/web/dist/assets/Personal-eaXb1zcj.js +1 -0
- package/web/dist/assets/{Tabs-_pjUFIEI.js → Tabs-BmGtt8E2.js} +1 -1
- package/web/dist/assets/api-Dc8f3ca-.js +1 -0
- package/web/dist/assets/drawable-Du_PZrQj.js +1 -0
- package/web/dist/assets/index-Bz_KMRDH.css +1 -0
- package/web/dist/assets/index-CNFH7o55.js +2 -0
- package/web/dist/index.html +3 -2
- package/QUICKSTART.md +0 -50
- package/SETUP.md +0 -1183
- package/adapters/copilot/README.md +0 -105
- package/docs/web/_groups.json +0 -45
- package/docs/web/about.md +0 -6
- package/docs/web/adapters-claude-code.md +0 -8
- package/docs/web/adapters-opencode-loads.md +0 -8
- package/docs/web/adapters-opencode-spec.md +0 -8
- package/docs/web/adapters-opencode.md +0 -8
- package/docs/web/agents.md +0 -8
- package/docs/web/authoring.md +0 -11
- package/docs/web/autostart.md +0 -9
- package/docs/web/claude-hooks.md +0 -37
- package/docs/web/cli.md +0 -6
- package/docs/web/collaboration.md +0 -27
- package/docs/web/configure-wiki.md +0 -9
- package/docs/web/design.md +0 -7
- package/docs/web/env-knobs.md +0 -9
- package/docs/web/exclusions.md +0 -27
- package/docs/web/first-session.md +0 -28
- package/docs/web/footprint.md +0 -38
- package/docs/web/foreman-mode.md +0 -33
- package/docs/web/glossary.md +0 -6
- package/docs/web/harnesses.md +0 -30
- package/docs/web/headless.md +0 -9
- package/docs/web/ingest-docs.md +0 -9
- package/docs/web/install-paths.md +0 -9
- package/docs/web/install-quick.md +0 -35
- package/docs/web/lsp-overview.md +0 -37
- package/docs/web/mcp-claude-code.md +0 -10
- package/docs/web/mcp-filesystem.md +0 -9
- package/docs/web/mcp-gitlab.md +0 -9
- package/docs/web/mcp-markitdown.md +0 -9
- package/docs/web/mcp-overview.md +0 -34
- package/docs/web/mcp-trouble.md +0 -9
- package/docs/web/mcp-verify.md +0 -9
- package/docs/web/migrate.md +0 -39
- package/docs/web/model.md +0 -20
- package/docs/web/pipelines.md +0 -24
- package/docs/web/plugin-activity.md +0 -23
- package/docs/web/plugin-context.md +0 -26
- package/docs/web/plugin-guard.md +0 -24
- package/docs/web/plugin-learn.md +0 -27
- package/docs/web/plugin-notify.md +0 -24
- package/docs/web/plugin-ponytail.md +0 -23
- package/docs/web/plugin-setup.md +0 -7
- package/docs/web/plugin-workflow.md +0 -25
- package/docs/web/plugins.md +0 -17
- package/docs/web/project-context.md +0 -9
- package/docs/web/rules.md +0 -30
- package/docs/web/run-anywhere.md +0 -9
- package/docs/web/self-improve.md +0 -8
- package/docs/web/setup-choices.md +0 -64
- package/docs/web/skills.md +0 -8
- package/docs/web/themes-author.md +0 -20
- package/docs/web/themes.md +0 -9
- package/docs/web/token-footprint.md +0 -7
- package/docs/web/trouble.md +0 -38
- package/docs/web/upgrade.md +0 -9
- package/docs/web/verify.md +0 -30
- package/docs/web/wiki.md +0 -7
- package/docs/web/worktree.md +0 -10
- package/docs/wiki.md +0 -43
- package/web/dist/assets/Activity-DAzV-qQj.js +0 -1
- package/web/dist/assets/ActivityDetail-CwEwMtKl.js +0 -1
- package/web/dist/assets/Dashboard-C2ELBW-X.js +0 -2
- package/web/dist/assets/Docs-iZrvQP8J.js +0 -2
- package/web/dist/assets/Installs-BiqR8xdO.js +0 -2
- package/web/dist/assets/JournalView-6wilG5Zd.js +0 -1
- package/web/dist/assets/Laws-D8YXD_XE.js +0 -3
- package/web/dist/assets/Library-DwuTZ2-L.js +0 -1
- package/web/dist/assets/Personal-BbUTmZQR.js +0 -1
- package/web/dist/assets/drawable-CoIulEH2.js +0 -1
- package/web/dist/assets/index-BMwSp8BP.js +0 -2
- package/web/dist/assets/index-CDPDk2vj.css +0 -1
package/js/build/emit-claude.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
|
11
11
|
import { ensureAgentOverridesStub } from '../hosts/opencode.mjs';
|
|
12
12
|
import {
|
|
13
13
|
managedBlockRemove, managedBlockWrite, mergeClaudeSettings,
|
|
14
|
-
unwireClaudeExcludes, unwireClaudeSettings,
|
|
14
|
+
unwireClaudeExcludes, unwireClaudeSettings, wireClaudeExcludes,
|
|
15
15
|
} from '../hosts/settings.mjs';
|
|
16
16
|
import { writeText } from '../lib/fs.mjs';
|
|
17
17
|
import { isTruthy } from '../lib/json.mjs';
|
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
} from './emit-common.mjs';
|
|
22
22
|
import { renderAll, renderFile } from './render.mjs';
|
|
23
23
|
import {
|
|
24
|
-
BOB_RULES_STUB, ensureExcludesStub, ensureMemoryIndex, ensureNotebookIndex,
|
|
24
|
+
BOB_RULES_STUB, ensureContextStub, ensureExcludesStub, ensureMemoryIndex, ensureNotebookIndex,
|
|
25
|
+
ensureProfileStub,
|
|
25
26
|
ensureRulesStub, ensureWikiStub,
|
|
26
27
|
} from './stubs.mjs';
|
|
27
28
|
import { writeVersion } from './version.mjs';
|
|
@@ -36,7 +37,7 @@ import { existsSync, mkdirSync } from 'node:fs';
|
|
|
36
37
|
* `claudeWire` below for the CLAUDE.md managed-block merge and the settings file. PRUNE,
|
|
37
38
|
* MANIFEST and VERIFY are the driver's, in `bin/build-driver.mjs`.
|
|
38
39
|
*
|
|
39
|
-
* Six emits route through here — claude, bob and
|
|
40
|
+
* Six emits route through here — claude, bob and openclaude at both scopes — so this one
|
|
40
41
|
* job is what puts two thirds of the matrix on the seam.
|
|
41
42
|
*
|
|
42
43
|
* `claudeMdText` is the payload's one item that is not a file this process wrote. WIRE
|
|
@@ -76,9 +77,11 @@ export function emitClaudeRender(cfg, job) {
|
|
|
76
77
|
// `themed_rel` and `lstripNewlines` are, so the two verdicts stay distinguishable.
|
|
77
78
|
if (!prefix) return tmplItem.text;
|
|
78
79
|
const ptheme = { ...theme };
|
|
79
|
-
for (const tok of ['DIR_LAWS', 'DIR_AGENTS', 'DIR_SKILLS', 'DIR_MEMORY', 'DIR_NOTEBOOK'
|
|
80
|
+
for (const tok of ['DIR_LAWS', 'DIR_AGENTS', 'DIR_SKILLS', 'DIR_MEMORY', 'DIR_NOTEBOOK',
|
|
81
|
+
'DIR_DOCTRINES']) {
|
|
80
82
|
ptheme[tok] = prefix + (ptheme[tok] ?? tok.split('_').slice(1).join('_').toLowerCase());
|
|
81
83
|
}
|
|
84
|
+
ptheme.AT_HARNESS = prefix;
|
|
82
85
|
// Same catalogue decision as the renderAll above — a re-render that forgot it would
|
|
83
86
|
// quietly put the stripped tables back.
|
|
84
87
|
return renderFile(cfg, tmplItem.src, ptheme, footprint, '', new Set(), nativeCatalog);
|
|
@@ -87,7 +90,6 @@ export function emitClaudeRender(cfg, job) {
|
|
|
87
90
|
const owned = [];
|
|
88
91
|
const agentText = items.find((i) => i.rel === 'AGENT.md' && i.text !== null)?.text ?? null;
|
|
89
92
|
const isBob = host === 'bob';
|
|
90
|
-
const isCopilot = host === 'copilot';
|
|
91
93
|
|
|
92
94
|
// Bob's always-injected channel is the rules folder. At GLOBAL scope rules/geneseed.md
|
|
93
95
|
// IS the preamble (a global ~/.bob/AGENTS.md is not auto-loaded), and its pointers need
|
|
@@ -102,19 +104,30 @@ export function emitClaudeRender(cfg, job) {
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
ensureAgentOverridesStub(cfg, cfgDir);
|
|
105
|
-
// Bob
|
|
107
|
+
// Bob gives the model a skill's text but not its directory, so a folder skill's
|
|
108
|
+
// `<this-skill-directory>` is resolved here. Per repo it is relative to the workspace root,
|
|
109
|
+
// where Bob runs commands, so a committed `.bob/` stays valid on a teammate's checkout;
|
|
110
|
+
// globally there is no workspace to be relative to, so it is absolute.
|
|
111
|
+
const skillDirOf = !isBob ? null
|
|
112
|
+
: scope === 'project' ? (name) => relPosix(path.dirname(cfgDir), path.join(cfgDir, 'skills', name))
|
|
113
|
+
: (name) => path.join(cfgDir, 'skills', name);
|
|
114
|
+
// Bob's and OpenClaude's agents/skills use the Claude dialect verbatim.
|
|
106
115
|
// `manifestExisted` is deliberately not passed: the Python does not pass it either, so
|
|
107
116
|
// the pre-manifest header line is unreachable from this emit on both sides.
|
|
108
117
|
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
109
118
|
items, path.join(cfgDir, 'agents'), path.join(cfgDir, 'skills'),
|
|
110
119
|
loadAgentOverrides(cfgDir),
|
|
111
|
-
{ host:
|
|
120
|
+
{ host: 'claude', oldOwned, cfg: cfgDir, src: cfg.src, skillDirOf });
|
|
112
121
|
for (const p of written) owned.push(relPosix(cfgDir, p));
|
|
113
122
|
|
|
114
123
|
const memStatus = globalMemory(cfgDir, items, out, cfg.src);
|
|
115
124
|
ensureMemoryIndex(path.join(cfgDir, 'memory'));
|
|
116
125
|
const nbStatus = globalNotebook(cfgDir, items, out, cfg.src);
|
|
117
126
|
ensureNotebookIndex(path.join(cfgDir, 'notebook'));
|
|
127
|
+
// context.json too: the root names it as the project's own manifest, and until 2026-10 this
|
|
128
|
+
// emit never seeded one — the bundle and OpenCode did — so §9 pointed at a file that was not
|
|
129
|
+
// there. The context hook reads it from this dir (`resolveContextSets`' last candidate).
|
|
130
|
+
ensureContextStub(cfgDir);
|
|
118
131
|
ensureWikiStub(cfgDir);
|
|
119
132
|
ensureRulesStub(cfgDir);
|
|
120
133
|
ensureProfileStub(cfgDir);
|
|
@@ -128,7 +141,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
128
141
|
const gi = path.join(cfgDir, '.gitignore');
|
|
129
142
|
const giLines = (host === 'claude' || host === 'openclaude' ? ['settings.local.json']
|
|
130
143
|
: host === 'bob' ? ['settings.json'] : [])
|
|
131
|
-
.concat(['wiki.jsonc', 'agent-overrides.json']);
|
|
144
|
+
.concat(['context.json', 'wiki.jsonc', 'agent-overrides.json']);
|
|
132
145
|
if (!existsSync(gi)) {
|
|
133
146
|
writeText(gi, `${giLines.join('\n')}\n`);
|
|
134
147
|
owned.push('.gitignore');
|
|
@@ -194,7 +207,6 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
194
207
|
const old = oldManaged && typeof oldManaged === 'object' && !Array.isArray(oldManaged)
|
|
195
208
|
? oldManaged : {};
|
|
196
209
|
const isBob = host === 'bob';
|
|
197
|
-
const isCopilot = host === 'copilot';
|
|
198
210
|
const managed = {};
|
|
199
211
|
|
|
200
212
|
// CLAUDE.md — Claude auto-loads it by location; merge as a delimited block so any user
|
|
@@ -228,80 +240,63 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
228
240
|
// settings.local.json — the personal, untracked file — never the team-shared
|
|
229
241
|
// settings.json, which would hand every teammate failing hooks pointing at this machine's
|
|
230
242
|
// node and this machine's checkout. (Bob documents no local variant, so it keeps
|
|
231
|
-
// settings.json.)
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
settingsPath, scope, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
|
|
260
|
-
doctrines, excludeRules, host, cfgDir,
|
|
261
|
-
);
|
|
262
|
-
managed.settings_hooks = managedHooks;
|
|
243
|
+
// settings.json.)
|
|
244
|
+
// Bob GLOBAL: its hooks doc puts the global file at `~/.bob/settings/settings.json`
|
|
245
|
+
// (nested), where an older emit wrote `~/.bob/settings.json`; the `oldSf !==
|
|
246
|
+
// settingsName` branch below unwires the old file, so a re-emit migrates. Bob's project
|
|
247
|
+
// file stays `.bob/settings.json`.
|
|
248
|
+
const settingsName = scope === 'project' && !isBob ? 'settings.local.json'
|
|
249
|
+
: isBob && scope === 'global' ? path.join('settings', 'settings.json') : 'settings.json';
|
|
250
|
+
const settingsPath = path.join(cfgDir, settingsName);
|
|
251
|
+
mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
252
|
+
managed.settings_file = settingsName;
|
|
253
|
+
// Migration: an older install wired hooks/excludes into a different file — unwire the
|
|
254
|
+
// recorded claims there, or they linger (and run) forever.
|
|
255
|
+
const oldSf = get(old, 'settings_file') || 'settings.json';
|
|
256
|
+
if (oldSf !== settingsName) {
|
|
257
|
+
unwireClaudeSettings(path.join(cfgDir, oldSf), get(old, 'settings_hooks') || []);
|
|
258
|
+
unwireClaudeExcludes(path.join(cfgDir, oldSf), get(old, 'settings_excludes') || []);
|
|
259
|
+
}
|
|
260
|
+
// The merge prunes recorded groups that are no longer canonical and returns the
|
|
261
|
+
// COMPLETE current claim set — store it as-is; unioning with prior would resurrect the
|
|
262
|
+
// stale claims.
|
|
263
|
+
// `doctrines` decides whether the git-gate group is part of the canonical claim set. It
|
|
264
|
+
// travels from `cfg` rather than being re-read off the deployment: this is the emit that
|
|
265
|
+
// DECIDES the install's packs, so the marker on disk is still the previous build's.
|
|
266
|
+
const [, managedHooks] = mergeClaudeSettings(
|
|
267
|
+
settingsPath, scope, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
|
|
268
|
+
doctrines, excludeRules, host, cfgDir,
|
|
269
|
+
);
|
|
270
|
+
managed.settings_hooks = managedHooks;
|
|
263
271
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
} else if (priorExcl.length && isBob) {
|
|
286
|
-
// Self-heal older Bob installs: earlier versions wrote the global AGENTS.md into
|
|
287
|
-
// claudeMdExcludes here. The key is Claude-only and its Bob semantics are unknown, so
|
|
288
|
-
// a re-emit removes it instead of carrying it forward.
|
|
289
|
-
unwireClaudeExcludes(settingsPath, priorExcl);
|
|
290
|
-
} else if (priorExcl.length) {
|
|
291
|
-
managed.settings_excludes = priorExcl;
|
|
272
|
+
// Project-bypasses-global (Claude only): a PROJECT install suppresses the GLOBAL
|
|
273
|
+
// ~/.claude/CLAUDE.md while cwd is this repo, via Claude's native claudeMdExcludes.
|
|
274
|
+
// Written only when this run actually emitted the project's own preamble (never
|
|
275
|
+
// suppress with no replacement); GENESEED_STACK_GLOBAL=1 opts out, and a re-emit with
|
|
276
|
+
// it set strips a prior exclude. Bob never gets one: its bypass is the same-named
|
|
277
|
+
// workspace rules file.
|
|
278
|
+
const priorRaw = get(old, 'settings_excludes');
|
|
279
|
+
const priorExcl = Array.isArray(priorRaw) ? priorRaw : [];
|
|
280
|
+
if (scope === 'project' && hasAgentText && preambleExclude) {
|
|
281
|
+
const wantExcl = [preambleExclude];
|
|
282
|
+
if (process.env.GENESEED_STACK_GLOBAL) {
|
|
283
|
+
unwireClaudeExcludes(settingsPath, wantExcl);
|
|
284
|
+
managed.settings_excludes = [];
|
|
285
|
+
} else {
|
|
286
|
+
const addedExcl = wireClaudeExcludes(settingsPath, wantExcl);
|
|
287
|
+
// Claim only what Geneseed itself wired (prior + newly added) — folding `wantExcl`
|
|
288
|
+
// in unconditionally would claim a user's own pre-existing exclude, and uninstall
|
|
289
|
+
// would then strip it. `sorted(set(a) | set(b))` — Python sorts strings by code
|
|
290
|
+
// point and so does the default `Array.sort`, which is why no comparator is passed.
|
|
291
|
+
managed.settings_excludes = [...new Set([...priorExcl, ...addedExcl])].sort();
|
|
292
292
|
}
|
|
293
|
-
} else if (
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
managed.
|
|
300
|
-
const legacy = get(old, 'copilot_hooks');
|
|
301
|
-
if (Array.isArray(legacy) && legacy.length) unwireCopilotSettings(settingsPath, legacy);
|
|
302
|
-
const [, managedHooks] = mergeClaudeSettings(settingsPath, scope,
|
|
303
|
-
Array.isArray(legacy) ? null : get(old, 'settings_hooks'), hookOpts, null, [], host, cfgDir);
|
|
304
|
-
managed.settings_hooks = managedHooks;
|
|
293
|
+
} else if (priorExcl.length && isBob) {
|
|
294
|
+
// Self-heal older Bob installs: earlier versions wrote the global AGENTS.md into
|
|
295
|
+
// claudeMdExcludes here. The key is Claude-only and its Bob semantics are unknown, so
|
|
296
|
+
// a re-emit removes it instead of carrying it forward.
|
|
297
|
+
unwireClaudeExcludes(settingsPath, priorExcl);
|
|
298
|
+
} else if (priorExcl.length) {
|
|
299
|
+
managed.settings_excludes = priorExcl;
|
|
305
300
|
}
|
|
306
301
|
return managed;
|
|
307
302
|
}
|
package/js/build/generate.mjs
CHANGED
|
@@ -173,7 +173,6 @@ export const DEFAULT_EMIT = new Map([
|
|
|
173
173
|
['opencode global', 'opencode-global'], ['opencode project', 'opencode'],
|
|
174
174
|
['claude global', 'claude-global'], ['claude project', 'claude'],
|
|
175
175
|
['bob global', 'bob-global'], ['bob project', 'bob'],
|
|
176
|
-
['copilot global', 'copilot-global'], ['copilot project', 'copilot'],
|
|
177
176
|
['openclaude global', 'openclaude-global'], ['openclaude project', 'openclaude'],
|
|
178
177
|
]);
|
|
179
178
|
|
|
@@ -255,7 +254,18 @@ export function rebuildCommand(argv) {
|
|
|
255
254
|
* choose — which is the same failure mode `_footprint_of_dir`'s WARN exists to prevent, at a
|
|
256
255
|
* larger scale.
|
|
257
256
|
*/
|
|
258
|
-
export function cmdRebuildAll() {
|
|
257
|
+
export async function cmdRebuildAll() {
|
|
258
|
+
const rc = rebuildAll();
|
|
259
|
+
// A console started before an upgrade keeps serving the old `index.html`; restart it — only
|
|
260
|
+
// when stale, see `bounceDaemonIfRunning`. Lazy: no other verb in this module needs js/web/.
|
|
261
|
+
const { bounceDaemonIfRunning } = await import('../web/daemon.mjs');
|
|
262
|
+
await bounceDaemonIfRunning(null, (m) => printOut(`${m}
|
|
263
|
+
`), { onlyIfStale: true });
|
|
264
|
+
return rc;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The rebuild itself, synchronous — `cmdRebuildAll` is it plus the console restart. */
|
|
268
|
+
export function rebuildAll() {
|
|
259
269
|
const targets = installTargets().filter(([h, s, r]) => installState(r, h, s) === 'active');
|
|
260
270
|
if (!targets.length) {
|
|
261
271
|
printOut('[rebuild-all] no active installs detected.\n');
|
package/js/build/render.mjs
CHANGED
|
@@ -42,6 +42,10 @@ export const STRUCTURE = {
|
|
|
42
42
|
SCRIPT: 'Script', SCRIPTS: 'Scripts',
|
|
43
43
|
DIR_LAWS: 'laws', DIR_AGENTS: 'agents', DIR_SKILLS: 'skills', DIR_MEMORY: 'memory',
|
|
44
44
|
DIR_NOTEBOOK: 'notebook', DIR_ONTOLOGY: 'ontology', DIR_DOCTRINES: 'doctrines',
|
|
45
|
+
// Where the harness's own files sit, as seen from the root instruction file: empty when they
|
|
46
|
+
// are its siblings (`files`, OpenCode), `.claude/`-style when the root sits a level above them —
|
|
47
|
+
// set per emit by `prefixedAgentText` in js/build/emit-claude.mjs, like the DIR_* prefixes.
|
|
48
|
+
AT_HARNESS: '',
|
|
45
49
|
// Ontology section names. Theme-INDEPENDENT on purpose: a citation reads
|
|
46
50
|
// `({{ONTOLOGY}}: {{ONT_TELOS}})` — token on both sides, so the heading and every
|
|
47
51
|
// reference to it move together and a rename can never desync them.
|
package/js/build/source.mjs
CHANGED
|
@@ -168,6 +168,12 @@ export const CONFIG = path.join(ROOT, 'harness.config.json');
|
|
|
168
168
|
export const THEMES = path.join(ROOT, 'themes');
|
|
169
169
|
export const PLUGIN_SRC = path.join(ROOT, 'adapters', 'opencode', 'plugins');
|
|
170
170
|
export const WORKFLOW_SRC = path.join(ROOT, 'adapters', 'opencode', 'workflows');
|
|
171
|
+
/**
|
|
172
|
+
* The user docs: one folder per kind of reading, all under `docs/` so GitHub renders the same
|
|
173
|
+
* files the console serves. A page's id is its basename, so ids must be unique across folders.
|
|
174
|
+
*/
|
|
175
|
+
export const DOCS = path.join(ROOT, 'docs');
|
|
176
|
+
export const DOC_FOLDERS = ['understand', 'guides', 'concepts', 'reference'];
|
|
171
177
|
|
|
172
178
|
/**
|
|
173
179
|
* `_build_render.posture_names()` / `mode_names()` — discovered, never hardcoded, so a new
|
package/js/build/stubs.mjs
CHANGED
|
@@ -245,9 +245,25 @@ export const ensureBundleGitignore = stubWriter('.gitignore', BUNDLE_GITIGNORE);
|
|
|
245
245
|
* right after the store dir is created, but never invent the dir themselves). */
|
|
246
246
|
const storeIndexWriter = (file, header) => (dir) => { if (isDir(dir)) seed(path.join(dir, file), header); };
|
|
247
247
|
|
|
248
|
+
const MEMORY_INDEX = '# Memory Index\n';
|
|
249
|
+
const NOTEBOOK_INDEX = '# Notebook Index\n';
|
|
250
|
+
|
|
248
251
|
/** `_build_render.ensure_memory_index` — only inside an EXISTING store dir. */
|
|
249
|
-
export const ensureMemoryIndex = storeIndexWriter('MEMORY.md',
|
|
252
|
+
export const ensureMemoryIndex = storeIndexWriter('MEMORY.md', MEMORY_INDEX);
|
|
250
253
|
|
|
251
254
|
/** `_build_render.ensure_notebook_index`. */
|
|
252
|
-
export const ensureNotebookIndex = storeIndexWriter('NOTEBOOK.md',
|
|
255
|
+
export const ensureNotebookIndex = storeIndexWriter('NOTEBOOK.md', NOTEBOOK_INDEX);
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The seeds of the files the session-start injection reads (`SESSION_FILES` in
|
|
259
|
+
* `js/hosts/hooks.mjs`, and its twin in the OpenCode context plugin). Both carry the SHA-256
|
|
260
|
+
* of these bodies rather than importing them — the hook path pays for every import, and the
|
|
261
|
+
* plugin is deployed on its own — so an untouched seed is skipped instead of injected as
|
|
262
|
+
* noise. `tests/unit/claude.test.mjs` gates both hash lists against this map: edit a stub and
|
|
263
|
+
* the two copies must move with it.
|
|
264
|
+
*/
|
|
265
|
+
export const SESSION_SEEDS = {
|
|
266
|
+
'user-rules.md': RULES_STUB, 'PROFILE.md': PROFILE_STUB, 'wiki.jsonc': WIKI_STUB,
|
|
267
|
+
'MEMORY.md': MEMORY_INDEX, 'NOTEBOOK.md': NOTEBOOK_INDEX,
|
|
268
|
+
};
|
|
253
269
|
|
package/js/cli-table.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"prog": "
|
|
2
|
+
"prog": "geneseed",
|
|
3
3
|
"commands": [
|
|
4
4
|
{
|
|
5
5
|
"name": "bootstrap",
|
|
@@ -297,7 +297,7 @@
|
|
|
297
297
|
],
|
|
298
298
|
"dest": "host",
|
|
299
299
|
"metavar": null,
|
|
300
|
-
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default)
|
|
300
|
+
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
|
|
301
301
|
"choices": null,
|
|
302
302
|
"default": null,
|
|
303
303
|
"required": false,
|
|
@@ -513,7 +513,7 @@
|
|
|
513
513
|
],
|
|
514
514
|
"dest": "host",
|
|
515
515
|
"metavar": null,
|
|
516
|
-
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default)
|
|
516
|
+
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
|
|
517
517
|
"choices": null,
|
|
518
518
|
"default": null,
|
|
519
519
|
"required": false,
|
|
@@ -791,7 +791,7 @@
|
|
|
791
791
|
],
|
|
792
792
|
"dest": "host",
|
|
793
793
|
"metavar": null,
|
|
794
|
-
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default)
|
|
794
|
+
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
|
|
795
795
|
"choices": null,
|
|
796
796
|
"default": null,
|
|
797
797
|
"required": false,
|
|
@@ -975,7 +975,7 @@
|
|
|
975
975
|
},
|
|
976
976
|
{
|
|
977
977
|
"name": "tool-gate",
|
|
978
|
-
"help": "PreToolUse hook for hosts with no tool matcher (
|
|
978
|
+
"help": "PreToolUse hook for hosts with no tool matcher (Bob): git-gate and rule-gate fused behind one command, dispatched on the payload",
|
|
979
979
|
"description": "",
|
|
980
980
|
"positionals": [],
|
|
981
981
|
"options": [
|
|
@@ -1000,7 +1000,7 @@
|
|
|
1000
1000
|
],
|
|
1001
1001
|
"dest": "host",
|
|
1002
1002
|
"metavar": null,
|
|
1003
|
-
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default)
|
|
1003
|
+
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'bob' has no ask tier, so Laws I/IV exit 2 and everything else is a stderr warning",
|
|
1004
1004
|
"choices": null,
|
|
1005
1005
|
"default": null,
|
|
1006
1006
|
"required": false,
|
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) {
|
|
@@ -759,9 +794,8 @@ function ruleDecide(args, payload) {
|
|
|
759
794
|
const ti = (payload && payload.tool_input) || {};
|
|
760
795
|
const p = ti.file_path || ti.path || '';
|
|
761
796
|
if (typeof p !== 'string' || !p) return 0;
|
|
762
|
-
// Write carries `content`, Edit carries `new_string
|
|
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
801
|
return ask(args, 'rule-gate', 'law-1', `Geneseed (Sealed Secrets) — ${p} would carry a `
|
|
@@ -781,8 +815,7 @@ export const cmdRuleGate = guardGate(ruleGate, 'rule-gate');
|
|
|
781
815
|
// ======================================================================================
|
|
782
816
|
|
|
783
817
|
/**
|
|
784
|
-
*
|
|
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
|
}
|