@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.
- package/README.md +85 -22
- 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 -347
- 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/vendor-provenance.ts
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
|
|
5
|
-
export const VENDOR_MANIFEST = "manifest.json";
|
|
6
|
-
export const VENDOR_SOURCES = Object.freeze([
|
|
7
|
-
{
|
|
8
|
-
source: "plugins/beads/agents/task-agent.md",
|
|
9
|
-
target: "agents/task-agent.md",
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
source: "plugins/beads/skills/beads/commands",
|
|
13
|
-
target: "commands",
|
|
14
|
-
},
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
interface Semver {
|
|
18
|
-
major: bigint;
|
|
19
|
-
minor: bigint;
|
|
20
|
-
patch: bigint;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface VendorFileRecord {
|
|
24
|
-
path: string;
|
|
25
|
-
bytes: number;
|
|
26
|
-
sha256: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface VendorManifest {
|
|
30
|
-
schemaVersion: 1;
|
|
31
|
-
repository: string;
|
|
32
|
-
tag: string;
|
|
33
|
-
commit: string;
|
|
34
|
-
sources: typeof VENDOR_SOURCES;
|
|
35
|
-
files: VendorFileRecord[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ExpectedVendorProvenance {
|
|
39
|
-
repository: string;
|
|
40
|
-
tag: string;
|
|
41
|
-
commit: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface VendorRelease {
|
|
45
|
-
tag: string;
|
|
46
|
-
commit: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function parseStableTag(tag: string): Semver | undefined {
|
|
50
|
-
const match = tag.match(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/);
|
|
51
|
-
if (!match) return undefined;
|
|
52
|
-
|
|
53
|
-
const [, major, minor, patch] = match;
|
|
54
|
-
if (major === undefined || minor === undefined || patch === undefined) return undefined;
|
|
55
|
-
return { major: BigInt(major), minor: BigInt(minor), patch: BigInt(patch) };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function compareSemver(left: Semver, right: Semver): number {
|
|
59
|
-
for (const key of ["major", "minor", "patch"] as const) {
|
|
60
|
-
if (left[key] < right[key]) return -1;
|
|
61
|
-
if (left[key] > right[key]) return 1;
|
|
62
|
-
}
|
|
63
|
-
return 0;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Select the newest stable semver release from git ls-remote output. */
|
|
67
|
-
export function selectStableRelease(lsRemoteOutput: string): VendorRelease {
|
|
68
|
-
const releases = new Map<
|
|
69
|
-
string,
|
|
70
|
-
{ tagCommit?: string; peeledCommit?: string; version: Semver }
|
|
71
|
-
>();
|
|
72
|
-
|
|
73
|
-
for (const line of lsRemoteOutput.split(/\r?\n/)) {
|
|
74
|
-
if (!line.trim()) continue;
|
|
75
|
-
const match = line.match(/^([0-9a-fA-F]{40})\s+refs\/tags\/([^\s^]+)(\^\{\})?$/);
|
|
76
|
-
const commit = match?.[1]?.toLowerCase();
|
|
77
|
-
const tag = match?.[2];
|
|
78
|
-
const version = tag ? parseStableTag(tag) : undefined;
|
|
79
|
-
if (!commit || !tag || !version) continue;
|
|
80
|
-
|
|
81
|
-
const release = releases.get(tag) ?? { version };
|
|
82
|
-
const existingCommit = match[3] ? release.peeledCommit : release.tagCommit;
|
|
83
|
-
if (releases.has(tag) && existingCommit && existingCommit !== commit) {
|
|
84
|
-
throw new Error(`Conflicting commits for vendor tag: ${tag}`);
|
|
85
|
-
}
|
|
86
|
-
if (match[3]) release.peeledCommit = commit;
|
|
87
|
-
else release.tagCommit = commit;
|
|
88
|
-
releases.set(tag, release);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const candidates = [...releases].flatMap(([tag, release]) => {
|
|
92
|
-
const commit = release.peeledCommit ?? release.tagCommit;
|
|
93
|
-
return commit ? [{ tag, commit, version: release.version }] : [];
|
|
94
|
-
});
|
|
95
|
-
candidates.sort(
|
|
96
|
-
(left, right) =>
|
|
97
|
-
compareSemver(right.version, left.version) ||
|
|
98
|
-
(left.tag < right.tag ? -1 : left.tag > right.tag ? 1 : 0)
|
|
99
|
-
);
|
|
100
|
-
const selected = candidates[0];
|
|
101
|
-
if (!selected) throw new Error("No stable semver Beads tags found");
|
|
102
|
-
return { tag: selected.tag, commit: selected.commit };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Select the newest stable semver tag from git ls-remote output. */
|
|
106
|
-
export function selectStableTag(lsRemoteOutput: string): string {
|
|
107
|
-
return selectStableRelease(lsRemoteOutput).tag;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function listVendorFiles(vendorDir: string): Promise<string[]> {
|
|
111
|
-
const files: string[] = [];
|
|
112
|
-
|
|
113
|
-
async function visit(relativeDir: string): Promise<void> {
|
|
114
|
-
const entries = await fs.readdir(path.join(vendorDir, relativeDir), { withFileTypes: true });
|
|
115
|
-
for (const entry of entries) {
|
|
116
|
-
const relativePath = path.posix.join(relativeDir, entry.name);
|
|
117
|
-
if (entry.isDirectory()) await visit(relativePath);
|
|
118
|
-
else if (entry.isFile()) files.push(relativePath);
|
|
119
|
-
else throw new Error(`Unsupported vendor entry: ${relativePath}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
await visit("agents");
|
|
124
|
-
await visit("commands");
|
|
125
|
-
return files.sort();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function fileRecord(vendorDir: string, relativePath: string): Promise<VendorFileRecord> {
|
|
129
|
-
const content = await fs.readFile(path.join(vendorDir, relativePath));
|
|
130
|
-
return {
|
|
131
|
-
path: relativePath,
|
|
132
|
-
bytes: content.byteLength,
|
|
133
|
-
sha256: createHash("sha256").update(content).digest("hex"),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Build deterministic provenance for a validated vendor candidate. */
|
|
138
|
-
export async function createVendorManifest(
|
|
139
|
-
vendorDir: string,
|
|
140
|
-
repository: string,
|
|
141
|
-
tag: string,
|
|
142
|
-
commit: string
|
|
143
|
-
): Promise<VendorManifest> {
|
|
144
|
-
if (!repository) throw new Error("Vendor repository is required");
|
|
145
|
-
if (!parseStableTag(tag)) throw new Error(`Vendor tag is not stable semver: ${tag}`);
|
|
146
|
-
if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error(`Invalid vendor commit SHA: ${commit}`);
|
|
147
|
-
|
|
148
|
-
const files = await listVendorFiles(vendorDir);
|
|
149
|
-
if (files.length === 0) throw new Error("Vendor inventory is empty");
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
schemaVersion: 1,
|
|
153
|
-
repository,
|
|
154
|
-
tag,
|
|
155
|
-
commit,
|
|
156
|
-
sources: VENDOR_SOURCES,
|
|
157
|
-
files: await Promise.all(files.map((relativePath) => fileRecord(vendorDir, relativePath))),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function serializeVendorManifest(manifest: VendorManifest): string {
|
|
162
|
-
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
166
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function hasExactKeys(value: Record<string, unknown>, expectedKeys: string[]): boolean {
|
|
170
|
-
const actualKeys = Object.keys(value);
|
|
171
|
-
return (
|
|
172
|
-
actualKeys.length === expectedKeys.length &&
|
|
173
|
-
expectedKeys.every((key) => Object.hasOwn(value, key))
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function parseManifest(content: string): VendorManifest {
|
|
178
|
-
const parsed: unknown = JSON.parse(content);
|
|
179
|
-
if (!isRecord(parsed)) {
|
|
180
|
-
throw new Error("Vendor manifest must be an object");
|
|
181
|
-
}
|
|
182
|
-
const values = parsed;
|
|
183
|
-
if (!hasExactKeys(values, ["schemaVersion", "repository", "tag", "commit", "sources", "files"])) {
|
|
184
|
-
throw new Error("Vendor manifest has unexpected or missing fields");
|
|
185
|
-
}
|
|
186
|
-
if (values.schemaVersion !== 1) throw new Error("Unsupported vendor manifest schema");
|
|
187
|
-
if (typeof values.repository !== "string" || !values.repository) throw new Error("Missing vendor repository");
|
|
188
|
-
if (typeof values.tag !== "string" || !parseStableTag(values.tag)) throw new Error("Invalid vendor tag");
|
|
189
|
-
if (typeof values.commit !== "string" || !/^[0-9a-f]{40}$/.test(values.commit)) throw new Error("Invalid vendor commit");
|
|
190
|
-
if (JSON.stringify(values.sources) !== JSON.stringify(VENDOR_SOURCES)) throw new Error("Vendor source paths differ from the expected inventory");
|
|
191
|
-
if (!Array.isArray(values.files) || values.files.length === 0) {
|
|
192
|
-
throw new Error("Missing vendor file inventory");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const files = values.files.map((value): VendorFileRecord => {
|
|
196
|
-
if (!isRecord(value)) {
|
|
197
|
-
throw new Error("Invalid vendor file record");
|
|
198
|
-
}
|
|
199
|
-
const record = value;
|
|
200
|
-
if (!hasExactKeys(record, ["path", "bytes", "sha256"])) {
|
|
201
|
-
throw new Error("Vendor file record has unexpected or missing fields");
|
|
202
|
-
}
|
|
203
|
-
if (typeof record.path !== "string" || !record.path) throw new Error("Invalid vendor file path");
|
|
204
|
-
if (typeof record.bytes !== "number" || !Number.isSafeInteger(record.bytes) || record.bytes < 0) throw new Error("Invalid vendor file size");
|
|
205
|
-
if (typeof record.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(record.sha256)) throw new Error("Invalid vendor file checksum");
|
|
206
|
-
return { path: record.path, bytes: record.bytes, sha256: record.sha256 };
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
schemaVersion: 1,
|
|
211
|
-
repository: values.repository,
|
|
212
|
-
tag: values.tag,
|
|
213
|
-
commit: values.commit,
|
|
214
|
-
sources: VENDOR_SOURCES,
|
|
215
|
-
files,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** Validate manifest inventory and checksums against files on disk. */
|
|
220
|
-
export async function validateVendorManifest(
|
|
221
|
-
vendorDir: string,
|
|
222
|
-
expected?: ExpectedVendorProvenance
|
|
223
|
-
): Promise<VendorManifest> {
|
|
224
|
-
const manifest = parseManifest(
|
|
225
|
-
await fs.readFile(path.join(vendorDir, VENDOR_MANIFEST), "utf-8")
|
|
226
|
-
);
|
|
227
|
-
if (
|
|
228
|
-
expected &&
|
|
229
|
-
(manifest.repository !== expected.repository ||
|
|
230
|
-
manifest.tag !== expected.tag ||
|
|
231
|
-
manifest.commit !== expected.commit)
|
|
232
|
-
) {
|
|
233
|
-
throw new Error("Vendor release provenance differs from the resolved upstream release");
|
|
234
|
-
}
|
|
235
|
-
const actualPaths = await listVendorFiles(vendorDir);
|
|
236
|
-
const manifestPaths = manifest.files.map((file) => file.path);
|
|
237
|
-
if (JSON.stringify(manifestPaths) !== JSON.stringify([...manifestPaths].sort())) {
|
|
238
|
-
throw new Error("Vendor manifest inventory is not sorted");
|
|
239
|
-
}
|
|
240
|
-
if (JSON.stringify(manifestPaths) !== JSON.stringify(actualPaths)) {
|
|
241
|
-
throw new Error("Vendor file inventory differs from the manifest");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
for (const expected of manifest.files) {
|
|
245
|
-
const actual = await fileRecord(vendorDir, expected.path);
|
|
246
|
-
if (actual.bytes !== expected.bytes || actual.sha256 !== expected.sha256) {
|
|
247
|
-
throw new Error(`Vendor checksum mismatch: ${expected.path}`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return manifest;
|
|
252
|
-
}
|
package/src/vendor.ts
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vendor file loaders for beads plugin.
|
|
3
|
-
*
|
|
4
|
-
* The vendor directory contains beads command definitions and agent prompts
|
|
5
|
-
* synced from the upstream beads repository via scripts/sync-beads.sh.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from "node:fs/promises";
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import type { AgentConfig, Config } from "@opencode-ai/sdk";
|
|
12
|
-
import { adaptVendorPrompt } from "./vendor-adaptations";
|
|
13
|
-
|
|
14
|
-
function getVendorDir(): string {
|
|
15
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
return path.join(__dirname, "..", "vendor");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface ParsedMarkdown {
|
|
20
|
-
frontmatter: Record<string, string>;
|
|
21
|
-
body: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const DOCUMENT_ONLY_COMMANDS = new Set(["commands/prime.md", "commands/template.md"]);
|
|
25
|
-
const COMMAND_FRONTMATTER_FIELDS = new Set([
|
|
26
|
-
"agent",
|
|
27
|
-
"argument-hint",
|
|
28
|
-
"description",
|
|
29
|
-
"model",
|
|
30
|
-
"subtask",
|
|
31
|
-
]);
|
|
32
|
-
const AGENT_FRONTMATTER_FIELDS = new Set([
|
|
33
|
-
"color",
|
|
34
|
-
"description",
|
|
35
|
-
"disable",
|
|
36
|
-
"maxSteps",
|
|
37
|
-
"mode",
|
|
38
|
-
"model",
|
|
39
|
-
"temperature",
|
|
40
|
-
"top_p",
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
function vendorError(relativePath: string, reason: string): Error {
|
|
44
|
-
return new Error(`Invalid vendor artifact ${relativePath}: ${reason}`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseMarkdownWithFrontmatter(content: string, relativePath: string): ParsedMarkdown {
|
|
48
|
-
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
49
|
-
const match = content.match(frontmatterRegex);
|
|
50
|
-
|
|
51
|
-
if (!match) throw vendorError(relativePath, "missing or malformed frontmatter delimiters");
|
|
52
|
-
|
|
53
|
-
const frontmatterStr = match[1];
|
|
54
|
-
const body = match[2];
|
|
55
|
-
|
|
56
|
-
if (frontmatterStr === undefined || body === undefined) {
|
|
57
|
-
throw vendorError(relativePath, "missing frontmatter or body");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const frontmatter: Record<string, string> = {};
|
|
61
|
-
|
|
62
|
-
for (const line of frontmatterStr.split("\n")) {
|
|
63
|
-
const trimmed = line.trim();
|
|
64
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
65
|
-
|
|
66
|
-
const colonIndex = trimmed.indexOf(":");
|
|
67
|
-
if (colonIndex <= 0) throw vendorError(relativePath, `malformed frontmatter line: ${trimmed}`);
|
|
68
|
-
|
|
69
|
-
const key = trimmed.slice(0, colonIndex).trim();
|
|
70
|
-
let value = trimmed.slice(colonIndex + 1).trim();
|
|
71
|
-
if (Object.hasOwn(frontmatter, key)) {
|
|
72
|
-
throw vendorError(relativePath, `duplicate frontmatter field: ${key}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Handle quoted strings
|
|
76
|
-
if (
|
|
77
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
78
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
79
|
-
) {
|
|
80
|
-
value = value.slice(1, -1);
|
|
81
|
-
} else if (value.startsWith('"') || value.startsWith("'")) {
|
|
82
|
-
throw vendorError(relativePath, `unterminated quoted value for ${key}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Handle empty array syntax like []
|
|
86
|
-
if (value === "[]") {
|
|
87
|
-
value = "";
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
frontmatter[key] = value;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const trimmedBody = body.trim();
|
|
94
|
-
if (!trimmedBody) throw vendorError(relativePath, "body must not be empty");
|
|
95
|
-
return { frontmatter, body: trimmedBody };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function readVendorFile(vendorDirectory: string, relativePath: string): Promise<string> {
|
|
99
|
-
try {
|
|
100
|
-
const fullPath = path.join(vendorDirectory, relativePath);
|
|
101
|
-
return await fs.readFile(fullPath, "utf-8");
|
|
102
|
-
} catch (error) {
|
|
103
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
104
|
-
throw vendorError(relativePath, `cannot read file: ${reason}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function manifestPaths(vendorDirectory: string): Promise<string[]> {
|
|
109
|
-
const content = await readVendorFile(vendorDirectory, "manifest.json");
|
|
110
|
-
let manifest: unknown;
|
|
111
|
-
try {
|
|
112
|
-
manifest = JSON.parse(content);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
115
|
-
throw vendorError("manifest.json", `invalid JSON: ${reason}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
typeof manifest !== "object" ||
|
|
120
|
-
manifest === null ||
|
|
121
|
-
!("files" in manifest) ||
|
|
122
|
-
!Array.isArray(manifest.files)
|
|
123
|
-
) {
|
|
124
|
-
throw vendorError("manifest.json", "files must be an array");
|
|
125
|
-
}
|
|
126
|
-
const paths = manifest.files.map((file, index) => {
|
|
127
|
-
if (
|
|
128
|
-
typeof file !== "object" ||
|
|
129
|
-
file === null ||
|
|
130
|
-
!("path" in file) ||
|
|
131
|
-
typeof file.path !== "string"
|
|
132
|
-
) {
|
|
133
|
-
throw vendorError("manifest.json", `files[${index}].path must be a string`);
|
|
134
|
-
}
|
|
135
|
-
return file.path;
|
|
136
|
-
});
|
|
137
|
-
if (new Set(paths).size !== paths.length) {
|
|
138
|
-
throw vendorError("manifest.json", "file paths must be unique");
|
|
139
|
-
}
|
|
140
|
-
return paths.sort();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function requiredString(
|
|
144
|
-
frontmatter: Record<string, string>,
|
|
145
|
-
field: string,
|
|
146
|
-
relativePath: string
|
|
147
|
-
): string {
|
|
148
|
-
const value = frontmatter[field];
|
|
149
|
-
if (!value?.trim()) throw vendorError(relativePath, `${field} must be a non-empty string`);
|
|
150
|
-
return value;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function parseBoolean(value: string, field: string, relativePath: string): boolean {
|
|
154
|
-
if (value === "true") return true;
|
|
155
|
-
if (value === "false") return false;
|
|
156
|
-
throw vendorError(relativePath, `${field} must be true or false`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function parseNumber(value: string, field: string, relativePath: string): number {
|
|
160
|
-
const parsed = Number(value);
|
|
161
|
-
if (!Number.isFinite(parsed)) throw vendorError(relativePath, `${field} must be a number`);
|
|
162
|
-
return parsed;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function rejectUnsupportedFields(
|
|
166
|
-
frontmatter: Record<string, string>,
|
|
167
|
-
supported: ReadonlySet<string>,
|
|
168
|
-
relativePath: string
|
|
169
|
-
): void {
|
|
170
|
-
const unsupported = Object.keys(frontmatter).find((field) => !supported.has(field));
|
|
171
|
-
if (unsupported) throw vendorError(relativePath, `unsupported frontmatter field: ${unsupported}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const BEADS_HOST_GUIDANCE = `## CLI Safety
|
|
175
|
-
|
|
176
|
-
There is no native \`bd\` or Beads MCP tool in OpenCode. Run \`bd\` through \`bash\`.
|
|
177
|
-
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.
|
|
178
|
-
|
|
179
|
-
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.`;
|
|
180
|
-
|
|
181
|
-
const BEADS_DELEGATION_GUIDANCE = `## Delegation
|
|
182
|
-
|
|
183
|
-
Delegate multi-command Beads work and issue-graph analysis to the \`beads-task-agent\`; perform only single atomic operations directly.`;
|
|
184
|
-
|
|
185
|
-
const BEADS_SUBAGENT_CONTEXT = `## Beads Task Agent
|
|
186
|
-
|
|
187
|
-
Full \`bd prime\` workflow context is injected at start and after compaction. Use \`bd\` via \`bash\`; rerun prime if context is missing or stale.
|
|
188
|
-
|
|
189
|
-
For status or graph analysis, never claim, update, close, or modify a bead. Return the analysis and stop.
|
|
190
|
-
|
|
191
|
-
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.
|
|
192
|
-
|
|
193
|
-
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.`;
|
|
194
|
-
|
|
195
|
-
/** Build the single shared guidance layer for one injected audience. */
|
|
196
|
-
export function beadsGuidance(
|
|
197
|
-
audience: "primary" | "task-agent"
|
|
198
|
-
): string {
|
|
199
|
-
const sections = [BEADS_HOST_GUIDANCE];
|
|
200
|
-
if (audience === "primary") sections.push(BEADS_DELEGATION_GUIDANCE);
|
|
201
|
-
return `<beads-guidance>\n${sections.join("\n\n")}\n</beads-guidance>`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export async function loadAgent(vendorDirectory = getVendorDir()): Promise<Config["agent"]> {
|
|
205
|
-
const relativePath = "agents/task-agent.md";
|
|
206
|
-
const paths = await manifestPaths(vendorDirectory);
|
|
207
|
-
if (!paths.includes(relativePath)) {
|
|
208
|
-
throw vendorError("manifest.json", `missing required file record: ${relativePath}`);
|
|
209
|
-
}
|
|
210
|
-
const content = await readVendorFile(vendorDirectory, relativePath);
|
|
211
|
-
const parsed = parseMarkdownWithFrontmatter(content, relativePath);
|
|
212
|
-
const { frontmatter } = parsed;
|
|
213
|
-
rejectUnsupportedFields(frontmatter, AGENT_FRONTMATTER_FIELDS, relativePath);
|
|
214
|
-
if (frontmatter.mode !== undefined && frontmatter.mode !== "subagent") {
|
|
215
|
-
throw vendorError(relativePath, "mode must be subagent when provided");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const agent: AgentConfig = {
|
|
219
|
-
description: requiredString(frontmatter, "description", relativePath),
|
|
220
|
-
prompt: BEADS_SUBAGENT_CONTEXT,
|
|
221
|
-
mode: "subagent",
|
|
222
|
-
};
|
|
223
|
-
// Keep upstream adaptation validation as a publish-time compatibility gate.
|
|
224
|
-
adaptVendorPrompt(relativePath, parsed.body);
|
|
225
|
-
if (frontmatter.model) agent.model = frontmatter.model;
|
|
226
|
-
if (frontmatter.temperature) {
|
|
227
|
-
agent.temperature = parseNumber(frontmatter.temperature, "temperature", relativePath);
|
|
228
|
-
}
|
|
229
|
-
if (frontmatter.top_p) agent.top_p = parseNumber(frontmatter.top_p, "top_p", relativePath);
|
|
230
|
-
if (frontmatter.disable) {
|
|
231
|
-
agent.disable = parseBoolean(frontmatter.disable, "disable", relativePath);
|
|
232
|
-
}
|
|
233
|
-
if (frontmatter.color) agent.color = frontmatter.color;
|
|
234
|
-
if (frontmatter.maxSteps) {
|
|
235
|
-
agent.maxSteps = parseNumber(frontmatter.maxSteps, "maxSteps", relativePath);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return { "beads-task-agent": agent };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export async function loadCommands(vendorDirectory = getVendorDir()): Promise<Config["command"]> {
|
|
242
|
-
const paths = await manifestPaths(vendorDirectory);
|
|
243
|
-
const commandPaths = paths.filter((file) => file.startsWith("commands/") && file.endsWith(".md"));
|
|
244
|
-
if (commandPaths.length === 0) {
|
|
245
|
-
throw vendorError("manifest.json", "no command Markdown files are recorded");
|
|
246
|
-
}
|
|
247
|
-
const commands: Config["command"] = {};
|
|
248
|
-
|
|
249
|
-
for (const relativePath of commandPaths) {
|
|
250
|
-
const content = await readVendorFile(vendorDirectory, relativePath);
|
|
251
|
-
if (DOCUMENT_ONLY_COMMANDS.has(relativePath)) continue;
|
|
252
|
-
const parsed = parseMarkdownWithFrontmatter(content, relativePath);
|
|
253
|
-
const { frontmatter } = parsed;
|
|
254
|
-
rejectUnsupportedFields(frontmatter, COMMAND_FRONTMATTER_FIELDS, relativePath);
|
|
255
|
-
const file = path.basename(relativePath);
|
|
256
|
-
const name = `beads:${file.replace(".md", "")}`;
|
|
257
|
-
|
|
258
|
-
const argHint = frontmatter["argument-hint"];
|
|
259
|
-
const baseDescription = requiredString(frontmatter, "description", relativePath);
|
|
260
|
-
const description = argHint
|
|
261
|
-
? `${baseDescription} (${argHint})`
|
|
262
|
-
: baseDescription;
|
|
263
|
-
|
|
264
|
-
const command: NonNullable<Config["command"]>[string] = {
|
|
265
|
-
description,
|
|
266
|
-
template: adaptVendorPrompt(relativePath, parsed.body),
|
|
267
|
-
};
|
|
268
|
-
if (frontmatter.agent) command.agent = frontmatter.agent;
|
|
269
|
-
if (frontmatter.model) command.model = frontmatter.model;
|
|
270
|
-
if (frontmatter.subtask) {
|
|
271
|
-
command.subtask = parseBoolean(frontmatter.subtask, "subtask", relativePath);
|
|
272
|
-
}
|
|
273
|
-
commands[name] = command;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return commands;
|
|
277
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Autonomous agent that finds and completes ready tasks
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
You are a task-completion agent for beads. Your goal is to find ready work and complete it autonomously.
|
|
6
|
-
|
|
7
|
-
# Agent Workflow
|
|
8
|
-
|
|
9
|
-
1. **Find Ready Work**
|
|
10
|
-
- Use the `ready` MCP tool to get unblocked tasks
|
|
11
|
-
- Prefer higher priority tasks (P0 > P1 > P2 > P3 > P4)
|
|
12
|
-
- If no ready tasks, report completion
|
|
13
|
-
|
|
14
|
-
2. **Claim the Task**
|
|
15
|
-
- Use the `show` tool to get full task details
|
|
16
|
-
- Use the `claim` tool for atomic start-work semantics
|
|
17
|
-
- Report what you're working on
|
|
18
|
-
|
|
19
|
-
3. **Execute the Task**
|
|
20
|
-
- Read the task description carefully
|
|
21
|
-
- Use available tools to complete the work
|
|
22
|
-
- Follow best practices from project documentation
|
|
23
|
-
- Run tests if applicable
|
|
24
|
-
|
|
25
|
-
4. **Track Discoveries**
|
|
26
|
-
- If you find bugs, TODOs, or related work:
|
|
27
|
-
- Use `create` tool to file new issues
|
|
28
|
-
- Use `dep` tool with `discovered-from` to link them
|
|
29
|
-
- This maintains context for future work
|
|
30
|
-
|
|
31
|
-
5. **Complete the Task**
|
|
32
|
-
- Verify the work is done correctly
|
|
33
|
-
- Use `close` tool with a clear completion message
|
|
34
|
-
- Report what was accomplished
|
|
35
|
-
|
|
36
|
-
6. **Continue**
|
|
37
|
-
- Check for newly unblocked work with `ready`
|
|
38
|
-
- Repeat the cycle
|
|
39
|
-
|
|
40
|
-
# Important Guidelines
|
|
41
|
-
|
|
42
|
-
- Always claim before working (MCP: `claim`; CLI: `--claim`) and close when done
|
|
43
|
-
- Link discovered work with `discovered-from` dependencies
|
|
44
|
-
- Don't close issues unless work is actually complete
|
|
45
|
-
- If blocked, use `update` to set status to `blocked` and explain why
|
|
46
|
-
- Communicate clearly about progress and blockers
|
|
47
|
-
|
|
48
|
-
# Available Tools
|
|
49
|
-
|
|
50
|
-
Via beads MCP server:
|
|
51
|
-
- `ready` - Find unblocked tasks
|
|
52
|
-
- `show` - Get task details
|
|
53
|
-
- `claim` - Atomically claim task for work
|
|
54
|
-
- `update` - Update task status/fields
|
|
55
|
-
- `create` - Create new issues
|
|
56
|
-
- `dep` - Manage dependencies
|
|
57
|
-
- `close` - Complete tasks
|
|
58
|
-
- `blocked` - Check blocked issues
|
|
59
|
-
- `stats` - View project stats
|
|
60
|
-
|
|
61
|
-
You are autonomous but should communicate your progress clearly. Start by finding ready work!
|
package/vendor/commands/audit.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Log and label agent interactions (append-only JSONL)
|
|
3
|
-
argument-hint: record|label
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Append-only audit logging for agent interactions (prompts, responses, tool calls) in `.beads/interactions.jsonl`.
|
|
7
|
-
|
|
8
|
-
Each line is one event. Labeling is done by appending a new `"label"` event referencing a previous entry.
|
|
9
|
-
|
|
10
|
-
## Usage
|
|
11
|
-
|
|
12
|
-
- **Record an interaction**:
|
|
13
|
-
- `bd audit record --kind llm_call --model "claude-3-5-haiku" --prompt "..." --response "..."`
|
|
14
|
-
- `bd audit record --kind tool_call --tool-name "go test" --exit-code 1 --error "..." --issue-id bd-42`
|
|
15
|
-
|
|
16
|
-
- **Pipe JSON via stdin**:
|
|
17
|
-
- `cat event.json | bd audit record`
|
|
18
|
-
|
|
19
|
-
- **Label an entry**:
|
|
20
|
-
- `bd audit label int-a1b2 --label good --reason "Worked perfectly"`
|
|
21
|
-
- `bd audit label int-a1b2 --label bad --reason "Hallucinated a file path"`
|
|
22
|
-
|
|
23
|
-
## Notes
|
|
24
|
-
|
|
25
|
-
- Audit entries are **append-only** (no in-place edits).
|
|
26
|
-
- `bd dolt push` includes `.beads/interactions.jsonl` in the commit allowlist.
|
|
27
|
-
|
|
28
|
-
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Show blocked issues
|
|
3
|
-
argument-hint: []
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Show all issues that are blocked by dependencies.
|
|
7
|
-
|
|
8
|
-
Use `bd blocked` to see which issues have blockers preventing them from being worked on. This is the inverse of `bd ready` - it shows what's NOT ready.
|
|
9
|
-
|
|
10
|
-
Blocked issues have one or more dependencies with type "blocks" that are still open. Once all blocking dependencies are closed, the issue becomes ready and will appear in `bd ready`.
|
|
11
|
-
|
|
12
|
-
Useful for:
|
|
13
|
-
- Understanding why work is stuck
|
|
14
|
-
- Identifying critical path items
|
|
15
|
-
- Planning dependency resolution
|
package/vendor/commands/close.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Close a completed issue
|
|
3
|
-
argument-hint: [issue-id] [reason]
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Close a beads issue that's been completed.
|
|
7
|
-
|
|
8
|
-
If arguments are provided:
|
|
9
|
-
- $1: Issue ID
|
|
10
|
-
- $2+: Completion reason (optional)
|
|
11
|
-
|
|
12
|
-
If the issue ID is missing, ask for it. Optionally ask for a reason describing what was done.
|
|
13
|
-
|
|
14
|
-
Use the beads MCP `close` tool to close the issue. Show confirmation with the issue details.
|
|
15
|
-
|
|
16
|
-
After closing, suggest checking for:
|
|
17
|
-
- Dependent issues that might now be unblocked (use `ready` tool)
|
|
18
|
-
- New work discovered during this task (use `create` tool with `discovered-from` link)
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: View or manage comments on an issue
|
|
3
|
-
argument-hint: [issue-id]
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
View or add comments to a beads issue.
|
|
7
|
-
|
|
8
|
-
Comments are separate from issue properties (title, description, etc.) because they serve a different purpose: they're a **discussion thread** rather than **singular editable fields**. Use `bd comments` for threaded conversations and `bd edit` for core issue metadata.
|
|
9
|
-
|
|
10
|
-
## View Comments
|
|
11
|
-
|
|
12
|
-
To view all comments on an issue:
|
|
13
|
-
- $1: Issue ID (e.g., bd-123)
|
|
14
|
-
|
|
15
|
-
Use the beads CLI `bd comments <issue-id>` to list all comments. Show them to the user with timestamps and authors.
|
|
16
|
-
|
|
17
|
-
## Add Comment
|
|
18
|
-
|
|
19
|
-
To add a comment:
|
|
20
|
-
- $1: "add"
|
|
21
|
-
- $2: Issue ID
|
|
22
|
-
- $3: Comment text (or use -f flag for file input)
|
|
23
|
-
|
|
24
|
-
Use `bd comments add <issue-id> "comment text"` to add a comment. Confirm the comment was added successfully.
|
|
25
|
-
|
|
26
|
-
Comments are useful for:
|
|
27
|
-
- Progress updates during work
|
|
28
|
-
- Design notes or technical decisions
|
|
29
|
-
- Links to related resources
|
|
30
|
-
- Questions or blockers
|