@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.
- package/README.md +77 -34
- 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 -371
- 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/src/guidance.ts
ADDED
|
@@ -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
|
|
4
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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(
|
|
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(
|
|
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
|
|
38
|
-
const child = Bun.spawn(["bd",
|
|
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
|
|
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
|
|
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
|
|
162
|
+
return runBd(directory, ["prime", ...flags], options);
|
|
152
163
|
}
|
package/src/opencode-runtime.ts
DELETED
|
@@ -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
|
-
}
|