@snarkipus/opencode-beads 1.0.1 → 1.1.0-beta.1

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/src/guard.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { PrimeProcessError } from "./prime";
3
4
 
4
5
  /** Oldest Beads release this adapter supports. */
5
- export const MINIMUM_BD_VERSION = "1.3.0";
6
+ export const MINIMUM_BD_VERSION = "1.3.1";
6
7
 
7
8
  export type Version = readonly [number, number, number];
8
9
 
@@ -11,6 +12,7 @@ export type GuardResult =
11
12
  | { kind: "ready" }
12
13
  | { kind: "uninitialized" }
13
14
  | { kind: "unavailable" }
15
+ | { kind: "probe-failed" }
14
16
  | { kind: "unsupported"; installed: string }
15
17
  | { kind: "migration-pending"; installed: string; store: string | undefined };
16
18
 
@@ -26,6 +28,35 @@ export interface GuardOptions {
26
28
  autoMigrate?: boolean;
27
29
  }
28
30
 
31
+ export interface BeadsWorkspace {
32
+ path: string;
33
+ }
34
+
35
+ /** Config-only context avoids `where`'s opportunistic embedded-store prefix lookup. */
36
+ export async function probeBeadsWorkspace(directory: string, runtime: GuardRuntime): Promise<BeadsWorkspace | undefined> {
37
+ let output: string;
38
+ try {
39
+ output = await runtime.bd(directory, ["context", "--json"]);
40
+ } catch (error) {
41
+ if (!(error instanceof PrimeProcessError) || error.exitCode !== 1) throw error;
42
+ // bd 1.3.1 reports absence through this versioned error rather than a successful context.
43
+ const response: unknown = JSON.parse(error.stdout);
44
+ if (typeof response === "object" && response !== null && !Array.isArray(response)) {
45
+ const data = response as Record<string, unknown>;
46
+ if (data.schema_version === 1 && data.error === "cannot resolve repo context: no .beads directory found") {
47
+ return undefined;
48
+ }
49
+ }
50
+ throw error;
51
+ }
52
+ const location: unknown = JSON.parse(output);
53
+ if (typeof location !== "object" || location === null) throw new Error("Invalid bd context response");
54
+ const data = location as Record<string, unknown>;
55
+ if (typeof data.beads_dir !== "string") throw new Error("Unsupported bd context response");
56
+ if (!data.beads_dir) return undefined;
57
+ return { path: data.beads_dir };
58
+ }
59
+
29
60
  /** Parse the leading `major.minor.patch` triple; pre-release suffixes are ignored. */
