@snarkipus/opencode-beads 1.0.2 → 1.1.0-beta.2

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.
@@ -0,0 +1,64 @@
1
+ # Workflows and checkpoints
2
+
3
+ > Adapted from [Beads marketplace-main `WORKFLOWS.md`](https://github.com/gastownhall/beads/blob/4a67d0d0288c1b4ee23e17fc896f47616931ba2d/plugins/beads/skills/beads/resources/WORKFLOWS.md). The append policy below resolves conflicting upstream examples: checkpoints append; replacing notes is deliberate.
4
+
5
+ ## Begin with the current context
6
+
7
+ Use current Beads context if it was supplied. Read the target issue, dependencies,
8
+ existing notes, and relevant memory before choosing an action. Confirm the exact
9
+ assignment and authority. Do not treat `ready` output as an assignment or
10
+ permission to work every listed item.
11
+
12
+ For an authorized task, claim it atomically with the actual executor actor. Keep
13
+ that actor on all subsequent writes and heartbeats. Direct execution is fine for
14
+ small tasks; use delegation where it improves the work, carrying a bounded scope
15
+ and the real delegate actor. Do not impose a one-item-per-invocation loop or
16
+ delegate by command count.
17
+
18
+ ## Checkpoint at useful transitions
19
+
20
+ Append when a failed approach changes the next step, a finding is validated, a
21
+ decision changes, work becomes blocked, a child returns, or a session may stop.
22
+ Include the attempt, result, why it matters, evidence, blocker, and next action.
23
+ Use `--append-notes`, for example:
24
+
25
+ ```sh
26
+ bd update '<id>' --append-notes="Validated …; evidence …; next …" --actor='<executor-actor>' --sandbox --json
27
+ ```
28
+
29
+ This is an append. Do not use the replacing `--notes` field as the default
30
+ checkpoint. If nothing material changed, do not write a redundant note just to
31
+ meet a quota. Avoid credentials, raw logs, and claims unsupported by evidence.
32
+
33
+ ## Capture reusable knowledge selectively
34
+
35
+ Task details and short-lived failures belong with the issue. Search existing
36
+ memory before recording a validated lesson that will likely help elsewhere.
37
+ Include its scope and evidence, use the actual executor actor for memory writes,
38
+ and amend stale knowledge intentionally. Do not save every task outcome as
39
+ global memory.
40
+
41
+ ## Validate and hand off
42
+
43
+ Check the issue's acceptance conditions, run authorized relevant validation,
44
+ and record exact results. If a parent or human owns acceptance, return the
45
+ evidence without closing for them. A delegate returns attempts, results,
46
+ rationale, validation, blockers, next action, and useful record references; the
47
+ parent reconciles before continuing. If the task was an exact assignment, stop
48
+ there. Explicitly authorized batches may continue within their stated scope;
49
+ new discoveries never expand it.
50
+
51
+ When context is held or a required guarantee is missing, stop the affected path
52
+ and state the limitation. Do not switch to a manual prime, unregistered tool,
53
+ automatic setup, sync, or permission change to complete it.
54
+
55
+ ## Planning and independently owned review
56
+
57
+ Follow project-owned planning and review policy. Track substantive multi-session
58
+ or delegated planning in one bounded planning item, separate from implementation;
59
+ a quick draft need not get its own ticket. Give substantive independently owned
60
+ review one bounded review item linked to planning and the exact candidate. Reuse
61
+ the same review item through corrections and rechecks. Record actual reviewer
62
+ actor/session, verdict, findings and evidence without retroactive claims.
63
+ Planning/review closure needs its own evidence and owner acceptance; readiness
64
+ alone never authorizes dispatch, implementation, release or deployment.
@@ -0,0 +1,24 @@
1
+ # Worktree routing
2
+
3
+ > Adapted from [Beads marketplace-main `WORKTREES.md`](https://github.com/gastownhall/beads/blob/4a67d0d0288c1b4ee23e17fc896f47616931ba2d/plugins/beads/skills/beads/resources/WORKTREES.md).
4
+
5
+ A worktree can isolate code changes while retaining a repository's shared Beads
6
+ database. The installed CLI documents common-directory discovery for its
7
+ worktree command. Do not assume that every unrelated checkout, redirected
8
+ workspace, or custom route shares the same store.
9
+
10
+ Use a worktree only when isolation helps and its creation is authorized. Before
11
+ changing issues, confirm which repository and Beads location the CLI resolved.
12
+ Keep the assignment and explicit executor actor the same across the worktree;
13
+ Git identity is not a Beads actor. Do not initialize a separate store, change
14
+ routes, or copy database state to make a task appear ready.
15
+
16
+ Use `bd worktree --help` and relevant subcommand help for current commands and
17
+ safety checks. Inspect local changes before removal, preserve work that is not
18
+ yours, and remove only a worktree whose cleanup is authorized. This resource
19
+ does not create or clean up a worktree automatically.
20
+
21
+ `oc-beads:sync` guides `bd sync` Dolt-remote synchronization, distinct from
22
+ `bd federation sync` and removed JSONL sync. Inspect current help and synchronize
23
+ only with explicit authority. Report conflicts without forcing a side; the
24
+ installed Beads CLI owns synchronization and consistency semantics.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snarkipus/opencode-beads",
3
- "version": "1.0.2",
3
+ "version": "1.1.0-beta.2",
4
4
  "type": "module",
5
5
  "description": "A maintained OpenCode V2 integration for the Beads issue tracker: bd prime context injection with a schema-migration guard, and a bounded task agent.",
6
6
  "author": "Matt Jackson",
@@ -43,6 +43,7 @@
43
43
  },
44
44
  "files": [
45
45
  "src",
46
+ "assets/oc-beads",
46
47
  "README.md",
47
48
  "LICENSE"
48
49
  ],
@@ -55,11 +56,11 @@
55
56
  "typecheck": "tsc --noEmit"
56
57
  },
