@snarkipus/opencode-beads 0.9.4 → 1.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/README.md +85 -22
- package/package.json +14 -13
- package/src/agents.ts +82 -0
- package/src/context.ts +148 -0
- package/src/guard.ts +138 -0
- package/src/guidance.ts +53 -0
- package/src/host.ts +140 -0
- package/src/instructions.ts +49 -0
- package/src/plugin.ts +9 -9
- package/src/prime.ts +23 -12
- package/src/opencode-runtime.ts +0 -156
- package/src/plugin-core.ts +0 -347
- package/src/plugin-hooks.ts +0 -56
- package/src/vendor-adaptations.ts +0 -146
- package/src/vendor-provenance.ts +0 -252
- package/src/vendor.ts +0 -277
- package/vendor/agents/task-agent.md +0 -61
- package/vendor/commands/audit.md +0 -28
- package/vendor/commands/blocked.md +0 -15
- package/vendor/commands/close.md +0 -18
- package/vendor/commands/comments.md +0 -30
- package/vendor/commands/compact.md +0 -31
- package/vendor/commands/create.md +0 -19
- package/vendor/commands/decision.md +0 -111
- package/vendor/commands/delete.md +0 -32
- package/vendor/commands/dep.md +0 -101
- package/vendor/commands/epic.md +0 -27
- package/vendor/commands/export.md +0 -25
- package/vendor/commands/import.md +0 -18
- package/vendor/commands/init.md +0 -18
- package/vendor/commands/label.md +0 -33
- package/vendor/commands/list.md +0 -64
- package/vendor/commands/prime.md +0 -10
- package/vendor/commands/quickstart.md +0 -13
- package/vendor/commands/ready.md +0 -15
- package/vendor/commands/rename-prefix.md +0 -24
- package/vendor/commands/reopen.md +0 -22
- package/vendor/commands/restore.md +0 -27
- package/vendor/commands/search.md +0 -103
- package/vendor/commands/show.md +0 -17
- package/vendor/commands/stats.md +0 -17
- package/vendor/commands/sync.md +0 -17
- package/vendor/commands/template.md +0 -337
- package/vendor/commands/update.md +0 -24
- package/vendor/commands/version.md +0 -26
- package/vendor/commands/workflow.md +0 -58
- package/vendor/manifest.json +0 -168
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[Beads](https://github.com/gastownhall/beads) issue tracker integration for [OpenCode](https://opencode.ai).
|
|
4
4
|
|
|
5
|
-
`@snarkipus/opencode-beads` is a maintained OpenCode adapter that provides full `bd prime` context injection
|
|
5
|
+
`@snarkipus/opencode-beads` is a maintained OpenCode adapter that provides full `bd prime` context injection and a bounded task agent. It does not replace Beads: the installed `bd` CLI remains authoritative for issues, project initialization, skills, migrations, backups, and Dolt synchronization.
|
|
6
6
|
|
|
7
7
|
The project originated as [Josh Thomas's `opencode-beads`](https://github.com/joshuadavidthomas/opencode-beads) and continues under the MIT license. This fork is maintained by Matt Jackson and tracks reviewed upstream Beads releases while adapting their plugin artifacts to OpenCode's CLI-only execution model.
|
|
8
8
|
|
|
@@ -24,64 +24,127 @@ git init # only when the directory is not already a Git worktree
|
|
|
24
24
|
bd init
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
`bd init` owns project initialization and creates the canonical shared Beads skill at `.agents/skills/beads`, which OpenCode discovers. This plugin has no separate skill lifecycle and does not write startup files.
|
|
27
|
+
`bd init` owns project initialization and creates the canonical shared Beads skill at `.agents/skills/beads`, which OpenCode discovers. It also writes a minimal Beads section to `AGENTS.md` that points to `bd prime`, the same pointer Beads gives Claude Code in `CLAUDE.md`. This plugin has no separate skill lifecycle and does not write startup files.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
**Do not run `bd setup opencode`.** That recipe is for OpenCode without this plugin: because Beads assumes OpenCode has no hooks, it replaces the minimal pointer with the full command reference (about 2 kB more on every request). With this plugin, `bd prime` arrives through OpenCode's hooks instead, as it does for Claude Code. If you already ran it, `bd setup opencode --remove` deletes that section, or simply leave it: while prime is attached, the plugin replaces it in the request with a one-line pointer.
|
|
30
|
+
|
|
31
|
+
Installing the plugin gives OpenCode the same integration that `bd setup claude` gives Claude Code:
|
|
32
|
+
|
|
33
|
+
| `bd setup claude` (Claude Code) | This plugin (OpenCode 2) |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `SessionStart` hook runs `bd prime --hook-json`, also on resume and after compaction | The session context hook attaches `bd prime` to each request; it is refreshed after compaction |
|
|
36
|
+
| Minimal `CLAUDE.md` pointer to `bd prime` | `bd init`'s minimal `AGENTS.md` pointer; bd's full or Codex sections are reduced to a pointer while prime is attached |
|
|
37
|
+
| `--global` or project install | Global `~/.config/opencode/opencode.json` or project `opencode.json` `plugins` entry |
|
|
38
|
+
| `--remove` | Remove the `plugins` entry |
|
|
39
|
+
| `--check` | `opencode debug agents` lists `beads-task-agent`; a session's agent sees a `<beads-context>` block |
|
|
40
|
+
|
|
41
|
+
The [Claude Code parity matrix](docs/claude-code-parity.md) records every `bd setup claude` behavior, its OpenCode equivalent and live evidence, the OpenCode-only capabilities, and the deliberate divergences.
|
|
42
|
+
|
|
43
|
+
This plugin requires OpenCode 2 and `bd` 1.3.0 or later. Add the version-pinned plugin to `~/.config/opencode/opencode.json`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"plugins": ["@snarkipus/opencode-beads@1.0.0"]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Plugin options use the object form:
|
|
30
52
|
|
|
31
53
|
```json
|
|
32
54
|
{
|
|
33
|
-
"
|
|
55
|
+
"plugins": [
|
|
56
|
+
{
|
|
57
|
+
"package": "@snarkipus/opencode-beads@1.0.0",
|
|
58
|
+
"options": { "luna": true, "autoMigrate": false }
|
|
59
|
+
}
|
|
60
|
+
]
|
|
34
61
|
}
|
|
35
62
|
```
|
|
36
63
|
|
|
37
|
-
|
|
64
|
+
| Option | Default | Effect |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `luna` | `false` | Also register the `beads-task-agent-luna` profile. |
|
|
67
|
+
| `stealth` | `false` | Run `bd prime --stealth` (no git operations in the session-close protocol), like `bd setup claude --stealth`. `bd config set no-git-ops true` is the project-level equivalent. |
|
|
68
|
+
| `taskAgentSteps` | `100` | Hard cap on task-agent steps per invocation, a backstop for "one bead, then return". `0` removes the cap. |
|
|
69
|
+
| `refreshAfterIdleMinutes` | `30` | Re-run `bd prime` when a session resumes after this much inactivity, as Claude Code does on resume. `0` disables idle refresh. |
|
|
70
|
+
| `autoMigrate` | `false` | Let automatic `bd prime` open a store that may need a schema migration. See [Schema migration guard](#schema-migration-guard). |
|
|
71
|
+
|
|
72
|
+
OpenCode loads configuration changes automatically. Update the pin explicitly when upgrading; an unpinned package entry is less reproducible. OpenCode 1.x plugins are not supported; use 0.10.x or earlier there.
|
|
38
73
|
|
|
39
74
|
### 0.9.3 recovery notice
|
|
40
75
|
|
|
41
|
-
Version 0.9.3 contained command guidance from the withdrawn upstream Beads v1.2.1 release. This plugin did not bundle the `bd` binary or implement database migrations, but it could invoke a consumer-installed v1.2.1 binary through `bd prime`. Even one v1.2.1 execution could migrate the database. Upgrade the plugin package to 0.9.4; if v1.2.1 was installed and may have run, also follow the [upstream database recovery guidance](https://github.com/gastownhall/beads/blob/v1.2.2/docs/RECOVERY-1.2.1.md).
|
|
76
|
+
Version 0.9.3 contained command guidance from the withdrawn upstream Beads v1.2.1 release. This plugin did not bundle the `bd` binary or implement database migrations, but it could invoke a consumer-installed v1.2.1 binary through `bd prime`. Even one v1.2.1 execution could migrate the database. Upgrade the plugin package to 0.9.4 or later; if v1.2.1 was installed and may have run, also follow the [upstream database recovery guidance](https://github.com/gastownhall/beads/blob/v1.2.2/docs/RECOVERY-1.2.1.md).
|
|
42
77
|
|
|
43
78
|
## Context injection
|
|
44
79
|
|
|
45
|
-
The plugin runs full `bd prime`
|
|
80
|
+
The plugin runs full `bd prime` in the project directory and attaches its output to each model request through OpenCode's session context hook. The output is cached per session, so prime runs once per session rather than per request. It runs again after compaction (automatic or manual), when a session resumes after `refreshAfterIdleMinutes` of inactivity, and when the plugin reloads, mirroring the `startup`, `resume`, `clear`, and `compact` sources of Claude Code's `SessionStart` hook. Steady work keeps the same text, which keeps provider prompt caches warm. Attached context is not stored in the conversation, so compaction cannot drop it. Prime supplies the canonical current workflow, command guidance, and persistent memories; the plugin adds only compact OpenCode-specific safety and delegation guidance.
|
|
46
81
|
|
|
47
|
-
|
|
82
|
+
While prime is attached, bd's full-profile `AGENTS.md` section and its Codex section (the blocks `bd init` and `bd setup opencode` manage) only repeat it. The plugin replaces those marked blocks in the outgoing request with a one-line pointer, matching the minimal `CLAUDE.md` pointer Beads uses for Claude Code. Your `AGENTS.md` file and any text outside those markers are not changed, and nothing is replaced when prime is withheld or unavailable.
|
|
48
83
|
|
|
49
|
-
|
|
84
|
+
Known regular subagents remain excluded, including `explore` and `general`, while `beads-task-agent` and `beads-task-agent-luna` remain eligible. If agent lookup fails, context injection fails open to preserve compatibility with unknown custom primary agents.
|
|
50
85
|
|
|
51
|
-
|
|
86
|
+
If `bd` is unavailable or the project is uninitialized, nothing is added. If `bd prime` fails or times out, the agent gets a short `<beads-notice>` saying context is unavailable and to run `bd prime`, as Beads' Codex hooks report refresh failures. Either way the plugin retries after 30 seconds, and failures are also logged as `[opencode-beads]` warnings. The task agent retains a bounded standalone quick reference for `ready`, `show`, atomic claim, discovered follow-up, validate before closure, and close.
|
|
52
87
|
|
|
53
|
-
|
|
88
|
+
## Schema migration guard
|
|
54
89
|
|
|
55
|
-
|
|
90
|
+
Beads 1.3 migrates an embedded store in place, irreversibly, the first time any command opens it, and `bd --readonly` does not prevent that. Because this plugin runs `bd prime` automatically, it would otherwise be the unattended first command after you upgrade `bd`.
|
|
91
|
+
|
|
92
|
+
Before running prime, the plugin checks `bd version` and `bd where --json`, which never open the store, and reads `.beads/.local_version`. If the installed `bd` is older than 1.3.0, or the store was last opened by an older `bd`, the plugin withholds prime and gives the agent a notice telling it not to run `bd` and to ask you to act. To proceed, back up the `.beads` directory and run `bd migrate schema`; the plugin picks up the change on its next retry. Setting the `autoMigrate` option skips the store-version check.
|
|
93
|
+
|
|
94
|
+
`bd prime` itself does not update `.beads/.local_version`. A store migrated only by prime keeps the old marker until you run `bd migrate schema` (or any ordinary `bd` command), which is safe and idempotent.
|
|
95
|
+
|
|
96
|
+
## Beads workflows
|
|
97
|
+
|
|
98
|
+
The plugin registers no `/beads:*` commands. Earlier versions vendored upstream's Claude Code command templates; they duplicated `bd prime` and the canonical Beads skill that `bd init` installs at `.agents/skills/beads`, which OpenCode discovers natively. Ask the agent to work with Beads, load the skill with `@beads`, or use the `bd` CLI and the generated [Beads CLI reference](https://beads.gascity.com/cli-reference/index) directly.
|
|
99
|
+
|
|
100
|
+
## Agent configuration
|
|
101
|
+
|
|
102
|
+
Entries under `agents` in OpenCode configuration override the plugin's task-agent fields one by one, so a partial override (for example only `description`) keeps the bounded `system` prompt and subagent `mode`. Fields you set explicitly, such as `system`, `model`, or `permissions`, win. OpenCode V2 does not expose configuration-only agents to plugins, so the plugin no longer warns about overrides.
|
|
56
103
|
|
|
57
104
|
## Task agent
|
|
58
105
|
|
|
59
106
|
`beads-task-agent` supports read-only status and graph analysis or one-Bead completion. Analysis requests never mutate Beads. Completion requests inspect and process one caller-selected or highest-priority ready Bead, quarantine work discovered during that invocation, validate before closure, and return after closing or blocking the selected Bead.
|
|
60
107
|
|
|
61
|
-
Its
|
|
108
|
+
Its prompt and description are fork-owned. Upstream Beads ships a task agent only for Claude Code, as an autonomous multi-task loop; this adapter deliberately keeps the bounded one-Bead contract instead, so it no longer vendors that artifact.
|
|
109
|
+
|
|
110
|
+
OpenCode lets the plugin enforce parts of that contract, beyond what Claude Code's hooks can do:
|
|
111
|
+
|
|
112
|
+
- **No automatic commit, push, or sync.** The task agents' permissions require approval (`ask`) for `git commit`, `git push`, `bd dolt push`, `bd dolt pull`, and `bd sync`, on top of OpenCode's defaults. Compound commands are checked per command. A non-interactive run rejects them. Set the agent's `permissions` in OpenCode configuration to replace these rules.
|
|
113
|
+
- **Step cap.** `taskAgentSteps` (default 100) limits a worker invocation; on its final step OpenCode removes tools and asks for a summary.
|
|
114
|
+
- **Context in the worker.** The worker runs in its own child session and receives `bd prime` there too; Claude Code's `SessionStart` hooks do not reach subagents.
|
|
115
|
+
- **Beads state across compaction.** Compaction requests in sessions with Beads context ask the summarizer to keep bead IDs, claim and status changes, discovered follow-ups, blockers, and pending validation.
|
|
116
|
+
|
|
117
|
+
### Opt-in Luna Max profile
|
|
118
|
+
|
|
119
|
+
The default `beads-task-agent` leaves its model unset, so a subagent inherits the parent session's model. Set the plugin option `"luna": true` to also register `beads-task-agent-luna`, which uses `openai/gpt-6-luna#max` with exactly the same prompt, subagent mode, and default permissions. Per-agent overrides remain independent: customizations to `beads-task-agent` are not copied to Luna. Both profiles receive full prime context and retain the same exact-one-Bead workflow.
|
|
120
|
+
|
|
121
|
+
Ask the primary agent to delegate to `beads-task-agent-luna` by name through the `subagent` tool. Default delegation guidance still names `beads-task-agent`; enabling Luna does not reroute existing calls or change the primary agent's model. Remove the option to unregister the profile.
|
|
122
|
+
|
|
123
|
+
Luna requires access to `openai/gpt-6-luna` and its `max` variant in your OpenCode provider setup. The plugin does not install models, configure authentication, or silently fall back to another model.
|
|
62
124
|
|
|
63
|
-
##
|
|
125
|
+
## Tracking upstream Beads
|
|
64
126
|
|
|
65
|
-
|
|
127
|
+
The package vendors nothing from upstream. It depends on the installed `bd` CLI at runtime: `bd prime` supplies the current workflow, and the schema migration guard enforces the supported `bd` range.
|
|
66
128
|
|
|
67
|
-
|
|
129
|
+
[`scripts/bd-contract.ts`](scripts/bd-contract.ts) checks a real `bd` against everything the plugin relies on: version parsing, `bd where --json`, the `.beads/.local_version` behavior the guard depends on (written by `bd init`, untouched by `bd prime`, refreshed by `bd migrate schema`), and every command the task-agent prompt names. CI runs it against the validated release from `package.json` (`beads.validated`). The scheduled `beads-contract` workflow runs it daily against the latest stable Beads release, and fails if that release breaks the contract or is newer than the validated one. When a new release appears, review its upgrade notes, then update `beads.validated` and the compatibility table. The [artifact policy](docs/beads-artifact-policy.md) records why upstream artifacts are not vendored.
|
|
68
130
|
|
|
69
131
|
## Compatibility
|
|
70
132
|
|
|
71
133
|
| Component | Expected range | Validated baseline |
|
|
72
134
|
| --- | --- | --- |
|
|
73
|
-
| OpenCode |
|
|
74
|
-
| `bd` CLI | 1.0
|
|
75
|
-
| Bun | >=1.3.14 | 1.
|
|
135
|
+
| OpenCode | 2.0.26 through 2.x | 2.0.26 |
|
|
136
|
+
| `bd` CLI | 1.3.0 through 1.x | 1.3.1 |
|
|
137
|
+
| Bun (OpenCode host runtime) | >=1.3.14 | 1.4.2 |
|
|
76
138
|
|
|
77
|
-
The adapter
|
|
139
|
+
The adapter type-checks against `@opencode/plugin` 2.0.26 and declares it as an optional peer from 2.0.26 through 2.x; the import is type-only, so OpenCode's own runtime is used. See the [OpenCode V2 contract](docs/opencode-sdk-contract.md). The `bd` baseline was reviewed against Beads v1.3.1 (commit `c1c4b642ac1c08d8c828007a1c2f96e47e43ef7c`).
|
|
78
140
|
|
|
79
141
|
## Troubleshooting
|
|
80
142
|
|
|
81
143
|
- **No Beads context:** Run `bd prime`. Install `bd` if missing or run `bd init` if the project has no Beads workspace.
|
|
144
|
+
- **Agent reports that `bd prime` failed or timed out:** Run `bd prime` in the project to see the error; the plugin retries every 30 seconds.
|
|
145
|
+
- **Agent reports a Beads notice instead of context:** The schema migration guard withheld prime. Back up `.beads`, run `bd migrate schema`, and continue; or upgrade `bd` if the notice says it is older than 1.3.0.
|
|
82
146
|
- **Canonical skill missing:** For a new workspace, run `bd init`. For an existing workspace, follow current upstream Beads guidance; this plugin does not install or repair skills.
|
|
83
|
-
- **
|
|
84
|
-
- **Unexpected definition:** Check OpenCode configuration for a colliding `beads:*` command or `beads-task-agent` definition. Explicit configuration wins.
|
|
147
|
+
- **Unexpected definition:** Check OpenCode configuration for a colliding `beads-task-agent` definition. Explicit configuration wins.
|
|
85
148
|
- **Regular subagent has no context:** This is intentional. Delegate Beads work to `beads-task-agent` or run the required `bd` command explicitly.
|
|
86
149
|
- **Behavior changed after upgrade:** Compare `opencode --version`, `bd version`, and `bun --version` with the compatibility table, then inspect [Beads releases](https://github.com/gastownhall/beads/releases) and this project's changelog.
|
|
87
150
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snarkipus/opencode-beads",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A maintained OpenCode integration for the Beads issue tracker
|
|
5
|
+
"description": "A maintained OpenCode V2 integration for the Beads issue tracker: bd prime context injection with a schema-migration guard, and a bounded task agent.",
|
|
6
6
|
"author": "Matt Jackson",
|
|
7
7
|
"contributors": [
|
|
8
8
|
{
|
|
@@ -35,11 +35,14 @@
|
|
|
35
35
|
".": {
|
|
36
36
|
"types": "./src/plugin.ts",
|
|
37
37
|
"import": "./src/plugin.ts"
|
|
38
|
+
},
|
|
39
|
+
"./server": {
|
|
40
|
+
"types": "./src/plugin.ts",
|
|
41
|
+
"import": "./src/plugin.ts"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"files": [
|
|
41
45
|
"src",
|
|
42
|
-
"vendor",
|
|
43
46
|
"README.md",
|
|
44
47
|
"LICENSE"
|
|
45
48
|
],
|
|
@@ -47,26 +50,24 @@
|
|
|
47
50
|
"test": "bun test",
|
|
48
51
|
"test:docs": "bun test tests/docs.test.ts",
|
|
49
52
|
"test:package": "bun scripts/test-package.ts",
|
|
50
|
-
"test:provenance": "bun test tests/provenance.test.ts",
|
|
51
53
|
"test:release": "bun test tests/release.test.ts",
|
|
52
|
-
"test:
|
|
54
|
+
"test:bd-contract": "bun scripts/bd-contract.ts",
|
|
53
55
|
"typecheck": "tsc --noEmit"
|
|
54
56
|
},
|
|
57
|
+
"beads": {
|
|
58
|
+
"minimum": "1.3.0",
|
|
59
|
+
"validated": "1.3.1"
|
|
60
|
+
},
|
|
55
61
|
"peerDependencies": {
|
|
56
|
-
"@opencode
|
|
57
|
-
"@opencode-ai/sdk": ">=1.18.3 <2"
|
|
62
|
+
"@opencode/plugin": ">=2.0.26 <3"
|
|
58
63
|
},
|
|
59
64
|
"peerDependenciesMeta": {
|
|
60
|
-
"@opencode
|
|
61
|
-
"optional": true
|
|
62
|
-
},
|
|
63
|
-
"@opencode-ai/sdk": {
|
|
65
|
+
"@opencode/plugin": {
|
|
64
66
|
"optional": true
|
|
65
67
|
}
|
|
66
68
|
},
|
|
67
69
|
"devDependencies": {
|
|
68
|
-
"@opencode
|
|
69
|
-
"@opencode-ai/sdk": "1.18.15",
|
|
70
|
+
"@opencode/plugin": "2.0.26",
|
|
70
71
|
"@types/bun": "1.3.4",
|
|
71
72
|
"@types/node": "^22.0.0",
|
|
72
73
|
"typescript": "5.9.3"
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin";
|
|
2
|
+
import { BEADS_TASK_AGENT_DESCRIPTION, BEADS_TASK_AGENT_PROMPT } from "./guidance";
|
|
3
|
+
|
|
4
|
+
export const BEADS_TASK_AGENT = "beads-task-agent";
|
|
5
|
+
export const BEADS_TASK_AGENT_LUNA = "beads-task-agent-luna";
|
|
6
|
+
export const BEADS_TASK_AGENTS: ReadonlySet<string> = new Set([
|
|
7
|
+
BEADS_TASK_AGENT,
|
|
8
|
+
BEADS_TASK_AGENT_LUNA,
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
/** Default hard cap on worker steps; a backstop for "one bead, then return", not a target. */
|
|
12
|
+
export const DEFAULT_TASK_AGENT_STEPS = 100;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enforce the no-automatic-commit/push/sync policy with OpenCode permissions rather than prompt
|
|
16
|
+
* text alone. Appended after OpenCode's defaults, so these `ask` rules win; compound commands are
|
|
17
|
+
* checked per command, so `cd x && git push` is caught too.
|
|
18
|
+
*/
|
|
19
|
+
export const TASK_AGENT_PERMISSIONS = [
|
|
20
|
+
{ action: "shell", resource: "git commit *", effect: "ask" },
|
|
21
|
+
{ action: "shell", resource: "git push *", effect: "ask" },
|
|
22
|
+
{ action: "shell", resource: "bd dolt push *", effect: "ask" },
|
|
23
|
+
{ action: "shell", resource: "bd dolt pull *", effect: "ask" },
|
|
24
|
+
{ action: "shell", resource: "bd sync *", effect: "ask" },
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
/** Routing for the opt-in Luna profile, `provider/model#variant` in OpenCode terms. */
|
|
28
|
+
export const LUNA_MODEL = { providerID: "openai", id: "gpt-6-luna", variant: "max" } as const;
|
|
29
|
+
|
|
30
|
+
export interface BeadsAgentDefinition {
|
|
31
|
+
description: string;
|
|
32
|
+
system: string;
|
|
33
|
+
model?: typeof LUNA_MODEL;
|
|
34
|
+
steps?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BeadsAgentOptions {
|
|
38
|
+
/** Register the Luna Max profile alongside the default task agent. */
|
|
39
|
+
luna?: boolean;
|
|
40
|
+
/** Hard cap on worker steps; 0 removes the cap. */
|
|
41
|
+
steps?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build the task-agent definitions from plugin options. */
|
|
45
|
+
export function beadsAgents(options: BeadsAgentOptions = {}): Record<string, BeadsAgentDefinition> {
|
|
46
|
+
const base: BeadsAgentDefinition = {
|
|
47
|
+
description: BEADS_TASK_AGENT_DESCRIPTION,
|
|
48
|
+
system: BEADS_TASK_AGENT_PROMPT,
|
|
49
|
+
};
|
|
50
|
+
const steps = options.steps ?? DEFAULT_TASK_AGENT_STEPS;
|
|
51
|
+
if (Number.isInteger(steps) && steps > 0) base.steps = steps;
|
|
52
|
+
const agents: Record<string, BeadsAgentDefinition> = { [BEADS_TASK_AGENT]: base };
|
|
53
|
+
if (options.luna) agents[BEADS_TASK_AGENT_LUNA] = { ...base, model: LUNA_MODEL };
|
|
54
|
+
return agents;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type AgentEditor = Parameters<Parameters<Plugin.Context["agent"]["transform"]>[0]>[0];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register task agents. `update` creates unknown agents from OpenCode defaults (including the
|
|
61
|
+
* default permission ruleset); user `agents` config entries still override these fields one by one.
|
|
62
|
+
*/
|
|
63
|
+
export function applyBeadsAgents(
|
|
64
|
+
editor: AgentEditor,
|
|
65
|
+
agents: Record<string, BeadsAgentDefinition>
|
|
66
|
+
): void {
|
|
67
|
+
for (const [id, definition] of Object.entries(agents)) {
|
|
68
|
+
editor.update(id, (agent) => {
|
|
69
|
+
// DeepMutable widens branded string IDs into object types; the runtime values are strings.
|
|
70
|
+
agent.name = id as unknown as typeof agent.name;
|
|
71
|
+
agent.description = definition.description;
|
|
72
|
+
agent.system = definition.system;
|
|
73
|
+
agent.mode = "subagent";
|
|
74
|
+
if (definition.model) agent.model = { ...definition.model } as unknown as typeof agent.model;
|
|
75
|
+
if (definition.steps !== undefined) agent.steps = definition.steps;
|
|
76
|
+
agent.permissions = [
|
|
77
|
+
...(agent.permissions ?? []),
|
|
78
|
+
...TASK_AGENT_PERMISSIONS.map((rule) => ({ ...rule })),
|
|
79
|
+
] as unknown as typeof agent.permissions;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { BEADS_TASK_AGENTS } from "./agents";
|
|
2
|
+
import { guardNotice, type GuardResult } from "./guard";
|
|
3
|
+
import { PrimeTimeoutError } from "./prime";
|
|
4
|
+
import { beadsGuidance, primeFailureNotice } from "./guidance";
|
|
5
|
+
|
|
6
|
+
/** Retry window for withheld or failed context, so model steps do not respawn `bd` each time. */
|
|
7
|
+
export const DEFAULT_CONTEXT_RETRY_MS = 30_000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Re-run prime when a session resumes after this much inactivity. Claude Code's SessionStart hook
|
|
11
|
+
* re-primes on resume; OpenCode has no resume event, so an idle gap stands in for it. Continuous
|
|
12
|
+
* work keeps identical prime text, which keeps provider prompt caches warm.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_IDLE_REFRESH_MS = 30 * 60_000;
|
|
15
|
+
|
|
16
|
+
export type AgentMode = "primary" | "subagent" | "all";
|
|
17
|
+
export type ContextAudience = "primary" | "task-agent";
|
|
18
|
+
export type ContextDiagnostic = "agent_lookup_failed" | "prime_failed" | "prime_timeout";
|
|
19
|
+
|
|
20
|
+
export interface ContextRuntime {
|
|
21
|
+
guard(directory: string): Promise<GuardResult>;
|
|
22
|
+
prime(directory: string): Promise<string>;
|
|
23
|
+
/** Resolve an agent's mode; undefined when the agent is unknown. */
|
|
24
|
+
agentMode(agentID: string): Promise<AgentMode | undefined>;
|
|
25
|
+
diagnose?(code: ContextDiagnostic, sessionID: string): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ContextControllerOptions {
|
|
29
|
+
retryMs?: number;
|
|
30
|
+
/** Refresh prime after this much session inactivity; 0 disables idle refresh. */
|
|
31
|
+
idleRefreshMs?: number;
|
|
32
|
+
now?: () => number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ContextController {
|
|
36
|
+
/** System text for one model request, or undefined when nothing should be added. */
|
|
37
|
+
systemFor(sessionID: string, agentID: string): Promise<string | undefined>;
|
|
38
|
+
/** Refresh prime on the next request, e.g. after compaction changed the session. */
|
|
39
|
+
invalidate(sessionID: string): void;
|
|
40
|
+
/** Drop all state for a deleted session. */
|
|
41
|
+
forget(sessionID: string): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type CacheEntry = { value: Promise<string | undefined>; expiresAt?: number; lastUsed: number };
|
|
45
|
+
|
|
46
|
+
/** Wrap prime output in the envelope and shared guidance for one audience. */
|
|
47
|
+
export function renderBeadsContext(prime: string, audience: ContextAudience): string {
|
|
48
|
+
return `<beads-context audience="${audience}">\n${prime}\n</beads-context>\n\n${beadsGuidance(audience)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Supply Beads context through the per-request session context hook.
|
|
53
|
+
*
|
|
54
|
+
* OpenCode V2 attaches system parts to every model request without storing them, so context
|
|
55
|
+
* survives compaction on its own. The rendered text is cached per session to keep it stable
|
|
56
|
+
* across requests; compaction invalidates it so persistent memories can refresh.
|
|
57
|
+
*/
|
|
58
|
+
export function createContextController(
|
|
59
|
+
runtime: ContextRuntime,
|
|
60
|
+
directory: string,
|
|
61
|
+
options: ContextControllerOptions = {}
|
|
62
|
+
): ContextController {
|
|
63
|
+
const retryMs = options.retryMs ?? DEFAULT_CONTEXT_RETRY_MS;
|
|
64
|
+
const idleRefreshMs = options.idleRefreshMs ?? DEFAULT_IDLE_REFRESH_MS;
|
|
65
|
+
const now = options.now ?? Date.now;
|
|
66
|
+
const cache = new Map<string, CacheEntry>();
|
|
67
|
+
const modes = new Map<string, Promise<AgentMode | undefined>>();
|
|
68
|
+
|
|
69
|
+
async function audienceFor(
|
|
70
|
+
sessionID: string,
|
|
71
|
+
agentID: string
|
|
72
|
+
): Promise<ContextAudience | undefined> {
|
|
73
|
+
if (BEADS_TASK_AGENTS.has(agentID)) return "task-agent";
|
|
74
|
+
|
|
75
|
+
let pending = modes.get(agentID);
|
|
76
|
+
if (!pending) {
|
|
77
|
+
pending = runtime.agentMode(agentID).catch(() => {
|
|
78
|
+
modes.delete(agentID);
|
|
79
|
+
runtime.diagnose?.("agent_lookup_failed", sessionID);
|
|
80
|
+
return undefined;
|
|
81
|
+
});
|
|
82
|
+
modes.set(agentID, pending);
|
|
83
|
+
}
|
|
84
|
+
// Unknown agents fail open so custom primary agents keep receiving context.
|
|
85
|
+
return (await pending) === "subagent" ? undefined : "primary";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function load(
|
|
89
|
+
sessionID: string,
|
|
90
|
+
audience: ContextAudience,
|
|
91
|
+
entry: CacheEntry
|
|
92
|
+
): Promise<string | undefined> {
|
|
93
|
+
const retryLater = (value: string | undefined) => {
|
|
94
|
+
entry.expiresAt = now() + retryMs;
|
|
95
|
+
return value;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let guard: GuardResult;
|
|
99
|
+
try {
|
|
100
|
+
guard = await runtime.guard(directory);
|
|
101
|
+
} catch {
|
|
102
|
+
return retryLater(undefined);
|
|
103
|
+
}
|
|
104
|
+
if (guard.kind !== "ready") return retryLater(guardNotice(guard));
|
|
105
|
+
|
|
106
|
+
let output: string;
|
|
107
|
+
try {
|
|
108
|
+
output = (await runtime.prime(directory)).trim();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const timedOut = error instanceof PrimeTimeoutError;
|
|
111
|
+
runtime.diagnose?.(timedOut ? "prime_timeout" : "prime_failed", sessionID);
|
|
112
|
+
return retryLater(primeFailureNotice(timedOut ? "timed out" : "failed"));
|
|
113
|
+
}
|
|
114
|
+
if (!output) return retryLater(undefined);
|
|
115
|
+
return renderBeadsContext(output, audience);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function dropSession(sessionID: string): void {
|
|
119
|
+
for (const key of cache.keys()) {
|
|
120
|
+
if (key.startsWith(`${sessionID}\0`)) cache.delete(key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
async systemFor(sessionID, agentID) {
|
|
126
|
+
const audience = await audienceFor(sessionID, agentID);
|
|
127
|
+
if (!audience) return undefined;
|
|
128
|
+
|
|
129
|
+
const key = `${sessionID}\0${audience}`;
|
|
130
|
+
const current = now();
|
|
131
|
+
const cached = cache.get(key);
|
|
132
|
+
const fresh = cached && (cached.expiresAt === undefined || cached.expiresAt > current);
|
|
133
|
+
const resumed = cached && idleRefreshMs > 0 && current - cached.lastUsed > idleRefreshMs;
|
|
134
|
+
if (cached && fresh && !resumed) {
|
|
135
|
+
cached.lastUsed = current;
|
|
136
|
+
return cached.value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const entry: CacheEntry = { value: Promise.resolve(undefined), lastUsed: current };
|
|
140
|
+
entry.value = load(sessionID, audience, entry);
|
|
141
|
+
cache.set(key, entry);
|
|
142
|
+
return entry.value;
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
invalidate: dropSession,
|
|
146
|
+
forget: dropSession,
|
|
147
|
+
};
|
|
148
|
+
}
|
package/src/guard.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Oldest Beads release this adapter supports. */
|
|
5
|
+
export const MINIMUM_BD_VERSION = "1.3.0";
|
|
6
|
+
|
|
7
|
+
export type Version = readonly [number, number, number];
|
|
8
|
+
|
|
9
|
+
/** Outcome of checking whether automatic `bd prime` may open the project store. */
|
|
10
|
+
export type GuardResult =
|
|
11
|
+
| { kind: "ready" }
|
|
12
|
+
| { kind: "uninitialized" }
|
|
13
|
+
| { kind: "unavailable" }
|
|
14
|
+
| { kind: "unsupported"; installed: string }
|
|
15
|
+
| { kind: "migration-pending"; installed: string; store: string | undefined };
|
|
16
|
+
|
|
17
|
+
export interface GuardRuntime {
|
|
18
|
+
/** Run `bd` with arguments in the project directory and return stdout. */
|
|
19
|
+
bd(directory: string, args: readonly string[]): Promise<string>;
|
|
20
|
+
/** Read a UTF-8 file, resolving undefined when it does not exist. */
|
|
21
|
+
readText(path: string): Promise<string | undefined>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GuardOptions {
|
|
25
|
+
/** Let automatic prime apply pending schema migrations instead of withholding context. */
|
|
26
|
+
autoMigrate?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Parse the leading `major.minor.patch` triple; pre-release suffixes are ignored. */
|
|
30
|
+
export function parseVersion(text: string): Version | undefined {
|
|
31
|
+
const match = text.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
32
|
+
if (!match) return undefined;
|
|
33
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Compare two parsed versions; negative when `left` is older. */
|
|
37
|
+
export function compareVersions(left: Version, right: Version): number {
|
|
38
|
+
for (let index = 0; index < 3; index++) {
|
|
39
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
40
|
+
if (difference !== 0) return difference;
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatVersion(version: Version): string {
|
|
46
|
+
return version.join(".");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readTextIfPresent(path: string): Promise<string | undefined> {
|
|
50
|
+
try {
|
|
51
|
+
return await readFile(path, "utf8");
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decide whether automatic `bd prime` is safe without opening the store.
|
|
60
|
+
*
|
|
61
|
+
* Beads 1.3+ migrates an embedded store in place, irreversibly, on the first command that opens
|
|
62
|
+
* it, and `--readonly` does not prevent that. `bd version` and `bd where --json` do not open the
|
|
63
|
+
* store. `.beads/.local_version` records the newest binary that opened the store through an
|
|
64
|
+
* ordinary command (but not `bd prime`), so an older or missing value means a migration may be
|
|
65
|
+
* pending. `bd migrate schema` is idempotent and updates it, clearing a stale marker.
|
|
66
|
+
*/
|
|
67
|
+
export async function checkBeadsStore(
|
|
68
|
+
directory: string,
|
|
69
|
+
runtime: GuardRuntime,
|
|
70
|
+
options: GuardOptions = {}
|
|
71
|
+
): Promise<GuardResult> {
|
|
72
|
+
let installedOutput: string;
|
|
73
|
+
try {
|
|
74
|
+
installedOutput = await runtime.bd(directory, ["version"]);
|
|
75
|
+
} catch {
|
|
76
|
+
return { kind: "unavailable" };
|
|
77
|
+
}
|
|
78
|
+
const installed = parseVersion(installedOutput);
|
|
79
|
+
if (!installed) return { kind: "unavailable" };
|
|
80
|
+
const minimum = parseVersion(MINIMUM_BD_VERSION) as Version;
|
|
81
|
+
if (compareVersions(installed, minimum) < 0) {
|
|
82
|
+
return { kind: "unsupported", installed: formatVersion(installed) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let location: unknown;
|
|
86
|
+
try {
|
|
87
|
+
location = JSON.parse(await runtime.bd(directory, ["where", "--json"]));
|
|
88
|
+
} catch {
|
|
89
|
+
// `bd where` exits non-zero without a workspace; treat every failure as no usable store.
|
|
90
|
+
return { kind: "uninitialized" };
|
|
91
|
+
}
|
|
92
|
+
const beadsPath =
|
|
93
|
+
typeof location === "object" && location !== null && "path" in location
|
|
94
|
+
? (location as { path: unknown }).path
|
|
95
|
+
: undefined;
|
|
96
|
+
if (typeof beadsPath !== "string" || !beadsPath) return { kind: "uninitialized" };
|
|
97
|
+
|
|
98
|
+
if (options.autoMigrate) return { kind: "ready" };
|
|
99
|
+
|
|
100
|
+
let marker: string | undefined;
|
|
101
|
+
try {
|
|
102
|
+
marker = await runtime.readText(join(beadsPath, ".local_version"));
|
|
103
|
+
} catch {
|
|
104
|
+
marker = undefined;
|
|
105
|
+
}
|
|
106
|
+
const store = marker === undefined ? undefined : parseVersion(marker);
|
|
107
|
+
if (!store || compareVersions(store, installed) < 0) {
|
|
108
|
+
return {
|
|
109
|
+
kind: "migration-pending",
|
|
110
|
+
installed: formatVersion(installed),
|
|
111
|
+
store: store ? formatVersion(store) : undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { kind: "ready" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Build the guard runtime from a bounded `bd` runner and the local filesystem. */
|
|
118
|
+
export function createGuardRuntime(bd: GuardRuntime["bd"]): GuardRuntime {
|
|
119
|
+
return { bd, readText: readTextIfPresent };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Explain withheld context to the agent so it neither migrates the store nor guesses. */
|
|
123
|
+
export function guardNotice(result: GuardResult): string | undefined {
|
|
124
|
+
switch (result.kind) {
|
|
125
|
+
case "unsupported":
|
|
126
|
+
return `<beads-notice>
|
|
127
|
+
Beads workflow context was not loaded: the installed bd ${result.installed} is older than ${MINIMUM_BD_VERSION}, the minimum this OpenCode integration supports. Tell the user to upgrade bd before relying on Beads in this session.
|
|
128
|
+
</beads-notice>`;
|
|
129
|
+
case "migration-pending": {
|
|
130
|
+
const store = result.store ? `bd ${result.store}` : "an unknown bd version";
|
|
131
|
+
return `<beads-notice>
|
|
132
|
+
Beads workflow context was withheld: this project's Beads store was last opened by ${store}, and the installed bd ${result.installed} may apply irreversible schema migrations on first use. Do not run bd commands in this session. Tell the user to back up the .beads directory and then run \`bd migrate schema\`, or to set the opencode-beads plugin option "autoMigrate": true to allow automatic migration.
|
|
133
|
+
</beads-notice>`;
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|