30
61
  export function parseVersion(text: string): Version | undefined {
31
62
  const match = text.match(/(\d+)\.(\d+)\.(\d+)/);
@@ -59,8 +90,9 @@ async function readTextIfPresent(path: string): Promise<string | undefined> {
59
90
  * Decide whether automatic `bd prime` is safe without opening the store.
60
91
  *
61
92
  * 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
93
+ * it, and `--readonly` does not prevent that. `bd version` and `bd context --json` skip store
94
+ * initialization; `where` can open storage to recover a missing prefix and is not used here.
95
+ * `.beads/.local_version` records the newest binary that opened the store through an
64
96
  * ordinary command (but not `bd prime`), so an older or missing value means a migration may be
65
97
  * pending. `bd migrate schema` is idempotent and updates it, clearing a stale marker.
66
98
  */
@@ -82,17 +114,14 @@ export async function checkBeadsStore(
82
114
  return { kind: "unsupported", installed: formatVersion(installed) };
83
115
  }
84
116
 
85
- let location: unknown;
117
+ let location: BeadsWorkspace | undefined;
86
118
  try {
87
- location = JSON.parse(await runtime.bd(directory, ["where", "--json"]));
119
+ location = await probeBeadsWorkspace(directory, runtime);
88
120
  } catch {
89
- // `bd where` exits non-zero without a workspace; treat every failure as no usable store.
90
- return { kind: "uninitialized" };
121
+ // Never substitute `where` or silently treat an unsupported/failed safety probe as absence.
122
+ return { kind: "probe-failed" };
91
123
  }
92
- const beadsPath =
93
- typeof location === "object" && location !== null && "path" in location
94
- ? (location as { path: unknown }).path
95
- : undefined;
124
+ const beadsPath = location?.path;
96
125
  if (typeof beadsPath !== "string" || !beadsPath) return { kind: "uninitialized" };
97
126
 
98
127
  if (options.autoMigrate) return { kind: "ready" };
@@ -122,6 +151,10 @@ export function createGuardRuntime(bd: GuardRuntime["bd"]): GuardRuntime {
122
151
  /** Explain withheld context to the agent so it neither migrates the store nor guesses. */
123
152
  export function guardNotice(result: GuardResult): string | undefined {
124
153
  switch (result.kind) {
154
+ case "probe-failed":
155
+ return `<beads-notice>
156
+ Beads workflow context was withheld: the config-only \`bd context --json\` safety probe failed or returned an unsupported result. Store authority is unknown. Do not substitute \`bd where\` or manual prime; report the CLI contract failure to the user.
157
+ </beads-notice>`;
125
158
  case "unsupported":
126
159
  return `<beads-notice>
127
160
  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.
package/src/guidance.ts CHANGED
@@ -1,35 +1,67 @@
1
1
  /**
2
- * Fork-owned guidance for OpenCode: the shared context layer and the bounded task-agent prompt.
2
+ * Project-owned guidance for OpenCode: the shared context layer and the bounded task-agent prompt.
3
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.
4
+ * Scope and acceptance come from the authorized assignment, not a command count or ready graph.
5
+ * The prompt remains project-owned; native child transport and Beads storage remain external.
6
6
  */
7
7
 
8
- /** Shown when OpenCode lists subagents; describes the one-bead worker contract. */
8
+ /** Shown when OpenCode lists subagents; describes the scope-bounded worker contract. */
9
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.";
10
+ "Bounded Beads worker: read-only issue-graph analysis or execution within an authorized task, batch or goal, with atomic claims, validation and evidence-bearing handoff.";
11
+
12
+ const BEADS_SCOPE_GUIDANCE = `## Authorized scope
13
+
14
+ For status or graph analysis, never claim, update, close, or modify a bead. Return the analysis and stop.
15
+
16
+ An exact assignment or explicit one-bead mode ends with that bead: report completion or a blocker and return without substitution. A single authorized operation ends with that operation; it does not authorize claiming ownership or selecting ready work. Only an already-authorized batch or goal allows continuation to another actionable bead within its stated scope. Readiness is not dispatch permission. If execution scope is missing or ambiguous, obtain it before claiming work.
17
+
18
+ For each bead authorized for task execution, first read the exact issue (typed get when available; direct shell equivalent \`bd show <id> --json --sandbox\`); confirm it is open, actionable, within scope and appropriate; then claim atomically (typed claim when available; direct shell equivalent \`bd update <id> --claim --json --actor='<executor-actor>' --sandbox\`) and read back assignment. If the claim fails or readback does not confirm your assignment, stop. If an assigned bead is closed, blocked, unsuitable, missing, or otherwise not actionable, report and return without mutation or substitution.
19
+
20
+ Implement within scope and run relevant authorized validation. Append per-bead checkpoints with \`--append-notes\`, preserving attempts, measured outcomes, rationale, validation, references, blockers and the next authorized action. Close only with acceptance evidence and closure authority; unresolved acceptance or failed validation remains incomplete. If the parent owns acceptance or closure, return evidence and leave the bead open. Continue within an authorized batch only after recording the current bead's outcome and confirming that continuation meets its acceptance and authority gates.
21
+
22
+ Discovery does not add execution authority. Report follow-ups; create them only when authorized, with \`discovered-from:<current-id>\` and a parent only when structurally appropriate. Leave new work unassigned unless assignment is intentional. Never claim or implement discoveries merely because you recorded them.
23
+
24
+ Stop on permission denial, cancellation, deliberate stop, exhausted scope or a blocker requiring a decision. A permission wait is pending authority, not completion or permission to bypass it. Keep partial progress and unresolved acceptance visible; never restart work from a completion notification.`;
25
+
26
+ const BEADS_EXECUTOR_GUIDANCE = `## Executor and work ownership
27
+
28
+ Use the actual executor actor from the authorized invocation or handoff on every agent-authored write, claim, update and heartbeat, with command-scoped \`BEADS_ACTOR\` or \`--actor\`. Replace '<executor-actor>' with that identity; if unavailable, refuse writes until the host supplies it. Never fall back to human Git identity, a shared user-global actor, a model name alone or another executor's identity. Keep full session IDs in evidence and readable unambiguous actor names in commands. Actor attribution grants no permission and is separate from the actual invoking host agent used for permission evaluation.
29
+
30
+ Read back \`created_by\` (audit author), \`owner\` (record/account ownership) and \`assignee\` (executor) separately. Preserve historical authorship, including unknown values; put provenance corrections in notes. A deliberate handoff records the change and authorized ownership update; parent coordination or review alone does not transfer the worker's claim. Delegated instructions carry the actual executor actor; the executing worker claims its own authorized work and retains that actor through updates and heartbeats.`;
11
31
 
12
32
  const BEADS_HOST_GUIDANCE = `## CLI Safety
13
33
 
14
- There is no native \`bd\` or Beads MCP tool in OpenCode. Run \`bd\` through \`bash\`.
34
+ Use actual request capabilities below. Native shell is direct-only and cannot be called inside execute.
15
35
  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
36
 
17
37
  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
38
 
39
+ const BEADS_RECOVERY_GUIDANCE = `## Work continuity
40
+
41
+ At the first eligible request after a fresh session, compaction, interruption, child handoff or important change, retrieve the assigned task and relevant reusable knowledge under the current permissions and guard. Re-read current state when external writers may have changed it; cached absence is not proof of absence. Missing, withheld, failed and intentionally empty workflow context are different outcomes; never bypass a hold with manual prime.
42
+
43
+ Append task-specific attempts, measured outcomes, rationale, blockers and the next authorized action at meaningful transitions. Keep hypotheses qualified. Reusable project memory is for validated findings with scope and evidence: search or recall relevant existing knowledge before adding or correcting it. No redundant write quota applies.
44
+
45
+ Only actual CLI results acknowledge persistence. A timeout or interrupted append/remember is uncertain: inspect durable state before another write, with no blind retry. A landed memory with failed or indeterminate commit retains its reference and qualified status; do not force it or automatically commit. Empty found memory differs from a missing key. Every authorized write uses the actual executor's explicit Beads actor, separate from the host permission agent; never use human Git identity or a model-supplied impersonation.
46
+
47
+ The installed Beads CLI owns database, routing and consistency semantics; the adapter invokes normal supported operations from the actual project context under OpenCode permissions. Typed writes derive a stable readable actor from the actual invocation session and agent. That attribution is not authentication and never transfers assignment: deliberate handoffs still require their own authorized claim/update and evidence. Where an authorized permission check uses save: [], an always reply grants this call only and ask may prompt again; existing shell grants remain subject to denies.`;
48
+
19
49
  const BEADS_DELEGATION_GUIDANCE = `## Delegation
20
50
 
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.`;
51
+ Choose direct work or delegation for the task's needs, not the number of commands. When authorized delegation helps, use the \`beads-task-agent\` subagent through the \`subagent\` tool with normal blocking child return. Carry the exact scope, actual executor actor, acceptance/closure owner, expected evidence and stop conditions. Explicit parent/child boundaries still apply; delegation does not grant a larger goal. Background or nested automation is not enabled by this guidance.
52
+
53
+ Reconcile the child's scope, actor/session, attempts, outcomes, rationale, validation, durable references, blockers, incomplete acceptance and next authorized action. A tool return alone is not completion evidence: NO_TEXT, failed/cancelled children and stale/late results remain incomplete until reconciled. A result after deliberate stop does not authorize resumption or closure. Use earlier acknowledged records and disclose gaps on later authorized recovery. Never synthesize a parent restart or approve a permission wait to obtain a final checkpoint.`;
22
54
 
23
- /** Fork-owned system prompt for the bounded one-bead task agent. */
55
+ /** Project-owned system prompt for the scope-bounded task agent. */
24
56
  export const BEADS_TASK_AGENT_PROMPT = `## Beads Task Agent
25
57
 
26
- Full \`bd prime\` workflow context is injected with each request. Use \`bd\` via \`bash\`; rerun prime if context is missing or stale.
58
+ Full \`bd prime\` workflow context is injected with each request. Use the request capability block and native beads namespace catalog when available; repository validation runs in the direct \`shell\` tool. Refresh missing or stale context only when authorized and no hold applies.
27
59
 
28
- For status or graph analysis, never claim, update, close, or modify a bead. Return the analysis and stop.
60
+ ${BEADS_SCOPE_GUIDANCE}
29
61
 
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.
62
+ ${BEADS_EXECUTOR_GUIDANCE}
31
63
 
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.`;
64
+ Return concise results, not raw JSON: include task/scope, actual actor and full session ID, attempts/outcomes/rationale, exact validation and candidate revision, durable references, blockers, unresolved acceptance and next authorized action. Distinguish completion from partial progress, NO_TEXT, failure or cancellation. The caller reconciles acceptance; a step cap or invocation ending does not establish completion. Do not launch background or nested automation or restart a stopped parent.`;
33
65
 
34
66
  /** Shown instead of context when `bd prime` fails, as upstream's Codex hook reports refresh failures. */
35
67
  export function primeFailureNotice(reason: "failed" | "timed out"): string {
@@ -40,14 +72,32 @@ Beads workflow context could not be loaded: \`bd prime\` ${reason}. Beads may st
40
72
 
41
73
  /** Added to compaction requests so the summary keeps the Beads work in flight. */
42
74
  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.
75
+ This session tracks work in Beads (bd). Carry acknowledged durable task/memory references, current scope, attempts, blockers, pending validation and known unsaved or uncertain gaps into the summary or native-provider context. A summary is not a Beads write. Do not persist, wait for permission or a queued write, or request a recursive same-session checkpoint during compaction. Earlier meaningful checkpoints support recovery; no final-write guarantee applies. Fresh guarded context is requested after compaction. Preserve host-owned native replay, metadata.keepID and legacy metadata.recent; do not inject result.native.messages or a replacement result to add references.
44
76
  </beads-compaction>`;
45
77
 
78
+ /** Observed after native request filtering; selective operation permission remains per call. */
79
+ export interface RequestCapabilities {
80
+ typed: "registered-execute-visible" | "unavailable" | "unknown";
81
+ execute: boolean | null;
82
+ shell: boolean | null;
83
+ }
84
+ export const UNKNOWN_CAPABILITIES: RequestCapabilities = { typed: "unknown", execute: null, shell: null };
85
+
86
+ /** Capability statements describe observations, never a permission grant. */
87
+ export function capabilityGuidance(capabilities: RequestCapabilities): string {
88
+ if (capabilities.typed === "registered-execute-visible") return "Beads namespace is registered and execute is visible. Discover current signatures with native search({namespace: \"beads\"}); compose only operations found in the native catalog. Some or all leaves may be filtered out. Each leaf still checks its own permissions; visibility grants no authority. Native shell is direct-only; run repository work and validation in direct shell turns. Do not call tools.shell inside execute.";
89
+ if (capabilities.shell === true) return "Typed Beads composition is unavailable for this request. Use permissioned bd through the direct shell tool only when authorized and no hold applies. Shell cannot run inside execute.";
90
+ if (capabilities.execute !== null && capabilities.shell === false) return "Neither typed Beads composition nor direct shell access is available for this request. Report unavailable capability and stop the affected work; do not route around it.";
91
+ return "Beads request capability is unknown. Check actual native catalog/tools before choosing typed composition or direct shell; do not infer permission or bypass a hold.";
92
+ }
93
+
46
94
  /** Build the single shared guidance layer for one injected audience. */
47
95
  export function beadsGuidance(
48
- audience: "primary" | "task-agent"
96
+ audience: "primary" | "task-agent",
97
+ capabilities: RequestCapabilities = UNKNOWN_CAPABILITIES
49
98
  ): string {
50
- const sections = [BEADS_HOST_GUIDANCE];
51
- if (audience === "primary") sections.push(BEADS_DELEGATION_GUIDANCE);
99
+ const sections = [BEADS_HOST_GUIDANCE, capabilityGuidance(capabilities), BEADS_RECOVERY_GUIDANCE];
100
+ if (audience === "primary") sections.push(BEADS_EXECUTOR_GUIDANCE);
101
+ if (audience === "primary") sections.push(BEADS_SCOPE_GUIDANCE, BEADS_DELEGATION_GUIDANCE);
52
102
  return `<beads-guidance>\n${sections.join("\n\n")}\n</beads-guidance>`;
53
103
  }