57
58
  "beads": {
58
- "minimum": "1.3.0",
59
+ "minimum": "1.3.1",
59
60
  "validated": "1.3.1"
60
61
  },
61
62
  "peerDependencies": {
62
- "@opencode/plugin": ">=2.0.26 <3"
63
+ "@opencode/plugin": ">=2.1.1 <2.2"
63
64
  },
64
65
  "peerDependenciesMeta": {
65
66
  "@opencode/plugin": {
@@ -67,7 +68,7 @@
67
68
  }
68
69
  },
69
70
  "devDependencies": {
70
- "@opencode/plugin": "2.0.26",
71
+ "@opencode/plugin": "2.1.1",
71
72
  "@types/bun": "1.3.4",
72
73
  "@types/node": "^22.0.0",
73
74
  "typescript": "5.9.3"
package/src/agents.ts CHANGED
@@ -8,7 +8,7 @@ export const BEADS_TASK_AGENTS: ReadonlySet<string> = new Set([
8
8
  BEADS_TASK_AGENT_LUNA,
9
9
  ]);
10
10
 
11
- /** Default hard cap on worker steps; a backstop for "one bead, then return", not a target. */
11
+ /** Default interruption cap on worker steps; never a completion target or authority to continue. */
12
12
  export const DEFAULT_TASK_AGENT_STEPS = 100;
13
13
 
14
14
  /**
@@ -45,7 +45,7 @@ export interface BeadsAgentDefinition {
45
45
  export interface BeadsAgentOptions {
46
46
  /** Register the Luna Max profile alongside the default task agent. */
47
47
  luna?: boolean;
48
- /** Hard cap on worker steps; 0 removes the cap. */
48
+ /** Interruption cap on worker steps; 0 removes the cap, never the assignment's scope limits. */
49
49
  steps?: number;
50
50
  }
51
51
 
package/src/context.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import { executorActor } from "./continuity";
1
2
  import { BEADS_TASK_AGENTS } from "./agents";
2
3
  import { guardNotice, type GuardResult } from "./guard";
3
4
  import { PrimeTimeoutError } from "./prime";
4
- import { beadsGuidance, primeFailureNotice } from "./guidance";
5
+ import { beadsGuidance, primeFailureNotice, type RequestCapabilities } from "./guidance";
5
6
 
6
7
  /** Retry window for withheld or failed context, so model steps do not respawn `bd` each time. */
7
8
  export const DEFAULT_CONTEXT_RETRY_MS = 30_000;
@@ -15,11 +16,12 @@ export const DEFAULT_IDLE_REFRESH_MS = 30 * 60_000;
15
16
 
16
17
  export type AgentMode = "primary" | "subagent" | "all";
17
18
  export type ContextAudience = "primary" | "task-agent";
