geneseed 3.12.0 → 3.14.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 +160 -0
- package/README.md +2 -2
- package/SHIPPED.md +1 -1
- package/adapters/bob/README.md +1 -1
- package/adapters/claude-code/README.md +1 -1
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/README.md +3 -3
- package/adapters/opencode/plugins/geneseed-learn.js +10 -1
- package/bin/geneseed-cli.mjs +10 -14
- package/bin/geneseed-hook.mjs +22 -15
- package/docs/README.md +41 -22
- package/docs/concepts/agents.md +3 -1
- package/docs/concepts/collaboration.md +3 -1
- package/docs/concepts/exclusions.md +4 -2
- package/docs/concepts/footprint.md +3 -1
- package/docs/concepts/foreman-mode.md +3 -1
- package/docs/concepts/hook-gates.md +61 -0
- package/docs/concepts/hooks.md +8 -56
- package/docs/concepts/host-matrix.md +38 -0
- package/docs/concepts/hosts.md +6 -32
- package/docs/concepts/loop-branch.md +66 -0
- package/docs/concepts/loop-bricks.md +48 -0
- package/docs/concepts/loop-contracts.md +33 -0
- package/docs/concepts/loop-gates.md +30 -0
- package/docs/concepts/loop-notes.md +43 -0
- package/docs/concepts/loop-risk.md +36 -0
- package/docs/concepts/loop-templates.md +50 -0
- package/docs/concepts/loops.md +24 -116
- package/docs/concepts/lsp.md +2 -0
- package/docs/concepts/memory.md +3 -1
- package/docs/concepts/notebook.md +3 -1
- package/docs/concepts/rules.md +4 -2
- package/docs/concepts/skills.md +3 -1
- package/docs/concepts/themes.md +3 -1
- package/docs/extending.md +28 -13
- package/docs/guides/autostart.md +3 -1
- package/docs/guides/choose-your-setup.md +5 -80
- package/docs/guides/headless.md +5 -3
- package/docs/guides/install-agent-md.md +37 -0
- package/docs/guides/install-bob.md +20 -0
- package/docs/guides/install-claude-code.md +22 -0
- package/docs/guides/install-clone.md +34 -0
- package/docs/guides/install-openclaude.md +25 -0
- package/docs/guides/install-opencode.md +35 -0
- package/docs/guides/install-scripted.md +21 -0
- package/docs/guides/install.md +9 -118
- package/docs/guides/loop-finish.md +54 -0
- package/docs/guides/loop-running.md +66 -0
- package/docs/guides/loop-which-template.md +35 -0
- package/docs/guides/mcp-filesystem.md +41 -0
- package/docs/guides/mcp-gitlab.md +66 -0
- package/docs/guides/mcp-markitdown.md +36 -0
- package/docs/guides/mcp.md +8 -137
- package/docs/guides/migrate.md +3 -1
- package/docs/guides/project-context.md +4 -2
- package/docs/guides/read-non-markdown.md +17 -0
- package/docs/guides/run-a-loop.md +64 -0
- package/docs/guides/run-anywhere.md +2 -0
- package/docs/guides/setup-doctrines.md +55 -0
- package/docs/guides/setup-footprint.md +48 -0
- package/docs/guides/uninstall.md +3 -1
- package/docs/guides/upgrade.md +3 -1
- package/docs/guides/verify.md +3 -1
- package/docs/guides/wiki.md +3 -1
- package/docs/guides/worktree.md +3 -1
- package/docs/reference/about.md +3 -1
- package/docs/reference/cli.md +2 -0
- package/docs/reference/env-context.md +40 -0
- package/docs/reference/environment.md +10 -59
- package/docs/reference/glossary.md +3 -1
- package/docs/reference/loop-actions.md +20 -0
- package/docs/reference/opencode-plugins.md +14 -118
- package/docs/reference/plugin-activity.md +20 -0
- package/docs/reference/plugin-context.md +27 -0
- package/docs/reference/plugin-guard.md +27 -0
- package/docs/reference/plugin-learn.md +27 -0
- package/docs/reference/plugin-notify.md +20 -0
- package/docs/reference/plugin-ponytail.md +19 -0
- package/docs/reference/plugin-workflow.md +25 -0
- package/docs/reference/troubleshoot-doctor.md +50 -0
- package/docs/reference/troubleshoot-hooks.md +99 -0
- package/docs/reference/troubleshoot-install.md +108 -0
- package/docs/reference/troubleshoot-tools.md +56 -0
- package/docs/reference/troubleshoot-update.md +23 -0
- package/docs/reference/troubleshoot-web.md +66 -0
- package/docs/reference/troubleshooting.md +13 -337
- package/docs/understand/a-day.md +3 -1
- package/docs/understand/enforced-vs-asked.md +3 -1
- package/docs/understand/harness.md +2 -0
- package/docs/understand/machine-bob.md +17 -0
- package/docs/understand/machine-claude.md +35 -0
- package/docs/understand/machine-opencode.md +36 -0
- package/docs/understand/on-your-machine.md +12 -55
- package/docs/understand/take-it-out.md +13 -0
- package/docs/web-ui.md +35 -15
- package/harness.config.json +1 -1
- package/js/README.md +30 -13
- package/js/build/args.mjs +374 -0
- package/js/build/driver.mjs +137 -553
- package/js/build/emit-claude.mjs +2 -4
- package/js/build/generate.mjs +6 -3
- package/js/build/render.mjs +5 -9
- package/js/build/source.mjs +3 -11
- package/js/cli-table.json +27 -1
- package/js/hosts/hooks-context.mjs +431 -0
- package/js/hosts/hooks-learn.mjs +275 -0
- package/js/hosts/hooks-prims.mjs +108 -0
- package/js/hosts/hooks.mjs +50 -977
- package/js/hosts/hosts.mjs +12 -4
- package/js/hosts/installs.mjs +5 -6
- package/js/hosts/mcp.mjs +13 -49
- package/js/hosts/memory-files.mjs +181 -0
- package/js/hosts/native.mjs +43 -5
- package/js/hosts/settings.mjs +135 -414
- package/js/hosts/shim.mjs +373 -0
- package/js/inspect/checks-authoring.mjs +21 -828
- package/js/inspect/checks-build.mjs +32 -4
- package/js/inspect/checks-constitution.mjs +493 -0
- package/js/inspect/checks-counts.mjs +242 -0
- package/js/inspect/checks-repo.mjs +17 -8
- package/js/inspect/diff.mjs +4 -1
- package/js/inspect/doctor.mjs +2 -2
- package/js/inspect/inventory.mjs +4 -4
- package/js/inspect/scan.mjs +1 -1
- package/js/inspect/status.mjs +1 -1
- package/js/inspect/validate.mjs +48 -35
- package/js/loop/catalog.mjs +17 -4
- package/js/loop/cli.mjs +52 -16
- package/js/loop/graph.mjs +19 -1
- package/js/loop/registry.mjs +191 -0
- package/js/loop/score.mjs +46 -7
- package/js/loop/state.mjs +157 -19
- package/js/maintain/memory.mjs +1 -1
- package/js/maintain/migrate.mjs +18 -30
- package/js/maintain/setup.mjs +1 -1
- package/js/maintain/uninstall.mjs +47 -88
- package/js/ui/cli.mjs +12 -12
- package/js/web/actions.mjs +39 -4
- package/js/web/api.mjs +34 -21
- package/js/web/docs.mjs +87 -78
- package/js/web/routes.mjs +5 -1
- package/package.json +1 -1
- package/src/agents/explorer.md +2 -2
- package/src/agents/researcher.md +3 -3
- package/src/bricks/README.md +3 -1
- package/src/bricks/adr-challenge.md +20 -0
- package/src/bricks/adr-draft.md +15 -0
- package/src/bricks/characterize.md +17 -0
- package/src/bricks/ci-fix.md +2 -1
- package/src/bricks/ci-triage.md +20 -0
- package/src/bricks/compat-check.md +16 -0
- package/src/bricks/done-check.md +16 -0
- package/src/bricks/fitness-define.md +22 -0
- package/src/bricks/flake-check.md +17 -0
- package/src/bricks/flake-repro.md +18 -0
- package/src/bricks/identify.md +6 -3
- package/src/bricks/migration-plan.md +21 -0
- package/src/bricks/mutation-check.md +18 -0
- package/src/bricks/plan.md +6 -6
- package/src/bricks/red-test.md +16 -0
- package/src/bricks/review.md +5 -3
- package/src/bricks/spec.md +18 -0
- package/src/bricks/upgrade-scout.md +16 -0
- package/src/loops/api-endpoint.json +31 -0
- package/src/loops/architecture-decision.json +22 -0
- package/src/loops/bugfix.json +1 -0
- package/src/loops/ci-repair.json +19 -0
- package/src/loops/deps-upgrade.json +29 -0
- package/src/loops/enforce-architecture-rule.json +28 -0
- package/src/loops/feature.json +8 -5
- package/src/loops/fix-flaky-tests.json +30 -0
- package/src/loops/legacy-refactor.json +26 -0
- package/src/loops/legacy-tests.json +29 -0
- package/src/loops/refactor.json +5 -1
- package/src/loops/remove-dead-code.json +30 -0
- package/src/loops/spec-first-feature.json +29 -0
- package/src/loops/tdd.json +25 -0
- package/src/loops/update-contract.json +33 -0
- package/src/skills/brick-forge.md +3 -1
- package/src/skills/bruno-collection-generator.md +3 -3
- package/src/skills/bruno-test-writer.md +3 -3
- package/src/skills/ci-fix.md +3 -3
- package/src/skills/consolidate-memory.md +4 -4
- package/src/skills/council.md +1 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/git-archaeology.md +1 -1
- package/src/skills/git-rescue.md +4 -3
- package/src/skills/handoff.md +1 -1
- package/src/skills/herdr.md +1 -1
- package/src/skills/learn-mode.md +2 -2
- package/src/skills/loop.md +22 -10
- package/src/skills/opencode-theme.md +3 -3
- package/src/skills/ponytail.md +1 -1
- package/src/skills/profile.md +3 -4
- package/src/skills/prose.md +2 -3
- package/src/skills/quiz.md +1 -1
- package/src/skills/react-view-transitions/SKILL.md +1 -1
- package/src/skills/react-view-transitions/VENDOR.md +8 -3
- package/src/skills/research.md +3 -3
- package/src/skills/review-response.md +2 -2
- package/src/skills/rule.md +3 -3
- package/src/skills/security-audit.md +4 -4
- package/src/skills/teach.md +1 -1
- package/src/skills/wiki.md +3 -3
- package/src/skills/workflow.md +4 -4
- package/src/skills/worktree.md +2 -2
- package/web/dist/assets/Activity-CjRqZnr-.js +1 -0
- package/web/dist/assets/ActivityDetail-OQTxKBj2.js +1 -0
- package/web/dist/assets/Dashboard-J-xCe_Gk.js +2 -0
- package/web/dist/assets/Docs-CvOZ8akL.js +5 -0
- package/web/dist/assets/{GreenhouseView-C6ibc8IE.js → GreenhouseView-6KDjX8da.js} +1 -1
- package/web/dist/assets/{Installs-DRYjYnxs.js → Installs-MWuhNjSm.js} +2 -2
- package/web/dist/assets/JournalView-C8MrOz2c.js +1 -0
- package/web/dist/assets/Laws-Du0-Zyp2.js +3 -0
- package/web/dist/assets/Library-DOkO0TtB.js +1 -0
- package/web/dist/assets/Loops-Dv_aLByJ.js +2 -0
- package/web/dist/assets/{Markdown-qjM2_ofS.js → Markdown-X2a3qvOh.js} +14 -14
- package/web/dist/assets/{OperatorHudView-Bs7paWmh.js → OperatorHudView-BPP1dtLz.js} +1 -1
- package/web/dist/assets/{Personal-BY5n4gTw.js → Personal-26VPYkK0.js} +1 -1
- package/web/dist/assets/Tabs-BzKuFskD.js +1 -0
- package/web/dist/assets/{usePoll-O0sJEw1Y.js → activity-Dr7ZMBI3.js} +1 -1
- package/web/dist/assets/{api-CyMRbU2x.js → api-D_ixdyEY.js} +1 -1
- package/web/dist/assets/drawable-zVVDbsyw.js +1 -0
- package/web/dist/assets/index-Ctp-4poP.css +1 -0
- package/web/dist/assets/index-KHt3wE_K.js +2 -0
- package/web/dist/assets/usePoll-DD8VjtSg.js +1 -0
- package/web/dist/index.html +3 -3
- package/web/src/pages/Laws.jsx +0 -1
- package/web/dist/assets/Activity-GaaZWXTY.js +0 -1
- package/web/dist/assets/ActivityDetail-4t7d45Du.js +0 -1
- package/web/dist/assets/Dashboard-CYn7Tp16.js +0 -2
- package/web/dist/assets/Docs-BeFKLg2T.js +0 -5
- package/web/dist/assets/JournalView-1HbU1mfg.js +0 -1
- package/web/dist/assets/Laws-DFoG8JWB.js +0 -3
- package/web/dist/assets/Library-DDjgNwz_.js +0 -1
- package/web/dist/assets/Loops-D9VEoO0F.js +0 -1
- package/web/dist/assets/Tabs-ho-EYD8i.js +0 -1
- package/web/dist/assets/drawable-CLS9drMb.js +0 -1
- package/web/dist/assets/index-CX_hXMi5.css +0 -1
- package/web/dist/assets/index-Cjr7XCA3.js +0 -2
package/js/build/emit-claude.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { existsSync, mkdirSync } from 'node:fs';
|
|
|
40
40
|
* Six emits route through here — claude, bob and openclaude at both scopes — so this one
|
|
41
41
|
* job is what puts two thirds of the matrix on the seam.
|
|
42
42
|
*
|
|
43
|
-
* `claudeMdText` is the
|
|
43
|
+
* `claudeMdText` is the one product of this render that is not a file it writes. WIRE
|
|
44
44
|
* needs the managed block's text, and that text is a RENDER (`prefixedAgentText`
|
|
45
45
|
* re-renders AGENT.md with every store dir prefixed), so it is computed here and merged
|
|
46
46
|
* there. `emitOpencodeRender` needed nothing of the kind — its wired file is derived from
|
|
@@ -174,8 +174,6 @@ export function emitClaudeRender(cfg, job) {
|
|
|
174
174
|
stats: { nAgents, nSkills },
|
|
175
175
|
memStatus,
|
|
176
176
|
nbStatus,
|
|
177
|
-
hasAgentText: agentText !== null,
|
|
178
|
-
claudeMdText,
|
|
179
177
|
managed,
|
|
180
178
|
};
|
|
181
179
|
}
|
|
@@ -264,7 +262,7 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
264
262
|
// travels from `cfg` rather than being re-read off the deployment: this is the emit that
|
|
265
263
|
// DECIDES the install's packs, so the marker on disk is still the previous build's.
|
|
266
264
|
const [, managedHooks] = mergeClaudeSettings(
|
|
267
|
-
settingsPath,
|
|
265
|
+
settingsPath, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
|
|
268
266
|
doctrines, excludeRules, host, cfgDir,
|
|
269
267
|
);
|
|
270
268
|
managed.settings_hooks = managedHooks;
|
package/js/build/generate.mjs
CHANGED
|
@@ -171,7 +171,7 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
|
|
|
171
171
|
* A pre-marker install still has a (host, scope), which is what the walk found it by, so it
|
|
172
172
|
* is rebuilt in its own mode rather than defaulted to OpenCode.
|
|
173
173
|
*/
|
|
174
|
-
|
|
174
|
+
const DEFAULT_EMIT = new Map([
|
|
175
175
|
['opencode global', 'opencode-global'], ['opencode project', 'opencode'],
|
|
176
176
|
['claude global', 'claude-global'], ['claude project', 'claude'],
|
|
177
177
|
['bob global', 'bob-global'], ['bob project', 'bob'],
|
|
@@ -483,7 +483,10 @@ export function cmdTheme(args) {
|
|
|
483
483
|
written.push(dest);
|
|
484
484
|
}
|
|
485
485
|
printOut(`[theme] wrote ${written.map((p) => path.basename(p)).join(', ')} to ${destDir}\n`);
|
|
486
|
-
|
|
487
|
-
|
|
486
|
+
// The hint names what was WRITTEN: `--transparent-only` writes no `${full}.json`, and a
|
|
487
|
+
// hint selecting it would point OpenCode at a theme that does not exist.
|
|
488
|
+
const [first, ...rest] = written.map((p) => path.basename(p, '.json'));
|
|
489
|
+
printOut(`[theme] select in OpenCode with: /theme ${first}`
|
|
490
|
+
+ rest.map((n) => ` (or /theme ${n})`).join('') + '\n');
|
|
488
491
|
return 0;
|
|
489
492
|
}
|
package/js/build/render.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { readText } from '../lib/fs.mjs';
|
|
|
34
34
|
import { parseJson } from '../lib/json.mjs';
|
|
35
35
|
import { normcase, comparePaths } from '../lib/paths.mjs';
|
|
36
36
|
// `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
|
|
37
|
-
import { PACK_ORDER, readPackText, resolveRuleIds } from './source.mjs';
|
|
37
|
+
import { PACK_ORDER, readPackText, resolveRuleIds, ruleCanon } from './source.mjs';
|
|
38
38
|
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
39
39
|
|
|
40
40
|
/** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
|
|
@@ -86,7 +86,7 @@ const CATALOG_BLOCK_RE =
|
|
|
86
86
|
// truncation — the objection `docs/token-footprint.md` raised against terse-ing the
|
|
87
87
|
// Ontology ("four orphan sentences") is answered by authoring the short form, not
|
|
88
88
|
// generating it. Same marker grammar as CATALOG so the two stay learnable as one idea.
|
|
89
|
-
const LEAN_BLOCK_RE =
|
|
89
|
+
export const LEAN_BLOCK_RE =
|
|
90
90
|
/[ \t]*<!-- LEAN:begin -->\n(?<full>[\s\S]*?)[ \t]*<!-- LEAN:else -->\n(?<lean>[\s\S]*?)[ \t]*<!-- LEAN:end -->\n/g;
|
|
91
91
|
|
|
92
92
|
/**
|
|
@@ -184,7 +184,7 @@ function resolveLean(text, footprint) {
|
|
|
184
184
|
* match is a corner both engines implement, and the leading '' Python produces when the
|
|
185
185
|
* text starts with a heading is load-bearing (it becomes `blocks[0]`, the preamble).
|
|
186
186
|
*/
|
|
187
|
-
function splitAtLawHeadings(text) {
|
|
187
|
+
export function splitAtLawHeadings(text) {
|
|
188
188
|
const out = [];
|
|
189
189
|
let start = 0;
|
|
190
190
|
for (let i = 0; i < text.length; i++) {
|
|
@@ -310,12 +310,8 @@ function activeDoctrines(cfg) {
|
|
|
310
310
|
// interpret. Dropped here rather than in `doctrinesBody` so `DOCTRINES_LIST` — the
|
|
311
311
|
// `Active packs:` marker a later reader parses back — agrees with what was rendered.
|
|
312
312
|
const excluded = new Set(cfg.excludeRules ?? []);
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
.matchAll(/^### \{\{DOCTRINE\}\} ([a-z]+) (\d+)\b/gm)]
|
|
316
|
-
.map((m) => `${m[1]}.${Number(m[2])}`);
|
|
317
|
-
return ids.some((id) => !excluded.has(id));
|
|
318
|
-
};
|
|
313
|
+
const { rules } = ruleCanon(cfg.src);
|
|
314
|
+
const survives = (pack) => rules.some((r) => r.pack === pack && !excluded.has(`${pack}.${r.n}`));
|
|
319
315
|
return PACK_ORDER.filter((p) => wanted.includes(p)).filter(survives);
|
|
320
316
|
}
|
|
321
317
|
|
package/js/build/source.mjs
CHANGED
|
@@ -222,7 +222,8 @@ export const PACK_ORDER = ['craft', 'rigor', 'ops', 'process', 'comms'];
|
|
|
222
222
|
* The ONE enumerator, for the same arithmetic reason `PACK_ORDER` is one array: the CLI
|
|
223
223
|
* flag, the console's trust boundary and the doctor all have to close `--exclude-rules`
|
|
224
224
|
* against the SAME set, and a second copy is a set that drifts by one rule the day a pack
|
|
225
|
-
* grows. Read
|
|
225
|
+
* grows. Read off `ruleCanon` — the same declarations, in the same order, that the render
|
|
226
|
+
* numbers its headings from and the doctor validates citations against — rather than from a
|
|
226
227
|
* built tree, because an address is a property of the source and the rendered heading is
|
|
227
228
|
* themed — fourteen spellings of a thing that must compare equal to itself.
|
|
228
229
|
*
|
|
@@ -232,16 +233,7 @@ export const PACK_ORDER = ['craft', 'rigor', 'ops', 'process', 'comms'];
|
|
|
232
233
|
* were ever out of order, and disagree in the direction that reads as correct.
|
|
233
234
|
*/
|
|
234
235
|
export function knownRuleIds() {
|
|
235
|
-
|
|
236
|
-
for (const pack of PACK_ORDER) {
|
|
237
|
-
const file = path.join(SRC, 'doctrines', `${pack}.md`);
|
|
238
|
-
let text = '';
|
|
239
|
-
try { text = readPackText(file); } catch { continue; }
|
|
240
|
-
for (const m of text.matchAll(/^### \{\{DOCTRINE\}\} ([a-z]+) (\d+)\b/gm)) {
|
|
241
|
-
if (m[1] === pack) out.push(`${pack}.${Number(m[2])}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return out;
|
|
236
|
+
return ruleCanon(SRC).rules.filter((r) => r.kind === 'DOCTRINE').map((r) => `${r.pack}.${r.n}`);
|
|
245
237
|
}
|
|
246
238
|
|
|
247
239
|
/**
|
package/js/cli-table.json
CHANGED
|
@@ -734,6 +734,19 @@
|
|
|
734
734
|
"type": null,
|
|
735
735
|
"hidden": false
|
|
736
736
|
},
|
|
737
|
+
{
|
|
738
|
+
"names": ["--contracts"],
|
|
739
|
+
"dest": "contracts",
|
|
740
|
+
"metavar": null,
|
|
741
|
+
"help": "comma-separated contract globs appended to the template's own (init)",
|
|
742
|
+
"choices": null,
|
|
743
|
+
"default": null,
|
|
744
|
+
"required": false,
|
|
745
|
+
"nargs": null,
|
|
746
|
+
"is_flag": false,
|
|
747
|
+
"type": null,
|
|
748
|
+
"hidden": false
|
|
749
|
+
},
|
|
737
750
|
{
|
|
738
751
|
"names": ["--declared"],
|
|
739
752
|
"dest": "declared",
|
|
@@ -846,7 +859,20 @@
|
|
|
846
859
|
"names": ["--note"],
|
|
847
860
|
"dest": "note",
|
|
848
861
|
"metavar": null,
|
|
849
|
-
"help": "a note attached to
|
|
862
|
+
"help": "a note attached to a record or a decide verdict, written into LOOP.md so it survives the unit",
|
|
863
|
+
"choices": null,
|
|
864
|
+
"default": null,
|
|
865
|
+
"required": false,
|
|
866
|
+
"nargs": null,
|
|
867
|
+
"is_flag": false,
|
|
868
|
+
"type": null,
|
|
869
|
+
"hidden": false
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
"names": ["--note-file"],
|
|
873
|
+
"dest": "note_file",
|
|
874
|
+
"metavar": null,
|
|
875
|
+
"help": "record's note read from this file instead of the command line (CRLF folded, trimmed) — for a multi-line note or one that mentions git commit/push",
|
|
850
876
|
"choices": null,
|
|
851
877
|
"default": null,
|
|
852
878
|
"required": false,
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `context` — the SessionStart verb: inject the harness's session files and the repo's context
|
|
3
|
+
* docs. Runs once per session, so it is loaded only when the hook entry dispatches it; see
|
|
4
|
+
* `js/hosts/hooks.mjs`'s header for the contract every hook verb holds.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { readText, printOut as out, printErr as err, withDiscardableStderr } from '../lib/fs.mjs';
|
|
10
|
+
import { normcase, toPlatformPath } from '../lib/paths.mjs';
|
|
11
|
+
import { resolvePath, sovereignBypass } from './hosts.mjs';
|
|
12
|
+
import { isFile, isDir, selfAndParents, sortPaths, listDir } from './hooks-prims.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `str(OSError)` — `[Errno N] strerror: 'filename'`, with the filename REPR'd.
|
|
16
|
+
*
|
|
17
|
+
* This is a compared string, not a log line: `cmd_context` prints it verbatim when an
|
|
18
|
+
* eager entry is missing, so `ENOENT: no such file or directory, open '…'` (Node's
|
|
19
|
+
* wording, Node's quoting, no errno) is a whole-line difference in that cell. Only the
|
|
20
|
+
* codes a verb here can actually raise are mapped; anything else falls back to Node's
|
|
21
|
+
* message, which is at least honest about being different.
|
|
22
|
+
*/
|
|
23
|
+
const _ERRNO = {
|
|
24
|
+
ENOENT: [2, 'No such file or directory'],
|
|
25
|
+
EACCES: [13, 'Permission denied'],
|
|
26
|
+
EISDIR: [21, 'Is a directory'],
|
|
27
|
+
ENOTDIR: [20, 'Not a directory'],
|
|
28
|
+
EPERM: [1, 'Operation not permitted'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function asOsError(e, filename) {
|
|
32
|
+
const hit = _ERRNO[e && e.code];
|
|
33
|
+
if (!hit) return String((e && e.message) || e);
|
|
34
|
+
// Python reprs the filename, so a Windows path comes out with its backslashes doubled.
|
|
35
|
+
return `[Errno ${hit[0]}] ${hit[1]}: ${JSON.stringify(filename).replace(/^"|"$/g, "'")
|
|
36
|
+
.replace(/\\"/g, '"')}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `fnmatch.fnmatch(name, pattern)` — case-folded on Windows on BOTH sides, and `*` matches
|
|
41
|
+
* a separator (it is shell-glob semantics, not path-glob: `docs/*.md` really does select
|
|
42
|
+
* `docs/nested/deep.md`, in both implementations).
|
|
43
|
+
*/
|
|
44
|
+
function fnmatch(name, pattern) {
|
|
45
|
+
return fnTranslate(normcase(pattern)).test(normcase(name));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fnTranslate(pat) {
|
|
49
|
+
let out = '';
|
|
50
|
+
for (let i = 0; i < pat.length; i += 1) {
|
|
51
|
+
const c = pat[i];
|
|
52
|
+
if (c === '*') { out += '[\\s\\S]*'; continue; }
|
|
53
|
+
if (c === '?') { out += '[\\s\\S]'; continue; }
|
|
54
|
+
if (c === '[') {
|
|
55
|
+
let j = i + 1;
|
|
56
|
+
if (pat[j] === '!') j += 1;
|
|
57
|
+
if (pat[j] === ']') j += 1;
|
|
58
|
+
while (j < pat.length && pat[j] !== ']') j += 1;
|
|
59
|
+
// An unterminated `[` is a LITERAL bracket in Python's translate, not an error.
|
|
60
|
+
if (j >= pat.length) { out += '\\['; continue; }
|
|
61
|
+
let body = pat.slice(i + 1, j).replaceAll('\\', '\\\\');
|
|
62
|
+
if (body.startsWith('!')) body = `^${body.slice(1)}`;
|
|
63
|
+
out += `[${body}]`;
|
|
64
|
+
i = j;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
out += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
68
|
+
}
|
|
69
|
+
return new RegExp(`^(?:${out})$`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The host whose own root file counts as native — `--host` on the emitted command, `claude`
|
|
74
|
+
* when absent. Module state for the reason `./hooks.mjs`'s gates keep theirs: a hook process
|
|
75
|
+
* answers exactly one call, so it is set once at `cmdContext`'s entry.
|
|
76
|
+
*/
|
|
77
|
+
let HOST = 'claude';
|
|
78
|
+
|
|
79
|
+
// Kept in step with adapters/opencode/plugins/geneseed-context.js, same as the Python.
|
|
80
|
+
const EAGER_ROOT = ['AGENTS.md', 'AGENT.md', 'CLAUDE.md', '.cursorrules',
|
|
81
|
+
'README.md', 'CONTRIBUTING.md', 'user-rules.md', 'PROFILE.md'];
|
|
82
|
+
const LAZY_DIRS = ['docs', 'doc', 'documentation', 'architecture', 'adr', 'ADR'];
|
|
83
|
+
const EXCLUDE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'vendor', '.next',
|
|
84
|
+
'target', '.venv', '__pycache__', '.opencode', '.harness']);
|
|
85
|
+
|
|
86
|
+
// The root instruction file a host already loads by itself. Injecting it again pays the
|
|
87
|
+
// whole harness twice per session — and was doing exactly that until 2026-09: every hooked
|
|
88
|
+
// host had its own root in EAGER_ROOT. Other tools' roots stay eager (a repo carrying a
|
|
89
|
+
// hand-written AGENTS.md is worth showing Claude Code); only the host's OWN is dropped.
|
|
90
|
+
// The OpenCode context plugin carries the same rule for AGENT.md/AGENTS.md/CLAUDE.md.
|
|
91
|
+
// OpenClaude's root file is AGENTS.md, or CLAUDE.md when AGENTS.md is absent — resolved
|
|
92
|
+
// per repo in `discoverContext`, since which one it loads depends on what is on disk.
|
|
93
|
+
const NATIVE_ROOT = {
|
|
94
|
+
claude: ['CLAUDE.md'], bob: ['AGENTS.md'],
|
|
95
|
+
openclaude: ['AGENTS.md', 'CLAUDE.md'],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Eager injection budget: a root README of 40k chars was going out whole, every session and
|
|
99
|
+
// every compaction, and cost more than the harness itself. Per-file cut at the last line
|
|
100
|
+
// break under the cap; files that would push the total over the budget are listed lazy.
|
|
101
|
+
const EAGER_FILE_BYTES = 16 * 1024;
|
|
102
|
+
const EAGER_TOTAL_BYTES = 48 * 1024;
|
|
103
|
+
|
|
104
|
+
// The files the root instruction file names for session start, relative to the HARNESS dir
|
|
105
|
+
// (`--root`: `.claude/`, `.bob/`, a global config dir) — not to the repo root discovery walks,
|
|
106
|
+
// which is why none of them reached a hooked host before 2026-10. Injected here so the read does
|
|
107
|
+
// not depend on the model remembering it. `context.json` is not in the list: it is a manifest,
|
|
108
|
+
// honoured by `resolveContextSets` (the harness dir is its last candidate). The OpenCode
|
|
109
|
+
// context plugin carries the same list.
|
|
110
|
+
const SESSION_FILES = ['user-rules.md', 'PROFILE.md', 'memory/MEMORY.md', 'anamnesis/MEMORY.md',
|
|
111
|
+
'notebook/NOTEBOOK.md', 'wiki.jsonc'];
|
|
112
|
+
// SHA-256 of each seed body in `js/build/stubs.mjs` (`SESSION_SEEDS`), CRLF folded. A file still
|
|
113
|
+
// byte-identical to its seed says nothing, so it is skipped rather than injected as noise.
|
|
114
|
+
// Hashes, not an import: stubs.mjs pulls in the build's writers, and this module loads on every
|
|
115
|
+
// tool call. tests/unit/claude.test.mjs gates this set, and the plugin's copy, against the seeds.
|
|
116
|
+
const SEED_SHA256 = new Set([
|
|
117
|
+
'5c2e92fb1acde041e02d9ebf158460d8ff7a07cbc2b447859631604610130721', // user-rules.md
|
|
118
|
+
'29e012c3c4349ea62a9082cbd04bb25599a0ec280d62c33f6e10742809471816', // PROFILE.md
|
|
119
|
+
'cccc917c34e6b990e821620bdb4739090151885ab71b6e85046f30a3b71fa5e8', // wiki.jsonc
|
|
120
|
+
'99c786049c260f6baf7aec68a5c0f59907861afe9b867da009440613bdddb907', // MEMORY.md
|
|
121
|
+
'9acb5c9d9cb5dac572104480f05b48cc144e676869bd60e86d8a15b1f6308184', // NOTEBOOK.md
|
|
122
|
+
]);
|
|
123
|
+
export { SESSION_FILES, SEED_SHA256 };
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The session files present under `hookRoot` and changed from their seed, as `{ rel, abs, text }`
|
|
127
|
+
* (text CRLF-folded, trailing newlines trimmed). `$GENESEED_WIKI` overrides the wiki declaration,
|
|
128
|
+
* as it does on OpenCode. A missing or unreadable file is simply absent — this is best-effort
|
|
129
|
+
* context, never a gate.
|
|
130
|
+
*/
|
|
131
|
+
export function sessionFiles(hookRoot) {
|
|
132
|
+
const found = [];
|
|
133
|
+
for (const rel of SESSION_FILES) {
|
|
134
|
+
let abs = path.join(hookRoot, rel);
|
|
135
|
+
if (rel === 'wiki.jsonc' && process.env.GENESEED_WIKI && isFile(process.env.GENESEED_WIKI)) {
|
|
136
|
+
abs = process.env.GENESEED_WIKI;
|
|
137
|
+
}
|
|
138
|
+
if (!isFile(abs)) continue;
|
|
139
|
+
let text;
|
|
140
|
+
try { text = readFileSync(abs, 'utf8').replace(/\r\n/g, '\n'); } catch { continue; }
|
|
141
|
+
if (SEED_SHA256.has(createHash('sha256').update(text).digest('hex'))) continue;
|
|
142
|
+
found.push({ rel, abs, text: text.replace(/\n+$/, '') });
|
|
143
|
+
}
|
|
144
|
+
return found;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const CLAUDE_MARKERS = ['.claude', '.bob', '.openclaude'];
|
|
148
|
+
const GENESEED_MANIFEST = '.geneseed-manifest.json';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* `_global_hook_standing_down` — project-bypasses-global.
|
|
152
|
+
*
|
|
153
|
+
* A GLOBAL install's hook passes its own dir as `--root`; when a Geneseed PROJECT install
|
|
154
|
+
* of the SAME host sits at or above cwd, the project's hook injects and this one must not
|
|
155
|
+
* double up.
|
|
156
|
+
*/
|
|
157
|
+
export function globalHookStandingDown(hookRoot, cwd) {
|
|
158
|
+
const marker = path.basename(hookRoot);
|
|
159
|
+
if (!CLAUDE_MARKERS.includes(marker)) return false;
|
|
160
|
+
for (const d of selfAndParents(cwd)) {
|
|
161
|
+
const cand = path.join(d, marker);
|
|
162
|
+
if (isFile(path.join(cand, GENESEED_MANIFEST))) {
|
|
163
|
+
// Path equality, which is case-folded on Windows — `~/.claude` and `~/.Claude` are
|
|
164
|
+
// the same install there and two different ones on Linux.
|
|
165
|
+
return normcase(resolvePath(cand)) !== normcase(resolvePath(hookRoot));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** `_disp` — relative to the repo root when it sits under it, else verbatim. */
|
|
172
|
+
function disp(pathStr, root) {
|
|
173
|
+
// `os.path.relpath` raises ValueError across drives and the Python falls back to the
|
|
174
|
+
// path as given; `path.relative` silently returns an absolute path instead, which would
|
|
175
|
+
// print a DIFFERENT string rather than the same one.
|
|
176
|
+
if (path.parse(path.resolve(pathStr)).root.toLowerCase()
|
|
177
|
+
!== path.parse(path.resolve(root)).root.toLowerCase()) {
|
|
178
|
+
return pathStr;
|
|
179
|
+
}
|
|
180
|
+
return path.relative(root, pathStr).split(path.sep).join('/');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
function rglobMd(dir, acc = []) {
|
|
185
|
+
for (const name of listDir(dir)) {
|
|
186
|
+
const full = path.join(dir, name);
|
|
187
|
+
if (isDir(full)) rglobMd(full, acc);
|
|
188
|
+
else if (isFile(full) && path.extname(name).toLowerCase() === '.md') acc.push(full);
|
|
189
|
+
}
|
|
190
|
+
return acc;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* `_discover_context` — the no-manifest path, mirroring the OpenCode context plugin.
|
|
195
|
+
* Root entry docs are eager; other root markdown, the doc trees and monorepo package
|
|
196
|
+
* READMEs are lazy.
|
|
197
|
+
*/
|
|
198
|
+
export function discoverContext(root, host = HOST) {
|
|
199
|
+
const eager = new Map();
|
|
200
|
+
const lazy = new Map();
|
|
201
|
+
const native = host === 'openclaude' && isFile(path.join(root, 'AGENTS.md'))
|
|
202
|
+
? ['AGENTS.md'] : NATIVE_ROOT[host] || [];
|
|
203
|
+
for (const full of sortPaths(listDir(root).map((n) => path.join(root, n)))) {
|
|
204
|
+
if (!isFile(full)) continue;
|
|
205
|
+
const name = path.basename(full);
|
|
206
|
+
if (native.includes(name)) continue;
|
|
207
|
+
if (EAGER_ROOT.includes(name)) eager.set(full, null);
|
|
208
|
+
else if (path.extname(name).toLowerCase() === '.md') lazy.set(full, null);
|
|
209
|
+
}
|
|
210
|
+
for (const d of LAZY_DIRS) {
|
|
211
|
+
const sub = path.join(root, d);
|
|
212
|
+
if (!isDir(sub)) continue;
|
|
213
|
+
for (const md of sortPaths(rglobMd(sub))) {
|
|
214
|
+
const parts = path.relative(root, md).split(/[\\/]/);
|
|
215
|
+
if (parts.some((part) => EXCLUDE_DIRS.has(part))) continue;
|
|
216
|
+
if (!eager.has(md) && !lazy.has(md)) lazy.set(md, null);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const group of ['packages', 'apps']) {
|
|
220
|
+
const base = path.join(root, group);
|
|
221
|
+
if (!isDir(base)) continue;
|
|
222
|
+
for (const pkg of sortPaths(listDir(base).map((n) => path.join(base, n)))) {
|
|
223
|
+
if (!isDir(pkg) || EXCLUDE_DIRS.has(path.basename(pkg))) continue;
|
|
224
|
+
const readme = path.join(pkg, 'README.md');
|
|
225
|
+
if (isFile(readme) && !eager.has(readme) && !lazy.has(readme)) lazy.set(readme, null);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return [
|
|
229
|
+
[...eager.keys()].map((p) => ({ path: p, description: '' })),
|
|
230
|
+
[...lazy.keys()].filter((p) => !eager.has(p)).map((p) => ({ path: p, description: '' })),
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* `_resolve_context_sets` — an explicit manifest wins, `"extend": true` layers it on top
|
|
236
|
+
* of discovery, and an empty stub falls through to pure discovery.
|
|
237
|
+
*
|
|
238
|
+
* `recs` is a Map, not an object: JS reorders integer-like keys on a plain object, and
|
|
239
|
+
* these keys are absolute paths whose iteration order becomes the printed order.
|
|
240
|
+
*/
|
|
241
|
+
export function resolveContextSets(root, hookRoot = null) {
|
|
242
|
+
let manifest = null;
|
|
243
|
+
const env = process.env.GENESEED_CONTEXT;
|
|
244
|
+
if (env && isFile(env)) {
|
|
245
|
+
// `Path(env)`, and the label is printed — so the separators are folded exactly the
|
|
246
|
+
// way `str(Path(...))` folds them, or the source line differs by every slash in it.
|
|
247
|
+
manifest = toPlatformPath(env);
|
|
248
|
+
} else {
|
|
249
|
+
// The harness dir last: a repo's own manifest outranks the one seeded beside the install.
|
|
250
|
+
for (const cand of [path.join(root, '.harness', 'context.json'),
|
|
251
|
+
path.join(root, 'context.json'), ...(hookRoot ? [path.join(hookRoot, 'context.json')] : [])]) {
|
|
252
|
+
if (isFile(cand)) { manifest = cand; break; }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (manifest === null) {
|
|
257
|
+
const [e, l] = discoverContext(root);
|
|
258
|
+
return [e, l, `auto-discovery [${root}]`];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let raw;
|
|
262
|
+
try {
|
|
263
|
+
raw = readText(manifest);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
err(`[context] could not read ${manifest}: ${asOsError(e, manifest)}\n`);
|
|
266
|
+
return [[], [], String(manifest)];
|
|
267
|
+
}
|
|
268
|
+
let entries;
|
|
269
|
+
let extend;
|
|
270
|
+
try {
|
|
271
|
+
let data = JSON.parse(raw);
|
|
272
|
+
// Valid JSON of the wrong SHAPE, guarded on both sides — see the Python original. JS
|
|
273
|
+
// would not have thrown here (`[].context` is merely undefined), which is exactly why
|
|
274
|
+
// the guard is written out rather than left to fall out of the language: without it,
|
|
275
|
+
// the two CLIs disagree about a file a user really does hand-edit.
|
|
276
|
+
data = data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
277
|
+
entries = data.context || [];
|
|
278
|
+
if (!Array.isArray(entries)) entries = [];
|
|
279
|
+
extend = Boolean(data.extend);
|
|
280
|
+
} catch {
|
|
281
|
+
// See the Python original for why the decoder's own message is not quoted: the two
|
|
282
|
+
// engines disagree on the wording and on the offset, and V8 supplies no offset at all
|
|
283
|
+
// for the commonest shapes. Reporting the file is what the user needs; reporting
|
|
284
|
+
// WHICH decoder found it is what makes two implementations impossible.
|
|
285
|
+
err(`[context] ${manifest} is not valid JSON — no context was loaded `
|
|
286
|
+
+ '(fix the syntax, then re-run).\n');
|
|
287
|
+
return [[], [], String(manifest)];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!entries.length && !extend) {
|
|
291
|
+
const [e, l] = discoverContext(root);
|
|
292
|
+
return [e, l, `auto-discovery [${root}] (empty ${path.basename(manifest)})`];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const recs = new Map();
|
|
296
|
+
const put = (pathStr, load, desc) => {
|
|
297
|
+
const prev = recs.get(pathStr);
|
|
298
|
+
recs.set(pathStr, { path: pathStr, load, description: desc || (prev ? prev.description : '') });
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (extend) {
|
|
302
|
+
const [de, dl] = discoverContext(root);
|
|
303
|
+
for (const x of de) put(x.path, 'eager', '');
|
|
304
|
+
for (const x of dl) put(x.path, 'lazy', '');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
309
|
+
const raw = typeof entry.path === 'string' ? entry.path.trim() : '';
|
|
310
|
+
if (!raw) continue;
|
|
311
|
+
const load = entry.load === undefined ? 'eager' : entry.load;
|
|
312
|
+
const desc = entry.description === undefined ? '' : entry.description;
|
|
313
|
+
if (raw.includes('*')) {
|
|
314
|
+
// A glob RECLASSIFIES files already known; it never pulls in new ones.
|
|
315
|
+
for (const [pathStr, rec] of [...recs.entries()]) {
|
|
316
|
+
if (fnmatch(disp(pathStr, root), raw)) put(pathStr, load, desc || rec.description);
|
|
317
|
+
}
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const abs = path.isAbsolute(raw) ? raw : resolvePath(path.join(root, raw));
|
|
321
|
+
put(abs, load, desc);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const eager = [];
|
|
325
|
+
const lazy = [];
|
|
326
|
+
for (const r of recs.values()) {
|
|
327
|
+
(r.load === 'eager' ? eager : lazy).push({ path: r.path, description: r.description });
|
|
328
|
+
}
|
|
329
|
+
return [eager, lazy, String(manifest)];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Cut `text` at the last line break under EAGER_FILE_BYTES and say where the rest is — one rule
|
|
334
|
+
* for a session file and an eager doc alike. `where` finishes the sentence.
|
|
335
|
+
*/
|
|
336
|
+
function capEager(text, where) {
|
|
337
|
+
if (text.length <= EAGER_FILE_BYTES) return text;
|
|
338
|
+
const nl = text.lastIndexOf('\n', EAGER_FILE_BYTES);
|
|
339
|
+
return `${text.slice(0, nl > 0 ? nl : EAGER_FILE_BYTES)}\n`
|
|
340
|
+
+ `[context] truncated at ${EAGER_FILE_BYTES / 1024} KB — read ${where}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function cmdContext(args) {
|
|
344
|
+
HOST = (args && args.host) || 'claude';
|
|
345
|
+
// Discovery runs against the project root the hook was launched from — Claude runs
|
|
346
|
+
// SessionStart hooks with cwd = repo root — not the harness package dir.
|
|
347
|
+
// The only two `resolvePath` calls in this file whose argument can be a `~user` path the
|
|
348
|
+
// user typed — `$GENESEED_ROOT` and `--root` — and `resolvePath` expands, so both can now
|
|
349
|
+
// throw. Every other caller either builds its argument by `path.join` from an absolute
|
|
350
|
+
// root (so no leading tilde survives) or already sits inside a `try`. Refused input
|
|
351
|
+
// injects nothing, which is the same degrade as "nothing to load" below.
|
|
352
|
+
let root;
|
|
353
|
+
let hookRoot;
|
|
354
|
+
try {
|
|
355
|
+
({ root, hookRoot } = withDiscardableStderr(() => ({
|
|
356
|
+
root: resolvePath(process.env.GENESEED_ROOT || process.cwd()),
|
|
357
|
+
hookRoot: args.root ? resolvePath(args.root) : null,
|
|
358
|
+
})));
|
|
359
|
+
} catch {
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
if (hookRoot && sovereignBypass(hookRoot)) return 0;
|
|
363
|
+
if (hookRoot && !process.env.GENESEED_STACK_GLOBAL
|
|
364
|
+
&& globalHookStandingDown(hookRoot, root)) return 0;
|
|
365
|
+
const session = hookRoot ? sessionFiles(hookRoot) : [];
|
|
366
|
+
// A session file discovery would also pick up (a `user-rules.md` at the repo root of an
|
|
367
|
+
// install whose harness dir IS the repo root) is injected once, in the session block.
|
|
368
|
+
const seen = new Set(session.map((s) => normcase(path.resolve(s.abs))));
|
|
369
|
+
const fresh = (e) => !seen.has(normcase(path.resolve(path.isAbsolute(e.path)
|
|
370
|
+
? e.path : path.join(root, e.path))));
|
|
371
|
+
let [eager, lazy, source] = resolveContextSets(root, hookRoot);
|
|
372
|
+
eager = eager.filter(fresh);
|
|
373
|
+
lazy = lazy.filter(fresh);
|
|
374
|
+
if (!session.length && !eager.length && !lazy.length) {
|
|
375
|
+
err(`[context] nothing to load for ${root} `
|
|
376
|
+
+ '(no docs discovered, no manifest entries).\n');
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const lines = [];
|
|
381
|
+
let spent = 0;
|
|
382
|
+
if (session.length) {
|
|
383
|
+
lines.push('=== SESSION FILES \u2014 your harness files, injected at session start '
|
|
384
|
+
+ '(untouched seeds skipped) ===', '');
|
|
385
|
+
for (const s of session) {
|
|
386
|
+
const text = capEager(s.text, `${s.abs} on demand`);
|
|
387
|
+
spent += text.length;
|
|
388
|
+
lines.push(`----- ${s.rel} -----`, text, '');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (eager.length || lazy.length) {
|
|
392
|
+
lines.push(`=== PROJECT CONTEXT \u2014 binding for this repo (via ${source}) ===`, '');
|
|
393
|
+
}
|
|
394
|
+
const demoted = [];
|
|
395
|
+
for (const entry of eager) {
|
|
396
|
+
const p = entry.path === undefined ? '' : entry.path;
|
|
397
|
+
const desc = entry.description === undefined ? '' : entry.description;
|
|
398
|
+
const target = path.isAbsolute(p) ? p : path.join(root, p);
|
|
399
|
+
let text;
|
|
400
|
+
try {
|
|
401
|
+
text = readText(target).replace(/\n+$/, '');
|
|
402
|
+
} catch (e) {
|
|
403
|
+
lines.push(`----- ${disp(p, root)}${desc ? ` \u2014 ${desc}` : ''} -----`,
|
|
404
|
+
`[context] MISSING eager file: ${asOsError(e, target)}`, '');
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
text = capEager(text, `${disp(p, root)} on demand for the rest`);
|
|
408
|
+
if (spent + text.length > EAGER_TOTAL_BYTES) {
|
|
409
|
+
demoted.push(entry);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
spent += text.length;
|
|
413
|
+
lines.push(`----- ${disp(p, root)}${desc ? ` \u2014 ${desc}` : ''} -----`, text, '');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (lazy.length || demoted.length) {
|
|
417
|
+
lines.push('--- Lazy entries (load only when the task needs them) ---');
|
|
418
|
+
for (const entry of [...lazy, ...demoted]) {
|
|
419
|
+
const p = entry.path === undefined ? '' : entry.path;
|
|
420
|
+
const desc = entry.description === undefined ? '' : entry.description;
|
|
421
|
+
const over = demoted.includes(entry)
|
|
422
|
+
? ` (eager, but over the ${EAGER_TOTAL_BYTES / 1024} KB session budget \u2014 read on demand)` : '';
|
|
423
|
+
lines.push(` - ${disp(p, root)}${desc ? ` \u2014 ${desc}` : ''}${over}`);
|
|
424
|
+
}
|
|
425
|
+
lines.push('');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Claude Code takes a SessionStart hook's plain stdout as context; Bob reads it the same way.
|
|
429
|
+
out(`${lines.join('\n')}\n`);
|
|
430
|
+
return 0;
|
|
431
|
+
}
|