@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.
- package/README.md +47 -24
- package/assets/oc-beads/SKILL.md +168 -0
- package/assets/oc-beads/adr/0001-bd-prime-as-source-of-truth.md +40 -0
- package/assets/oc-beads/resources/AGENTS.md +43 -0
- package/assets/oc-beads/resources/ASYNC_GATES.md +63 -0
- package/assets/oc-beads/resources/BOUNDARIES.md +57 -0
- package/assets/oc-beads/resources/CHEMISTRY_PATTERNS.md +44 -0
- package/assets/oc-beads/resources/CLI_REFERENCE.md +42 -0
- package/assets/oc-beads/resources/CODE_MODE.md +78 -0
- package/assets/oc-beads/resources/DEPENDENCIES.md +82 -0
- package/assets/oc-beads/resources/INTEGRATION_PATTERNS.md +74 -0
- package/assets/oc-beads/resources/ISSUE_CREATION.md +45 -0
- package/assets/oc-beads/resources/MOLECULES.md +65 -0
- package/assets/oc-beads/resources/PATTERNS.md +43 -0
- package/assets/oc-beads/resources/RESUMABILITY.md +92 -0
- package/assets/oc-beads/resources/STATIC_DATA.md +19 -0
- package/assets/oc-beads/resources/TROUBLESHOOTING.md +71 -0
- package/assets/oc-beads/resources/WORKFLOWS.md +64 -0
- package/assets/oc-beads/resources/WORKTREES.md +24 -0
- package/package.json +5 -4
- package/src/agents.ts +2 -2
- package/src/context.ts +67 -22
- package/src/continuity.ts +380 -0
- package/src/guard.ts +44 -11
- package/src/guidance.ts +65 -15
- package/src/host.ts +323 -42
- package/src/instructions.ts +11 -8
- package/src/plugin.ts +2 -1
- package/src/prime.ts +15 -4
- package/src/workflows.ts +132 -0
|
@@ -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.
|
|
59
|
+
"minimum": "1.3.1",
|
|
59
60
|
"validated": "1.3.1"
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|
|
62
|
-
"@opencode/plugin": ">=2.
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
56
|
-
*
|
|
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
|
|
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 (
|
|
115
|
-
|
|
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`))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|