18
- export type ContextDiagnostic = "agent_lookup_failed" | "prime_failed" | "prime_timeout";
19
+ export type ContextDiagnostic = "agent_lookup_failed" | "guard_failed" | "prime_failed" | "prime_timeout";
20
+ export type ContextStatus = "loading" | "missing" | "withheld" | "failed" | "empty" | "ready";
19
21
 
20
22
  export interface ContextRuntime {
21
- guard(directory: string): Promise<GuardResult>;
22
- prime(directory: string): Promise<string>;
23
+ guard(directory: string, signal?: AbortSignal): Promise<GuardResult>;
24
+ prime(directory: string, signal?: AbortSignal): Promise<string>;
23
25
  /** Resolve an agent's mode; undefined when the agent is unknown. */
24
26
  agentMode(agentID: string): Promise<AgentMode | undefined>;
25
27
  diagnose?(code: ContextDiagnostic, sessionID: string): void;
@@ -34,26 +36,33 @@ export interface ContextControllerOptions {
34
36
 
35
37
  export interface ContextController {
36
38
  /** System text for one model request, or undefined when nothing should be added. */
37
- systemFor(sessionID: string, agentID: string): Promise<string | undefined>;
39
+ systemFor(sessionID: string, agentID: string, capabilities?: RequestCapabilities): Promise<string | undefined>;
38
40
  /** Refresh prime on the next request, e.g. after compaction changed the session. */
39
41
  invalidate(sessionID: string): void;
40
42
  /** Drop all state for a deleted session. */
41
43
  forget(sessionID: string): void;
44
+ /** Context state is distinct from an absent text part (including intentional empty prime). */
45
+ statusFor(sessionID: string, audience: ContextAudience): ContextStatus | undefined;
46
+ /** A write can affect every cached session in this location. */
47
+ invalidateAll(): void;
48
+ /** Abort owned loads and prevent late context injection after unload. */
49
+ dispose(): void;
42
50
  }
43
51
 
44
- type CacheEntry = { value: Promise<string | undefined>; expiresAt?: number; lastUsed: number };
52
+ type CacheEntry = { value: Promise<string | undefined>; expiresAt?: number; lastUsed: number; abort: AbortController; status?: ContextStatus };
45
53
 
46
54
  /** Wrap prime output in the envelope and shared guidance for one audience. */
47
- export function renderBeadsContext(prime: string, audience: ContextAudience): string {
48
- return `<beads-context audience="${audience}">\n${prime}\n</beads-context>\n\n${beadsGuidance(audience)}`;
55
+ export function renderBeadsContext(prime: string, audience: ContextAudience, request?: { sessionID: string; agent: string; capabilities?: RequestCapabilities }): string {
56
+ const identity = request ? `\nYour Beads executor actor: ${executorActor(request)}. Use this exact actor for every direct shell write, claim and heartbeat (command-scoped --actor or BEADS_ACTOR); children use their own actual request actor. Attribution grants no permission.\n` : "";
57
+ return `<beads-context audience="${audience}">\n${prime}\n</beads-context>\n${identity}\n${beadsGuidance(audience, request?.capabilities)}`;
49
58
  }
50
59
 
51
60
  /**
52
61
  * Supply Beads context through the per-request session context hook.
53
62
  *
54
63
  * OpenCode V2 attaches system parts to every model request without storing them, so context
55
- * survives compaction on its own. The rendered text is cached per session to keep it stable
56
- * across requests; compaction invalidates it so persistent memories can refresh.
64
+ * survives compaction on its own. Guarded prime is cached separately from per-request actor/capability rendering;
65
+ * compaction invalidates it so persistent memories can refresh.
57
66
  */
58
67
  export function createContextController(
59
68
  runtime: ContextRuntime,
@@ -65,6 +74,7 @@ export function createContextController(
65
74
  const now = options.now ?? Date.now;
66
75
  const cache = new Map<string, CacheEntry>();
67
76
  const modes = new Map<string, Promise<AgentMode | undefined>>();
77
+ let disposed = false;
68
78
 
69
79
  async function audienceFor(
70
80
  sessionID: string,
@@ -97,34 +107,53 @@ export function createContextController(
97
107
 
98
108
  let guard: GuardResult;
99
109
  try {
100
- guard = await runtime.guard(directory);
110
+ guard = await runtime.guard(directory, entry.abort.signal);
101
111
  } catch {
102
- return retryLater(undefined);
112
+ if (entry.abort.signal.aborted) return undefined;
113
+ entry.status = "failed";
114
+ runtime.diagnose?.("guard_failed", sessionID);
115
+ return retryLater("<beads-notice>Beads workflow context failed during the store safety check. Store authority is unknown; do not bypass this failure with manual prime.</beads-notice>");
116
+ }
117
+ if (entry.abort.signal.aborted) return undefined;
118
+ if (guard.kind !== "ready") {
119
+ entry.status = guard.kind === "probe-failed" ? "failed" : guard.kind === "migration-pending" || guard.kind === "unsupported" ? "withheld" : "missing";
120
+ return retryLater(guardNotice(guard));
103
121
  }
104
- if (guard.kind !== "ready") return retryLater(guardNotice(guard));
105
122
 
106
123
  let output: string;
107
124
  try {
108
- output = (await runtime.prime(directory)).trim();
125
+ output = (await runtime.prime(directory, entry.abort.signal)).trim();
109
126
  } catch (error) {
127
+ if (entry.abort.signal.aborted) return undefined;
128
+ entry.status = "failed";
110
129
  const timedOut = error instanceof PrimeTimeoutError;
111
130
  runtime.diagnose?.(timedOut ? "prime_timeout" : "prime_failed", sessionID);
112
131
  return retryLater(primeFailureNotice(timedOut ? "timed out" : "failed"));
113
132
  }
114
- if (!output) return retryLater(undefined);
115
- return renderBeadsContext(output, audience);
133
+ if (entry.abort.signal.aborted) return undefined;
134
+ if (!output) {
135
+ entry.status = "empty";
136
+ return retryLater(undefined);
137
+ }
138
+ entry.status = "ready";
139
+ return output;
116
140
  }
117
141
 
118
142
  function dropSession(sessionID: string): void {
143
+ modes.clear();
119
144
  for (const key of cache.keys()) {
120
- if (key.startsWith(`${sessionID}\0`)) cache.delete(key);
145
+ if (key.startsWith(`${sessionID}\0`)) {
146
+ cache.get(key)?.abort.abort();
147
+ cache.delete(key);
148
+ }
121
149
  }
122
150
  }
123
151
 
124
152
  return {
125
- async systemFor(sessionID, agentID) {
153
+ async systemFor(sessionID, agentID, capabilities) {
154
+ if (disposed) return undefined;
126
155
  const audience = await audienceFor(sessionID, agentID);
127
- if (!audience) return undefined;
156
+ if (!audience || disposed) return undefined;
128
157
 
129
158
  const key = `${sessionID}\0${audience}`;
130
159
  const current = now();
@@ -133,16 +162,32 @@ export function createContextController(
133
162
  const resumed = cached && idleRefreshMs > 0 && current - cached.lastUsed > idleRefreshMs;
134
163
  if (cached && fresh && !resumed) {
135
164
  cached.lastUsed = current;
136
- return cached.value;
165
+ const output = await cached.value;
166
+ if (disposed || cached.abort.signal.aborted) return undefined;
167
+ return cached.status === "ready" ? renderBeadsContext(output!, audience, { sessionID, agent: agentID, capabilities }) : output;
137
168
  }
138
169
 
139
- const entry: CacheEntry = { value: Promise.resolve(undefined), lastUsed: current };
170
+ cached?.abort.abort();
171
+ const entry: CacheEntry = { value: Promise.resolve(undefined), lastUsed: current, abort: new AbortController(), status: "loading" };
140
172
  entry.value = load(sessionID, audience, entry);
141
173
  cache.set(key, entry);
142
- return entry.value;
174
+ const output = await entry.value;
175
+ if (disposed || entry.abort.signal.aborted) return undefined;
176
+ return entry.status === "ready" ? renderBeadsContext(output!, audience, { sessionID, agent: agentID, capabilities }) : output;
143
177
  },
144
178
 
145
179
  invalidate: dropSession,
146
180
  forget: dropSession,
181
+ statusFor: (sessionID, audience) => cache.get(`${sessionID}\0${audience}`)?.status,
182
+ invalidateAll() {
183
+ for (const entry of cache.values()) entry.abort.abort();
184
+ cache.clear();
185
+ },
186
+ dispose() {
187
+ disposed = true;
188
+ for (const entry of cache.values()) entry.abort.abort();
189
+ cache.clear();
190
+ modes.clear();
191
+ },
147
192
  };
148
193
  }