@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.
Files changed (47) hide show
  1. package/README.md +85 -22
  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 -347
  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
@@ -1,347 +0,0 @@
1
- import type { Hooks } from "@opencode-ai/plugin";
2
- import type {
3
- Agent,
4
- SessionMessagesResponse,
5
- SessionPromptData,
6
- TextPart,
7
- TextPartInput,
8
- UserMessage,
9
- } from "@opencode-ai/sdk";
10
- import { PrimeTimeoutError } from "./prime";
11
- import { beadsGuidance, loadAgent, loadCommands } from "./vendor";
12
-
13
- const BEADS_TASK_AGENT = "beads-task-agent";
14
-
15
- type ChatMessageInput = Parameters<NonNullable<Hooks["chat.message"]>>[0];
16
- type SessionMessageResponse = SessionMessagesResponse[number];
17
- type SessionPromptBody = NonNullable<SessionPromptData["body"]>;
18
-
19
- export type ModelContext = NonNullable<ChatMessageInput["model"]>;
20
- export type MessageContext = Pick<ChatMessageInput, "sessionID" | "agent" | "model">;
21
- export type ContextSink = (context: string) => void | Promise<void>;
22
-
23
- // The controller only needs this projection; the OpenCode adapter validates the full response.
24
- export type SessionMessage = {
25
- info: Pick<SessionMessageResponse["info"], "role"> &
26
- Partial<Pick<UserMessage, "agent" | "model" | "system">>;
27
- parts?: ReadonlyArray<
28
- Pick<SessionMessageResponse["parts"][number], "type"> &
29
- Partial<Pick<TextPart, "text" | "synthetic">>
30
- >;
31
- };
32
-
33
- export type AgentInfo = Pick<Agent, "name" | "mode">;
34
-
35
- export type PromptBody = Omit<SessionPromptBody, "noReply" | "parts"> & {
36
- noReply: true;
37
- parts: Array<TextPartInput & { synthetic: true }>;
38
- };
39
-
40
- export interface PluginRuntime {
41
- getMessages(
42
- directory: string,
43
- sessionID: string,
44
- limit?: number
45
- ): Promise<ReadonlyArray<SessionMessage> | undefined>;
46
- getAgents(directory: string): Promise<ReadonlyArray<AgentInfo> | undefined>;
47
- prompt(directory: string, sessionID: string, body: PromptBody): Promise<void>;
48
- prime(directory: string): Promise<string>;
49
- diagnose(diagnostic: PluginDiagnostic): Promise<void>;
50
- }
51
-
52
- export type DiagnosticCode =
53
- | "agents_lookup_failed"
54
- | "config_collision"
55
- | "messages_lookup_failed"
56
- | "prime_failed"
57
- | "prime_timeout"
58
- | "prompt_failed";
59
-
60
- export type PluginDiagnostic =
61
- | {
62
- code: Exclude<DiagnosticCode, "config_collision">;
63
- directory: string;
64
- sessionID: string;
65
- }
66
- | {
67
- code: "config_collision";
68
- directory: string;
69
- surface: "agent" | "command";
70
- names: string[];
71
- };
72
-
73
- export interface ControllerOptions {
74
- diagnosticIntervalMs?: number;
75
- now?: () => number;
76
- }
77
-
78
- export type MutablePluginConfig = Parameters<NonNullable<Hooks["config"]>>[0];
79
-
80
- export interface BeadsController {
81
- onMessage(message: MessageContext, sink: ContextSink): Promise<void>;
82
- onCompacted(sessionID: string): Promise<void>;
83
- onSessionDeleted(sessionID: string): void;
84
- configure(config: MutablePluginConfig): Promise<void>;
85
- }
86
-
87
- type InjectionAudience = "primary" | "task-agent";
88
- const BEADS_CONTEXT_HEADER = /^<beads-context(?: audience="(primary|task-agent)")?>\n/;
89
-
90
- /** Recognize only the complete envelope emitted by this plugin. */
91
- export function isBeadsContextEnvelope(text: string): boolean {
92
- const trimmed = text.trim();
93
- return BEADS_CONTEXT_HEADER.test(trimmed) && trimmed.includes("\n</beads-context>");
94
- }
95
-
96
- function beadsContextAudience(text: string): InjectionAudience | undefined {
97
- const audience = text.trim().match(BEADS_CONTEXT_HEADER)?.[1];
98
- return audience === "primary" || audience === "task-agent" ? audience : undefined;
99
- }
100
-
101
- function injectionAudience(agentName: string | undefined): InjectionAudience {
102
- return agentName === BEADS_TASK_AGENT ? "task-agent" : "primary";
103
- }
104
-
105
- /** Resolve OpenCode project scope without falling back to the process directory. */
106
- export function resolveProjectDirectory(directory: string, worktree: string): string {
107
- if (directory.trim()) return directory;
108
- if (worktree.trim()) return worktree;
109
- throw new Error("OpenCode did not provide a project directory or worktree");
110
- }
111
-
112
- /** Select the newest eligible user context from OpenCode's oldest-first message list. */
113
- function latestSessionContext(messages: ReadonlyArray<SessionMessage> | undefined) {
114
- if (!messages) return undefined;
115
-
116
- for (let index = messages.length - 1; index >= 0; index--) {
117
- const message = messages[index];
118
- if (message?.info.role === "user" && message.info.model) {
119
- return { model: message.info.model, agent: message.info.agent };
120
- }
121
- }
122
-
123
- return undefined;
124
- }
125
-
126
- /** Create the stateful Beads behavior behind the OpenCode hook adapter. */
127
- export async function createBeadsController(
128
- runtime: PluginRuntime,
129
- directory: string,
130
- options: ControllerOptions = {}
131
- ): Promise<BeadsController> {
132
- const [loadedCommands, loadedAgents] = await Promise.all([loadCommands(), loadAgent()]);
133
- const commands = loadedCommands ?? {};
134
- const agents = loadedAgents ?? {};
135
- const injectedAudiences = new Map<string, Set<InjectionAudience>>();
136
- const injectionAttempts = new Map<string, { promise: Promise<void>; cancelled: boolean }>();
137
- const diagnosticTimes = new Map<string, number>();
138
- const diagnosticIntervalMs = options.diagnosticIntervalMs ?? 60_000;
139
- const now = options.now ?? Date.now;
140
-
141
- async function diagnose(diagnostic: PluginDiagnostic, key: string): Promise<void> {
142
- const currentTime = now();
143
- const previousTime = diagnosticTimes.get(key);
144
- if (previousTime !== undefined && currentTime - previousTime < diagnosticIntervalMs) return;
145
-
146
- diagnosticTimes.set(key, currentTime);
147
- void Promise.resolve()
148
- .then(() => runtime.diagnose(diagnostic))
149
- .catch(() => undefined);
150
- }
151
-
152
- async function diagnoseSession(
153
- code: Exclude<DiagnosticCode, "config_collision">,
154
- sessionID: string
155
- ): Promise<void> {
156
- await diagnose({ code, directory, sessionID }, `${code}:${sessionID}`);
157
- }
158
-
159
- async function shouldInject(
160
- agentName: string | undefined,
161
- sessionID: string
162
- ): Promise<boolean> {
163
- if (!agentName || agentName === BEADS_TASK_AGENT) return true;
164
-
165
- const availableAgents = await runtime.getAgents(directory).catch(async () => {
166
- await diagnoseSession("agents_lookup_failed", sessionID);
167
- return undefined;
168
- });
169
- const agent = availableAgents?.find((candidate) => candidate.name === agentName);
170
- return agent ? agent.mode === "primary" || agent.mode === "all" : true;
171
- }
172
-
173
- async function renderContext(
174
- sessionID: string,
175
- context?: { model?: ModelContext; agent?: string }
176
- ): Promise<string | undefined> {
177
- let output: string;
178
- try {
179
- output = (await runtime.prime(directory)).trim();
180
- } catch (error) {
181
- const code = error instanceof PrimeTimeoutError ? "prime_timeout" : "prime_failed";
182
- await diagnoseSession(code, sessionID);
183
- return undefined;
184
- }
185
- if (!output) return undefined;
186
-
187
- const audience = injectionAudience(context?.agent);
188
- return `<beads-context audience="${audience}">\n${output}\n</beads-context>\n\n${beadsGuidance(audience)}`;
189
- }
190
-
191
- function markInjected(sessionID: string, audience: InjectionAudience): void {
192
- const audiences = injectedAudiences.get(sessionID) ?? new Set<InjectionAudience>();
193
- audiences.add(audience);
194
- injectedAudiences.set(sessionID, audiences);
195
- }
196
-
197
- async function injectPrompt(
198
- sessionID: string,
199
- context?: { model?: ModelContext; agent?: string }
200
- ): Promise<boolean> {
201
- const rendered = await renderContext(sessionID, context);
202
- if (!rendered) return false;
203
-
204
- try {
205
- await runtime.prompt(directory, sessionID, {
206
- noReply: true,
207
- model: context?.model,
208
- agent: context?.agent,
209
- parts: [
210
- {
211
- type: "text",
212
- text: rendered,
213
- synthetic: true,
214
- },
215
- ],
216
- });
217
- return true;
218
- } catch {
219
- await diagnoseSession("prompt_failed", sessionID);
220
- return false;
221
- }
222
- }
223
-
224
- async function performInitialInjection(
225
- message: MessageContext,
226
- sink: ContextSink,
227
- isActive: () => boolean
228
- ): Promise<void> {
229
- if (!(await shouldInject(message.agent, message.sessionID))) return;
230
-
231
- const audience = injectionAudience(message.agent);
232
- const knownAudiences = injectedAudiences.get(message.sessionID);
233
- if (knownAudiences?.has(audience)) return;
234
-
235
- // On plugin reload there is no local audience state, so consult persisted envelopes.
236
- if (!knownAudiences) {
237
- try {
238
- const existing = await runtime.getMessages(directory, message.sessionID);
239
- const hasBeadsContext = existing?.some(
240
- (item) =>
241
- (item.info.system !== undefined &&
242
- beadsContextAudience(item.info.system) === audience) ||
243
- item.parts?.some(
244
- (part) =>
245
- part.type === "text" &&
246
- part.synthetic === true &&
247
- part.text !== undefined &&
248
- beadsContextAudience(part.text) === audience
249
- )
250
- );
251
- if (hasBeadsContext) {
252
- if (isActive()) markInjected(message.sessionID, audience);
253
- return;
254
- }
255
- } catch {
256
- await diagnoseSession("messages_lookup_failed", message.sessionID);
257
- // Message lookup is advisory; injection remains the safe fallback.
258
- }
259
- }
260
-
261
- const rendered = await renderContext(message.sessionID, message);
262
- if (!rendered) return;
263
-
264
- try {
265
- await sink(rendered);
266
- if (isActive()) markInjected(message.sessionID, audience);
267
- } catch {
268
- await diagnoseSession("prompt_failed", message.sessionID);
269
- }
270
- }
271
-
272
- return {
273
- async onMessage(message, sink) {
274
- const audience = injectionAudience(message.agent);
275
- if (injectedAudiences.get(message.sessionID)?.has(audience)) return;
276
-
277
- const attemptKey = `${message.sessionID}\0${audience}`;
278
- const pending = injectionAttempts.get(attemptKey);
279
- if (pending) return pending.promise;
280
-
281
- const attempt = { promise: Promise.resolve(), cancelled: false };
282
- attempt.promise = performInitialInjection(message, sink, () => !attempt.cancelled);
283
- injectionAttempts.set(attemptKey, attempt);
284
- try {
285
- await attempt.promise;
286
- } finally {
287
- if (injectionAttempts.get(attemptKey) === attempt) {
288
- injectionAttempts.delete(attemptKey);
289
- }
290
- }
291
- },
292
-
293
- async onCompacted(sessionID) {
294
- const messages = await runtime.getMessages(directory, sessionID, 50).catch(async () => {
295
- await diagnoseSession("messages_lookup_failed", sessionID);
296
- return undefined;
297
- });
298
- const context = latestSessionContext(messages);
299
- if (await shouldInject(context?.agent, sessionID)) {
300
- await injectPrompt(sessionID, context);
301
- }
302
- },
303
-
304
- onSessionDeleted(sessionID) {
305
- injectedAudiences.delete(sessionID);
306
- for (const [key, attempt] of injectionAttempts) {
307
- if (key.startsWith(`${sessionID}\0`)) {
308
- attempt.cancelled = true;
309
- injectionAttempts.delete(key);
310
- }
311
- }
312
- for (const key of diagnosticTimes.keys()) {
313
- if (key.endsWith(`:${sessionID}`)) diagnosticTimes.delete(key);
314
- }
315
- },
316
-
317
- async configure(config) {
318
- const commandCollisions = Object.keys(config.command ?? {})
319
- .filter(
320
- (name) =>
321
- Object.hasOwn(commands, name) && config.command?.[name] !== commands[name]
322
- )
323
- .sort();
324
- const agentCollisions = Object.keys(config.agent ?? {})
325
- .filter(
326
- (name) => Object.hasOwn(agents, name) && config.agent?.[name] !== agents[name]
327
- )
328
- .sort();
329
-
330
- await Promise.all(
331
- ([
332
- ["command", commandCollisions],
333
- ["agent", agentCollisions],
334
- ] as const).map(async ([surface, names]) => {
335
- if (names.length === 0) return;
336
- await diagnose(
337
- { code: "config_collision", directory, surface, names },
338
- `config_collision:${surface}:${names.join("\0")}`
339
- );
340
- })
341
- );
342
-
343
- config.command = { ...commands, ...config.command };
344
- config.agent = { ...agents, ...config.agent };
345
- },
346
- };
347
- }
@@ -1,56 +0,0 @@
1
- import type { Hooks } from "@opencode-ai/plugin";
2
- import type { Part } from "@opencode-ai/sdk";
3
- import {
4
- createBeadsController,
5
- isBeadsContextEnvelope,
6
- type PluginRuntime,
7
- } from "./plugin-core";
8
-
9
- /** Prevent the synthetic no-reply prompt from recursively triggering its own injection. */
10
- function isBeadsContextInjection(parts: ReadonlyArray<Part>): boolean {
11
- return parts.some(
12
- (part) =>
13
- part.type === "text" &&
14
- part.synthetic === true &&
15
- isBeadsContextEnvelope(part.text)
16
- );
17
- }
18
-
19
- /** Build OpenCode hooks around an explicit runtime boundary. */
20
- export async function createBeadsHooks(
21
- runtime: PluginRuntime,
22
- projectDirectory: string
23
- ): Promise<Hooks> {
24
- const controller = await createBeadsController(runtime, projectDirectory);
25
-
26
- return {
27
- "chat.message": async (input, output) => {
28
- if (isBeadsContextInjection(output.parts)) return;
29
-
30
- await controller.onMessage(
31
- {
32
- sessionID: input.sessionID,
33
- model: input.model,
34
- agent: input.agent,
35
- },
36
- (context) => {
37
- output.message.system = output.message.system
38
- ? `${output.message.system}\n\n${context}`
39
- : context;
40
- }
41
- );
42
- },
43
-
44
- event: async ({ event }) => {
45
- if (event.type === "session.compacted") {
46
- await controller.onCompacted(event.properties.sessionID);
47
- } else if (event.type === "session.deleted") {
48
- controller.onSessionDeleted(event.properties.info.id);
49
- }
50
- },
51
-
52
- config: async (config) => {
53
- await controller.configure(config);
54
- },
55
- };
56
- }
@@ -1,146 +0,0 @@
1
- interface Replacement {
2
- from: string;
3
- to: string;
4
- }
5
-
6
- const replacements: Readonly<Record<string, readonly Replacement[]>> = {
7
- "agents/task-agent.md": [
8
- { from: "Use the `ready` MCP tool to get unblocked tasks", to: "Run `bd ready --json` via bash to get unblocked tasks" },
9
- { from: "Use the `show` tool to get full task details", to: "Run `bd show <id> --json` via bash to get full task details" },
10
- { from: "Use the `claim` tool for atomic start-work semantics", to: "Run `bd update <id> --claim --json` via bash for atomic start-work semantics" },
11
- { from: "Use `create` tool to file new issues", to: "Run `bd create \"<title>\" ... --json` via bash to file new issues" },
12
- { from: "Use `dep` tool with `discovered-from` to link them", to: "Run `bd dep add <new-id> <current-id> --type discovered-from --json` via bash to link them" },
13
- { from: "Use `close` tool with a clear completion message", to: "Run `bd close <id> --reason \"<message>\" --json` via bash" },
14
- { from: "Always claim before working (MCP: `claim`; CLI: `--claim`) and close when done", to: "Always claim with `bd update <id> --claim --json` before working and close when done" },
15
- { from: "If blocked, use `update` to set status to `blocked` and explain why", to: "If blocked, run `bd update <id> --status blocked --json` and explain why" },
16
- {
17
- from: `# Available Tools
18
-
19
- Via beads MCP server:
20
- - \`ready\` - Find unblocked tasks
21
- - \`show\` - Get task details
22
- - \`claim\` - Atomically claim task for work
23
- - \`update\` - Update task status/fields
24
- - \`create\` - Create new issues
25
- - \`dep\` - Manage dependencies
26
- - \`close\` - Complete tasks
27
- - \`blocked\` - Check blocked issues
28
- - \`stats\` - View project stats`,
29
- to: `# Available Commands
30
-
31
- Run the \`bd\` CLI via bash. Use \`--json\` when structured output improves reliability:
32
- - \`bd ready --json\` - Find unblocked tasks
33
- - \`bd show <id> --json\` - Get task details
34
- - \`bd update <id> --claim --json\` - Atomically claim task for work
35
- - \`bd create ... --json\` - Create new issues
36
- - \`bd dep ... --json\` - Manage dependencies
37
- - \`bd close <id> --reason \"...\" --json\` - Complete tasks
38
- - \`bd blocked --json\` - Check blocked issues
39
- - \`bd stats --json\` - View project stats`,
40
- },
41
- ],
42
- "commands/close.md": [
43
- { from: "Use the beads MCP `close` tool to close the issue.", to: "Run `bd close <id> --reason \"<reason>\" --json` via bash to close the issue." },
44
- { from: "(use `ready` tool)", to: "(run `bd ready --json`)" },
45
- { from: "(use `create` tool with `discovered-from` link)", to: "(run `bd create ... --deps discovered-from:<id> --json`)" },
46
- ],
47
- "commands/create.md": [
48
- { from: "Use the beads MCP `create` tool to create the issue.", to: "Run `bd create \"<title>\" --type <type> --priority <priority> --description \"<description>\" --json` via bash." },
49
- ],
50
- "commands/init.md": [
51
- { from: "Use the beads MCP `init` tool with the prefix parameter (if provided) to set up a new beads database.", to: "Run `bd init [--prefix <prefix>]` via bash to set up a new beads database." },
52
- { from: "show project stats using the `stats` tool", to: "show project stats by running `bd stats --json`" },
53
- ],
54
- "commands/ready.md": [
55
- { from: "Use the beads MCP server to find tasks that are ready to work on (no blocking dependencies).", to: "Run `bd ready --json` via bash to find tasks that are ready to work on (no blocking dependencies)." },
56
- { from: "Call the `ready` tool to get a list of unblocked issues.", to: "Use the command output to get a list of unblocked issues." },
57
- { from: "use the `claim` tool to start work atomically", to: "run `bd update <id> --claim --json` via bash to start work atomically" },
58
- { from: "checking `blocked` issues or creating a new issue with the `create` tool", to: "running `bd blocked --json` or creating a new issue with `bd create ... --json`" },
59
- ],
60
- "commands/search.md": [
61
- { from: "when accessed via MCP", to: "when using structured CLI output" },
62
- { from: "You're using an LLM/MCP and want to minimize context usage", to: "You're using an LLM and want to minimize context usage" },
63
- ],
64
- "commands/show.md": [
65
- { from: "Use the beads MCP `show` tool to retrieve issue details", to: "Run `bd show <id> --json` via bash to retrieve issue details" },
66
- ],
67
- "commands/stats.md": [
68
- { from: "Use the beads MCP `stats` tool to retrieve project metrics", to: "Run `bd stats --json` via bash to retrieve project metrics" },
69
- ],
70
- "commands/update.md": [
71
- { from: "Use the beads MCP `update` tool to apply the changes.", to: "Run the appropriate `bd update <id> ... --json` command via bash." },
72
- ],
73
- "commands/version.md": [
74
- {
75
- from: `Check the installed versions of beads components and verify compatibility.
76
-
77
- **Note:** The MCP server automatically checks bd CLI version >= 0.9.0 on startup. This command provides detailed version info and update instructions.
78
-
79
- Use the beads MCP tools to:
80
- 1. Run \`bd version\` via bash to get the CLI version
81
- 2. Check the plugin version (0.9.2)
82
- 3. Compare versions and report any mismatches
83
-
84
- Display:
85
- - bd CLI version (from \`bd version\`)
86
- - Plugin version (0.9.2)
87
- - MCP server version (0.9.2)
88
- - MCP server status (from \`stats\` tool or connection test)
89
- - Compatibility status (✓ compatible or ⚠️ update needed)
90
-
91
- If versions are mismatched, provide instructions:
92
- - Update bd CLI: \`curl -fsSL https://raw.githubusercontent.com/gastownhall/beads/main/scripts/install.sh | bash\`
93
- - Update plugin: \`/plugin update beads\`
94
- - Restart Claude Code after updating
95
-
96
- Suggest checking for updates if the user is on an older version.`,
97
- to: `Check the installed Beads and OpenCode versions against this plugin's documented compatibility range.
98
-
99
- Run \`bd version\` and \`opencode --version\` via bash. Report both versions and point to the opencode-beads README compatibility table. Do not infer compatibility from unrelated integration or upstream plugin versions.
100
-
101
- If \`bd\` is outdated, link to the current Beads installation guide. If opencode-beads is pinned in the OpenCode configuration, explain that updating requires changing the pinned package version and restarting OpenCode.`,
102
- },
103
- ],
104
- "commands/workflow.md": [
105
- { from: "Use `/beads:ready` or the `ready` MCP tool", to: "Use `/beads:ready` or run `bd ready --json`" },
106
- { from: "- Via MCP tool: `claim` with `issue_id: \"<id>\"`", to: "- Via CLI: `bd update <id> --claim --json`" },
107
- { from: "- Create issues: `/beads:create` or `create` MCP tool", to: "- Create issues: `/beads:create` or `bd create ... --json`" },
108
- { from: "- Link them: Use `dep` MCP tool with `type: \"discovered-from\"`", to: "- Link them: `bd dep add <new-id> <current-id> --type discovered-from --json`" },
109
- { from: "- Via MCP tool: `close` with reason", to: "- Via CLI: `bd close <id> --reason \"<summary>\" --json`" },
110
- {
111
- from: `## MCP Tools Available
112
- Use these via the beads MCP server:
113
- - \`ready\`, \`list\`, \`show\`, \`create\`, \`claim\`, \`update\`, \`close\`
114
- - \`dep\` (manage dependencies), \`blocked\`, \`stats\`
115
- - \`init\` (initialize bd in a project)`,
116
- to: `## CLI Access
117
- Run the \`bd\` CLI via bash for operations not covered by a slash command. Use \`bd <command> --help\` for current syntax and \`--json\` when structured output improves reliability.`,
118
- },
119
- ],
120
- };
121
-
122
- const unsupportedPattern = /\bmcp\b|claude code|\/plugin update beads/i;
123
-
124
- /** Paths with explicit OpenCode adaptations, used as the expected inventory. */
125
- export const adaptedVendorPaths = Object.freeze(Object.keys(replacements).sort());
126
-
127
- /** Apply strict, reviewable OpenCode adaptations to one upstream prompt body. */
128
- export function adaptVendorPrompt(relativePath: string, content: string): string {
129
- let adapted = content;
130
-
131
- for (const replacement of replacements[relativePath] ?? []) {
132
- const first = adapted.indexOf(replacement.from);
133
- const last = adapted.lastIndexOf(replacement.from);
134
- if (first === -1 || first !== last) {
135
- throw new Error(`Expected exactly one adaptation source in ${relativePath}: ${replacement.from}`);
136
- }
137
- adapted = adapted.replace(replacement.from, replacement.to);
138
- }
139
-
140
- const unsupported = adapted.match(unsupportedPattern)?.[0];
141
- if (unsupported) {
142
- throw new Error(`Unsupported ${unsupported} instruction remains in ${relativePath}`);
143
- }
144
-
145
- return adapted;
146
- }