geneseed 2.0.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 +938 -0
- package/DESIGN.md +1423 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +48 -0
- package/README.md +336 -0
- package/SETUP.md +1027 -0
- package/SHIPPED.md +70 -0
- package/adapters/bob/README.md +100 -0
- package/adapters/claude-code/README.md +101 -0
- package/adapters/claude-code/settings.json +47 -0
- package/adapters/claude-code/skills/geneseed/SKILL.md +72 -0
- package/adapters/copilot/README.md +76 -0
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +434 -0
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +158 -0
- package/adapters/opencode/README.md +582 -0
- package/adapters/opencode/agents/orchestrator.md +25 -0
- package/adapters/opencode/opencode.json +44 -0
- package/adapters/opencode/plugins/geneseed-activity.js +430 -0
- package/adapters/opencode/plugins/geneseed-context.js +876 -0
- package/adapters/opencode/plugins/geneseed-guard.js +290 -0
- package/adapters/opencode/plugins/geneseed-learn.js +396 -0
- package/adapters/opencode/plugins/geneseed-notify.js +142 -0
- package/adapters/opencode/plugins/geneseed-ponytail.js +148 -0
- package/adapters/opencode/plugins/geneseed-workflow.js +183 -0
- package/adapters/opencode/workflows/_runtime.js +277 -0
- package/adapters/opencode/workflows/council.js +64 -0
- package/adapters/opencode/workflows/dispatch.js +89 -0
- package/adapters/opencode/workflows/research-plan-implement.js +43 -0
- package/adapters/opencode/workflows/review.js +57 -0
- package/bin/geneseed-cli.mjs +412 -0
- package/bin/geneseed-hook.mjs +129 -0
- package/bin/geneseed.mjs +1086 -0
- package/docs/declined.md +149 -0
- package/docs/limits.md +270 -0
- package/docs/opencode-plugin-setup.md +64 -0
- package/docs/token-footprint.md +68 -0
- package/docs/web/_groups.json +45 -0
- package/docs/web/about.md +6 -0
- package/docs/web/adapters-claude-code.md +8 -0
- package/docs/web/adapters-opencode-loads.md +8 -0
- package/docs/web/adapters-opencode-spec.md +8 -0
- package/docs/web/adapters-opencode.md +8 -0
- package/docs/web/agents.md +8 -0
- package/docs/web/authoring.md +11 -0
- package/docs/web/autostart.md +9 -0
- package/docs/web/claude-hooks.md +36 -0
- package/docs/web/cli.md +6 -0
- package/docs/web/collaboration.md +27 -0
- package/docs/web/configure-wiki.md +9 -0
- package/docs/web/design.md +7 -0
- package/docs/web/env-knobs.md +9 -0
- package/docs/web/exclusions.md +26 -0
- package/docs/web/first-session.md +28 -0
- package/docs/web/footprint.md +31 -0
- package/docs/web/foreman-mode.md +33 -0
- package/docs/web/glossary.md +6 -0
- package/docs/web/harnesses.md +28 -0
- package/docs/web/headless.md +9 -0
- package/docs/web/ingest-docs.md +9 -0
- package/docs/web/install-paths.md +9 -0
- package/docs/web/install-quick.md +35 -0
- package/docs/web/lsp-overview.md +37 -0
- package/docs/web/mcp-claude-code.md +10 -0
- package/docs/web/mcp-filesystem.md +9 -0
- package/docs/web/mcp-gitlab.md +9 -0
- package/docs/web/mcp-markitdown.md +9 -0
- package/docs/web/mcp-overview.md +34 -0
- package/docs/web/mcp-trouble.md +9 -0
- package/docs/web/mcp-verify.md +9 -0
- package/docs/web/memory.md +7 -0
- package/docs/web/migrate.md +39 -0
- package/docs/web/model.md +20 -0
- package/docs/web/notebook.md +7 -0
- package/docs/web/pipelines.md +24 -0
- package/docs/web/plugin-activity.md +23 -0
- package/docs/web/plugin-context.md +26 -0
- package/docs/web/plugin-guard.md +24 -0
- package/docs/web/plugin-learn.md +27 -0
- package/docs/web/plugin-notify.md +24 -0
- package/docs/web/plugin-ponytail.md +23 -0
- package/docs/web/plugin-setup.md +7 -0
- package/docs/web/plugin-workflow.md +24 -0
- package/docs/web/plugins.md +17 -0
- package/docs/web/project-context.md +9 -0
- package/docs/web/rules.md +8 -0
- package/docs/web/run-anywhere.md +9 -0
- package/docs/web/self-improve.md +8 -0
- package/docs/web/setup-choices.md +39 -0
- package/docs/web/skills.md +8 -0
- package/docs/web/themes-author.md +8 -0
- package/docs/web/themes.md +9 -0
- package/docs/web/token-footprint.md +7 -0
- package/docs/web/trouble.md +38 -0
- package/docs/web/upgrade.md +9 -0
- package/docs/web/verify.md +30 -0
- package/docs/web/wiki.md +7 -0
- package/docs/web/worktree.md +10 -0
- package/docs/web-ui.md +144 -0
- package/docs/wiki.md +43 -0
- package/geneseed +39 -0
- package/geneseed.cmd +34 -0
- package/harness.config.json +6 -0
- package/js/anim.mjs +326 -0
- package/js/catalog.mjs +112 -0
- package/js/checkout.mjs +88 -0
- package/js/cli-table.json +1034 -0
- package/js/cli.mjs +498 -0
- package/js/diff.mjs +384 -0
- package/js/doctor.mjs +1364 -0
- package/js/emit.mjs +1373 -0
- package/js/excludes.mjs +271 -0
- package/js/generate.mjs +379 -0
- package/js/hooks.mjs +1026 -0
- package/js/hosts.mjs +253 -0
- package/js/installs.mjs +378 -0
- package/js/inventory.mjs +175 -0
- package/js/lib/fs.mjs +913 -0
- package/js/lib/proc.mjs +28 -0
- package/js/lib/udiff.mjs +275 -0
- package/js/link.mjs +286 -0
- package/js/mcp.mjs +335 -0
- package/js/memory.mjs +108 -0
- package/js/menu.mjs +118 -0
- package/js/migrate.mjs +251 -0
- package/js/native.mjs +426 -0
- package/js/opencode.mjs +325 -0
- package/js/registry.mjs +110 -0
- package/js/render.mjs +282 -0
- package/js/settings.mjs +924 -0
- package/js/setup.mjs +492 -0
- package/js/status.mjs +281 -0
- package/js/themes.mjs +195 -0
- package/js/tui.mjs +444 -0
- package/js/uninstall.mjs +1137 -0
- package/js/update.mjs +910 -0
- package/js/web/actions.mjs +868 -0
- package/js/web/activity.mjs +284 -0
- package/js/web/api.mjs +873 -0
- package/js/web/docs.mjs +531 -0
- package/js/web/graph.mjs +0 -0
- package/js/web/jobs.mjs +480 -0
- package/js/web/server.mjs +1135 -0
- package/package.json +54 -0
- package/registry.json +474 -0
- package/src/AGENT.md.tmpl +390 -0
- package/src/agents/_template.md +76 -0
- package/src/agents/advocate.md +36 -0
- package/src/agents/architect.md +55 -0
- package/src/agents/developer.md +66 -0
- package/src/agents/docs.md +54 -0
- package/src/agents/empiricist.md +39 -0
- package/src/agents/explorer.md +54 -0
- package/src/agents/framer.md +36 -0
- package/src/agents/historian.md +51 -0
- package/src/agents/operator.md +37 -0
- package/src/agents/pragmatist.md +37 -0
- package/src/agents/reviewer.md +52 -0
- package/src/agents/security.md +57 -0
- package/src/agents/skeptic.md +48 -0
- package/src/agents/steward.md +36 -0
- package/src/agents/tester.md +54 -0
- package/src/agents/user-advocate.md +36 -0
- package/src/agents/visionary.md +36 -0
- package/src/laws/universal.md +387 -0
- package/src/memory/README.md +109 -0
- package/src/memory/gitignore +6 -0
- package/src/modes/README.md +26 -0
- package/src/modes/direct.md +2 -0
- package/src/modes/foreman.md +19 -0
- package/src/notebook/README.md +39 -0
- package/src/notebook/gitignore +7 -0
- package/src/postures/README.md +31 -0
- package/src/postures/artisan.md +14 -0
- package/src/postures/assistant.md +12 -0
- package/src/postures/expert.md +11 -0
- package/src/postures/mentor.md +11 -0
- package/src/postures/peer.md +12 -0
- package/src/skills/_template.md +47 -0
- package/src/skills/brainstorm.md +28 -0
- package/src/skills/clarify.md +28 -0
- package/src/skills/codebase-design.md +44 -0
- package/src/skills/commit.md +48 -0
- package/src/skills/council.md +34 -0
- package/src/skills/crash-course.md +28 -0
- package/src/skills/daydream/.claude-plugin/plugin.json +9 -0
- package/src/skills/daydream/SKILL.md +74 -0
- package/src/skills/daydream/VENDOR.md +25 -0
- package/src/skills/daydream/critic-prompt.md +38 -0
- package/src/skills/daydream/instructions.md +422 -0
- package/src/skills/daydream/synthesizer-prompt.md +41 -0
- package/src/skills/debug.md +37 -0
- package/src/skills/decode.md +28 -0
- package/src/skills/develop.md +29 -0
- package/src/skills/document-project.md +79 -0
- package/src/skills/domain-modeling.md +38 -0
- package/src/skills/drill.md +28 -0
- package/src/skills/feynman.md +26 -0
- package/src/skills/forge-mcp.md +48 -0
- package/src/skills/fresh-eyes.md +29 -0
- package/src/skills/frontend-design.md +58 -0
- package/src/skills/gap-detector.md +27 -0
- package/src/skills/geneseed-code-review.md +33 -0
- package/src/skills/geneseed.md +29 -0
- package/src/skills/git-archaeology.md +47 -0
- package/src/skills/git-rescue.md +68 -0
- package/src/skills/handoff.md +29 -0
- package/src/skills/herdr.md +164 -0
- package/src/skills/ingest.md +49 -0
- package/src/skills/learning-path.md +26 -0
- package/src/skills/migrate.md +45 -0
- package/src/skills/opencode-theme.md +52 -0
- package/src/skills/parallel-agents.md +28 -0
- package/src/skills/pipeline.md +52 -0
- package/src/skills/plan.md +45 -0
- package/src/skills/ponytail.md +29 -0
- package/src/skills/profile.md +58 -0
- package/src/skills/prose.md +42 -0
- package/src/skills/prototype.md +44 -0
- package/src/skills/react-view-transitions/AGENTS.md +955 -0
- package/src/skills/react-view-transitions/README.md +42 -0
- package/src/skills/react-view-transitions/SKILL.md +320 -0
- package/src/skills/react-view-transitions/VENDOR.md +16 -0
- package/src/skills/react-view-transitions/metadata.json +12 -0
- package/src/skills/react-view-transitions/references/css-recipes.md +242 -0
- package/src/skills/react-view-transitions/references/implementation.md +182 -0
- package/src/skills/react-view-transitions/references/nextjs.md +176 -0
- package/src/skills/react-view-transitions/references/patterns.md +262 -0
- package/src/skills/refactor.md +32 -0
- package/src/skills/release.md +43 -0
- package/src/skills/repo-map.md +36 -0
- package/src/skills/research.md +42 -0
- package/src/skills/review-response.md +36 -0
- package/src/skills/roast-me.md +28 -0
- package/src/skills/rule.md +100 -0
- package/src/skills/ship.md +49 -0
- package/src/skills/tdd.md +31 -0
- package/src/skills/tickets.md +44 -0
- package/src/skills/token-report/SKILL.md +76 -0
- package/src/skills/token-report/VENDOR.md +23 -0
- package/src/skills/token-report/scripts/token_report.mjs +1301 -0
- package/src/skills/wayfinder.md +49 -0
- package/src/skills/wiki.md +68 -0
- package/src/skills/workflow.md +38 -0
- package/themes/README.md +69 -0
- package/themes/_TEMPLATE.json +147 -0
- package/themes/biker.json +147 -0
- package/themes/commentator.json +147 -0
- package/themes/cyberpunk.json +147 -0
- package/themes/gamer.json +147 -0
- package/themes/imperial.json +147 -0
- package/themes/joker.json +147 -0
- package/themes/marvin.json +147 -0
- package/themes/mean.json +147 -0
- package/themes/military.json +147 -0
- package/themes/neutral.json +147 -0
- package/themes/opencode/README.md +58 -0
- package/themes/opencode/catppuccin.json +27 -0
- package/themes/opencode/dracula.json +27 -0
- package/themes/opencode/everforest.json +27 -0
- package/themes/opencode/gruvbox.json +27 -0
- package/themes/opencode/kanagawa.json +27 -0
- package/themes/opencode/neon.json +27 -0
- package/themes/opencode/nord.json +27 -0
- package/themes/opencode/rosepine.json +27 -0
- package/themes/opencode/tokyonight.json +27 -0
- package/themes/pirate.json +147 -0
- package/themes/sports.json +147 -0
- package/themes/verstappen.json +147 -0
- package/themes/wizard.json +147 -0
- package/web/dist/assets/About-DX1-VTBP.js +1 -0
- package/web/dist/assets/Activity-Dp9NaXOn.js +1 -0
- package/web/dist/assets/ActivityDetail-HpOBtrXS.js +1 -0
- package/web/dist/assets/Diff-Ct4G3Se8.js +5 -0
- package/web/dist/assets/Doctor-DGjkZFuA.js +1 -0
- package/web/dist/assets/Graph-b8o-S6kv.js +1 -0
- package/web/dist/assets/Harnesses-gr3-aHBF.js +1 -0
- package/web/dist/assets/Laws-DTwF5tTC.js +1 -0
- package/web/dist/assets/Library-BEZscyaQ.js +1 -0
- package/web/dist/assets/Markdown-k_LSLBGq.js +1 -0
- package/web/dist/assets/Profile-DydukTxP.js +1 -0
- package/web/dist/assets/Rules-CIRG4Ldt.js +1 -0
- package/web/dist/assets/Skills-Buoi1IBr.js +1 -0
- package/web/dist/assets/StatusBadge-CE_AJttl.js +1 -0
- package/web/dist/assets/Themes-CI4o3gJC.js +2 -0
- package/web/dist/assets/activity-BCW7e6Cy.js +1 -0
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-italic-Bserb8X5.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-normal-BG6hkEXj.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-italic-BGfwwhek.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-normal-DrDcrrxK.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-600-normal-CIXX6EOa.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-700-normal-CeQ8H3UY.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-B1PIx7RM.js +1 -0
- package/web/dist/assets/index-BnTQUSUz.js +12 -0
- package/web/dist/assets/index-Ck2iCdaC.js +2 -0
- package/web/dist/assets/index-DUCsDv7z.css +1 -0
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/web/dist/assets/marked.esm-JKvyS0Pp.js +46 -0
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/react-DOpiTn5v.js +32 -0
- package/web/dist/assets/roman-DkYN0LX9.js +1 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-italic-YylcN9Ay.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-normal-Rg4St2Dn.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-italic-vNvENeTh.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-normal-mWgeinG7.woff2 +0 -0
- package/web/dist/icon-maskable.svg +10 -0
- package/web/dist/icon.svg +8 -0
- package/web/dist/index.html +21 -0
- package/web/dist/manifest.webmanifest +14 -0
- package/web/dist/sw.js +40 -0
- package/web/src/pages/Laws.jsx +230 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
// Geneseed — OpenCode project-context plugin (v2, convention-glob).
|
|
2
|
+
//
|
|
3
|
+
// Enforces Law XVIII ("Load the Project Context") by INJECTION, not instruction:
|
|
4
|
+
// on `session.created` it puts the repo's documentation in context before your
|
|
5
|
+
// first turn, rather than trusting the agent to read it. v2 removes the need for a
|
|
6
|
+
// committed `context.json` — it AUTO-DISCOVERS the current repo's docs by
|
|
7
|
+
// convention (so the harness can live entirely in the global config dir, with zero
|
|
8
|
+
// per-repo files), while still honouring an explicit manifest when one exists.
|
|
9
|
+
//
|
|
10
|
+
// Source resolution (first match wins):
|
|
11
|
+
// 1. $GENESEED_CONTEXT explicit manifest path -> declarative mode
|
|
12
|
+
// 2. <repo>/.harness/context.json per-repo override -> declarative mode
|
|
13
|
+
// 3. <repo>/context.json legacy per-repo manifest -> declarative mode
|
|
14
|
+
// 4. (none) -> AUTO-DISCOVERY (convention glob)
|
|
15
|
+
// A manifest may set "extend": true to run auto-discovery first, then add/override.
|
|
16
|
+
//
|
|
17
|
+
// Eager docs are injected in full (budget-capped, oversized ones demoted to lazy);
|
|
18
|
+
// lazy docs are only listed (path + first heading) to be read on demand. Output
|
|
19
|
+
// mirrors the harness's own `context` verb. Every error is swallowed — it never blocks
|
|
20
|
+
// a session. See adapters/opencode/GLOBAL-HARNESS-SPEC.md.
|
|
21
|
+
//
|
|
22
|
+
// The block also carries two best-effort self-orientation lines:
|
|
23
|
+
// - COMMANDS: the repo's runnable targets (Makefile / package.json scripts /
|
|
24
|
+
// justfile / Taskfile), so the agent invokes real entry points, not guesses.
|
|
25
|
+
// - MODEL: the model the session is using (read from the transcript, else
|
|
26
|
+
// $GENESEED_MODEL), so the agent reasons within its real limits. The model line
|
|
27
|
+
// is added per request, outside the cached block; unknown -> omitted.
|
|
28
|
+
//
|
|
29
|
+
// MACHINE WIKI (AGENT.md §7): the same block also carries the user's own knowledge
|
|
30
|
+
// base(s) — typically an Obsidian vault — declared once per machine in `wiki.jsonc`
|
|
31
|
+
// ($GENESEED_WIKI -> $GENESEED_HARNESS/wiki.jsonc -> beside this plugin's install).
|
|
32
|
+
// Each wiki's eager entries inject in full and lazy entries list, drawing on the
|
|
33
|
+
// SAME budgets as the project context; its conventions/inbox/protected metadata is
|
|
34
|
+
// surfaced so the agent knows the house rules before writing. No wiki.jsonc, or an
|
|
35
|
+
// empty `wikis` list, costs nothing.
|
|
36
|
+
//
|
|
37
|
+
// DELIVERY (invisible by default): the context rides into each request's message
|
|
38
|
+
// array via `experimental.chat.messages.transform` — nothing shows in the
|
|
39
|
+
// conversation, and it survives compaction inherently (re-sent per request). The
|
|
40
|
+
// hook is experimental; on a build that lacks it the plugin notices (a request
|
|
41
|
+
// completes without the hook ever firing) and FALLS BACK to the classic visible
|
|
42
|
+
// delivery — a `session.created` noReply message — so no build is left without
|
|
43
|
+
// context. GENESEED_CONTEXT_VISIBLE=1 forces the visible block up front (legacy
|
|
44
|
+
// GENESEED_CONTEXT_TRANSFORM=0/off does the same; =1 matches the default).
|
|
45
|
+
//
|
|
46
|
+
// Quiet: by default it logs NOTHING (OpenCode shows a plugin's stderr as red text).
|
|
47
|
+
// GENESEED_DEBUG=1 re-enables discovery/inject logs. GENESEED_CONTEXT_INJECT=off
|
|
48
|
+
// disables injection entirely — no PROJECT CONTEXT in any form — leaving project
|
|
49
|
+
// context to the AGENT.md Law (soft, agent-discipline) instead of injection.
|
|
50
|
+
//
|
|
51
|
+
// Install: copy into ~/.config/opencode/plugins/ (global) — `build --emit
|
|
52
|
+
// opencode-global` and `--emit opencode` both place it for you. Keep ONE copy:
|
|
53
|
+
// OpenCode dedups plugins by npm name+version only, so two local copies both load.
|
|
54
|
+
|
|
55
|
+
import { promises as fs } from "node:fs"
|
|
56
|
+
import * as path from "node:path"
|
|
57
|
+
import { fileURLToPath } from "node:url"
|
|
58
|
+
import { homedir } from "node:os"
|
|
59
|
+
|
|
60
|
+
const MARKER = "<!-- geneseed-context:v2 -->"
|
|
61
|
+
const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
62
|
+
|
|
63
|
+
// Sovereign-repo bypass — twin of the harness's own `sovereignBypass()`.
|
|
64
|
+
// <cfg>/excludes.json (user-owned, managed by `harness exclude`) lists folders where
|
|
65
|
+
// this GLOBAL install goes dormant. Any error degrades to "not excluded".
|
|
66
|
+
const norm = (p) => {
|
|
67
|
+
// Expand a leading "~" / "~/…" to the home dir — a hand-edited excludes.json entry
|
|
68
|
+
// like "~/vault" must resolve the same way here as it does for the Claude/Bob
|
|
69
|
+
// hook guard.
|
|
70
|
+
let raw = String(p)
|
|
71
|
+
if (raw === "~" || raw.startsWith("~/") || raw.startsWith("~\\")) {
|
|
72
|
+
raw = raw === "~" ? homedir() : path.join(homedir(), raw.slice(2))
|
|
73
|
+
}
|
|
74
|
+
let s = path.resolve(raw)
|
|
75
|
+
return process.platform === "win32" ? s.toLowerCase() : s
|
|
76
|
+
}
|
|
77
|
+
async function sovereignBypass(cwd) {
|
|
78
|
+
try {
|
|
79
|
+
const cfg = path.dirname(PLUGIN_DIR) // plugins/ sits directly in <cfg>
|
|
80
|
+
const raw = await fs.readFile(path.join(cfg, "excludes.json"), "utf8")
|
|
81
|
+
const entries = (JSON.parse(raw).excludes) || []
|
|
82
|
+
const here = norm(cwd || process.cwd())
|
|
83
|
+
for (const e of entries) {
|
|
84
|
+
const p = typeof e === "string" ? e : e && e.path
|
|
85
|
+
if (typeof p !== "string" || !p.trim()) continue
|
|
86
|
+
const base = norm(p.trim()).replace(/[\\/]+$/, "")
|
|
87
|
+
if (here === base || here.startsWith(base + path.sep)) return true
|
|
88
|
+
}
|
|
89
|
+
} catch { /* degrade to active */ }
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---- tunable budgets (env-overridable) -------------------------------------
|
|
94
|
+
// Garbage in the env var must fall back to the default, not poison the budget as
|
|
95
|
+
// NaN — every `>` comparison against NaN is false, which silently DISABLES the
|
|
96
|
+
// demotion checks and injects oversized docs unbounded on every request.
|
|
97
|
+
const envNum = (name, dflt) => {
|
|
98
|
+
const n = Number(process.env[name])
|
|
99
|
+
return Number.isFinite(n) && n > 0 ? n : dflt
|
|
100
|
+
}
|
|
101
|
+
const EAGER_FILE_KB = envNum("GENESEED_EAGER_FILE_KB", 16)
|
|
102
|
+
const EAGER_TOTAL_KB = envNum("GENESEED_EAGER_TOTAL_KB", 48)
|
|
103
|
+
const MAX_FILES_SCANNED = 2000
|
|
104
|
+
const MAX_DEPTH = 6
|
|
105
|
+
// Bounds on lazy-listing cost: read at most this many headings per session, and only
|
|
106
|
+
// the head of each file (enough for an H1) rather than the whole thing — a large
|
|
107
|
+
// docs/ tree must not cost one full-file read per entry on every session start.
|
|
108
|
+
const LAZY_HEADING_LIMIT = envNum("GENESEED_LAZY_HEADINGS", 64)
|
|
109
|
+
const HEADING_SLICE_BYTES = 4096
|
|
110
|
+
|
|
111
|
+
// Quiet by default — OpenCode surfaces a plugin's stderr in the UI (red text). Set
|
|
112
|
+
// GENESEED_DEBUG=1 to see discovery/inject logs. Set GENESEED_CONTEXT_INJECT=off to
|
|
113
|
+
// disable injection entirely and lean on the AGENT.md project-context Law instead
|
|
114
|
+
// (no PROJECT CONTEXT delivered at all; enforcement becomes soft/agent-discipline).
|
|
115
|
+
const DEBUG = !!process.env.GENESEED_DEBUG
|
|
116
|
+
const INJECT_OFF = ["off", "0", "false", "no"].includes(
|
|
117
|
+
(process.env.GENESEED_CONTEXT_INJECT || "on").toLowerCase())
|
|
118
|
+
// Delivery — INVISIBLE by default: the context is prepended to each request's message
|
|
119
|
+
// array via `experimental.chat.messages.transform`, so no PROJECT CONTEXT block shows
|
|
120
|
+
// in the conversation and compaction survival is inherent (re-sent per request).
|
|
121
|
+
// GENESEED_CONTEXT_VISIBLE=1 forces the classic visible session.created message
|
|
122
|
+
// instead; legacy GENESEED_CONTEXT_TRANSFORM=0/off is honoured the same way (=1, the
|
|
123
|
+
// old opt-in, now matches the default). The transform hook is experimental — when a
|
|
124
|
+
// build lacks it the event handler detects that and engages the visible fallback, so
|
|
125
|
+
// no build is left without context.
|
|
126
|
+
const VISIBLE =
|
|
127
|
+
["1", "on", "true", "yes"].includes(
|
|
128
|
+
(process.env.GENESEED_CONTEXT_VISIBLE || "").toLowerCase()) ||
|
|
129
|
+
["0", "off", "false", "no"].includes(
|
|
130
|
+
(process.env.GENESEED_CONTEXT_TRANSFORM || "").toLowerCase())
|
|
131
|
+
const TRANSFORM = !VISIBLE
|
|
132
|
+
// Fallback state (module-level — OpenCode keeps ONE copy of the plugin loaded):
|
|
133
|
+
// `transformSeen` flips the first time the experimental hook fires; if a request
|
|
134
|
+
// completes (session.idle) while it is still false, the build lacks the hook and
|
|
135
|
+
// `fallbackVisible` switches delivery to the classic visible injection.
|
|
136
|
+
let transformSeen = false
|
|
137
|
+
let fallbackVisible = false
|
|
138
|
+
function log(msg) { if (DEBUG) console.error(`[geneseed-context] ${msg}`) }
|
|
139
|
+
|
|
140
|
+
// ---- convention --------------------------------------------------------------
|
|
141
|
+
// Root-level files injected in full. Agent-directed rules + canonical entry docs.
|
|
142
|
+
const EAGER_ROOT = new Set([
|
|
143
|
+
"AGENTS.md", "AGENT.md", "CLAUDE.md", ".cursorrules",
|
|
144
|
+
"README.md", "CONTRIBUTING.md", "user-rules.md", "PROFILE.md",
|
|
145
|
+
])
|
|
146
|
+
// Doc trees walked recursively; everything found is lazy (listed, not injected).
|
|
147
|
+
const LAZY_DIRS = ["docs", "doc", "documentation", "architecture", "adr", "ADR"]
|
|
148
|
+
// Never descend into these.
|
|
149
|
+
const EXCLUDE_DIRS = new Set([
|
|
150
|
+
"node_modules", ".git", "dist", "build", "vendor", ".next", "target",
|
|
151
|
+
".venv", "__pycache__", ".opencode", ".harness",
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
async function isFile(p) { try { return (await fs.stat(p)).isFile() } catch { return false } }
|
|
155
|
+
async function isDir(p) { try { return (await fs.stat(p)).isDirectory() } catch { return false } }
|
|
156
|
+
|
|
157
|
+
async function readJson(p) {
|
|
158
|
+
try { return JSON.parse(await fs.readFile(p, "utf8")) } catch { return null }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Strip JSONC niceties — // and /* */ comments plus trailing commas — string-aware,
|
|
162
|
+
// so a "https://…" or "C:/Users/…" inside quotes is untouched. wiki.jsonc is seeded
|
|
163
|
+
// with a commented example, so its readers must tolerate comments.
|
|
164
|
+
function stripJsonc(text) {
|
|
165
|
+
let out = "", inStr = false, esc = false
|
|
166
|
+
for (let i = 0; i < text.length; i++) {
|
|
167
|
+
const c = text[i]
|
|
168
|
+
if (inStr) {
|
|
169
|
+
out += c
|
|
170
|
+
if (esc) esc = false
|
|
171
|
+
else if (c === "\\") esc = true
|
|
172
|
+
else if (c === '"') inStr = false
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
if (c === '"') { inStr = true; out += c; continue }
|
|
176
|
+
if (c === "/" && text[i + 1] === "/") { while (i < text.length && text[i] !== "\n") i++; out += "\n"; continue }
|
|
177
|
+
if (c === "/" && text[i + 1] === "*") { i += 2; while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++; i++; continue }
|
|
178
|
+
out += c
|
|
179
|
+
}
|
|
180
|
+
let res = ""
|
|
181
|
+
inStr = false; esc = false
|
|
182
|
+
for (let i = 0; i < out.length; i++) {
|
|
183
|
+
const c = out[i]
|
|
184
|
+
if (inStr) {
|
|
185
|
+
res += c
|
|
186
|
+
if (esc) esc = false
|
|
187
|
+
else if (c === "\\") esc = true
|
|
188
|
+
else if (c === '"') inStr = false
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
if (c === '"') { inStr = true; res += c; continue }
|
|
192
|
+
if (c === ",") {
|
|
193
|
+
let j = i + 1
|
|
194
|
+
while (j < out.length && /\s/.test(out[j])) j++
|
|
195
|
+
if (out[j] === "]" || out[j] === "}") continue
|
|
196
|
+
}
|
|
197
|
+
res += c
|
|
198
|
+
}
|
|
199
|
+
return res
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function readJsonc(p) {
|
|
203
|
+
try { return JSON.parse(stripJsonc(await fs.readFile(p, "utf8"))) } catch { return null }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Minimal glob -> RegExp: ** spans path separators, * does not.
|
|
207
|
+
function globToRegExp(glob) {
|
|
208
|
+
let re = ""
|
|
209
|
+
for (let i = 0; i < glob.length; i++) {
|
|
210
|
+
const c = glob[i]
|
|
211
|
+
if (c === "*") {
|
|
212
|
+
if (glob[i + 1] === "*") { re += ".*"; i++ } else { re += "[^/]*" }
|
|
213
|
+
} else if ("\\^$+?.()|[]{}".includes(c)) {
|
|
214
|
+
re += "\\" + c
|
|
215
|
+
} else {
|
|
216
|
+
re += c
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return new RegExp("^" + re + "$")
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// First H1 / heading line of a markdown file, for the lazy listing.
|
|
223
|
+
function firstHeading(text) {
|
|
224
|
+
for (const line of text.split("\n")) {
|
|
225
|
+
const s = line.trim()
|
|
226
|
+
if (s.startsWith("#")) return s.replace(/^#+\s*/, "").trim()
|
|
227
|
+
}
|
|
228
|
+
return ""
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Read only the head of a file (enough to find an H1) instead of the whole thing —
|
|
232
|
+
// bounds per-file cost when listing a large docs/ tree.
|
|
233
|
+
async function readHeadSlice(p, max = HEADING_SLICE_BYTES) {
|
|
234
|
+
let fh
|
|
235
|
+
try {
|
|
236
|
+
fh = await fs.open(p, "r")
|
|
237
|
+
const buf = Buffer.alloc(max)
|
|
238
|
+
const { bytesRead } = await fh.read(buf, 0, max, 0)
|
|
239
|
+
return buf.subarray(0, bytesRead).toString("utf8")
|
|
240
|
+
} catch {
|
|
241
|
+
return ""
|
|
242
|
+
} finally {
|
|
243
|
+
if (fh) await fh.close().catch(() => {})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function kb(bytes) { return (bytes / 1024).toFixed(0) }
|
|
248
|
+
|
|
249
|
+
function repoRoot(ctx) {
|
|
250
|
+
return ctx?.worktree || ctx?.directory || process.cwd()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---- model self-awareness (best-effort) --------------------------------------
|
|
254
|
+
// Surface the model the session is actually using so the agent reasons within its
|
|
255
|
+
// real limits (awesome-opencode "Model Announcer"). Sources, first hit wins: an
|
|
256
|
+
// assistant message's info.providerID/modelID in the outgoing transcript — the same
|
|
257
|
+
// fields the learn plugin reads — then $GENESEED_MODEL ("provider/model"). Unknown ->
|
|
258
|
+
// no line (graceful: the first turn often has no assistant message yet). It is added
|
|
259
|
+
// per request, OUTSIDE the cached block, so one cache serves every model.
|
|
260
|
+
function modelFromMessages(messages) {
|
|
261
|
+
if (!Array.isArray(messages)) return null
|
|
262
|
+
let found = null
|
|
263
|
+
for (const m of messages) {
|
|
264
|
+
const info = m?.info ?? {}
|
|
265
|
+
if (info.role !== "assistant") continue
|
|
266
|
+
const providerID = info.providerID ?? info.provider
|
|
267
|
+
const modelID = info.modelID ?? info.model
|
|
268
|
+
if (providerID && modelID) found = `${providerID}/${modelID}` // last wins (latest turn)
|
|
269
|
+
}
|
|
270
|
+
return found
|
|
271
|
+
}
|
|
272
|
+
function envModelStr() {
|
|
273
|
+
const v = process.env.GENESEED_MODEL
|
|
274
|
+
return v && v.includes("/") ? v : null
|
|
275
|
+
}
|
|
276
|
+
// Insert the model line just after the MARKER (first line) of an already-rendered
|
|
277
|
+
// block, leaving the model-agnostic cache untouched.
|
|
278
|
+
function withModel(text, model) {
|
|
279
|
+
if (!model || !text) return text
|
|
280
|
+
const nl = text.indexOf("\n")
|
|
281
|
+
if (nl === -1) return text
|
|
282
|
+
const line = `current model: ${model} — reason within this model's real limits`
|
|
283
|
+
return `${text.slice(0, nl + 1)}${line}\n${text.slice(nl + 1)}`
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---- command discovery -------------------------------------------------------
|
|
287
|
+
// List the project's runnable command targets (awesome-opencode "Command Inject")
|
|
288
|
+
// so the agent invokes the repo's real entry points instead of guessing — Makefile
|
|
289
|
+
// targets, package.json scripts (with the right runner per lockfile), justfile
|
|
290
|
+
// recipes, Taskfile tasks. Root-level only, best-effort parse, capped. Each group is
|
|
291
|
+
// one rendered line: "make — build, test, lint".
|
|
292
|
+
const COMMANDS_CAP = 40
|
|
293
|
+
async function discoverCommands(root) {
|
|
294
|
+
const groups = []
|
|
295
|
+
const readSafe = async (p) => { try { return await fs.readFile(p, "utf8") } catch { return null } }
|
|
296
|
+
|
|
297
|
+
for (const name of ["Makefile", "makefile", "GNUmakefile"]) {
|
|
298
|
+
const text = await readSafe(path.join(root, name))
|
|
299
|
+
if (text == null) continue
|
|
300
|
+
const targets = []
|
|
301
|
+
for (const line of text.split("\n")) {
|
|
302
|
+
const m = /^([a-zA-Z0-9][\w.-]*)\s*:(?!=)/.exec(line) // target: (not := assignment)
|
|
303
|
+
if (m && !m[1].startsWith(".") && !line.includes("%") && !targets.includes(m[1])) targets.push(m[1])
|
|
304
|
+
if (targets.length >= COMMANDS_CAP) break
|
|
305
|
+
}
|
|
306
|
+
if (targets.length) groups.push(`make — ${targets.join(", ")}`)
|
|
307
|
+
break
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const pkgText = await readSafe(path.join(root, "package.json"))
|
|
311
|
+
if (pkgText) {
|
|
312
|
+
try {
|
|
313
|
+
const pkg = JSON.parse(pkgText)
|
|
314
|
+
const scripts = pkg && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : []
|
|
315
|
+
if (scripts.length) {
|
|
316
|
+
let mgr = "npm run"
|
|
317
|
+
if (await isFile(path.join(root, "bun.lockb"))) mgr = "bun run"
|
|
318
|
+
else if (await isFile(path.join(root, "pnpm-lock.yaml"))) mgr = "pnpm"
|
|
319
|
+
else if (await isFile(path.join(root, "yarn.lock"))) mgr = "yarn"
|
|
320
|
+
groups.push(`${mgr} — ${scripts.slice(0, COMMANDS_CAP).join(", ")}`)
|
|
321
|
+
}
|
|
322
|
+
} catch {}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const name of ["justfile", "Justfile", ".justfile"]) {
|
|
326
|
+
const text = await readSafe(path.join(root, name))
|
|
327
|
+
if (text == null) continue
|
|
328
|
+
const recipes = []
|
|
329
|
+
for (const line of text.split("\n")) {
|
|
330
|
+
const m = /^([a-zA-Z0-9][\w-]*)(?:[ \t][^\n:]*)?:(?!=)/.exec(line) // recipe name [args]:
|
|
331
|
+
if (m && !recipes.includes(m[1])) recipes.push(m[1])
|
|
332
|
+
if (recipes.length >= COMMANDS_CAP) break
|
|
333
|
+
}
|
|
334
|
+
if (recipes.length) groups.push(`just — ${recipes.join(", ")}`)
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const name of ["Taskfile.yml", "Taskfile.yaml"]) {
|
|
339
|
+
const text = await readSafe(path.join(root, name))
|
|
340
|
+
if (text == null) continue
|
|
341
|
+
const tasks = []
|
|
342
|
+
let inTasks = false
|
|
343
|
+
for (const line of text.split("\n")) {
|
|
344
|
+
if (/^tasks:\s*$/.test(line)) { inTasks = true; continue }
|
|
345
|
+
if (inTasks) {
|
|
346
|
+
if (/^\S/.test(line)) break // dedented to a new top-level key
|
|
347
|
+
const m = /^\s{2}([a-zA-Z0-9][\w:.-]*):/.exec(line) // two-space-indented task key
|
|
348
|
+
if (m && !tasks.includes(m[1])) tasks.push(m[1])
|
|
349
|
+
}
|
|
350
|
+
if (tasks.length >= COMMANDS_CAP) break
|
|
351
|
+
}
|
|
352
|
+
if (tasks.length) groups.push(`task — ${tasks.join(", ")}`)
|
|
353
|
+
break
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return groups
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ---- discovery ---------------------------------------------------------------
|
|
360
|
+
async function walkMd(dir, root, depth, acc) {
|
|
361
|
+
if (depth > MAX_DEPTH || acc.length >= MAX_FILES_SCANNED) return
|
|
362
|
+
let entries
|
|
363
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }) } catch { return }
|
|
364
|
+
for (const e of entries) {
|
|
365
|
+
if (acc.length >= MAX_FILES_SCANNED) return
|
|
366
|
+
const full = path.join(dir, e.name)
|
|
367
|
+
if (e.isDirectory()) {
|
|
368
|
+
if (EXCLUDE_DIRS.has(e.name)) continue
|
|
369
|
+
await walkMd(full, root, depth + 1, acc)
|
|
370
|
+
} else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
|
|
371
|
+
acc.push(full)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Targeted convention discovery — never walks the whole tree (skips node_modules
|
|
377
|
+
// etc.). Returns { eager:[{rel,abs}], lazy:[{rel,abs}] }, deduped, eager wins.
|
|
378
|
+
async function discover(root) {
|
|
379
|
+
const eager = new Map() // abs -> {rel, abs}
|
|
380
|
+
const lazy = new Map()
|
|
381
|
+
const add = (map, abs) => {
|
|
382
|
+
const rel = path.relative(root, abs).split(path.sep).join("/")
|
|
383
|
+
map.set(abs, { rel, abs })
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 1. Root-level files.
|
|
387
|
+
let rootEntries
|
|
388
|
+
try { rootEntries = await fs.readdir(root, { withFileTypes: true }) } catch { rootEntries = [] }
|
|
389
|
+
for (const e of rootEntries) {
|
|
390
|
+
if (!e.isFile()) continue
|
|
391
|
+
const abs = path.join(root, e.name)
|
|
392
|
+
if (EAGER_ROOT.has(e.name)) add(eager, abs)
|
|
393
|
+
else if (e.name.toLowerCase().endsWith(".md")) add(lazy, abs) // misc root .md
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 2. Doc trees (recursive) -> lazy.
|
|
397
|
+
for (const d of LAZY_DIRS) {
|
|
398
|
+
const dir = path.join(root, d)
|
|
399
|
+
if (await isDir(dir)) {
|
|
400
|
+
const acc = []
|
|
401
|
+
await walkMd(dir, root, 1, acc)
|
|
402
|
+
for (const abs of acc) if (!eager.has(abs)) add(lazy, abs)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 3. Monorepo package entry docs -> lazy.
|
|
407
|
+
for (const group of ["packages", "apps"]) {
|
|
408
|
+
const base = path.join(root, group)
|
|
409
|
+
if (!(await isDir(base))) continue
|
|
410
|
+
let pkgs
|
|
411
|
+
try { pkgs = await fs.readdir(base, { withFileTypes: true }) } catch { continue }
|
|
412
|
+
for (const p of pkgs) {
|
|
413
|
+
if (!p.isDirectory() || EXCLUDE_DIRS.has(p.name)) continue
|
|
414
|
+
const readme = path.join(base, p.name, "README.md")
|
|
415
|
+
if (await isFile(readme) && !eager.has(readme)) add(lazy, readme)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const sortByRel = (a, b) => a.rel.localeCompare(b.rel)
|
|
420
|
+
return {
|
|
421
|
+
eager: [...eager.values()].sort(sortByRel),
|
|
422
|
+
lazy: [...lazy.values()].filter((x) => !eager.has(x.abs)).sort(sortByRel),
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ---- declarative manifest (override / extend) --------------------------------
|
|
427
|
+
// Returns { eager, lazy } from a manifest. Each entry: { path, load, description }
|
|
428
|
+
// where load is eager | lazy | exclude and path may be absolute, repo-relative, or
|
|
429
|
+
// a glob (a glob only reclassifies files already discovered). With "extend": true,
|
|
430
|
+
// auto-discovery runs first and entries add to / override / exclude its results;
|
|
431
|
+
// otherwise the manifest is the complete list (legacy semantics).
|
|
432
|
+
async function fromManifest(manifestFile, root) {
|
|
433
|
+
const data = await readJson(manifestFile)
|
|
434
|
+
const entries = Array.isArray(data?.context) ? data.context : []
|
|
435
|
+
const recs = new Map() // abs -> { rel, abs, load, desc }
|
|
436
|
+
|
|
437
|
+
const put = (abs, load, desc) => {
|
|
438
|
+
const rel = path.relative(root, abs).split(path.sep).join("/")
|
|
439
|
+
const prev = recs.get(abs)
|
|
440
|
+
recs.set(abs, { rel, abs, load, desc: desc || prev?.desc || "" })
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (data?.extend) {
|
|
444
|
+
const d = await discover(root)
|
|
445
|
+
for (const x of d.eager) put(x.abs, "eager", "")
|
|
446
|
+
for (const x of d.lazy) put(x.abs, "lazy", "")
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
for (const entry of entries) {
|
|
450
|
+
const raw = entry?.path ?? ""
|
|
451
|
+
const load = entry?.load ?? "eager"
|
|
452
|
+
const desc = entry?.description ?? ""
|
|
453
|
+
if (!raw) continue
|
|
454
|
+
if (raw.includes("*")) {
|
|
455
|
+
const re = globToRegExp(raw)
|
|
456
|
+
for (const [abs, x] of recs) if (re.test(x.rel)) put(abs, load, desc)
|
|
457
|
+
continue // a glob reclassifies discovered files; it does not add new ones
|
|
458
|
+
}
|
|
459
|
+
// Relative entry paths resolve against the REPO ROOT (matches the context.json
|
|
460
|
+
// stub's documented "relative to the repo root" — important when the manifest
|
|
461
|
+
// lives in .harness/, whose dir is not the root).
|
|
462
|
+
const abs = path.isAbsolute(raw) ? raw : path.resolve(root, raw)
|
|
463
|
+
put(abs, load, desc)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const eager = [], lazy = []
|
|
467
|
+
for (const x of recs.values()) {
|
|
468
|
+
if (x.load === "exclude") continue
|
|
469
|
+
;(x.load === "lazy" ? lazy : eager).push(x)
|
|
470
|
+
}
|
|
471
|
+
const byRel = (a, b) => a.rel.localeCompare(b.rel)
|
|
472
|
+
return { eager: eager.sort(byRel), lazy: lazy.sort(byRel) }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function resolveSource(root) {
|
|
476
|
+
const explicit = process.env.GENESEED_CONTEXT
|
|
477
|
+
if (explicit && (await isFile(explicit))) return { mode: "manifest", file: explicit }
|
|
478
|
+
for (const rel of [path.join(".harness", "context.json"), "context.json"]) {
|
|
479
|
+
const p = path.join(root, rel)
|
|
480
|
+
if (await isFile(p)) return { mode: "manifest", file: p }
|
|
481
|
+
}
|
|
482
|
+
return { mode: "discover" }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---- machine wiki (wiki.jsonc) --------------------------------------------------
|
|
486
|
+
// The user's own knowledge base(s) — typically an Obsidian vault — declared once per
|
|
487
|
+
// machine, not per repo (AGENT.md §7). Same injection mechanics as project context,
|
|
488
|
+
// different scope. Resolution (first match wins, mirroring the learn plugin):
|
|
489
|
+
// 1. $GENESEED_WIKI explicit manifest path
|
|
490
|
+
// 2. $GENESEED_HARNESS/wiki.jsonc pinned install dir
|
|
491
|
+
// 3. <plugin dir>/../wiki.jsonc auto-locate: beside the installed AGENT.md
|
|
492
|
+
async function resolveWikiFile() {
|
|
493
|
+
const explicit = process.env.GENESEED_WIKI
|
|
494
|
+
if (explicit && (await isFile(explicit))) return explicit
|
|
495
|
+
const bases = []
|
|
496
|
+
if (process.env.GENESEED_HARNESS) bases.push(process.env.GENESEED_HARNESS)
|
|
497
|
+
bases.push(path.resolve(PLUGIN_DIR, ".."))
|
|
498
|
+
for (const base of bases) {
|
|
499
|
+
// wiki.json is the legacy name from earlier seeds — still honoured.
|
|
500
|
+
for (const name of ["wiki.jsonc", "wiki.json"]) {
|
|
501
|
+
const p = path.join(base, name)
|
|
502
|
+
if (await isFile(p)) return p
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return null
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Walk a vault folder for .md notes — like the project walk, but dot-folders are
|
|
509
|
+
// skipped too (.obsidian, .trash — a vault keeps its config and trash inside).
|
|
510
|
+
// Shares the global MAX_DEPTH / MAX_FILES_SCANNED bounds.
|
|
511
|
+
async function walkWikiDir(dir, depth, acc) {
|
|
512
|
+
if (depth > MAX_DEPTH || acc.length >= MAX_FILES_SCANNED) return
|
|
513
|
+
let entries
|
|
514
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }) } catch { return }
|
|
515
|
+
for (const e of entries) {
|
|
516
|
+
if (acc.length >= MAX_FILES_SCANNED) return
|
|
517
|
+
const full = path.join(dir, e.name)
|
|
518
|
+
if (e.isDirectory()) {
|
|
519
|
+
if (EXCLUDE_DIRS.has(e.name) || e.name.startsWith(".")) continue
|
|
520
|
+
await walkWikiDir(full, depth + 1, acc)
|
|
521
|
+
} else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
|
|
522
|
+
acc.push(full)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// How many lazy notes one wiki may LIST per session — a `.` (whole-vault) folder
|
|
528
|
+
// entry on a large vault would otherwise inject thousands of listing lines. Beyond
|
|
529
|
+
// the cap the listing truncates with a visible count and the agent explores the
|
|
530
|
+
// folders on demand instead.
|
|
531
|
+
const WIKI_LAZY_LIMIT = Number(process.env.GENESEED_WIKI_LAZY_LIMIT || 200)
|
|
532
|
+
|
|
533
|
+
// Parse wiki.jsonc into renderable wikis: [{ name, root, desc, conventions, inbox,
|
|
534
|
+
// protected, eager:[{rel,abs,desc}], lazy:[...], truncated }]. Entry paths resolve
|
|
535
|
+
// against the wiki's own root and may name a single note OR a folder — a folder
|
|
536
|
+
// applies its load mode to every note beneath it (`.` covers the whole vault).
|
|
537
|
+
// Folders are processed first so a specific file entry always overrides the mode
|
|
538
|
+
// its folder gave it, regardless of manifest order; within each class, later
|
|
539
|
+
// entries win; `load: "exclude"` prunes. A missing root skips that wiki (never
|
|
540
|
+
// blocks the session).
|
|
541
|
+
async function wikiSets() {
|
|
542
|
+
const file = await resolveWikiFile()
|
|
543
|
+
if (!file) return []
|
|
544
|
+
const data = await readJsonc(file) // wiki.jsonc is JSONC: stub ships commented
|
|
545
|
+
const wikis = Array.isArray(data?.wikis) ? data.wikis : []
|
|
546
|
+
const out = []
|
|
547
|
+
for (const w of wikis) {
|
|
548
|
+
if (!w?.path) continue
|
|
549
|
+
if (!(await isDir(w.path))) { log(`wiki '${w.name ?? w.path}': root not found, skipped`); continue }
|
|
550
|
+
const name = w.name || path.basename(w.path)
|
|
551
|
+
|
|
552
|
+
const recs = new Map() // abs -> { rel, abs, load, desc }
|
|
553
|
+
const put = (abs, load, desc) => {
|
|
554
|
+
let rel = path.relative(w.path, abs).split(path.sep).join("/")
|
|
555
|
+
if (rel.startsWith("..")) rel = abs.split(path.sep).join("/") // outside the root
|
|
556
|
+
const prev = recs.get(abs)
|
|
557
|
+
recs.set(abs, { rel: `${name}/${rel}`, abs, load, desc: desc || prev?.desc || "" })
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const resolved = []
|
|
561
|
+
for (const e of Array.isArray(w.entries) ? w.entries : []) {
|
|
562
|
+
if (!e?.path) continue
|
|
563
|
+
const abs = path.isAbsolute(e.path) ? e.path : path.resolve(w.path, e.path)
|
|
564
|
+
const load = e.load === "lazy" || e.load === "exclude" ? e.load : "eager"
|
|
565
|
+
resolved.push({ abs, load, desc: e.description || "", dir: await isDir(abs) })
|
|
566
|
+
}
|
|
567
|
+
for (const folderPhase of [true, false]) { // folders first, then files
|
|
568
|
+
for (const e of resolved) {
|
|
569
|
+
if (e.dir !== folderPhase) continue
|
|
570
|
+
if (e.dir) {
|
|
571
|
+
const acc = []
|
|
572
|
+
await walkWikiDir(e.abs, 1, acc)
|
|
573
|
+
for (const f of acc) e.load === "exclude" ? recs.delete(f) : put(f, e.load, "")
|
|
574
|
+
} else {
|
|
575
|
+
e.load === "exclude" ? recs.delete(e.abs) : put(e.abs, e.load, e.desc)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const byRel = (a, b) => a.rel.localeCompare(b.rel)
|
|
581
|
+
const eager = [], lazy = []
|
|
582
|
+
for (const r of recs.values()) (r.load === "lazy" ? lazy : eager).push(r)
|
|
583
|
+
eager.sort(byRel)
|
|
584
|
+
lazy.sort(byRel)
|
|
585
|
+
let truncated = 0
|
|
586
|
+
if (lazy.length > WIKI_LAZY_LIMIT) {
|
|
587
|
+
truncated = lazy.length - WIKI_LAZY_LIMIT
|
|
588
|
+
lazy.length = WIKI_LAZY_LIMIT
|
|
589
|
+
log(`wiki '${name}': lazy listing truncated (${truncated} beyond the ${WIKI_LAZY_LIMIT} cap)`)
|
|
590
|
+
}
|
|
591
|
+
out.push({
|
|
592
|
+
name, root: w.path, desc: w.description || "",
|
|
593
|
+
conventions: w.conventions || "", inbox: w.inbox || "",
|
|
594
|
+
protected: Array.isArray(w.protected) ? w.protected : [],
|
|
595
|
+
eager, lazy, truncated,
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
return out
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ---- injection block ---------------------------------------------------------
|
|
602
|
+
// Render one eager+lazy set into `out`. The budget state (eager bytes, heading
|
|
603
|
+
// reads) is SHARED across segments — project context and every wiki draw from the
|
|
604
|
+
// same caps, so the whole block stays bounded no matter how many wikis are declared.
|
|
605
|
+
async function renderSet(out, { eager, lazy }, state) {
|
|
606
|
+
const perFile = EAGER_FILE_KB * 1024
|
|
607
|
+
const total = EAGER_TOTAL_KB * 1024
|
|
608
|
+
const demoted = []
|
|
609
|
+
|
|
610
|
+
for (const e of eager) {
|
|
611
|
+
let text
|
|
612
|
+
try { text = await fs.readFile(e.abs, "utf8") } catch (err) {
|
|
613
|
+
out.push(`----- ${e.rel}${e.desc ? ` — ${e.desc}` : ""} -----`)
|
|
614
|
+
out.push(`[context] MISSING eager file: ${err?.message ?? err}`, "")
|
|
615
|
+
continue
|
|
616
|
+
}
|
|
617
|
+
const size = Buffer.byteLength(text, "utf8")
|
|
618
|
+
if (size > perFile) {
|
|
619
|
+
demoted.push(`[demoted: ${e.rel} exceeded ${EAGER_FILE_KB} KB — read on demand]`)
|
|
620
|
+
log(`demoted ${e.rel} -> lazy (${kb(size)} KB > ${EAGER_FILE_KB} KB cap)`)
|
|
621
|
+
continue
|
|
622
|
+
}
|
|
623
|
+
if (state.spent + size > total) {
|
|
624
|
+
demoted.push(`[demoted: ${e.rel} — eager budget (${EAGER_TOTAL_KB} KB) reached]`)
|
|
625
|
+
continue
|
|
626
|
+
}
|
|
627
|
+
out.push(`----- ${e.rel}${e.desc ? ` — ${e.desc}` : ""} -----`)
|
|
628
|
+
out.push(text.replace(/\n+$/, ""), "")
|
|
629
|
+
state.spent += size
|
|
630
|
+
state.injected++
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const lazyLines = []
|
|
634
|
+
for (const l of lazy) {
|
|
635
|
+
let head = l.desc
|
|
636
|
+
// Only crack open files we have no description for, and cap how many — bounded by
|
|
637
|
+
// a head-slice read so a big docs/ tree stays cheap on every session start.
|
|
638
|
+
if (!head && state.headingsRead < LAZY_HEADING_LIMIT) {
|
|
639
|
+
head = firstHeading(await readHeadSlice(l.abs))
|
|
640
|
+
state.headingsRead++
|
|
641
|
+
}
|
|
642
|
+
lazyLines.push(` - ${l.rel}${head ? ` — ${head}` : ""}`)
|
|
643
|
+
}
|
|
644
|
+
if (lazyLines.length || demoted.length) {
|
|
645
|
+
out.push("--- Lazy entries (load only when the task needs them) ---")
|
|
646
|
+
out.push(...lazyLines)
|
|
647
|
+
if (demoted.length) out.push(...demoted.map((d) => ` ${d}`))
|
|
648
|
+
out.push("")
|
|
649
|
+
}
|
|
650
|
+
state.lazy += lazyLines.length
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function buildBlock(sets, wikis = [], commands = []) {
|
|
654
|
+
const state = { spent: 0, injected: 0, lazy: 0, headingsRead: 0 }
|
|
655
|
+
|
|
656
|
+
const proj = []
|
|
657
|
+
await renderSet(proj, sets, state)
|
|
658
|
+
|
|
659
|
+
if (commands.length) {
|
|
660
|
+
proj.push("--- Project commands (runnable targets discovered in the repo root) ---")
|
|
661
|
+
for (const g of commands) proj.push(` ${g}`)
|
|
662
|
+
proj.push("")
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const wik = []
|
|
666
|
+
for (const w of wikis) {
|
|
667
|
+
wik.push(`--- wiki: ${w.name}${w.desc ? ` — ${w.desc}` : ""} (root: ${w.root}) ---`)
|
|
668
|
+
if (w.conventions) wik.push(` conventions: ${w.conventions} (read before your first write there)`)
|
|
669
|
+
if (w.inbox) wik.push(` inbox: ${w.inbox} (drop notes you cannot confidently file)`)
|
|
670
|
+
if (w.protected.length) wik.push(` protected (never write): ${w.protected.join(", ")}`)
|
|
671
|
+
wik.push("")
|
|
672
|
+
await renderSet(wik, { eager: w.eager, lazy: w.lazy }, state)
|
|
673
|
+
if (w.truncated) wik.push(` [+${w.truncated} more notes in this wiki — explore its folders on demand]`, "")
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!proj.length && !wik.length) return null
|
|
677
|
+
const out = [MARKER]
|
|
678
|
+
if (proj.length) out.push("=== PROJECT CONTEXT — binding for this repo per Law XVIII ===", "", ...proj)
|
|
679
|
+
if (wik.length) out.push("=== MACHINE WIKI — the user's knowledge base, binding per AGENT.md §7 ===", "", ...wik)
|
|
680
|
+
return { text: out.join("\n"), injected: state.injected, lazy: state.lazy, kb: kb(state.spent) }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Resolve the eager/lazy sets (manifest or auto-discovery) plus the machine wikis,
|
|
684
|
+
// and render the injection block — shared by the session.created injection and the
|
|
685
|
+
// compaction hook (the transform path reuses it via cachedBlockText).
|
|
686
|
+
async function resolveBlock(root) {
|
|
687
|
+
const src = await resolveSource(root)
|
|
688
|
+
const sets = src.mode === "manifest"
|
|
689
|
+
? await fromManifest(src.file, root)
|
|
690
|
+
: await discover(root)
|
|
691
|
+
const wikis = await wikiSets()
|
|
692
|
+
const commands = await discoverCommands(root)
|
|
693
|
+
return { block: await buildBlock(sets, wikis, commands), src }
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Per-process cache of the rendered block text for the transform path (root is fixed
|
|
697
|
+
// per plugin instance). Recomputed at most once per TTL so doc edits are picked up.
|
|
698
|
+
let _blockCache = { text: null, at: 0 }
|
|
699
|
+
const BLOCK_TTL_MS = 30000
|
|
700
|
+
async function cachedBlockText(root) {
|
|
701
|
+
const now = Date.now()
|
|
702
|
+
if (_blockCache.text !== null && now - _blockCache.at < BLOCK_TTL_MS) return _blockCache.text
|
|
703
|
+
let text = ""
|
|
704
|
+
try { const { block } = await resolveBlock(root); text = block?.text || "" } catch {}
|
|
705
|
+
_blockCache = { text, at: now }
|
|
706
|
+
return text
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ---- cross-instance idempotency ----------------------------------------------
|
|
710
|
+
async function alreadyInjected(client, sid) {
|
|
711
|
+
try {
|
|
712
|
+
const msgs = await client.session.messages({ path: { id: sid } })
|
|
713
|
+
const arr = Array.isArray(msgs) ? msgs : (msgs?.data ?? [])
|
|
714
|
+
for (const m of arr) {
|
|
715
|
+
const parts = m?.parts ?? m?.info?.parts ?? []
|
|
716
|
+
for (const p of parts) {
|
|
717
|
+
if (typeof p?.text === "string" && p.text.includes(MARKER)) return true
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
} catch {}
|
|
721
|
+
return false
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export const GeneseedContext = async (ctx) => {
|
|
725
|
+
const { client } = ctx
|
|
726
|
+
const done = new Set()
|
|
727
|
+
const root = repoRoot(ctx)
|
|
728
|
+
|
|
729
|
+
// Visible delivery — post the block as a noReply session message. Used when
|
|
730
|
+
// GENESEED_CONTEXT_VISIBLE forces it, and by the transform fallback below.
|
|
731
|
+
async function injectVisible(sid) {
|
|
732
|
+
if (!sid || done.has(sid)) return
|
|
733
|
+
try {
|
|
734
|
+
// Don't pollute the learn plugin's throwaway distil sessions, and don't
|
|
735
|
+
// re-inject into native subagent child sessions (they inherit the parent's
|
|
736
|
+
// context). The sid is only marked done AFTER the guards pass — marking it
|
|
737
|
+
// earlier would permanently skip a session when a transient session.get
|
|
738
|
+
// failure let it through the title check.
|
|
739
|
+
let title = "", parent
|
|
740
|
+
try {
|
|
741
|
+
const info = await client.session.get({ path: { id: sid } })
|
|
742
|
+
const meta = info?.data ?? info
|
|
743
|
+
title = meta?.title ?? ""
|
|
744
|
+
parent = meta?.parentID ?? meta?.parentId
|
|
745
|
+
} catch {}
|
|
746
|
+
if (parent) { log("skipped: child/subagent session"); return }
|
|
747
|
+
if (title.startsWith("geneseed-")) { log("skipped: geneseed-* session"); return }
|
|
748
|
+
|
|
749
|
+
if (await alreadyInjected(client, sid)) { log("skipped: already injected (marker present)"); return }
|
|
750
|
+
done.add(sid)
|
|
751
|
+
|
|
752
|
+
const { block, src } = await resolveBlock(root)
|
|
753
|
+
if (!block) { log(`no docs discovered in ${root}`); return }
|
|
754
|
+
|
|
755
|
+
// Best-effort model line: read the session transcript, else $GENESEED_MODEL.
|
|
756
|
+
let model = null
|
|
757
|
+
try {
|
|
758
|
+
const msgs = await client.session.messages({ path: { id: sid } })
|
|
759
|
+
model = modelFromMessages(Array.isArray(msgs) ? msgs : (msgs?.data ?? []))
|
|
760
|
+
} catch {}
|
|
761
|
+
const text = withModel(block.text, model || envModelStr())
|
|
762
|
+
|
|
763
|
+
await client.session.prompt({
|
|
764
|
+
path: { id: sid },
|
|
765
|
+
body: { noReply: true, parts: [{ type: "text", text }] },
|
|
766
|
+
})
|
|
767
|
+
const via = src.mode === "manifest" ? `manifest ${src.file}` : `auto-discovery [${root}]`
|
|
768
|
+
log(`injected: ${block.injected} eager (${block.kb} KB), ${block.lazy} lazy listed — via ${via}`)
|
|
769
|
+
} catch (err) {
|
|
770
|
+
log(`error: ${err?.message ?? err}`)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Session-scope filter for the INVISIBLE delivery, mirroring injectVisible's
|
|
775
|
+
// guards: child/subagent sessions inherit the parent's context, and geneseed-*
|
|
776
|
+
// sessions (learn distils, geneseed-wf:* children) must stay clean — without this
|
|
777
|
+
// the transform prepends the full context block to every request of every session
|
|
778
|
+
// in-process. Cached per sid (the transform runs on EVERY request); a transient
|
|
779
|
+
// session.get failure is NOT cached so the next request re-checks.
|
|
780
|
+
const sessionSkip = new Map()
|
|
781
|
+
async function skipSession(sid) {
|
|
782
|
+
if (!sid) return false
|
|
783
|
+
const hit = sessionSkip.get(sid)
|
|
784
|
+
if (hit !== undefined) return hit
|
|
785
|
+
try {
|
|
786
|
+
const info = await client.session.get({ path: { id: sid } })
|
|
787
|
+
const meta = info?.data ?? info
|
|
788
|
+
const skip = Boolean(meta?.parentID ?? meta?.parentId) ||
|
|
789
|
+
String(meta?.title ?? "").startsWith("geneseed-")
|
|
790
|
+
sessionSkip.set(sid, skip)
|
|
791
|
+
return skip
|
|
792
|
+
} catch {
|
|
793
|
+
return false
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return {
|
|
798
|
+
event: async ({ event }) => {
|
|
799
|
+
if (!event || INJECT_OFF) return
|
|
800
|
+
if (await sovereignBypass(root)) return
|
|
801
|
+
const sid =
|
|
802
|
+
event.properties?.sessionID ?? event.payload?.sessionID ??
|
|
803
|
+
event.properties?.info?.id ?? event.payload?.info?.id
|
|
804
|
+
if (event.type === "session.created") {
|
|
805
|
+
if (TRANSFORM && !fallbackVisible) return // invisible delivery handles it
|
|
806
|
+
await injectVisible(sid)
|
|
807
|
+
return
|
|
808
|
+
}
|
|
809
|
+
// Transform fallback: the session went idle (a request completed) yet the
|
|
810
|
+
// experimental hook never fired in this process — this build lacks it. Engage
|
|
811
|
+
// the visible delivery so context still arrives, injecting into THIS session
|
|
812
|
+
// right away (one turn late) rather than waiting for the next session.
|
|
813
|
+
if (event.type === "session.idle" && TRANSFORM && !transformSeen && !fallbackVisible) {
|
|
814
|
+
fallbackVisible = true
|
|
815
|
+
log("transform hook absent on this build — falling back to visible injection")
|
|
816
|
+
await injectVisible(sid)
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
|
|
820
|
+
// Survive compaction. The visible injection above is a conversation message, so
|
|
821
|
+
// OpenCode summarises it away when a long session compacts (the AGENT.md rules
|
|
822
|
+
// persist — they load via opencode.json `instructions`, not the conversation).
|
|
823
|
+
// Re-push the eager docs into the compaction context so the project context —
|
|
824
|
+
// Law XVIII — outlives the summary. Only needed when delivery is visible
|
|
825
|
+
// (forced or fallback); the transform re-sends per request. Experimental
|
|
826
|
+
// OpenCode hook; if it is absent in a build this key is simply never called.
|
|
827
|
+
"experimental.session.compacting": async (_input, output) => {
|
|
828
|
+
if (INJECT_OFF || (TRANSFORM && !fallbackVisible)) return
|
|
829
|
+
if (await sovereignBypass(root)) return
|
|
830
|
+
try {
|
|
831
|
+
const { block } = await resolveBlock(root)
|
|
832
|
+
if (block && output && Array.isArray(output.context)) {
|
|
833
|
+
output.context.push(block.text)
|
|
834
|
+
log(`compaction: re-pushed ${block.injected} eager doc(s)`)
|
|
835
|
+
}
|
|
836
|
+
} catch (err) {
|
|
837
|
+
log(`compaction error: ${err?.message ?? err}`)
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
|
|
841
|
+
// Invisible delivery (the default). Prepend the context to each request's
|
|
842
|
+
// outgoing message array — no persisted/visible session message, and it survives
|
|
843
|
+
// compaction because it is re-sent every request. Experimental OpenCode hook;
|
|
844
|
+
// never called on a build that lacks it — the session.idle fallback above
|
|
845
|
+
// detects that and takes over with the visible delivery.
|
|
846
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
847
|
+
transformSeen = true // the hook exists on this build
|
|
848
|
+
if (INJECT_OFF || !TRANSFORM || fallbackVisible) return
|
|
849
|
+
if (await sovereignBypass(root)) return
|
|
850
|
+
try {
|
|
851
|
+
if (!output || !Array.isArray(output.messages) || !output.messages.length) return
|
|
852
|
+
const sid = output.messages[0]?.info?.sessionID
|
|
853
|
+
if (await skipSession(sid)) return
|
|
854
|
+
// Already present this request? (idempotent across plugin copies / retries.)
|
|
855
|
+
const present = output.messages.some((m) =>
|
|
856
|
+
(m?.parts ?? []).some((p) => typeof p?.text === "string" && p.text.includes(MARKER)))
|
|
857
|
+
if (present) return
|
|
858
|
+
const cached = await cachedBlockText(root)
|
|
859
|
+
if (!cached) return
|
|
860
|
+
// Model line is request-specific (kept out of the cache): prefer the live
|
|
861
|
+
// transcript on this request, fall back to $GENESEED_MODEL.
|
|
862
|
+
const text = withModel(cached, modelFromMessages(output.messages) || envModelStr())
|
|
863
|
+
const stamp = Date.now()
|
|
864
|
+
output.messages.unshift({
|
|
865
|
+
info: { id: `geneseed-context-${stamp}`, role: "user", sessionID: sid, time: { created: stamp } },
|
|
866
|
+
parts: [{ id: `geneseed-context-part-${stamp}`, type: "text", text }],
|
|
867
|
+
})
|
|
868
|
+
log("transform: injected context invisibly")
|
|
869
|
+
} catch (err) {
|
|
870
|
+
log(`transform error: ${err?.message ?? err}`)
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export default GeneseedContext
|