opencodekit 0.15.10 → 0.15.12
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/dist/index.js +1 -1
- package/dist/template/.opencode/agent/build.md +390 -0
- package/dist/template/.opencode/command/implement.md +136 -10
- package/dist/template/.opencode/memory/observations/2026-01-25-decision-agent-roles-build-orchestrates-general-e.md +14 -0
- package/dist/template/.opencode/memory/observations/2026-01-25-decision-simplified-swarm-helper-tool-to-fix-type.md +20 -0
- package/dist/template/.opencode/memory/observations/2026-01-25-decision-use-beads-as-swarm-board-source-of-truth.md +14 -0
- package/dist/template/.opencode/memory/observations/2026-01-25-learning-user-wants-real-swarm-coordination-guida.md +15 -0
- package/dist/template/.opencode/memory/research/opencode-mcp-bug-report.md +126 -0
- package/dist/template/.opencode/opencode.json +151 -46
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plans/swarm-protocol.md +123 -0
- package/dist/template/.opencode/plugin/README.md +10 -0
- package/dist/template/.opencode/plugin/copilot-auth.ts +104 -77
- package/dist/template/.opencode/plugin/swarm-enforcer.ts +297 -0
- package/dist/template/.opencode/skill/swarm-coordination/SKILL.md +405 -0
- package/dist/template/.opencode/tool/swarm-delegate.ts +175 -0
- package/dist/template/.opencode/tool/swarm-helper.ts +164 -0
- package/package.json +1 -1
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
7
7
|
|
|
8
|
-
const CLIENT_ID = "
|
|
8
|
+
const CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
9
|
+
|
|
10
|
+
// Add a small safety buffer when polling to avoid hitting the server
|
|
11
|
+
// slightly too early due to clock skew / timer drift.
|
|
12
|
+
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000; // 3 seconds
|
|
9
13
|
|
|
10
14
|
const HEADERS = {
|
|
11
15
|
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
@@ -40,11 +44,12 @@ function getUrls(domain: string) {
|
|
|
40
44
|
return {
|
|
41
45
|
DEVICE_CODE_URL: `https://${domain}/login/device/code`,
|
|
42
46
|
ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
|
|
43
|
-
COPILOT_API_KEY_URL: `https://api.github.com/copilot_internal/v2/token`,
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
|
|
52
|
+
export const CopilotAuthPlugin: Plugin = async ({ client: _client }) => {
|
|
48
53
|
return {
|
|
49
54
|
auth: {
|
|
50
55
|
provider: "github-copilot",
|
|
@@ -52,6 +57,11 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
52
57
|
const info = await getAuth();
|
|
53
58
|
if (!info || info.type !== "oauth") return {};
|
|
54
59
|
|
|
60
|
+
const enterpriseUrl = info.enterpriseUrl;
|
|
61
|
+
const baseURL = enterpriseUrl
|
|
62
|
+
? `https://copilot-api.${normalizeDomain(enterpriseUrl)}`
|
|
63
|
+
: undefined;
|
|
64
|
+
|
|
55
65
|
if (provider && provider.models) {
|
|
56
66
|
for (const model of Object.values(provider.models)) {
|
|
57
67
|
model.cost = {
|
|
@@ -62,71 +72,47 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
62
72
|
write: 0,
|
|
63
73
|
},
|
|
64
74
|
};
|
|
75
|
+
|
|
76
|
+
// Sync with official: Handle Claude routing and SDK mapping
|
|
77
|
+
const base =
|
|
78
|
+
baseURL ?? model.api.url ?? "https://api.githubcopilot.com";
|
|
79
|
+
const isClaude = model.id.includes("claude");
|
|
80
|
+
|
|
81
|
+
let url = base;
|
|
82
|
+
if (isClaude) {
|
|
83
|
+
if (!url.endsWith("/v1")) {
|
|
84
|
+
url = url.endsWith("/") ? `${url}v1` : `${url}/v1`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
model.api.url = url;
|
|
89
|
+
model.api.npm = isClaude
|
|
90
|
+
? "@ai-sdk/anthropic"
|
|
91
|
+
: "@ai-sdk/github-copilot";
|
|
65
92
|
}
|
|
66
93
|
}
|
|
67
94
|
|
|
68
|
-
const enterpriseUrl = info.enterpriseUrl;
|
|
69
|
-
const baseURL = enterpriseUrl
|
|
70
|
-
? `https://copilot-api.${normalizeDomain(enterpriseUrl)}`
|
|
71
|
-
: "https://api.githubcopilot.com";
|
|
72
|
-
|
|
73
95
|
return {
|
|
74
|
-
baseURL,
|
|
75
96
|
apiKey: "",
|
|
76
97
|
async fetch(input, init) {
|
|
77
98
|
const info = await getAuth();
|
|
78
|
-
if (info.type !== "oauth") return
|
|
79
|
-
if (!info.access) {
|
|
80
|
-
const domain = info.enterpriseUrl
|
|
81
|
-
? normalizeDomain(info.enterpriseUrl)
|
|
82
|
-
: "github.com";
|
|
83
|
-
const urls = getUrls(domain);
|
|
84
|
-
|
|
85
|
-
const response = await fetch(urls.COPILOT_API_KEY_URL, {
|
|
86
|
-
headers: {
|
|
87
|
-
Accept: "application/json",
|
|
88
|
-
Authorization: `Bearer ${info.refresh}`,
|
|
89
|
-
...HEADERS,
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
throw new Error(`Token refresh failed: ${response.status}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const tokenData = await response.json();
|
|
98
|
-
|
|
99
|
-
const saveProviderID = info.enterpriseUrl
|
|
100
|
-
? "github-copilot-enterprise"
|
|
101
|
-
: "github-copilot";
|
|
102
|
-
await client.auth.set({
|
|
103
|
-
path: {
|
|
104
|
-
id: saveProviderID,
|
|
105
|
-
},
|
|
106
|
-
body: {
|
|
107
|
-
type: "oauth",
|
|
108
|
-
refresh: info.refresh,
|
|
109
|
-
access: tokenData.token,
|
|
110
|
-
expires: tokenData.expires_at * 1000 - 5 * 60 * 1000,
|
|
111
|
-
...(info.enterpriseUrl && {
|
|
112
|
-
enterpriseUrl: info.enterpriseUrl,
|
|
113
|
-
}),
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
info.access = tokenData.token;
|
|
117
|
-
}
|
|
99
|
+
if (info.type !== "oauth") return fetch(input, init);
|
|
118
100
|
|
|
119
101
|
let isAgentCall = false;
|
|
120
102
|
let isVisionRequest = false;
|
|
121
103
|
try {
|
|
122
104
|
const body =
|
|
123
|
-
typeof init
|
|
105
|
+
typeof init?.body === "string"
|
|
124
106
|
? JSON.parse(init.body)
|
|
125
|
-
: init
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
107
|
+
: init?.body;
|
|
108
|
+
|
|
109
|
+
const url = input.toString();
|
|
110
|
+
|
|
111
|
+
// Completions API
|
|
112
|
+
if (body?.messages && url.includes("completions")) {
|
|
113
|
+
// Keep local logic: detect if any message is assistant/tool
|
|
114
|
+
isAgentCall = body.messages.some((msg: any) =>
|
|
115
|
+
["tool", "assistant"].includes(msg.role),
|
|
130
116
|
);
|
|
131
117
|
isVisionRequest = body.messages.some(
|
|
132
118
|
(msg: any) =>
|
|
@@ -135,34 +121,58 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
135
121
|
);
|
|
136
122
|
}
|
|
137
123
|
|
|
124
|
+
// Responses API
|
|
138
125
|
if (body?.input) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
126
|
+
isAgentCall = body.input.some(
|
|
127
|
+
(item: any) =>
|
|
128
|
+
item?.role === "assistant" ||
|
|
129
|
+
(item?.type &&
|
|
130
|
+
RESPONSES_API_ALTERNATE_INPUT_TYPES.includes(item.type)),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
isVisionRequest = body.input.some(
|
|
134
|
+
(item: any) =>
|
|
135
|
+
Array.isArray(item?.content) &&
|
|
136
|
+
item.content.some(
|
|
137
|
+
(part: any) => part.type === "input_image",
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Messages API (Anthropic style)
|
|
143
|
+
if (body?.messages && !url.includes("completions")) {
|
|
144
|
+
isAgentCall = body.messages.some((msg: any) =>
|
|
145
|
+
["tool", "assistant"].includes(msg.role),
|
|
146
|
+
);
|
|
147
|
+
isVisionRequest = body.messages.some(
|
|
148
|
+
(item: any) =>
|
|
149
|
+
Array.isArray(item?.content) &&
|
|
150
|
+
item.content.some(
|
|
151
|
+
(part: any) =>
|
|
152
|
+
part?.type === "image" ||
|
|
153
|
+
(part?.type === "tool_result" &&
|
|
154
|
+
Array.isArray(part?.content) &&
|
|
155
|
+
part.content.some(
|
|
156
|
+
(nested: any) => nested?.type === "image",
|
|
157
|
+
)),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
152
160
|
}
|
|
153
161
|
} catch {}
|
|
154
162
|
|
|
155
|
-
const headers = {
|
|
156
|
-
|
|
163
|
+
const headers: Record<string, string> = {
|
|
164
|
+
"x-initiator": isAgentCall ? "agent" : "user",
|
|
165
|
+
...(init?.headers as Record<string, string>),
|
|
157
166
|
...HEADERS,
|
|
158
|
-
Authorization: `Bearer ${info.
|
|
167
|
+
Authorization: `Bearer ${info.refresh}`,
|
|
159
168
|
"Openai-Intent": "conversation-edits",
|
|
160
|
-
"X-Initiator": isAgentCall ? "agent" : "user",
|
|
161
169
|
};
|
|
170
|
+
|
|
162
171
|
if (isVisionRequest) {
|
|
163
172
|
headers["Copilot-Vision-Request"] = "true";
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
// Official only deletes lowercase "authorization"
|
|
166
176
|
delete headers["x-api-key"];
|
|
167
177
|
delete headers["authorization"];
|
|
168
178
|
|
|
@@ -286,7 +296,7 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
286
296
|
} = {
|
|
287
297
|
type: "success",
|
|
288
298
|
refresh: data.access_token,
|
|
289
|
-
access:
|
|
299
|
+
access: data.access_token,
|
|
290
300
|
expires: 0,
|
|
291
301
|
};
|
|
292
302
|
|
|
@@ -299,16 +309,33 @@ export const CopilotAuthPlugin: Plugin = async ({ client }) => {
|
|
|
299
309
|
}
|
|
300
310
|
|
|
301
311
|
if (data.error === "authorization_pending") {
|
|
302
|
-
await
|
|
303
|
-
|
|
312
|
+
await sleep(
|
|
313
|
+
deviceData.interval * 1000 +
|
|
314
|
+
OAUTH_POLLING_SAFETY_MARGIN_MS,
|
|
304
315
|
);
|
|
305
316
|
continue;
|
|
306
317
|
}
|
|
307
318
|
|
|
319
|
+
if (data.error === "slow_down") {
|
|
320
|
+
// Based on the RFC spec, we must add 5 seconds to our current polling interval.
|
|
321
|
+
let newInterval = (deviceData.interval + 5) * 1000;
|
|
322
|
+
|
|
323
|
+
if (
|
|
324
|
+
data.interval &&
|
|
325
|
+
typeof data.interval === "number" &&
|
|
326
|
+
data.interval > 0
|
|
327
|
+
) {
|
|
328
|
+
newInterval = data.interval * 1000;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
308
335
|
if (data.error) return { type: "failed" };
|
|
309
336
|
|
|
310
|
-
await
|
|
311
|
-
|
|
337
|
+
await sleep(
|
|
338
|
+
deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS,
|
|
312
339
|
);
|
|
313
340
|
continue;
|
|
314
341
|
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Enforcer Plugin
|
|
3
|
+
*
|
|
4
|
+
* Beads is the single source of truth for the swarm board.
|
|
5
|
+
* This plugin nudges agents to:
|
|
6
|
+
* - Claim a Beads task before making code changes
|
|
7
|
+
* - Ensure `spec.md` exists for in-progress tasks
|
|
8
|
+
* - Close/sync in-progress work at session end
|
|
9
|
+
*
|
|
10
|
+
* This plugin is intentionally non-destructive: it never runs `bd update/close/sync`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fsPromises from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
16
|
+
|
|
17
|
+
type BeadsIssue = {
|
|
18
|
+
id: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
status?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const BEADS_DIR = ".beads";
|
|
24
|
+
const ISSUES_FILE = "issues.jsonl";
|
|
25
|
+
|
|
26
|
+
const CODE_EXTENSIONS = [
|
|
27
|
+
".ts",
|
|
28
|
+
".tsx",
|
|
29
|
+
".js",
|
|
30
|
+
".jsx",
|
|
31
|
+
".mjs",
|
|
32
|
+
".cjs",
|
|
33
|
+
".py",
|
|
34
|
+
".go",
|
|
35
|
+
".rs",
|
|
36
|
+
".java",
|
|
37
|
+
".c",
|
|
38
|
+
".cpp",
|
|
39
|
+
".h",
|
|
40
|
+
".hpp",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const WORK_INTENT_PATTERNS = [
|
|
44
|
+
/\b(implement|fix|refactor|add|remove|delete|update|change|modify|create|build)\b/i,
|
|
45
|
+
/\b(edit|patch)\b/i,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function looksLikeWorkIntent(text: string): boolean {
|
|
49
|
+
return WORK_INTENT_PATTERNS.some((p) => p.test(text));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isCodeFile(filePath: string): boolean {
|
|
53
|
+
return CODE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isIgnoredPath(repoDir: string, filePath: string): boolean {
|
|
57
|
+
const absPath = path.isAbsolute(filePath)
|
|
58
|
+
? filePath
|
|
59
|
+
: path.join(repoDir, filePath);
|
|
60
|
+
const rel = path.relative(repoDir, absPath);
|
|
61
|
+
|
|
62
|
+
// Outside repo: ignore
|
|
63
|
+
if (rel.startsWith("..")) return true;
|
|
64
|
+
|
|
65
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
66
|
+
return (
|
|
67
|
+
normalized.startsWith("node_modules/") ||
|
|
68
|
+
normalized.startsWith("dist/") ||
|
|
69
|
+
normalized.startsWith(".beads/") ||
|
|
70
|
+
normalized.startsWith(".git/")
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function summarizeIssues(issues: BeadsIssue[], limit = 5): string {
|
|
75
|
+
return issues
|
|
76
|
+
.slice(0, limit)
|
|
77
|
+
.map((i) => `${i.id}${i.title ? `: ${i.title}` : ""}`)
|
|
78
|
+
.join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function readIssuesJsonl(repoDir: string): Promise<BeadsIssue[]> {
|
|
82
|
+
const issuesPath = path.join(repoDir, BEADS_DIR, ISSUES_FILE);
|
|
83
|
+
|
|
84
|
+
let content: string;
|
|
85
|
+
try {
|
|
86
|
+
content = await fsPromises.readFile(issuesPath, "utf-8");
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const issues: BeadsIssue[] = [];
|
|
92
|
+
const lines = content.split(/\r?\n/);
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const trimmed = line.trim();
|
|
95
|
+
if (!trimmed) continue;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(trimmed);
|
|
98
|
+
if (parsed && typeof parsed.id === "string") {
|
|
99
|
+
issues.push({
|
|
100
|
+
id: parsed.id,
|
|
101
|
+
title: typeof parsed.title === "string" ? parsed.title : undefined,
|
|
102
|
+
status: typeof parsed.status === "string" ? parsed.status : undefined,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Ignore malformed JSONL lines
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return issues;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function specExists(repoDir: string, issueId: string): Promise<boolean> {
|
|
114
|
+
const specPath = path.join(
|
|
115
|
+
repoDir,
|
|
116
|
+
BEADS_DIR,
|
|
117
|
+
"artifacts",
|
|
118
|
+
issueId,
|
|
119
|
+
"spec.md",
|
|
120
|
+
);
|
|
121
|
+
try {
|
|
122
|
+
await fsPromises.access(specPath);
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildNudge(params: {
|
|
130
|
+
inProgress: BeadsIssue[];
|
|
131
|
+
missingSpec: BeadsIssue[];
|
|
132
|
+
}): string {
|
|
133
|
+
const { inProgress, missingSpec } = params;
|
|
134
|
+
|
|
135
|
+
if (inProgress.length === 0) {
|
|
136
|
+
return `
|
|
137
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
138
|
+
⚡ [SWARM PROTOCOL]
|
|
139
|
+
|
|
140
|
+
Beads is the swarm board. Before any code changes:
|
|
141
|
+
|
|
142
|
+
1) Pick a task: \`bd ready\` (or \`bd list\`)
|
|
143
|
+
2) Inspect: \`bd show <id>\`
|
|
144
|
+
3) Claim: \`bd update <id> --status=in_progress\`
|
|
145
|
+
|
|
146
|
+
Then proceed with work and collect verification evidence.
|
|
147
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (missingSpec.length > 0) {
|
|
152
|
+
return `
|
|
153
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
154
|
+
⚡ [SWARM PROTOCOL]
|
|
155
|
+
|
|
156
|
+
In-progress Beads exist, but \`spec.md\` is missing for:
|
|
157
|
+
|
|
158
|
+
${summarizeIssues(missingSpec)}
|
|
159
|
+
|
|
160
|
+
Create \`.beads/artifacts/<id>/spec.md\` before implementation.
|
|
161
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const SwarmEnforcer: Plugin = async ({ client, directory }) => {
|
|
169
|
+
const repoDir = directory || process.cwd();
|
|
170
|
+
let lastStateAt = 0;
|
|
171
|
+
let cachedInProgress: BeadsIssue[] = [];
|
|
172
|
+
let cachedMissingSpec: BeadsIssue[] = [];
|
|
173
|
+
|
|
174
|
+
const refreshState = async () => {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
if (now - lastStateAt < 1500) return;
|
|
177
|
+
lastStateAt = now;
|
|
178
|
+
|
|
179
|
+
const issues = await readIssuesJsonl(repoDir);
|
|
180
|
+
const inProgress = issues.filter((i) => i.status === "in_progress");
|
|
181
|
+
|
|
182
|
+
const missingSpec: BeadsIssue[] = [];
|
|
183
|
+
for (const issue of inProgress.slice(0, 10)) {
|
|
184
|
+
if (!(await specExists(repoDir, issue.id))) {
|
|
185
|
+
missingSpec.push(issue);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
cachedInProgress = inProgress;
|
|
190
|
+
cachedMissingSpec = missingSpec;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const showToast = async (
|
|
194
|
+
title: string,
|
|
195
|
+
message: string,
|
|
196
|
+
variant: "info" | "success" | "warning" | "error" = "info",
|
|
197
|
+
) => {
|
|
198
|
+
try {
|
|
199
|
+
await client.tui.showToast({
|
|
200
|
+
body: {
|
|
201
|
+
title,
|
|
202
|
+
message,
|
|
203
|
+
variant,
|
|
204
|
+
duration: variant === "error" ? 8000 : 5000,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
} catch {
|
|
208
|
+
// If toast is unavailable, fail silently
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
// Nudge early when user expresses implementation intent
|
|
214
|
+
"chat.message": async (input, output) => {
|
|
215
|
+
const { sessionID, messageID } = input;
|
|
216
|
+
const { message, parts } = output;
|
|
217
|
+
if (message.role !== "user") return;
|
|
218
|
+
|
|
219
|
+
const fullText = parts
|
|
220
|
+
.filter((p) => p.type === "text" && !("synthetic" in p && p.synthetic))
|
|
221
|
+
.map((p) => ("text" in p ? p.text : ""))
|
|
222
|
+
.join(" ");
|
|
223
|
+
|
|
224
|
+
if (!looksLikeWorkIntent(fullText)) return;
|
|
225
|
+
|
|
226
|
+
await refreshState();
|
|
227
|
+
|
|
228
|
+
const nudge = buildNudge({
|
|
229
|
+
inProgress: cachedInProgress,
|
|
230
|
+
missingSpec: cachedMissingSpec,
|
|
231
|
+
});
|
|
232
|
+
if (!nudge) return;
|
|
233
|
+
|
|
234
|
+
parts.push({
|
|
235
|
+
id: `swarm-nudge-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
236
|
+
sessionID,
|
|
237
|
+
messageID: messageID || "",
|
|
238
|
+
type: "text",
|
|
239
|
+
text: nudge,
|
|
240
|
+
synthetic: true,
|
|
241
|
+
} as import("@opencode-ai/sdk").Part);
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// Warn if code gets edited while no task is claimed / spec missing
|
|
245
|
+
"file.edited": async ({ event }) => {
|
|
246
|
+
const filePath = event.properties?.file || event.properties?.path;
|
|
247
|
+
if (!filePath || typeof filePath !== "string") return;
|
|
248
|
+
if (isIgnoredPath(repoDir, filePath)) return;
|
|
249
|
+
|
|
250
|
+
const absPath = path.isAbsolute(filePath)
|
|
251
|
+
? filePath
|
|
252
|
+
: path.join(repoDir, filePath);
|
|
253
|
+
|
|
254
|
+
if (!isCodeFile(absPath)) return;
|
|
255
|
+
|
|
256
|
+
await refreshState();
|
|
257
|
+
|
|
258
|
+
if (cachedInProgress.length === 0) {
|
|
259
|
+
await showToast(
|
|
260
|
+
"Swarm: No task claimed",
|
|
261
|
+
"Beads is the board. Claim a task before code edits (bd ready/show/update).",
|
|
262
|
+
"warning",
|
|
263
|
+
);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (cachedMissingSpec.length > 0) {
|
|
268
|
+
await showToast(
|
|
269
|
+
"Swarm: Missing spec.md",
|
|
270
|
+
`Create .beads/artifacts/<id>/spec.md for: ${cachedMissingSpec
|
|
271
|
+
.slice(0, 3)
|
|
272
|
+
.map((i) => i.id)
|
|
273
|
+
.join(", ")}`,
|
|
274
|
+
"warning",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// Session end reminder: close/sync if tasks still in progress
|
|
280
|
+
"session.idle": async () => {
|
|
281
|
+
await refreshState();
|
|
282
|
+
if (cachedInProgress.length === 0) return;
|
|
283
|
+
|
|
284
|
+
const list = cachedInProgress
|
|
285
|
+
.slice(0, 5)
|
|
286
|
+
.map((i) => i.id)
|
|
287
|
+
.join(", ");
|
|
288
|
+
await showToast(
|
|
289
|
+
"Swarm: Work still in progress",
|
|
290
|
+
`In-progress Beads: ${list}. Close with bd close + bd sync when done.`,
|
|
291
|
+
"info",
|
|
292
|
+
);
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export default SwarmEnforcer;
|