geneseed 3.20.0 → 4.1.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.
Files changed (125) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +9 -1
  3. package/SHIPPED.md +1 -1
  4. package/adapters/bob/README.md +31 -12
  5. package/adapters/claude-code/README.md +24 -17
  6. package/adapters/claude-code/settings.json +4 -6
  7. package/adapters/openclaude/README.md +13 -5
  8. package/adapters/opencode/HOW-OPENCODE-LOADS.md +35 -0
  9. package/adapters/opencode/plugins/geneseed-activity.js +105 -2
  10. package/adapters/opencode/plugins/geneseed-context.js +96 -1
  11. package/adapters/opencode/plugins/geneseed-guard.js +324 -175
  12. package/adapters/opencode/plugins/geneseed-learn.js +127 -14
  13. package/adapters/opencode/plugins/geneseed-notify.js +46 -1
  14. package/adapters/opencode/plugins/geneseed-ponytail.js +44 -2
  15. package/adapters/opencode/plugins/geneseed-workflow.js +124 -24
  16. package/bin/geneseed-hook.mjs +36 -5
  17. package/docs/README.md +3 -1
  18. package/docs/candidate-hosts.md +0 -8
  19. package/docs/concepts/enforcement-table.md +66 -0
  20. package/docs/concepts/hook-gates.md +2 -2
  21. package/docs/concepts/hooks.md +6 -6
  22. package/docs/concepts/host-matrix.md +4 -3
  23. package/docs/concepts/loop-branch.md +6 -1
  24. package/docs/concepts/rules.md +1 -1
  25. package/docs/design-history.md +59 -0
  26. package/docs/extending.md +11 -3
  27. package/docs/guides/install-opencode.md +1 -1
  28. package/docs/guides/loop-running.md +1 -1
  29. package/docs/guides/mcp-filesystem.md +2 -2
  30. package/docs/guides/mcp-gitlab.md +2 -2
  31. package/docs/guides/mcp-markitdown.md +3 -3
  32. package/docs/guides/mcp.md +7 -7
  33. package/docs/guides/project-context.md +1 -1
  34. package/docs/reference/env-context.md +3 -2
  35. package/docs/reference/environment.md +2 -1
  36. package/docs/reference/opencode-plugins.md +9 -0
  37. package/docs/reference/plugin-guard.md +3 -3
  38. package/docs/reference/troubleshoot-hooks.md +34 -0
  39. package/docs/understand/design-choices-engineering.md +29 -0
  40. package/docs/understand/design-choices-gates.md +23 -0
  41. package/docs/understand/design-choices-hosts.md +21 -0
  42. package/docs/understand/design-choices-install.md +19 -0
  43. package/docs/understand/design-choices-shape.md +21 -0
  44. package/docs/understand/design-choices.md +34 -0
  45. package/docs/understand/enforced-vs-asked.md +5 -4
  46. package/docs/understand/machine-bob.md +1 -1
  47. package/docs/understand/machine-opencode.md +2 -2
  48. package/harness.config.json +1 -1
  49. package/js/build/driver.mjs +42 -16
  50. package/js/build/emit-claude.mjs +24 -8
  51. package/js/build/emit-opencode.mjs +33 -15
  52. package/js/build/generate.mjs +38 -9
  53. package/js/build/source.mjs +39 -0
  54. package/js/cli-table.json +7 -7
  55. package/js/hosts/gitref.mjs +3 -3
  56. package/js/hosts/hooks-context.mjs +11 -30
  57. package/js/hosts/hooks-learn.mjs +89 -8
  58. package/js/hosts/hooks.mjs +212 -18
  59. package/js/hosts/hosts.mjs +217 -13
  60. package/js/hosts/installs.mjs +259 -46
  61. package/js/hosts/mcp.mjs +23 -152
  62. package/js/hosts/native.mjs +48 -12
  63. package/js/hosts/opencode.mjs +48 -18
  64. package/js/hosts/settings.mjs +366 -42
  65. package/js/hosts/shim.mjs +37 -0
  66. package/js/inspect/checks-authoring.mjs +81 -1
  67. package/js/inspect/checks-constitution.mjs +2 -1
  68. package/js/inspect/checks-repo.mjs +68 -4
  69. package/js/inspect/doctor.mjs +40 -9
  70. package/js/inspect/scan.mjs +5 -4
  71. package/js/inspect/status.mjs +62 -12
  72. package/js/lib/text.mjs +23 -0
  73. package/js/loop/catalog.mjs +7 -2
  74. package/js/maintain/migrate.mjs +42 -5
  75. package/js/maintain/uninstall.mjs +150 -38
  76. package/js/maintain/update.mjs +22 -2
  77. package/js/web/actions.mjs +43 -87
  78. package/js/web/api.mjs +13 -4
  79. package/js/web/handler.mjs +2 -2
  80. package/js/web/routes.mjs +1 -2
  81. package/js/web/state.mjs +28 -15
  82. package/package.json +1 -1
  83. package/src/agents/_template.md +3 -2
  84. package/src/agents/researcher.md +2 -1
  85. package/src/agents/reviewer.md +9 -4
  86. package/src/agents/tester.md +3 -3
  87. package/src/bricks/_template.md +1 -1
  88. package/src/doctrines/ops.md +9 -7
  89. package/src/doctrines/rigor.md +8 -7
  90. package/src/laws/universal.md +1 -1
  91. package/src/skills/_template.md +3 -3
  92. package/src/skills/brainstorm/builder-check.md +16 -0
  93. package/src/skills/brainstorm.md +19 -11
  94. package/src/skills/daydream/SKILL.md +1 -1
  95. package/src/skills/daydream/VENDOR.md +4 -0
  96. package/src/skills/debug/ci.md +4 -4
  97. package/src/skills/debug.md +4 -4
  98. package/src/skills/develop.md +6 -4
  99. package/src/skills/fresh-eyes.md +1 -1
  100. package/src/skills/geneseed-code-review.md +8 -2
  101. package/src/skills/handoff.md +1 -1
  102. package/src/skills/herdr.md +1 -1
  103. package/src/skills/parallel-agents.md +4 -4
  104. package/src/skills/pipeline.md +3 -3
  105. package/src/skills/plan.md +18 -6
  106. package/src/skills/review-response.md +5 -2
  107. package/src/skills/ship.md +2 -1
  108. package/src/skills/worktree.md +2 -0
  109. package/web/dist/assets/{Activity-IbwdHHyx.js → Activity-CqVt2u9s.js} +1 -1
  110. package/web/dist/assets/{ActivityDetail-B5uivdWI.js → ActivityDetail-Bui5Gomc.js} +1 -1
  111. package/web/dist/assets/{Dashboard-v6WQ37bD.js → Dashboard-VlIx1-At.js} +2 -2
  112. package/web/dist/assets/{Docs-CypIfDBX.js → Docs-6cYYkVeA.js} +1 -1
  113. package/web/dist/assets/{GreenhouseView-ClxP0hQG.js → GreenhouseView-DgNY8QKU.js} +1 -1
  114. package/web/dist/assets/{Installs-CPkQeFjO.js → Installs-DZwZOUZJ.js} +1 -1
  115. package/web/dist/assets/{JournalView-BeEInhUf.js → JournalView-DXpFbKOG.js} +1 -1
  116. package/web/dist/assets/{Laws-Ds6U5xBQ.js → Laws-Bh7ztekk.js} +1 -1
  117. package/web/dist/assets/{Library-BraUKPyQ.js → Library-DBLX-1SD.js} +1 -1
  118. package/web/dist/assets/{Loops-DWcy3mwK.js → Loops-CvFMZ7_P.js} +1 -1
  119. package/web/dist/assets/{OperatorHudView-CQyrJQ9m.js → OperatorHudView-FPTUH1CB.js} +1 -1
  120. package/web/dist/assets/{Personal-YWqs6qFL.js → Personal-DcVA502F.js} +1 -1
  121. package/web/dist/assets/{Tabs-EhzEC-Xh.js → Tabs-DtyR4h75.js} +1 -1
  122. package/web/dist/assets/{api-CxgfYu18.js → api-CRqWld2H.js} +1 -1
  123. package/web/dist/assets/{drawable-CEKPNV7X.js → drawable-CNSPVZxX.js} +1 -1
  124. package/web/dist/assets/{index-CPi22b7y.js → index-BmjcLIOr.js} +2 -2
  125. package/web/dist/index.html +2 -2
