@snarkipus/opencode-beads 0.10.0 → 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.
Files changed (47) hide show
  1. package/README.md +77 -34
  2. package/package.json +14 -13
  3. package/src/agents.ts +82 -0
  4. package/src/context.ts +148 -0
  5. package/src/guard.ts +138 -0
  6. package/src/guidance.ts +53 -0
  7. package/src/host.ts +140 -0
  8. package/src/instructions.ts +49 -0
  9. package/src/plugin.ts +9 -9
  10. package/src/prime.ts +23 -12
  11. package/src/opencode-runtime.ts +0 -156
  12. package/src/plugin-core.ts +0 -371
  13. package/src/plugin-hooks.ts +0 -56
  14. package/src/vendor-adaptations.ts +0 -146
  15. package/src/vendor-provenance.ts +0 -252
  16. package/src/vendor.ts +0 -277
  17. package/vendor/agents/task-agent.md +0 -61
  18. package/vendor/commands/audit.md +0 -28
  19. package/vendor/commands/blocked.md +0 -15
  20. package/vendor/commands/close.md +0 -18
  21. package/vendor/commands/comments.md +0 -30
  22. package/vendor/commands/compact.md +0 -31
  23. package/vendor/commands/create.md +0 -19
  24. package/vendor/commands/decision.md +0 -111
  25. package/vendor/commands/delete.md +0 -32
  26. package/vendor/commands/dep.md +0 -101
  27. package/vendor/commands/epic.md +0 -27
  28. package/vendor/commands/export.md +0 -25
  29. package/vendor/commands/import.md +0 -18
  30. package/vendor/commands/init.md +0 -18
  31. package/vendor/commands/label.md +0 -33
  32. package/vendor/commands/list.md +0 -64
  33. package/vendor/commands/prime.md +0 -10
  34. package/vendor/commands/quickstart.md +0 -13
  35. package/vendor/commands/ready.md +0 -15
  36. package/vendor/commands/rename-prefix.md +0 -24
  37. package/vendor/commands/reopen.md +0 -22
  38. package/vendor/commands/restore.md +0 -27
  39. package/vendor/commands/search.md +0 -103
  40. package/vendor/commands/show.md +0 -17
  41. package/vendor/commands/stats.md +0 -17
  42. package/vendor/commands/sync.md +0 -17
  43. package/vendor/commands/template.md +0 -337
  44. package/vendor/commands/update.md +0 -24
  45. package/vendor/commands/version.md +0 -26
  46. package/vendor/commands/workflow.md +0 -58
  47. package/vendor/manifest.json +0 -168
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Fork-owned guidance for OpenCode: the shared context layer and the bounded task-agent prompt.
3
+ *
4
+ * Upstream Beads ships its task agent only for Claude Code, as an autonomous multi-task loop. This
5
+ * adapter deliberately diverges (one bead per invocation), so the prompt and description live here.
6
+ */
7
+
8
+ /** Shown when OpenCode lists subagents; describes the one-bead worker contract. */
9
+ export const BEADS_TASK_AGENT_DESCRIPTION =
10
+ "Bounded Beads worker: analyzes the issue graph read-only, or claims, completes, and closes or blocks exactly one bead per invocation.";
11
+
12
+ const BEADS_HOST_GUIDANCE = `## CLI Safety
13
+
14
+ There is no native \`bd\` or Beads MCP tool in OpenCode. Run \`bd\` through \`bash\`.
15
+ Treat the injected \`bd prime\` output as the canonical workflow reference. Use \`bd --help\` to discover commands, \`bd <command> --help\` for current syntax, and \`--json\` when structured output improves reliability. Distinguish command failures from output parsing failures.
16
+
17
+ Run relevant validation before closing an issue. Do not commit, push, or run Dolt synchronization automatically; do so only when current instructions or repository policy explicitly require it.`;
18
+
19
+ const BEADS_DELEGATION_GUIDANCE = `## Delegation
20
+
21
+ Delegate multi-command Beads work and issue-graph analysis to the \`beads-task-agent\` subagent through the \`subagent\` tool; perform only single atomic operations directly.`;
22
+
23
+ /** Fork-owned system prompt for the bounded one-bead task agent. */
24
+ export const BEADS_TASK_AGENT_PROMPT = `## Beads Task Agent
25
+
26
+ Full \`bd prime\` workflow context is injected with each request. Use \`bd\` via \`bash\`; rerun prime if context is missing or stale.
27
+
28
+ For status or graph analysis, never claim, update, close, or modify a bead. Return the analysis and stop.
29
+
30
+ For completion, process exactly one bead. Use a caller-supplied ID; otherwise choose one highest-priority ready bead. First run \`bd show <id> --json\`; confirm it is open, actionable, and appropriate; then claim with \`bd update <id> --claim --json\`. If an assigned bead is closed, blocked, unsuitable, missing, or otherwise not actionable, report and return without mutation or substitution.
31
+
32
+ Implement and validate it. Create discoveries with \`discovered-from:<current-id>\` and a parent only when structurally obvious. Never claim or implement work created this invocation; it may be eligible later. Close or block the current bead, report discoveries, and return without seeking more work. Return concise results, not raw JSON.`;
33
+
34
+ /** Shown instead of context when `bd prime` fails, as upstream's Codex hook reports refresh failures. */
35
+ export function primeFailureNotice(reason: "failed" | "timed out"): string {
36
+ return `<beads-notice>
37
+ Beads workflow context could not be loaded: \`bd prime\` ${reason}. Beads may still work; run \`bd prime\` to load the workflow and see the error, and tell the user if it keeps failing.
38
+ </beads-notice>`;
39
+ }
40
+
41
+ /** Added to compaction requests so the summary keeps the Beads work in flight. */
42
+ export const BEADS_COMPACTION_GUIDANCE = `<beads-compaction>
43
+ This session tracks work in Beads (bd). In the summary, preserve every bead ID mentioned, which bead is claimed or in progress and its current status, discovered follow-up beads and how they were linked, blockers, and validation still pending. Fresh \`bd prime\` context is attached again after compaction, so do not restate the Beads workflow itself.
44
+ </beads-compaction>`;
45
+
46
+ /** Build the single shared guidance layer for one injected audience. */
47
+ export function beadsGuidance(
48
+ audience: "primary" | "task-agent"
49
+ ): string {
50
+ const sections = [BEADS_HOST_GUIDANCE];
51
+ if (audience === "primary") sections.push(BEADS_DELEGATION_GUIDANCE);
52
+ return `<beads-guidance>\n${sections.join("\n\n")}\n</beads-guidance>`;
53
+ }
package/src/host.ts ADDED
@@ -0,0 +1,140 @@
1
+ import type { Plugin } from "@opencode/plugin";
2
+ import { applyBeadsAgents, beadsAgents } from "./agents";
3
+ import { BEADS_COMPACTION_GUIDANCE } from "./guidance";
4
+ import {
5
+ createContextController,
6
+ type AgentMode,
7
+ type ContextDiagnostic,
8
+ type ContextRuntime,
9
+ } from "./context";
10
+ import { checkBeadsStore, createGuardRuntime } from "./guard";
11
+ import { minimizeSystemParts } from "./instructions";
12
+ import { runBd, runBdPrime } from "./prime";
13
+
14
+ /** The slice of the OpenCode V2 plugin context this adapter uses. */
15
+ export type HostContext = Pick<Plugin.Context, "location" | "options" | "event"> & {
16
+ agent: Pick<Plugin.Context["agent"], "get" | "transform">;
17
+ session: Pick<Plugin.Context["session"], "hook">;
18
+ };
19
+
20
+ export interface HostDependencies {
21
+ /** Override process-backed Beads calls, mainly for tests. */
22
+ runtime?: Partial<Pick<ContextRuntime, "guard" | "prime">>;
23
+ /** Override the `bd prime` runner (receives extra flags such as `--stealth`). */
24
+ runPrime?: (directory: string, flags: readonly string[]) => Promise<string>;
25
+ /** Override the bounded `bd` runner used by the store guard. */
26
+ bd?: (directory: string, args: readonly string[]) => Promise<string>;
27
+ warn?: (message: string) => void;
28
+ diagnosticIntervalMs?: number;
29
+ now?: () => number;
30
+ retryMs?: number;
31
+ }
32
+
33
+ const DEFAULT_DIAGNOSTIC_INTERVAL_MS = 60_000;
34
+
35
+ function eventSessionID(event: unknown): string | undefined {
36
+ if (typeof event !== "object" || event === null) return undefined;
37
+ const data = (event as { data?: unknown }).data;
38
+ if (typeof data !== "object" || data === null) return undefined;
39
+ const sessionID = (data as { sessionID?: unknown }).sessionID;
40
+ return typeof sessionID === "string" ? sessionID : undefined;
41
+ }
42
+
43
+ /** Wire Beads context into an OpenCode V2 plugin context; returns the plugin cleanup. */
44
+ export async function setupBeads(
45
+ ctx: HostContext,
46
+ deps: HostDependencies = {}
47
+ ): Promise<() => void> {
48
+ const directory = ctx.location.directory;
49
+ const autoMigrate = ctx.options?.autoMigrate === true;
50
+ // Mirrors `bd setup claude --stealth`: prime without git operations in the session-close protocol.
51
+ const stealth = ctx.options?.stealth === true;
52
+ const now = deps.now ?? Date.now;
53
+ const warn = deps.warn ?? ((message: string) => console.warn(message));
54
+ const diagnosticIntervalMs = deps.diagnosticIntervalMs ?? DEFAULT_DIAGNOSTIC_INTERVAL_MS;
55
+ const diagnosticTimes = new Map<string, number>();
56
+
57
+ const guardRuntime = createGuardRuntime(deps.bd ?? ((cwd, args) => runBd(cwd, args)));
58
+ const runtime: ContextRuntime = {
59
+ guard: deps.runtime?.guard ?? ((cwd) => checkBeadsStore(cwd, guardRuntime, { autoMigrate })),
60
+ prime:
61
+ deps.runtime?.prime ??
62
+ ((cwd) => (deps.runPrime ?? ((dir, flags) => runBdPrime(dir, {}, flags)))(cwd, stealth ? ["--stealth"] : [])),
63
+ async agentMode(agentID): Promise<AgentMode | undefined> {
64
+ const response = await ctx.agent.get({ agentID });
65
+ return response.data.mode;
66
+ },
67
+ diagnose(code: ContextDiagnostic, sessionID: string) {
68
+ const key = `${code}:${sessionID}`;
69
+ const previous = diagnosticTimes.get(key);
70
+ const current = now();
71
+ if (previous !== undefined && current - previous < diagnosticIntervalMs) return;
72
+ diagnosticTimes.set(key, current);
73
+ try {
74
+ warn(`[opencode-beads] ${code} session=${sessionID} directory=${directory}`);
75
+ } catch {
76
+ // Diagnostics never affect model requests.
77
+ }
78
+ },
79
+ };
80
+
81
+ const stepsOption = ctx.options?.taskAgentSteps;
82
+ const agents = beadsAgents({
83
+ luna: ctx.options?.luna === true,
84
+ steps: typeof stepsOption === "number" && Number.isInteger(stepsOption) && stepsOption >= 0 ? stepsOption : undefined,
85
+ });
86
+ await ctx.agent.transform((editor) => applyBeadsAgents(editor, agents));
87
+
88
+ const idleMinutes = ctx.options?.refreshAfterIdleMinutes;
89
+ const controller = createContextController(runtime, directory, {
90
+ now: deps.now,
91
+ retryMs: deps.retryMs,
92
+ idleRefreshMs:
93
+ typeof idleMinutes === "number" && Number.isFinite(idleMinutes) && idleMinutes >= 0
94
+ ? idleMinutes * 60_000
95
+ : undefined,
96
+ });
97
+
98
+ // Sessions that have received Beads context; their compaction summaries should keep Beads state.
99
+ const beadsSessions = new Set<string>();
100
+
101
+ await ctx.session.hook("context", async (event) => {
102
+ const text = await controller.systemFor(event.sessionID, event.agent);
103
+ if (!text) return;
104
+ if (text.startsWith("<beads-context")) {
105
+ beadsSessions.add(event.sessionID);
106
+ // With prime attached, bd's full AGENTS.md sections only repeat it (Claude Code gets a pointer).
107
+ minimizeSystemParts(event.system);
108
+ }
109
+ event.system.push({ type: "text", text });
110
+ });
111
+
112
+ await ctx.session.hook("compaction", (event) => {
113
+ if (beadsSessions.has(event.sessionID)) {
114
+ event.system.push({ type: "text", text: BEADS_COMPACTION_GUIDANCE });
115
+ }
116
+ });
117
+
118
+ const abort = new AbortController();
119
+ void (async () => {
120
+ try {
121
+ for await (const event of ctx.event.subscribe({ signal: abort.signal })) {
122
+ const type = (event as { type?: unknown }).type;
123
+ const sessionID = eventSessionID(event);
124
+ if (!sessionID) continue;
125
+ if (type === "session.compaction.ended") controller.invalidate(sessionID);
126
+ else if (type === "session.deleted") {
127
+ controller.forget(sessionID);
128
+ beadsSessions.delete(sessionID);
129
+ for (const key of diagnosticTimes.keys()) {
130
+ if (key.endsWith(`:${sessionID}`)) diagnosticTimes.delete(key);
131
+ }
132
+ }
133
+ }
134
+ } catch {
135
+ // The stream ends on cleanup; lost events only delay a cache refresh.
136
+ }
137
+ })();
138
+
139
+ return () => abort.abort();
140
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Minimal Beads instructions while prime is attached, matching Claude Code's integration.
3
+ *
4
+ * `bd setup claude` writes only a short pointer into CLAUDE.md because its hooks run `bd prime`.
5
+ * OpenCode projects instead get bd's full-profile AGENTS.md section (and often a Codex section),
6
+ * which repeats what injected prime already provides on every request. When this plugin attaches
7
+ * prime, those bd-managed blocks are replaced in the outgoing system text only; files are untouched.
8
+ */
9
+
10
+ /** Replaces bd-managed blocks whose content duplicates injected prime. */
11
+ export const MINIMAL_BEADS_POINTER =
12
+ "Beads (bd) workflow context for this project is provided by the opencode-beads plugin in the <beads-context> block, from `bd prime`.";
13
+
14
+ // bd's full-profile integration block and its Codex setup block. Minimal-profile blocks are already
15
+ // equivalent to Claude Code's pointer and are left alone.
16
+ const DUPLICATE_BLOCKS: readonly RegExp[] = [
17
+ /<!-- BEGIN BEADS INTEGRATION\b[^>]*\bprofile:full\b[^>]*-->[\s\S]*?<!-- END BEADS INTEGRATION -->/g,
18
+ /<!-- BEGIN BEADS CODEX SETUP\b[^>]*-->[\s\S]*?<!-- END BEADS CODEX SETUP -->/g,
19
+ ];
20
+
21
+ /** Return `text` with duplicate bd blocks collapsed into one pointer, and how many were replaced. */
22
+ export function minimizeBeadsInstructions(text: string): { text: string; replaced: number } {
23
+ let replaced = 0;
24
+ let pointerPlaced = false;
25
+ let result = text;
26
+ for (const pattern of DUPLICATE_BLOCKS) {
27
+ result = result.replace(pattern, () => {
28
+ replaced++;
29
+ if (pointerPlaced) return "";
30
+ pointerPlaced = true;
31
+ return MINIMAL_BEADS_POINTER;
32
+ });
33
+ }
34
+ return { text: result, replaced };
35
+ }
36
+
37
+ /** Apply {@link minimizeBeadsInstructions} to each text system part in place. */
38
+ export function minimizeSystemParts(parts: Array<{ type: string; text?: string }>): number {
39
+ let replaced = 0;
40
+ for (const part of parts) {
41
+ if (part.type !== "text" || typeof part.text !== "string") continue;
42
+ const minimized = minimizeBeadsInstructions(part.text);
43
+ if (minimized.replaced > 0) {
44
+ part.text = minimized.text;
45
+ replaced += minimized.replaced;
46
+ }
47
+ }
48
+ return replaced;
49
+ }
package/src/plugin.ts CHANGED
@@ -1,12 +1,12 @@
1
- /** OpenCode adapter for the Beads issue tracker. */
1
+ /** OpenCode V2 adapter for the Beads issue tracker. */
2
2
 
3
- import type { Plugin } from "@opencode-ai/plugin";
4
- import { resolveProjectDirectory } from "./plugin-core";
5
- import { createBeadsHooks } from "./plugin-hooks";
6
- import { createOpenCodeRuntime } from "./opencode-runtime";
3
+ import type { Plugin } from "@opencode/plugin";
4
+ import { setupBeads } from "./host";
7
5
 
8
- export const BeadsPlugin: Plugin = async ({ client, directory, worktree }) => {
9
- const projectDirectory = resolveProjectDirectory(directory, worktree);
10
- const runtime = createOpenCodeRuntime(client);
11
- return createBeadsHooks(runtime, projectDirectory);
6
+ // A plain object satisfies Plugin.define's identity contract without a runtime import.
7
+ const BeadsPlugin: Plugin.Plugin = {
8
+ id: "opencode-beads",
9
+ setup: (ctx) => setupBeads(ctx),
12
10
  };
11
+
12
+ export default BeadsPlugin;
package/src/prime.ts CHANGED
@@ -4,15 +4,15 @@ export const DEFAULT_PRIME_TIMEOUT_MS = 10_000;
4
4
  export const DEFAULT_PRIME_DRAIN_TIMEOUT_MS = 1_000;
5
5
 
6
6
  export class PrimeTimeoutError extends Error {
7
- constructor(timeoutMs: number) {
8
- super(`bd prime timed out after ${timeoutMs}ms`);
7
+ constructor(timeoutMs: number, command = "bd prime") {
8
+ super(`${command} timed out after ${timeoutMs}ms`);
9
9
  this.name = "PrimeTimeoutError";
10
10
  }
11
11
  }
12
12
 
13
13
  export class PrimeProcessError extends Error {
14
- constructor(readonly exitCode: number, readonly stderr: string) {
15
- super(`bd prime exited with code ${exitCode}`);
14
+ constructor(readonly exitCode: number, readonly stderr: string, command = "bd prime") {
15
+ super(`${command} exited with code ${exitCode}`);
16
16
  this.name = "PrimeProcessError";
17
17
  }
18
18
  }
@@ -34,8 +34,8 @@ export interface PrimeExecutionOptions {
34
34
  scheduleDrainTimeout?: (callback: () => void, delayMs: number) => () => void;
35
35
  }
36
36
 
37
- function spawnPrime(directory: string, args: readonly string[]): PrimeProcess {
38
- const child = Bun.spawn(["bd", "prime", ...args], {
37
+ function spawnBd(directory: string, args: readonly string[]): PrimeProcess {
38
+ const child = Bun.spawn(["bd", ...args], {
39
39
  cwd: directory,
40
40
  stdin: "ignore",
41
41
  stdout: "pipe",
@@ -100,14 +100,15 @@ async function drainAfterKill(
100
100
  });
101
101
  }
102
102
 
103
- async function runPrimeAttempt(
103
+ async function runBdAttempt(
104
104
  directory: string,
105
105
  args: readonly string[],
106
106
  options: PrimeExecutionOptions = {}
107
107
  ): Promise<string> {
108
108
  const timeoutMs = options.timeoutMs ?? DEFAULT_PRIME_TIMEOUT_MS;
109
109
  const drainTimeoutMs = options.drainTimeoutMs ?? DEFAULT_PRIME_DRAIN_TIMEOUT_MS;
110
- const child = (options.spawn ?? spawnPrime)(directory, args);
110
+ const command = ["bd", ...args].join(" ");
111
+ const child = (options.spawn ?? spawnBd)(directory, args);
111
112
  const stdout = new Response(child.stdout).text();
112
113
  const stderr = new Response(child.stderr).text();
113
114
 
@@ -119,12 +120,12 @@ async function runPrimeAttempt(
119
120
  const cancelTimeout = (options.scheduleTimeout ?? scheduleTimeout)(() => {
120
121
  timedOut = true;
121
122
  terminateProcessTree(child);
122
- rejectTimeout(new PrimeTimeoutError(timeoutMs));
123
+ rejectTimeout(new PrimeTimeoutError(timeoutMs, command));
123
124
  }, timeoutMs);
124
125
 
125
126
  const completion = Promise.all([child.exited, stdout, stderr]).then(
126
127
  ([exitCode, output, errorOutput]) => {
127
- if (exitCode !== 0) throw new PrimeProcessError(exitCode, errorOutput);
128
+ if (exitCode !== 0) throw new PrimeProcessError(exitCode, errorOutput, command);
128
129
  return output;
129
130
  }
130
131
  );
@@ -143,10 +144,20 @@ async function runPrimeAttempt(
143
144
  }
144
145
  }
145
146
 
147
+ /** Run one bounded `bd` invocation in the project directory and return its stdout. */
148
+ export async function runBd(
149
+ directory: string,
150
+ args: readonly string[],
151
+ options: PrimeExecutionOptions = {}
152
+ ): Promise<string> {
153
+ return runBdAttempt(directory, args, options);
154
+ }
155
+
146
156
  /** Load the canonical Beads workflow and persistent project memories. */
147
157
  export async function runBdPrime(
148
158
  directory: string,
149
- options: PrimeExecutionOptions = {}
159
+ options: PrimeExecutionOptions = {},
160
+ flags: readonly string[] = []
150
161
  ): Promise<string> {
151
- return runPrimeAttempt(directory, [], options);
162
+ return runBd(directory, ["prime", ...flags], options);
152
163
  }
@@ -1,156 +0,0 @@
1
- import type { PluginInput } from "@opencode-ai/plugin";
2
- import type { Agent, SessionMessagesResponse } from "@opencode-ai/sdk";
3
- import type { PluginRuntime } from "./plugin-core";
4
- import { runBdPrime } from "./prime";
5
-
6
- export const DEFAULT_OPENCODE_REQUEST_TIMEOUT_MS = 5_000;
7
- export const DEFAULT_OPENCODE_DIAGNOSTIC_TIMEOUT_MS = 1_000;
8
-
9
- export class OpenCodeTimeoutError extends Error {
10
- constructor(readonly operation: string, readonly timeoutMs: number) {
11
- super(`OpenCode ${operation} timed out after ${timeoutMs}ms`);
12
- this.name = "OpenCodeTimeoutError";
13
- }
14
- }
15
-
16
- export interface OpenCodeRuntimeOptions {
17
- requestTimeoutMs?: number;
18
- diagnosticTimeoutMs?: number;
19
- prime?: PluginRuntime["prime"];
20
- }
21
-
22
- async function withDeadline<T>(
23
- operation: string,
24
- timeoutMs: number,
25
- execute: (signal: AbortSignal) => Promise<T>
26
- ): Promise<T> {
27
- const controller = new AbortController();
28
- let rejectTimeout: (error: OpenCodeTimeoutError) => void = () => {};
29
- const timeout = new Promise<never>((_resolve, reject) => {
30
- rejectTimeout = reject;
31
- });
32
- const timer = setTimeout(() => {
33
- const error = new OpenCodeTimeoutError(operation, timeoutMs);
34
- controller.abort(error);
35
- rejectTimeout(error);
36
- }, timeoutMs);
37
-
38
- try {
39
- return await Promise.race([execute(controller.signal), timeout]);
40
- } finally {
41
- clearTimeout(timer);
42
- }
43
- }
44
-
45
- function isRecord(value: unknown): value is Record<string, unknown> {
46
- return typeof value === "object" && value !== null;
47
- }
48
-
49
- function isSessionMessage(value: unknown): boolean {
50
- if (
51
- !isRecord(value) ||
52
- !isRecord(value.info) ||
53
- (value.info.role !== "user" && value.info.role !== "assistant") ||
54
- (value.info.agent !== undefined && typeof value.info.agent !== "string") ||
55
- (value.info.system !== undefined && typeof value.info.system !== "string") ||
56
- (value.info.model !== undefined &&
57
- (!isRecord(value.info.model) ||
58
- typeof value.info.model.providerID !== "string" ||
59
- typeof value.info.model.modelID !== "string"))
60
- ) {
61
- return false;
62
- }
63
- if (!Array.isArray(value.parts)) return false;
64
-
65
- return value.parts.every(
66
- (part) =>
67
- isRecord(part) &&
68
- typeof part.type === "string" &&
69
- (part.text === undefined || typeof part.text === "string") &&
70
- (part.synthetic === undefined || typeof part.synthetic === "boolean")
71
- );
72
- }
73
-
74
- function isAgent(value: unknown): boolean {
75
- return (
76
- isRecord(value) &&
77
- typeof value.name === "string" &&
78
- (value.mode === "subagent" || value.mode === "primary" || value.mode === "all")
79
- );
80
- }
81
-
82
- /** Adapt the official OpenCode client to the controller's small deterministic boundary. */
83
- export function createOpenCodeRuntime(
84
- client: PluginInput["client"],
85
- options: OpenCodeRuntimeOptions = {}
86
- ): PluginRuntime {
87
- const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_OPENCODE_REQUEST_TIMEOUT_MS;
88
- const diagnosticTimeoutMs =
89
- options.diagnosticTimeoutMs ?? DEFAULT_OPENCODE_DIAGNOSTIC_TIMEOUT_MS;
90
-
91
- return {
92
- async getMessages(projectDirectory, sessionID, limit) {
93
- const response = await withDeadline("session.messages", requestTimeoutMs, (signal) =>
94
- client.session.messages({
95
- path: { id: sessionID },
96
- query: { directory: projectDirectory, limit },
97
- signal,
98
- })
99
- );
100
- if (response.error !== undefined) throw response.error;
101
- if (!Array.isArray(response.data) || !response.data.every(isSessionMessage)) {
102
- throw new Error("OpenCode returned malformed session messages");
103
- }
104
- return response.data satisfies SessionMessagesResponse;
105
- },
106
-
107
- async getAgents(projectDirectory) {
108
- const response = await withDeadline("app.agents", requestTimeoutMs, (signal) =>
109
- client.app.agents({ query: { directory: projectDirectory }, signal })
110
- );
111
- if (response.error !== undefined) throw response.error;
112
- if (!Array.isArray(response.data) || !response.data.every(isAgent)) {
113
- throw new Error("OpenCode returned malformed agents");
114
- }
115
- return response.data satisfies Agent[];
116
- },
117
-
118
- async prompt(projectDirectory, sessionID, body) {
119
- const response = await withDeadline("session.prompt", requestTimeoutMs, (signal) =>
120
- client.session.prompt({
121
- path: { id: sessionID },
122
- query: { directory: projectDirectory },
123
- body,
124
- signal,
125
- })
126
- );
127
- if (response.error !== undefined) throw response.error;
128
- if (response.data === undefined) throw new Error("OpenCode returned no prompt result");
129
- },
130
-
131
- async prime(projectDirectory) {
132
- return (options.prime ?? runBdPrime)(projectDirectory);
133
- },
134
-
135
- async diagnose(diagnostic) {
136
- const extra =
137
- diagnostic.code === "config_collision"
138
- ? { surface: diagnostic.surface, names: diagnostic.names }
139
- : { sessionID: diagnostic.sessionID };
140
- const response = await withDeadline("app.log", diagnosticTimeoutMs, (signal) =>
141
- client.app.log({
142
- query: { directory: diagnostic.directory },
143
- body: {
144
- service: "opencode-beads",
145
- level: "warn",
146
- message: diagnostic.code,
147
- extra,
148
- },
149
- signal,
150
- })
151
- );
152
- if (response.error !== undefined) throw response.error;
153
- if (response.data === undefined) throw new Error("OpenCode returned no log result");
154
- },
155
- };
156
- }