package/CHANGELOG.md CHANGED
@@ -8,6 +8,77 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [4.1.0] — 2026-10-13
12
+
13
+ Minor: the planning, review, debugging and multi-agent skills take the lessons of Superpowers 7.0, and the host canary.
14
+
15
+ ### Changed
16
+ - **brainstorm** asks which build choices (platform, language, key libraries) you want to own; one the agent makes goes in the decision list as its own call. It sizes the work: a quick task is just done, a small change keeps the confirmed list in chat as its spec, and only a project gets a spec file. A fresh subagent then reads that spec as the person who will build it and asks what they would have to guess, replacing the agent rereading its own spec.
17
+ - **plan**: a step records decisions (files, signatures, the spec's values, the test, the check command), not code. A step is marked done only once its check has run green, with the command on the line. A decision taken without asking gets a `Ruling:` line that is reported at the end, and the mid-run "stop for review" clause is gone. The plan file names its task and its spec, and the spec wins a conflict.
18
+ - **debug**: compare against the nearest working twin, trace a bad value back to where it was born, cite evidence as `file:line`, and probe a secret's presence, never its value. The CI branch diffs against the last green run first and replaces guessed sleeps with bounded waits.
19
+ - **Code review**: where the spec says nothing, a crash, hang or data loss on an input it implies is still blocking. Findings end with a *Set aside* list the caller rules on, and `ship` rules on it before pushing. The base is `origin/<default branch>` after a fetch. The reviewer never moves HEAD and never dispatches another reviewer.
20
+ - **pipeline / parallel-agents**: the developer↔tester loop gets two repair rounds, then one fresh developer, then stops (it allowed five, against `bound-the-loop`). An ambiguity is decided and recorded rather than stalling, small edits of one kind run as one unit, no dispatched agent dispatches its own, and an empty diff counts as a failure. `worktree` never forces a refused removal.
21
+ - The `cover-and-verify` doctrine and `develop` run the whole suite once at the end and name every failure, including ones the change did not cause; per step, only the affected tests still run.
22
+
23
+ ### Fixed
24
+ - The daydream skill's cost line showed a mangled price on Claude Code, which replaces a dollar sign followed by a digit with the skill's arguments.
25
+ - On OpenCode 2, the Activity view now names each tool call. OpenCode 2 sends a tool's name only when its input starts streaming, not with the call itself, so every tool card read "undefined".
26
+
27
+ ### Added
28
+ - A host canary, `node tests/host_canary.mjs`. It installs the real OpenCode 1, OpenCode 2, Claude Code and OpenClaude from npm and points each at a throwaway home holding the Geneseed install. It fails when a host refuses one of our OpenCode plugins, reports one of our Claude settings as invalid, or never runs our session-start hook. It never calls a model and needs no account. It runs in CI every Monday against the latest releases and on every pull request that changes what Geneseed installs, so the next host release that breaks the contract is caught within a week rather than by a user. IBM Bob is not covered, because Bob Shell is not published on npm.
29
+
30
+ ## [4.0.2] — 2026-10-12
31
+
32
+ Patch: none of the OpenCode plugins loaded on OpenCode 2, and MCP is read-only.
33
+
34
+ ### Fixed
35
+ - The seven OpenCode plugins now load on OpenCode 2. OpenCode 2 rewrote the plugin API and refused every Geneseed plugin with *"Plugin must export a default definition with an id and an effect or setup function"*. It skipped them without crashing, so the context load, learn, notify, ponytail, activity and the `workflow` tool were all gone, and the guard's safety gates were silently off. Each plugin now carries both APIs in one file and works on OpenCode 1.3.4 or newer and on OpenCode 2. Plugins no longer load on OpenCode older than 1.3.4. On OpenCode 2 a guard block shows up as a failed tool call that gives the reason, and the Activity view shows no todo list or files-touched count, because OpenCode 2 sends no event for either. Run `geneseed rebuild-all` after updating to install the new plugin files.
36
+
37
+ ### Changed
38
+ - MCP is read-only in the web console and in `geneseed mcp`. Each active install lists only the servers its config already holds, enabled or disabled, with its config path and an **Open folder** button. The **Add** button, the on/off switch and the three built-in presets are gone. Which servers to wire depends on you and on your host, so you edit the config yourself; [MCP servers](docs/guides/mcp.md) still has the blocks for MarkItDown, GitLab and Filesystem. Geneseed no longer writes any MCP config, and the `POST /api/mcp` endpoint is removed.
39
+
40
+ ## [4.0.1] — 2026-10-12
41
+
42
+ Patch: the web console could not tell apart two project installs of the same host (#244).
43
+
44
+ ### Fixed
45
+ - Two project installs of one host (two repos on Bob, say) are now separate rows in the web console. Both used to get the id `bob:project`, so the Installs table showed both as selected and the topbar harness selector always switched back to the first. The id now carries the install root, and the selector names a project install by its folder. All four hosts had the bug and all four are covered by a test. Nothing is re-emitted; run `geneseed web restart` after updating.
46
+
47
+ ## [4.0.0] — 2026-10-11
48
+
49
+ Major: a host-compatibility pass against the current IBM Bob, Claude Code, OpenCode and OpenClaude docs (#240), the host opportunities and a root-scan guard (#241), and author notes kept out of what the agent reads (#242). It is a major because three behaviours change visibly: Claude Code gates now block when they cannot run, OpenCode no longer falls back to `~/.claude/CLAUDE.md`, and the silent *process 8* migration is gone.
50
+
51
+ ### Upgrade notes
52
+ - Run `geneseed rebuild-all` once after updating: nothing on a machine changes until an install is re-emitted.
53
+ - An install built before the comms pack (3.7.0) is now skipped by `rebuild-all`, `migrate` and `upgrade`; run `geneseed setup` for it.
54
+ - If a Claude Code session suddenly refuses every Bash or Write call, the machine-wide hook shim is missing or stale: run `! geneseed rebuild-all` (see docs/reference/troubleshoot-hooks.md).
55
+
56
+ ### Added
57
+ - A Claude Code plugin marketplace: `/plugin marketplace add Arylmera/Geneseed`, then `/plugin install geneseed@geneseed`. The plugin carries one skill, `/geneseed:setup`, which asks for scope and theme and runs the published CLI (`npm install -g geneseed`, `geneseed build --emit claude-global` or `--emit claude`). It carries no harness of its own, so nothing in it can drift from the build.
58
+ - Bob gets `/`-commands: each skill alias and each user-only skill is a `.bob/commands/<name>.md`, and a user-only skill writes no SKILL.md on Bob, so the model cannot start it there.
59
+ - OpenCode hides user-only skills from the model: each one gets a `permission.skill` deny in `opencode.json`, and `/name` still runs it. Geneseed records the denies it writes in its manifest (`skill_denies`); a rebuild removes a recorded deny once the skill is no longer user-only, and uninstall takes the recorded ones back. A deny you set yourself, even on a Geneseed skill, is never touched.
60
+ - A root-scan check in the shell gate: a recursive scan whose argument is a whole filesystem root (`find /`, `du -sh /`, `grep -r foo /`, `rg x C:\`, `Get-ChildItem C:\ -Recurse`) now asks on Claude Code and OpenClaude and is refused on Bob and by the OpenCode guard plugin, citing *Commands Must Return* (ops 2), which gains a sentence saying so in both halves. In Git Bash `/` mounts every drive and share: a `find / … | head -5` ran for 3.5 hours on 2026-10-10. A deeper path, `ls /` without `-R`, a quoted argument, a comment, and the remote half of an `ssh` command pass. It runs inside `git-gate`/`tool-gate`, so no emitted hook command changes. Separators inside quotes and heredoc bodies are data, so a commit message that names such a scan is not one.
61
+ - A dedicated author-note prefix: an HTML comment in `src/` whose body starts with `note:` (`<!-- note: ... -->`, single or multi-line) is stripped before any render sees it — `resolveRuleIds` in `js/build/source.mjs` is the one function every text file threads through (and `parseBrick` in `js/loop/catalog.mjs` calls the same `stripAuthorNotes` directly, since a brick body is handed to the model raw and never goes through `resolveRuleIds` at all), so the strip covers the carrier, the on-disk `laws/`/`doctrines/` copies, every skill, and every brick alike. A comment shown only as a literal example — inside a fenced code block or an inline code span — is left un-stripped, via `blankCodeSpans` (`js/lib/text.mjs`, shared with the doctor arm below so the two can never disagree about what counts as "shown as an example"). Maintainer-only prose that used to ship verbatim (`src/laws/universal.md`'s "Amend BOTH halves", `src/skills/herdr.md`'s "Authoring note") is migrated to the prefix and no longer reaches the agent; the full carrier gains ~176 characters of headroom against its ceiling (unchanged, not lowered), lean is unaffected. `js/inspect/checks-authoring.mjs`'s new `authorNoteProblems` (wired into `doctor --all`) flags any other bare HTML comment in `src/` that is not the `note:` prefix and not a known functional marker (`LEAN:`/`CATALOG:`/`INCLUDE:`, `invocation: user`, `aliases:`, `webfetch: allow`, `harness:` followed by one or more of OpenCode/Claude Code/OpenClaude/Bob's ids, `bash: allow`), the same fence/code-span exemption applied, and separately refuses a `note:` comment whose own prose quotes a literal `<!--`/`-->` — HTML comments do not nest, so such a comment closes at its own inner marker and leaks the rest as live text (the landmine `src/skills/_template.md` hit once, fixed there by rewording the example to prose).
62
+
63
+ ### Changed
64
+ - Claude Code gates now block the tool call when the gate itself fails: `git-gate` and `rule-gate` carry `onFailure: "block"`, so a dead or missing shim, a gate process that crashes or a timeout refuses every Bash, PowerShell, Write, Edit and NotebookEdit call instead of letting it through. This needs Claude Code v2.1.295 or later; whether an older version ignores the key or rejects the settings file is unverified. `doctor` and `status` now report a deleted shim. To recover from inside a blocked session, see [Troubleshoot hooks](docs/reference/troubleshoot-hooks.md). OpenClaude and Bob are unchanged.
65
+
66
+ ### Removed
67
+ - The automatic *process 8* migration. An install built before the comms pack (3.7.0, 2026-09-27) used to be silently rewritten on every rebuild: `doctrinesOfDir` added `comms` so the codes rule it carried inside `process` was not lost, and `excludedRulesOfDir` re-addressed any `process 7`/`process 8` exclusion. Both relied on sniffing `process 8` in the rendered carrier, which breaks the day the process pack ever regains an eighth rule for an unrelated reason — it would then fire on a current install and rewrite data nobody asked rewritten. 4.0.0 removes the rewrite: a pre-comms carrier is now read exactly as written, and reading it (`doctor`, `diff`, `status`, `uninstall`) prints a warning on stderr instead, once per process. `rebuild-all` (and `upgrade`, which shells into it) now **skips** a pre-comms install entirely rather than re-emitting it — re-emitting would have written a fresh carrier that still drops `comms` and still carries unaddressed `process 7`/`process 8` exclusions, except now permanently, with no `process 8` text left for anything to ever catch again. **Upgrading:** if your install predates 3.7.0 and still names `process 8` in its carrier, run `geneseed setup` for it — `rebuild-all`/`upgrade` will skip it and name it in their summary until you do.
68
+
69
+ ### Fixed
70
+ - Bob gates read Bob's own payload (`{event, tool, input}`); they used to read only `tool_input`, so on Bob no gate ever fired. Bob also scans `apply_diff` and `search_and_replace` bodies.
71
+ - The Claude Code and OpenClaude git gate also matches the PowerShell tool (`Bash|PowerShell`); on Windows a destructive git command run through PowerShell used to pass unchecked. Read-only agents deny PowerShell too.
72
+ - A hook argv error never exits 2: a stale machine-wide shim handed a newer flag used to block every tool call.
73
+ - Windows without Git Bash: Claude Code runs hooks under PowerShell, where the emitted command was a parse error. The emit now detects Git Bash and writes a PowerShell form when it is absent; `doctor` and `status` warn when the machine changes after the emit.
74
+ - `CLAUDE_CONFIG_DIR` is honoured. A global hook stands down only for a live project install in `$CLAUDE_PROJECT_DIR` whose settings file still runs the same hook, never for a parent folder, another global install, a deactivated install or a manifest alone.
75
+ - The destructive-git gate also covers `clean --force`, `restore`, `push --delete` and `:branch`, `checkout -f`, `switch -f`, `stash drop/clear`, `worktree remove --force`, `reflog expire` and `gc --prune`, in the hook, the OpenCode guard and OpenCode's permissions.
76
+ - OpenCode: the guard checks every path an `apply_patch` touches (the only edit tool of gpt-* models) and scans written content for secrets; per-repo builds honour `--footprint lean`; plugins, workflows and themes are claimed on create, so a same-named user file survives emit and uninstall; a one-line managed `AGENTS.md` stops OpenCode loading the harness a second time through `~/.claude/CLAUDE.md`; a shadowed install under `OPENCODE_CONFIG_DIR` is reported. The dead `permission.ask` loop exemption is removed; pushes to `loop/*` are allowed by a static rule, and force, delete, an extra refspec and commits still ask.
77
+ - Settings are read back per host, so `rebuild-all`, `upgrade` and the console keep an OpenClaude per-repo install's theme, posture, packs and exclusions, also in a repo shared with another host.
78
+ - Uninstalling one host from a shared repo hands the root marker to the surviving host instead of deleting it; `migrate` reads the settings file the install really wired.
79
+ - `learn` cannot recurse through `claude -p`, reads nothing when `GENESEED_LLM` is unset, and returns at once on Bob, which sends no transcript.
80
+ - `learn` on `SubagentStop` reads the subagent's own transcript (`agent_transcript_path`); it used to distil a lesson from the main session's. When the transcript cannot be read, or lags behind the final turn, it falls back to or appends `last_assistant_message`.
81
+
11
82
  ## [3.20.0] — 2026-10-09
12
83
 
13
84
  ### Changed
package/README.md CHANGED
@@ -90,6 +90,14 @@ npm install -g geneseed # then plain `geneseed <command>` from any directo
90
90
  npm install -g geneseed@latest # …and that is also the update
91
91
  ```
92
92
 
93
+ Already in Claude Code? Add the plugin marketplace and let Claude run the install:
94
+
95
+ ```
96
+ /plugin marketplace add Arylmera/Geneseed
97
+ /plugin install geneseed@geneseed
98
+ /geneseed:setup
99
+ ```
100
+
93
101
  Every other route (Claude Code, Bob, OpenClaude, plain `AGENT.md`, per-repo installs) is in the **[install guide](docs/guides/install.md)**.
94
102
 
95
103
  ### 🧬 The long way — a git checkout
@@ -238,7 +246,7 @@ is **identical on every host**. What differs is how much of it the host can
238
246
  | **Colour themes** | ✅ full palette | ➖ | ➖ | ➖ |
239
247
  | **LSP · workflow runner · primary-agent · `/`-commands** | ✅ | ➖ | ➖ | ➖ |
240
248
 
241
- <sub>✅ native support · ⚙️ OpenCode plugin · 🪝 `settings.json` hook · 📄 carried by preamble prose only · ➖ no host mechanism (harness discipline still applies) · ¹ Bob's own hook contract: global hooks in `~/.bob/settings/settings.json`, Claude's event names but stdout ignored on `PreToolUse` — a refusal is **exit code 2**. So Laws I/IV exit 2, the consent rules are a stderr line, `SessionStart` context is plain stdout, `Stop` runs learn; `SubagentStop`/`PreCompact` are not Bob events and are not written. Unverified live (no Bob install on the authoring machine); the harness still holds via the rules preamble. · ² OpenCode's `tool.execute.before` can only allow or throw, with no "ask the user" tier, so the rule gate is a one-shot speed bump there rather than a prompt. · ³ OpenClaude is a Claude Code fork: Claude's hook groups and verdicts verbatim, in `~/.openclaude/settings.json` (global) or `.openclaude/settings.local.json` (per repo). Unverified live (no OpenClaude install on the authoring machine).</sub>
249
+ <sub>✅ native support · ⚙️ OpenCode plugin · 🪝 `settings.json` hook · 📄 carried by preamble prose only · ➖ no host mechanism (harness discipline still applies) · ¹ Bob's own hook contract: global hooks in `~/.bob/settings/settings.json`, Claude's event names but stdout ignored on `PreToolUse` — a refusal is **exit code 2**. So Laws I/IV, rigor-5 (External Gate) and ops-2 (a recursive scan of a whole filesystem) exit 2, the consent rules are a stderr line, `SessionStart` context is plain stdout, `Stop` runs learn; `SubagentStop`/`PreCompact` are not Bob events and are not written. Unverified live (no Bob install on the authoring machine); the harness still holds via the rules preamble. · ² OpenCode's `tool.execute.before` can only allow or throw, with no "ask the user" tier, so the rule gate is a one-shot speed bump there rather than a prompt. · ³ OpenClaude is a Claude Code fork: Claude's hook groups and verdicts verbatim, in `~/.openclaude/settings.json` (global) or `.openclaude/settings.local.json` (per repo). Unverified live (no OpenClaude install on the authoring machine).</sub>
242
250
 
243
251
  **Reading the matrix.** Everything above the divider is at full parity — no host
244
252
  drops an Agent, Skill, or the memory convention. The asymmetry is entirely in
package/SHIPPED.md CHANGED
@@ -20,7 +20,7 @@ historical references, not files distributed with the repo.
20
20
  > **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
21
21
  > human-readable label only. The canonical identity of an installed harness is the
22
22
  > **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
23
- > string. Current label: **3.20.0**.
23
+ > string. Current label: **4.1.0**.
24
24
 
25
25
  ## Capabilities
26
26
 
@@ -99,16 +99,22 @@ What the global emit writes into `~/.bob/settings/settings.json`:
99
99
  | event | verb | how the verdict travels |
100
100
  | --- | --- | --- |
101
101
  | `SessionStart` | `context --host bob` | plain stdout, as on Claude |
102
- | `PreToolUse` | `tool-gate --host bob` — one group, no matcher | Laws I/IV: **exit 2**, reason on stderr; process 1/5: a stderr line, exit 0 |
102
+ | `PreToolUse` | `tool-gate --host bob` — one group, no matcher | Laws I/IV, rigor-5 and ops-2: **exit 2**, reason on stderr; process 1/5: a stderr line, exit 0 |
103
103
  | `Stop` | `learn` | never a verdict |
104
104
 
105
- No matcher on `PreToolUse` because Bob's tool names are undocumented (its IDE and its
106
- Shell differ); `tool-gate` reads the payload's shape instead — a `command` field gets
107
- the git checks, a path gets the rule checks — and defers on anything it does not
108
- recognise. Bob has **no "ask the user" tier**, so the two Laws that admit no judgement
109
- call are refused outright and the two consent rules become a warning. Because that
110
- warning is all process 5 can be here, the `process` pack toggle changes nothing in
111
- Bob's hooks: the one group only ever exits 2 for Laws I and IV, which every build carries.
105
+ No matcher on `PreToolUse`, deliberately: Bob's tools page already drifts from its own
106
+ changelog (`web_fetch`, the 2.1.0 Office/IBM-docs tools are undocumented there), so an
107
+ allow-list built from it would fail open on the next tool Bob adds, and a deny-list needs a
108
+ regex dialect (lookahead) the docs never confirm — bob-verdict.md O1 rates the matcher's own
109
+ upside "low, performance only", not worth either risk. `tool-gate` reads the payload's shape
110
+ instead — a `command` field gets the git checks, a path gets the rule checks — and defers on
111
+ anything it does not recognise. Bob has **no "ask the user" tier**, so the Laws and checks
112
+ that admit no judgement call — Laws I and IV, rigor-5 (External Gate, when a project
113
+ opts in) and ops-2 (Commands Must Return: a recursive scan rooted at a whole filesystem) —
114
+ are refused outright, and the two consent rules become a warning. Because that
115
+ warning is all process 5 can be here, the `process` pack toggle changes nothing in Bob's
116
+ hooks: the one group only ever exits 2 for Laws I and IV, rigor-5 and ops-2, which every build
117
+ carries.
112
118
 
113
119
  **Migration.** A re-emit over an older install unwires Geneseed's groups from the flat
114
120
  `~/.bob/settings.json` (your own keys there are kept) and writes the new set to the
@@ -140,10 +146,23 @@ MCP servers are wired at runtime (web console, listed by `geneseed mcp`) into th
140
146
  `~/.bob/settings/mcp.json` global — never `settings.json`, which Bob does not read
141
147
  servers from ([Bob docs](https://bob.ibm.com/docs/ide/configuration/mcp/mcp-in-bob)).
142
148
 
149
+ ## Slash commands
150
+
151
+ Bob documents `.bob/commands/*.md` (project) and `~/.bob/commands/*.md` (global), with
152
+ `description` frontmatter (bob-docs.md §6 — no use for `argument-hint` yet: no Geneseed
153
+ skill takes positional arguments). Geneseed writes one command per merged skill's OLD name
154
+ (`/old-name`, the same alias problem OpenCode's `command/` layer solves) and one per
155
+ genuinely user-only skill (host-compat O2/R1): Bob's skills page documents only
156
+ `name`/`description`, no model-invocation key, so a user-only skill gets **no** SKILL.md on
157
+ Bob at all — `/its-name` is the only way to run it, and the model never sees it in its
158
+ `use_skill` catalogue. Claim-on-create and manifest-owned like every other native file;
159
+ uninstall removes them the same way.
160
+
143
161
  ## What Bob does not get
144
162
 
145
163
  Bob rides the Claude engine, so — like Claude — it has no OpenCode-only extras:
146
- colour themes, JS plugins, the saved-workflow runner, the primary-agent
147
- orchestrator, LSP enablement, or the `/`-command layer. Those capabilities have
148
- no analogue on a Claude-shaped host; the harness discipline is carried by the
149
- preamble and skills instead. See the [host matrix](../../README.md#-supported-harnesses).
164
+ colour themes, JS plugins, the saved-workflow runner, the primary-agent orchestrator,
165
+ LSP enablement, or OpenCode's full `/`-command layer (`COMMAND_SET`, `/ponytail`) — only
166
+ the narrow alias/user-only-skill commands above. Those capabilities have no analogue on a
167
+ Claude-shaped host; the harness discipline is carried by the preamble and skills instead.
168
+ See the [host matrix](../../README.md#-supported-harnesses).
@@ -14,7 +14,7 @@ bin/geneseed-hook.mjs …` — so at user scope they would fire and fail in ever
14
14
  repo that doesn't vendor the harness. A generated global install wires
15
15
  absolute-path hooks instead — `geneseed setup` does that for you.) It:
16
16
 
17
- - on **PreToolUse** (matcher `Bash`), runs `geneseed-hook git-gate` — the tool-boundary
17
+ - on **PreToolUse** (matcher `Bash|PowerShell`), runs `geneseed-hook git-gate` — the tool-boundary
18
18
  half of Doctrine process 5 (*consent before every commit and push*) and of the
19
19
  destructive-git Rule. Drop the `process` pack (or exclude process 5) and the hook stays,
20
20
  wired with `--no-consent`: commits and pushes pass without asking, while the
@@ -27,22 +27,27 @@ absolute-path hooks instead — `geneseed setup` does that for you.) It:
27
27
  next time regardless. Every other Bash command (and any unreadable payload) is
28
28
  deferred to the normal permission flow — the hook never blocks unrelated work.
29
29
 
30
- **Caveat — the GitHub MCP vector.** The hook matches the `Bash` tool only. If your
31
- session has the GitHub MCP server, an agent can commit/push via `push_files` /
32
- `create_or_update_file` / `merge_pull_request`, which bypass Bash. To gate those too,
33
- widen the matcher to `"Bash|mcp__github__.*"` — `git-gate` ignores any payload it
34
- doesn't recognise as a commit/push, so over-matching the tool surface is harmless.
35
- - on **PreToolUse** (matcher `Write|Edit|MultiEdit|NotebookEdit`), runs
30
+ **Caveat — the GitHub MCP vector.** The hook matches the `Bash` and `PowerShell` tools
31
+ only. If your session has the GitHub MCP server, an agent can commit/push via
32
+ `push_files` / `create_or_update_file` / `merge_pull_request`, which bypass both. To
33
+ gate those too, widen the matcher to `"Bash|PowerShell|mcp__github__.*"` —
34
+ `git-gate` ignores any payload it doesn't recognise as a commit/push, so
35
+ over-matching the tool surface is harmless.
36
+ - on **PreToolUse** (matcher `Write|Edit|NotebookEdit`), runs
36
37
  `geneseed-hook rule-gate` — refuses a write that would put a credential into a tracked
37
38
  file, and asks first on the first write to `user-rules.md` or a memory file (whether
38
39
  something you want kept is a rule or a fact is your call). Ordinary edits pass.
39
- - on **SessionStart** (`startup`/`clear` only — a fresh context), prints `AGENT.md`
40
- so the harness is in context from the first turn, then runs `geneseed-hook context` to
41
- **inject the project context** directly into the session — so the project-context load is
42
- by the hook, not left to the agent to remember (lazy entries are only listed). On
43
- **`resume`** it runs `geneseed-hook context` *without* re-`cat`ting `AGENT.md`: the resumed
44
- conversation already carries the harness, so re-injecting the static file each resume
45
- is pure token waste — only the (possibly changed) project context is refreshed;
40
+ (`MultiEdit` is not a Claude Code tool and is not in the matcher; the gate reads
41
+ `edits[].new_string` as a cheap guard regardless.)
42
+ - on **SessionStart**, every source (`startup`, `resume`, `clear`, `compact`, `fork`), runs
43
+ `geneseed-hook context` to **inject the project context** directly into the session — so
44
+ the project-context load is by the hook, not left to the agent to remember (lazy entries
45
+ are only listed). This is one matcher-less group, exactly as a generated install wires it:
46
+ a matcher list is one more place to miss a source (`fork` once was). A second group,
47
+ `startup`/`clear` only — a fresh context — prints `AGENT.md` so the vendored harness is in
48
+ context from the first turn. A generated install has no such group: its root file is
49
+ `CLAUDE.md`, which Claude Code loads by itself. A resumed or forked conversation already
50
+ carries the harness, so re-printing the static file there is pure token waste;
46
51
  - on **Stop**, runs `geneseed-hook learn` over the session to capture durable memories.
47
52
  Claude Code pipes the hook payload (with the session's `transcript_path`) to the
48
53
  command on stdin; `learn` reads that, flattens the transcript, distils new
@@ -59,12 +64,14 @@ absolute-path hooks instead — `geneseed setup` does that for you.) It:
59
64
  - on **PreCompact**, runs the same `geneseed-hook learn` once more. `learn` distils the
60
65
  *tail* of the transcript, so per-turn Stop is a sliding window over the session —
61
66
  and auto-compaction is the moment that window is about to be summarised away.
62
- This captures memory *before* the summary; the SessionStart `compact` matcher
63
- re-seeds context *after* it.
67
+ This captures memory *before* the summary; the SessionStart `context` hook (which
68
+ also fires on the `compact` source) re-seeds context *after* it.
64
69
 
65
70
  This step is **opt-in on a model CLI**: set `GENESEED_LLM` (e.g. `claude -p`,
66
71
  `llm`, `ollama run …`) for `learn` to actually distil. With it unset the hook is
67
- a harmless no-op that just prints the prompt. Geneseed never embeds an API key.
72
+ a harmless no-op — it returns immediately rather than reading the transcript for
73
+ nothing; running `learn` by hand with notes piped in still prints the prompt it
74
+ would have sent. Geneseed never embeds an API key.
68
75
  If your bundle's `memory/` is not at `./memory` or `./Harness/memory`, point at
69
76
  it with `GENESEED_MEMORY=/abs/path/to/memory`.
70
77
 
@@ -2,13 +2,13 @@
2
2
  "hooks": {
3
3
  "PreToolUse": [
4
4
  {
5
- "matcher": "Bash",
5
+ "matcher": "Bash|PowerShell",
6
6
  "hooks": [
7
7
  { "type": "command", "command": "node bin/geneseed-hook.mjs git-gate" }
8
8
  ]
9
9
  },
10
10
  {
11
- "matcher": "Write|Edit|MultiEdit|NotebookEdit",
11
+ "matcher": "Write|Edit|NotebookEdit",
12
12
  "hooks": [
13
13
  { "type": "command", "command": "node bin/geneseed-hook.mjs rule-gate" }
14
14
  ]
@@ -16,16 +16,14 @@
16
16
  ],
17
17
  "SessionStart": [
18
18
  {
19
- "matcher": "startup|clear",
20
19
  "hooks": [
21
- { "type": "command", "command": "node -e \"process.stdout.write(require('fs').readFileSync('AGENT.md','utf8'))\"" },
22
20
  { "type": "command", "command": "node bin/geneseed-hook.mjs context" }
23
21
  ]
24
22
  },
25
23
  {
26
- "matcher": "resume|compact",
24
+ "matcher": "startup|clear",
27
25
  "hooks": [
28
- { "type": "command", "command": "node bin/geneseed-hook.mjs context" }
26
+ { "type": "command", "command": "node -e \"process.stdout.write(require('fs').readFileSync('AGENT.md','utf8'))\"" }
29
27
  ]
30
28
  }
31
29
  ],
@@ -12,14 +12,21 @@ There is nothing to install by hand: `geneseed setup` (or `geneseed-build --emit
12
12
  openclaude` / `--emit openclaude-global`) writes everything.
13
13
 
14
14
  **OpenClaude never reads `~/.claude`, a project `.claude/`, or `$CLAUDE_CONFIG_DIR`.**
15
- A Claude Code install is invisible to it, so it gets its own. The two coexist on one
16
- machine and in one repo without touching each other.
15
+ A Claude Code install is invisible to it, so it gets its own, and neither emit
16
+ overwrites the other's files. On one machine the two coexist cleanly. In one
17
+ repo they do not fully: OpenClaude also reads the root `CLAUDE.md`/`AGENTS.md`
18
+ a Claude Code or Bob per-repo install already wrote there, on top of its own
19
+ `.openclaude/CLAUDE.md` — both harnesses load, and there is no clean exclusion
20
+ (excluding the root file would hide your own content in it too). `geneseed status`
21
+ and `geneseed doctor` warn when this is happening in your repo.
17
22
 
18
23
  ## What the emit writes
19
24
 
20
25
  ### Per-repo (`--emit openclaude`)
21
26
 
22
- Everything lives under **`.openclaude/`**. Nothing is written at the repo root.
27
+ Everything lives under **`.openclaude/`**, except the `.geneseed-emit`/
28
+ `.geneseed-footprint` markers and `.mcp.json` below, which the driver writes at the
29
+ repo root like every other host.
23
30
 
24
31
  - `CLAUDE.md`: the harness preamble as a delimited **managed block**. OpenClaude
25
32
  loads the root `AGENTS.md`, or the root `CLAUDE.md` only when `AGENTS.md` is
@@ -47,8 +54,9 @@ global config file.
47
54
 
48
55
  ## Hooks
49
56
 
50
- The Claude group set, unchanged: `PreToolUse` (git gate on `Bash`, rule gate on
51
- writes), `SessionStart` (context on `startup|clear` and `resume|compact`), `Stop`,
57
+ The Claude group set, unchanged: `PreToolUse` (git gate on `Bash|PowerShell`, rule gate on
58
+ writes), `SessionStart` (context on every source — one matcher-less group covers
59
+ `startup`, `resume`, `clear`, `compact` and `fork` alike), `Stop`,
52
60
  `SubagentStop` and `PreCompact` (learn). OpenClaude reads Claude's
53
61
  `hookSpecificOutput.permissionDecision`, so the gates **ask** where Claude asks.
54
62
  The context hook carries `--host openclaude` for one reason. It tells the hook
@@ -64,6 +64,41 @@ export const MyPlugin = async ({ client, $, directory, worktree }) => ({
64
64
  **without** triggering a reply with `client.session.prompt({ path, body: { noReply: true, parts } })`.
65
65
  - **`$`** — a shell executor for running commands.
66
66
 
67
+ ### OpenCode 2 — a different plugin API
68
+
69
+ OpenCode 2 (the `@opencode/cli` package; V1 is still `opencode-ai`) still auto-loads every
70
+ `.js`/`.ts` in `plugins/` (or `plugin/`), but it accepts **only** a default-exported
71
+ definition: `export default { id, setup(ctx) }`. A V1 plugin fails to load with *"Plugin
72
+ must export a default definition with an id and an effect or setup function"*. Each one
73
+ is marked failed on its own and OpenCode carries on, so nothing warns you.
74
+
75
+ V1 (>= 1.3.4) also accepts an object default, `{ id, server }`, and each loader ignores
76
+ the other's key. So every Geneseed plugin exports **one object for both majors**:
77
+ `export default { id: "<file stem>", server: <V1 factory>, setup: <V2 setup> }`. On V1
78
+ older than 1.3.4 the plugins no longer load.
79
+
80
+ `setup(ctx)` gets domains instead of hooks returned in a map. Hooks register by name
81
+ (`ctx.tool.hook("execute.before")`, `ctx.session.hook("context" | "prompt" |
82
+ "compaction")`), tools through `ctx.tool.transform`, and events come from
83
+ `ctx.event.subscribe()` as `{ type, created, data }` (not `{ type, properties }`). What
84
+ changed for each plugin:
85
+
86
+ | V1 | V2 |
87
+ | --- | --- |
88
+ | `session.idle` | not published by core (deprecated). The turn ends with `session.execution.succeeded` / `failed` / `interrupted` |
89
+ | `experimental.chat.messages.transform` / `chat.system.transform` | `session.hook("context")`: mutable `system` parts and `LLM.Message` instances, on every request |
90
+ | `command.execute.before` | none. A markdown command's expanded template arrives through `session.hook("prompt")` |
91
+ | `client.session.messages` | `ctx.session.context({ sessionID })`: `user` / `assistant` messages after the last compaction |
92
+ | throwaway session + `prompt` to distil | `ctx.generate.text({ prompt, model })`: one stateless call |
93
+ | a hook throws to block a tool | a throw is a **defect that fails the whole step**. The guard renames the call instead (see `geneseed-guard.js`) |
94
+ | `@opencode-ai/plugin` `tool()` helper | `@opencode/plugin`; a plain JSON-schema `input` needs no helper |
95
+ | `todo.updated`, session `summary` diffs | no V2 event. The activity cards show no todos or files on V2 |
96
+ | tool part carries its name | `session.tool.called` has no name; it rides on `session.tool.input.started` |
97
+ | tools `bash`, `apply_patch`, `task` | renamed `shell`, `patch`, `subagent` (the old ids are aliases). The guard's substring classes cover both spellings |
98
+
99
+ None of this is checked by a mock alone: `node tests/host_canary.mjs` starts the real V1 and V2
100
+ and fails on the host's own `failed to load plugin` log line.
101
+
67
102
  ## 4. Subagents, skills & commands
68
103
 
69
104
  Subdir names are **plural** canonically (`agents/`, `skills/`, `commands/`,
@@ -425,6 +425,109 @@ export const GeneseedActivity = async () => {
425
425
 
426
426
  // ponytail: OpenCode treats every export as a plugin and rejects non-functions; hang
427
427
  // the test helpers off the factory instead — reachable via import, invisible to the loader.
428
- Object.assign(GeneseedActivity, { sidOf, nextStatus, applyEvent, acctTotals, errStr, snip, safeName, activityDir, enabledFromFlag })
428
+ Object.assign(GeneseedActivity, { sidOf, nextStatus, applyEvent, acctTotals, errStr, snip, safeName, activityDir, enabledFromFlag, toV1Events })
429
429
 
430
- export default GeneseedActivity
430
+ // --- OpenCode 2 --------------------------------------------------------------
431
+ // `export default { id, server, setup }` serves V1 (>= 1.3.4) and V2 from one file — why, and
432
+ // each V1 hook's V2 counterpart: adapters/opencode/HOW-OPENCODE-LOADS.md.
433
+ //
434
+ // V2's event stream is a different vocabulary (`{ type, created, data }`, the turn as an
435
+ // execution, a message as steps, tool calls as called/success/failed), but every fact the
436
+ // cards show is still in it. Rather than a second reducer, `toV1Events` translates each
437
+ // V2 event into the V1 event(s) the handler above already reduces, so both majors write
438
+ // the same files. Not carried: todos (V2 publishes no todo event) and files-touched (the
439
+ // V1 session summary has no V2 event).
440
+
441
+ // A V2 SessionError ({ type, message }) in the V1 error union errStr reads.
442
+ const v1Error = (e) => (e ? { name: e.type, data: { message: e.message || e.type } } : undefined)
443
+
444
+ // V2 event -> the V1 events it means ([] for one the cards ignore). `tools` remembers each
445
+ // running call's name and start (callID -> { name, start }) so its end can say both.
446
+ // Pure but for `tools`; exported for tests.
447
+ function toV1Events(ev, tools) {
448
+ const d = ev?.data ?? {}
449
+ const sessionID = d.sessionID
450
+ if (!sessionID) return []
451
+ const t = typeof ev.created === "number" ? ev.created : Date.now()
452
+ const msg = (info) => ({ type: "message.updated", properties: { info: { role: "assistant", sessionID, ...info } } })
453
+ const part = (p) => ({ type: "message.part.updated", properties: { part: { sessionID, ...p } } })
454
+ switch (ev.type) {
455
+ case "session.created":
456
+ return [{ type: "session.created", properties: { info: {
457
+ id: sessionID, title: d.title, parentID: d.parentID, directory: d.location?.directory } } }]
458
+ case "session.renamed":
459
+ return [{ type: "session.updated", properties: { info: { id: sessionID, title: d.title } } }]
460
+ case "session.deleted":
461
+ return [{ type: "session.deleted", properties: { sessionID } }]
462
+ case "session.execution.started":
463
+ return [{ type: "session.status", properties: { sessionID, status: { type: "busy" } } }]
464
+ case "session.execution.succeeded":
465
+ return [{ type: "session.idle", properties: { sessionID } }]
466
+ case "session.execution.failed":
467
+ return [{ type: "session.error", properties: { sessionID, error: v1Error(d.error) } },
468
+ { type: "session.idle", properties: { sessionID } }]
469
+ case "session.execution.interrupted":
470
+ return [{ type: "session.status", properties: { sessionID, status: { type: "idle" } } }]
471
+ case "session.retry.scheduled":
472
+ return [{ type: "session.status", properties: { sessionID, status: { type: "retry" } } }]
473
+ case "session.step.started":
474
+ return [msg({ id: d.assistantMessageID, mode: d.agent, modelID: d.model?.id, time: { created: t } })]
475
+ case "session.step.ended":
476
+ return [msg({ id: d.assistantMessageID, cost: d.cost, tokens: d.tokens, time: { completed: t } })]
477
+ case "session.step.failed":
478
+ return [msg({ id: d.assistantMessageID, cost: d.cost, tokens: d.tokens, error: v1Error(d.error), time: { completed: t } })]
479
+ // The name rides only on `input.started`; `called` carries the input but no name.
480
+ case "session.tool.input.started":
481
+ tools.set(d.id, { name: d.name, start: undefined })
482
+ return []
483
+ case "session.tool.called": {
484
+ const name = tools.get(d.id)?.name ?? "tool"
485
+ tools.set(d.id, { name, start: t })
486
+ return [part({ type: "tool", callID: d.id, tool: name, messageID: d.assistantMessageID,
487
+ state: { status: "running", title: name, time: { start: t } } })]
488
+ }
489
+ case "session.tool.success":
490
+ case "session.tool.failed": {
491
+ const call = tools.get(d.id) ?? { name: "tool", start: undefined }
492
+ tools.delete(d.id)
493
+ const failed = ev.type === "session.tool.failed"
494
+ return [part({ type: "tool", callID: d.id, tool: call.name, messageID: d.assistantMessageID,
495
+ state: { status: failed ? "error" : "completed", title: call.name,
496
+ error: failed ? d.error?.message || d.error?.type : undefined, time: { start: call.start, end: t } } })]
497
+ }
498
+ case "session.reasoning.started":
499
+ case "session.reasoning.ended":
500
+ return [part({ type: "reasoning", id: `r:${d.assistantMessageID}:${d.ordinal}`, text: d.text ?? "" })]
501
+ case "session.text.ended":
502
+ return [part({ type: "text", id: `t:${d.assistantMessageID}:${d.ordinal}`, messageID: d.assistantMessageID, text: d.text })]
503
+ case "permission.asked":
504
+ return [{ type: "permission.updated", properties: { sessionID, id: d.id, title: d.message || d.action } }]
505
+ case "permission.replied":
506
+ return [{ type: "permission.replied", properties: { sessionID, permissionID: d.requestID } }]
507
+ default:
508
+ return []
509
+ }
510
+ }
511
+
512
+ async function setupV2(ctx) {
513
+ const hooks = await GeneseedActivity()
514
+ const tools = new Map()
515
+ // The user's prompt, as V1's chat.message hook received it.
516
+ await ctx.session.hook("prompt", async (event) => {
517
+ try {
518
+ await hooks["chat.message"]({ sessionID: event?.sessionID, messageID: event?.messageID },
519
+ { parts: [{ type: "text", text: event?.prompt?.text }] })
520
+ } catch (err) { log(`prompt hook failed: ${err?.message ?? err}`) }
521
+ })
522
+ const controller = new AbortController()
523
+ void (async () => {
524
+ for await (const ev of ctx.event.subscribe({ signal: controller.signal })) {
525
+ for (const event of toV1Events(ev, tools)) {
526
+ try { await hooks.event({ event }) } catch (err) { log(`event ${event.type} failed: ${err?.message ?? err}`) }
527
+ }
528
+ }
529
+ })().catch((err) => log(`event stream ended: ${err?.message ?? err}`))
530
+ return () => controller.abort()
531
+ }
532
+
533
+ export default { id: "geneseed-activity", server: GeneseedActivity, setup: setupV2 }
@@ -989,4 +989,99 @@ export const GeneseedContext = async (ctx) => {
989
989
  }
990
990
  }
991
991
 
992
- export default GeneseedContext
992
+ // ---- OpenCode 2 ----------------------------------------------------------------
993
+ // `export default { id, server, setup }` serves V1 (>= 1.3.4) and V2 from one file — why, and
994
+ // each V1 hook's V2 counterpart: adapters/opencode/HOW-OPENCODE-LOADS.md.
995
+ //
996
+ // V2 has no `messages.transform`: `session.hook("context")` is its stable per-request
997
+ // equivalent (the outgoing system + messages, mutable), so the invisible delivery needs
998
+ // no fallback detection there. Messages are `LLM.Message` class instances, not V1's
999
+ // `{info, parts}`. A V2 hook that throws is a defect that fails the whole request,
1000
+ // so every callback below swallows its own errors.
1001
+
1002
+ // Build a user message the request will accept: reuse the class of a message already in
1003
+ // it (LLM.Message validates on construction), else a plain object of the same shape.
1004
+ function v2UserMessage(messages, text) {
1005
+ const content = [{ type: "text", text }]
1006
+ const Ctor = messages.find((m) => m && m.constructor && m.constructor !== Object)?.constructor
1007
+ if (Ctor) { try { return new Ctor({ role: "user", content }) } catch {} }
1008
+ return { role: "user", content }
1009
+ }
1010
+ const v2HasMarker = (messages) => messages.some((m) =>
1011
+ (m?.content ?? []).some((p) => typeof p?.text === "string" && p.text.includes(MARKER)))
1012
+ const v2Model = (ref) => (ref?.providerID && ref?.id ? `${ref.providerID}/${ref.id}` : null)
1013
+
1014
+ async function setupV2(ctx) {
1015
+ const root = repoRoot({ worktree: ctx?.location?.project?.directory, directory: ctx?.location?.directory })
1016
+ const blockCache = { text: null, at: 0 }
1017
+ // Same scope rule as V1's skipSession: child/subagent sessions inherit the parent's
1018
+ // context, geneseed-* sessions stay clean. A failed lookup is not cached.
1019
+ const sessionSkip = new Map()
1020
+ async function skipSession(sid) {
1021
+ if (!sid) return false
1022
+ const hit = sessionSkip.get(sid)
1023
+ if (hit !== undefined) return hit
1024
+ try {
1025
+ const info = await ctx.session.get({ sessionID: sid })
1026
+ const skip = Boolean(info?.parentID) || String(info?.title ?? "").startsWith("geneseed-")
1027
+ sessionSkip.set(sid, skip)
1028
+ return skip
1029
+ } catch {
1030
+ return false
1031
+ }
1032
+ }
1033
+
1034
+ if (!VISIBLE) {
1035
+ await ctx.session.hook("context", async (event) => {
1036
+ if (INJECT_OFF) return
1037
+ try {
1038
+ if (await sovereignBypass(root)) return
1039
+ if (!Array.isArray(event?.messages) || v2HasMarker(event.messages)) return
1040
+ if (await skipSession(event.sessionID)) return
1041
+ const cached = await cachedBlockText(root, blockCache)
1042
+ if (!cached) return
1043
+ event.messages.unshift(v2UserMessage(event.messages, withModel(cached, v2Model(event.model) || envModelStr())))
1044
+ log("context hook: injected context invisibly")
1045
+ } catch (err) {
1046
+ log(`context hook error: ${err?.message ?? err}`)
1047
+ }
1048
+ })
1049
+ return
1050
+ }
1051
+
1052
+ // Visible delivery (GENESEED_CONTEXT_VISIBLE): one synthetic message when a session is
1053
+ // created, re-pushed into the compaction request so it outlives the summary.
1054
+ await ctx.session.hook("compaction", async (event) => {
1055
+ if (INJECT_OFF) return
1056
+ try {
1057
+ if (await sovereignBypass(root)) return
1058
+ if (!Array.isArray(event?.messages) || await skipSession(event.sessionID)) return
1059
+ const { block } = await resolveBlock(root)
1060
+ if (block) event.messages.unshift(v2UserMessage(event.messages, block.text))
1061
+ } catch (err) {
1062
+ log(`compaction error: ${err?.message ?? err}`)
1063
+ }
1064
+ })
1065
+ const controller = new AbortController()
1066
+ void (async () => {
1067
+ for await (const ev of ctx.event.subscribe({ signal: controller.signal })) {
1068
+ if (ev?.type !== "session.created" || INJECT_OFF) continue
1069
+ const d = ev.data ?? {}
1070
+ try {
1071
+ if (d.parentID || String(d.title ?? "").startsWith("geneseed-")) continue
1072
+ if (await sovereignBypass(root)) continue
1073
+ const { block } = await resolveBlock(root)
1074
+ if (!block) continue
1075
+ await ctx.session.synthetic({
1076
+ sessionID: d.sessionID, text: withModel(block.text, v2Model(d.model) || envModelStr()),
1077
+ description: "Geneseed project context", resume: false,
1078
+ })
1079
+ } catch (err) {
1080
+ log(`visible injection error: ${err?.message ?? err}`)
1081
+ }
1082
+ }
1083
+ })().catch((err) => log(`event stream ended: ${err?.message ?? err}`))
1084
+ return () => controller.abort()
1085
+ }
1086
+
1087
+ export default { id: "geneseed-context", server: GeneseedContext, setup: setupV2 }