opencode-swarm-plugin 0.12.19 → 0.12.22
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/.beads/issues.jsonl +12 -1
- package/.github/workflows/ci.yml +26 -0
- package/bun.lock +21 -0
- package/dist/index.js +25271 -20278
- package/dist/plugin.js +25257 -20278
- package/examples/skills/beads-workflow/SKILL.md +165 -0
- package/examples/skills/skill-creator/SKILL.md +223 -0
- package/examples/skills/swarm-coordination/SKILL.md +148 -0
- package/global-skills/cli-builder/SKILL.md +344 -0
- package/global-skills/cli-builder/references/advanced-patterns.md +244 -0
- package/global-skills/code-review/SKILL.md +166 -0
- package/global-skills/debugging/SKILL.md +150 -0
- package/global-skills/skill-creator/LICENSE.txt +202 -0
- package/global-skills/skill-creator/SKILL.md +352 -0
- package/global-skills/skill-creator/references/output-patterns.md +82 -0
- package/global-skills/skill-creator/references/workflows.md +28 -0
- package/global-skills/swarm-coordination/SKILL.md +166 -0
- package/package.json +6 -5
- package/scripts/init-skill.ts +222 -0
- package/scripts/validate-skill.ts +204 -0
- package/src/agent-mail.integration.test.ts +108 -0
- package/src/agent-mail.ts +222 -4
- package/src/beads.ts +74 -0
- package/src/index.ts +49 -0
- package/src/schemas/bead.ts +21 -1
- package/src/skills.test.ts +408 -0
- package/src/skills.ts +1364 -0
- package/src/swarm.ts +300 -14
package/src/agent-mail.ts
CHANGED
|
@@ -27,6 +27,35 @@ const AGENT_MAIL_URL = "http://127.0.0.1:8765";
|
|
|
27
27
|
const DEFAULT_TTL_SECONDS = 3600; // 1 hour
|
|
28
28
|
const MAX_INBOX_LIMIT = 5; // HARD CAP - never exceed this
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Default project directory for Agent Mail operations
|
|
32
|
+
*
|
|
33
|
+
* This is set by the plugin init to the actual working directory (from OpenCode).
|
|
34
|
+
* Without this, tools might use the plugin's directory instead of the project's.
|
|
35
|
+
*
|
|
36
|
+
* Set this via setAgentMailProjectDirectory() before using tools.
|
|
37
|
+
*/
|
|
38
|
+
let agentMailProjectDirectory: string | null = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set the default project directory for Agent Mail operations
|
|
42
|
+
*
|
|
43
|
+
* Called during plugin initialization with the actual project directory.
|
|
44
|
+
* This ensures agentmail_init uses the correct project path by default.
|
|
45
|
+
*/
|
|
46
|
+
export function setAgentMailProjectDirectory(directory: string): void {
|
|
47
|
+
agentMailProjectDirectory = directory;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the default project directory
|
|
52
|
+
*
|
|
53
|
+
* Returns the configured directory, or falls back to cwd if not set.
|
|
54
|
+
*/
|
|
55
|
+
export function getAgentMailProjectDirectory(): string {
|
|
56
|
+
return agentMailProjectDirectory || process.cwd();
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
// Retry configuration
|
|
31
60
|
const RETRY_CONFIG = {
|
|
32
61
|
maxRetries: parseInt(process.env.OPENCODE_AGENT_MAIL_MAX_RETRIES || "3"),
|
|
@@ -541,6 +570,39 @@ function isRetryableError(error: unknown): boolean {
|
|
|
541
570
|
return false;
|
|
542
571
|
}
|
|
543
572
|
|
|
573
|
+
/**
|
|
574
|
+
* Check if an error indicates the project was not found
|
|
575
|
+
*
|
|
576
|
+
* This happens when Agent Mail server restarts and loses project registrations.
|
|
577
|
+
* The fix is to re-register the project and retry the operation.
|
|
578
|
+
*/
|
|
579
|
+
export function isProjectNotFoundError(error: unknown): boolean {
|
|
580
|
+
if (error instanceof Error) {
|
|
581
|
+
const message = error.message.toLowerCase();
|
|
582
|
+
return (
|
|
583
|
+
message.includes("project") &&
|
|
584
|
+
(message.includes("not found") || message.includes("does not exist"))
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Check if an error indicates the agent was not found
|
|
592
|
+
*
|
|
593
|
+
* Similar to project not found - server restart loses agent registrations.
|
|
594
|
+
*/
|
|
595
|
+
export function isAgentNotFoundError(error: unknown): boolean {
|
|
596
|
+
if (error instanceof Error) {
|
|
597
|
+
const message = error.message.toLowerCase();
|
|
598
|
+
return (
|
|
599
|
+
message.includes("agent") &&
|
|
600
|
+
(message.includes("not found") || message.includes("does not exist"))
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
|
|
544
606
|
// ============================================================================
|
|
545
607
|
// MCP Client
|
|
546
608
|
// ============================================================================
|
|
@@ -823,6 +885,153 @@ export async function mcpCall<T>(
|
|
|
823
885
|
throw lastError || new Error("Unknown error in mcpCall");
|
|
824
886
|
}
|
|
825
887
|
|
|
888
|
+
/**
|
|
889
|
+
* Re-register a project with Agent Mail server
|
|
890
|
+
*
|
|
891
|
+
* Called when we detect "Project not found" error, indicating server restart.
|
|
892
|
+
* This is a lightweight operation that just ensures the project exists.
|
|
893
|
+
*/
|
|
894
|
+
async function reRegisterProject(projectKey: string): Promise<boolean> {
|
|
895
|
+
try {
|
|
896
|
+
console.warn(
|
|
897
|
+
`[agent-mail] Re-registering project "${projectKey}" after server restart...`,
|
|
898
|
+
);
|
|
899
|
+
await mcpCall<ProjectInfo>("ensure_project", {
|
|
900
|
+
human_key: projectKey,
|
|
901
|
+
});
|
|
902
|
+
console.warn(
|
|
903
|
+
`[agent-mail] Project "${projectKey}" re-registered successfully`,
|
|
904
|
+
);
|
|
905
|
+
return true;
|
|
906
|
+
} catch (error) {
|
|
907
|
+
console.error(
|
|
908
|
+
`[agent-mail] Failed to re-register project "${projectKey}":`,
|
|
909
|
+
error,
|
|
910
|
+
);
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Re-register an agent with Agent Mail server
|
|
917
|
+
*
|
|
918
|
+
* Called when we detect "Agent not found" error, indicating server restart.
|
|
919
|
+
*/
|
|
920
|
+
async function reRegisterAgent(
|
|
921
|
+
projectKey: string,
|
|
922
|
+
agentName: string,
|
|
923
|
+
taskDescription?: string,
|
|
924
|
+
): Promise<boolean> {
|
|
925
|
+
try {
|
|
926
|
+
console.warn(
|
|
927
|
+
`[agent-mail] Re-registering agent "${agentName}" for project "${projectKey}"...`,
|
|
928
|
+
);
|
|
929
|
+
await mcpCall<AgentInfo>("register_agent", {
|
|
930
|
+
project_key: projectKey,
|
|
931
|
+
program: "opencode",
|
|
932
|
+
model: "claude-opus-4",
|
|
933
|
+
name: agentName,
|
|
934
|
+
task_description: taskDescription || "Re-registered after server restart",
|
|
935
|
+
});
|
|
936
|
+
console.warn(
|
|
937
|
+
`[agent-mail] Agent "${agentName}" re-registered successfully`,
|
|
938
|
+
);
|
|
939
|
+
return true;
|
|
940
|
+
} catch (error) {
|
|
941
|
+
console.error(
|
|
942
|
+
`[agent-mail] Failed to re-register agent "${agentName}":`,
|
|
943
|
+
error,
|
|
944
|
+
);
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* MCP call with automatic project/agent re-registration on "not found" errors
|
|
951
|
+
*
|
|
952
|
+
* This is the self-healing wrapper that handles Agent Mail server restarts.
|
|
953
|
+
* When the server restarts, it loses all project and agent registrations.
|
|
954
|
+
* This wrapper detects those errors and automatically re-registers before retrying.
|
|
955
|
+
*
|
|
956
|
+
* Use this instead of raw mcpCall when you have project_key and agent_name context.
|
|
957
|
+
*
|
|
958
|
+
* @param toolName - The MCP tool to call
|
|
959
|
+
* @param args - Arguments including project_key and optionally agent_name
|
|
960
|
+
* @param options - Optional configuration for re-registration
|
|
961
|
+
* @returns The result of the MCP call
|
|
962
|
+
*/
|
|
963
|
+
export async function mcpCallWithAutoInit<T>(
|
|
964
|
+
toolName: string,
|
|
965
|
+
args: Record<string, unknown> & { project_key: string; agent_name?: string },
|
|
966
|
+
options?: {
|
|
967
|
+
/** Task description for agent re-registration */
|
|
968
|
+
taskDescription?: string;
|
|
969
|
+
/** Max re-registration attempts (default: 1) */
|
|
970
|
+
maxReregistrationAttempts?: number;
|
|
971
|
+
},
|
|
972
|
+
): Promise<T> {
|
|
973
|
+
const maxAttempts = options?.maxReregistrationAttempts ?? 1;
|
|
974
|
+
let reregistrationAttempts = 0;
|
|
975
|
+
|
|
976
|
+
while (true) {
|
|
977
|
+
try {
|
|
978
|
+
return await mcpCall<T>(toolName, args);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
// Check if this is a recoverable "not found" error
|
|
981
|
+
const isProjectError = isProjectNotFoundError(error);
|
|
982
|
+
const isAgentError = isAgentNotFoundError(error);
|
|
983
|
+
|
|
984
|
+
if (!isProjectError && !isAgentError) {
|
|
985
|
+
// Not a recoverable error, rethrow
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Check if we've exhausted re-registration attempts
|
|
990
|
+
if (reregistrationAttempts >= maxAttempts) {
|
|
991
|
+
console.error(
|
|
992
|
+
`[agent-mail] Exhausted ${maxAttempts} re-registration attempt(s) for ${toolName}`,
|
|
993
|
+
);
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
reregistrationAttempts++;
|
|
998
|
+
console.warn(
|
|
999
|
+
`[agent-mail] Detected "${isProjectError ? "project" : "agent"} not found" for ${toolName}, ` +
|
|
1000
|
+
`attempting re-registration (attempt ${reregistrationAttempts}/${maxAttempts})...`,
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
// Re-register project first (always needed)
|
|
1004
|
+
const projectOk = await reRegisterProject(args.project_key);
|
|
1005
|
+
if (!projectOk) {
|
|
1006
|
+
throw error; // Can't recover without project
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Re-register agent if we have one and it was an agent error
|
|
1010
|
+
// (or if the original call needs an agent)
|
|
1011
|
+
if (args.agent_name && (isAgentError || toolName !== "ensure_project")) {
|
|
1012
|
+
const agentOk = await reRegisterAgent(
|
|
1013
|
+
args.project_key,
|
|
1014
|
+
args.agent_name,
|
|
1015
|
+
options?.taskDescription,
|
|
1016
|
+
);
|
|
1017
|
+
if (!agentOk) {
|
|
1018
|
+
// Agent re-registration failed, but project is OK
|
|
1019
|
+
// Some operations might still work, so continue
|
|
1020
|
+
console.warn(
|
|
1021
|
+
`[agent-mail] Agent re-registration failed, but continuing with retry...`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Retry the original call
|
|
1027
|
+
console.warn(
|
|
1028
|
+
`[agent-mail] Retrying ${toolName} after re-registration...`,
|
|
1029
|
+
);
|
|
1030
|
+
// Loop continues to retry
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
826
1035
|
/**
|
|
827
1036
|
* Get Agent Mail state for a session, or throw if not initialized
|
|
828
1037
|
*
|
|
@@ -897,7 +1106,10 @@ export const agentmail_init = tool({
|
|
|
897
1106
|
args: {
|
|
898
1107
|
project_path: tool.schema
|
|
899
1108
|
.string()
|
|
900
|
-
.
|
|
1109
|
+
.optional()
|
|
1110
|
+
.describe(
|
|
1111
|
+
"Absolute path to the project/repo (defaults to current working directory)",
|
|
1112
|
+
),
|
|
901
1113
|
agent_name: tool.schema
|
|
902
1114
|
.string()
|
|
903
1115
|
.optional()
|
|
@@ -908,6 +1120,10 @@ export const agentmail_init = tool({
|
|
|
908
1120
|
.describe("Description of current task"),
|
|
909
1121
|
},
|
|
910
1122
|
async execute(args, ctx) {
|
|
1123
|
+
// Use provided path or fall back to configured project directory
|
|
1124
|
+
// This prevents using the plugin's directory when working in a different project
|
|
1125
|
+
const projectPath = args.project_path || getAgentMailProjectDirectory();
|
|
1126
|
+
|
|
911
1127
|
// Check if Agent Mail is available
|
|
912
1128
|
const available = await checkAgentMailAvailable();
|
|
913
1129
|
if (!available) {
|
|
@@ -933,12 +1149,12 @@ export const agentmail_init = tool({
|
|
|
933
1149
|
try {
|
|
934
1150
|
// 1. Ensure project exists
|
|
935
1151
|
const project = await mcpCall<ProjectInfo>("ensure_project", {
|
|
936
|
-
human_key:
|
|
1152
|
+
human_key: projectPath,
|
|
937
1153
|
});
|
|
938
1154
|
|
|
939
1155
|
// 2. Register agent
|
|
940
1156
|
const agent = await mcpCall<AgentInfo>("register_agent", {
|
|
941
|
-
project_key:
|
|
1157
|
+
project_key: projectPath,
|
|
942
1158
|
program: "opencode",
|
|
943
1159
|
model: "claude-opus-4",
|
|
944
1160
|
name: args.agent_name, // undefined = auto-generate
|
|
@@ -947,7 +1163,7 @@ export const agentmail_init = tool({
|
|
|
947
1163
|
|
|
948
1164
|
// 3. Store state using sessionID
|
|
949
1165
|
const state: AgentMailState = {
|
|
950
|
-
projectKey:
|
|
1166
|
+
projectKey: projectPath,
|
|
951
1167
|
agentName: agent.name,
|
|
952
1168
|
reservations: [],
|
|
953
1169
|
startedAt: new Date().toISOString(),
|
|
@@ -1490,4 +1706,6 @@ export {
|
|
|
1490
1706
|
restartServer,
|
|
1491
1707
|
RETRY_CONFIG,
|
|
1492
1708
|
RECOVERY_CONFIG,
|
|
1709
|
+
// Note: isProjectNotFoundError, isAgentNotFoundError, mcpCallWithAutoInit
|
|
1710
|
+
// are exported at their definitions
|
|
1493
1711
|
};
|
package/src/beads.ts
CHANGED
|
@@ -159,6 +159,11 @@ function buildCreateCommand(args: BeadCreateArgs): string[] {
|
|
|
159
159
|
parts.push("--parent", args.parent_id);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Custom ID for human-readable bead names (e.g., 'phase-0', 'phase-1.e2e-test')
|
|
163
|
+
if (args.id) {
|
|
164
|
+
parts.push("--id", args.id);
|
|
165
|
+
}
|
|
166
|
+
|
|
162
167
|
parts.push("--json");
|
|
163
168
|
return parts;
|
|
164
169
|
}
|
|
@@ -288,12 +293,22 @@ export const beads_create_epic = tool({
|
|
|
288
293
|
.string()
|
|
289
294
|
.optional()
|
|
290
295
|
.describe("Epic description"),
|
|
296
|
+
epic_id: tool.schema
|
|
297
|
+
.string()
|
|
298
|
+
.optional()
|
|
299
|
+
.describe("Custom ID for the epic (e.g., 'phase-0')"),
|
|
291
300
|
subtasks: tool.schema
|
|
292
301
|
.array(
|
|
293
302
|
tool.schema.object({
|
|
294
303
|
title: tool.schema.string(),
|
|
295
304
|
priority: tool.schema.number().min(0).max(3).optional(),
|
|
296
305
|
files: tool.schema.array(tool.schema.string()).optional(),
|
|
306
|
+
id_suffix: tool.schema
|
|
307
|
+
.string()
|
|
308
|
+
.optional()
|
|
309
|
+
.describe(
|
|
310
|
+
"Custom ID suffix (e.g., 'e2e-test' becomes 'phase-0.e2e-test')",
|
|
311
|
+
),
|
|
297
312
|
}),
|
|
298
313
|
)
|
|
299
314
|
.describe("Subtasks to create under the epic"),
|
|
@@ -309,6 +324,7 @@ export const beads_create_epic = tool({
|
|
|
309
324
|
type: "epic",
|
|
310
325
|
priority: 1,
|
|
311
326
|
description: validated.epic_description,
|
|
327
|
+
id: validated.epic_id,
|
|
312
328
|
});
|
|
313
329
|
|
|
314
330
|
const epicResult = await runBdCommand(epicCmd.slice(1)); // Remove 'bd' prefix
|
|
@@ -326,11 +342,19 @@ export const beads_create_epic = tool({
|
|
|
326
342
|
|
|
327
343
|
// 2. Create subtasks
|
|
328
344
|
for (const subtask of validated.subtasks) {
|
|
345
|
+
// Build subtask ID: if epic has custom ID and subtask has suffix, combine them
|
|
346
|
+
// e.g., epic_id='phase-0', id_suffix='e2e-test' → 'phase-0.e2e-test'
|
|
347
|
+
let subtaskId: string | undefined;
|
|
348
|
+
if (validated.epic_id && subtask.id_suffix) {
|
|
349
|
+
subtaskId = `${validated.epic_id}.${subtask.id_suffix}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
329
352
|
const subtaskCmd = buildCreateCommand({
|
|
330
353
|
title: subtask.title,
|
|
331
354
|
type: "task",
|
|
332
355
|
priority: subtask.priority ?? 2,
|
|
333
356
|
parent_id: epic.id,
|
|
357
|
+
id: subtaskId,
|
|
334
358
|
});
|
|
335
359
|
|
|
336
360
|
const subtaskResult = await runBdCommand(subtaskCmd.slice(1)); // Remove 'bd' prefix
|
|
@@ -717,11 +741,61 @@ export const beads_sync = tool({
|
|
|
717
741
|
|
|
718
742
|
// 5. Pull if requested (with rebase to avoid merge commits)
|
|
719
743
|
if (autoPull) {
|
|
744
|
+
// Check for unstaged changes that would block pull --rebase
|
|
745
|
+
const dirtyCheckResult = await runGitCommand([
|
|
746
|
+
"status",
|
|
747
|
+
"--porcelain",
|
|
748
|
+
"--untracked-files=no",
|
|
749
|
+
]);
|
|
750
|
+
const hasDirtyFiles = dirtyCheckResult.stdout.trim() !== "";
|
|
751
|
+
let didStash = false;
|
|
752
|
+
|
|
753
|
+
// Stash dirty files before pull (self-healing for "unstaged changes" error)
|
|
754
|
+
if (hasDirtyFiles) {
|
|
755
|
+
console.warn(
|
|
756
|
+
"[beads] Detected unstaged changes, stashing before pull...",
|
|
757
|
+
);
|
|
758
|
+
const stashResult = await runGitCommand([
|
|
759
|
+
"stash",
|
|
760
|
+
"push",
|
|
761
|
+
"-m",
|
|
762
|
+
"beads_sync: auto-stash before pull",
|
|
763
|
+
"--include-untracked",
|
|
764
|
+
]);
|
|
765
|
+
if (stashResult.exitCode === 0) {
|
|
766
|
+
didStash = true;
|
|
767
|
+
console.warn("[beads] Changes stashed successfully");
|
|
768
|
+
} else {
|
|
769
|
+
// Stash failed - try pull anyway, it might work
|
|
770
|
+
console.warn(
|
|
771
|
+
`[beads] Stash failed (${stashResult.stderr}), attempting pull anyway...`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
720
776
|
const pullResult = await withTimeout(
|
|
721
777
|
runGitCommand(["pull", "--rebase"]),
|
|
722
778
|
TIMEOUT_MS,
|
|
723
779
|
"git pull --rebase",
|
|
724
780
|
);
|
|
781
|
+
|
|
782
|
+
// Restore stashed changes regardless of pull result
|
|
783
|
+
if (didStash) {
|
|
784
|
+
console.warn("[beads] Restoring stashed changes...");
|
|
785
|
+
const unstashResult = await runGitCommand(["stash", "pop"]);
|
|
786
|
+
if (unstashResult.exitCode !== 0) {
|
|
787
|
+
// Unstash failed - this is bad, user needs to know
|
|
788
|
+
console.error(
|
|
789
|
+
`[beads] WARNING: Failed to restore stashed changes: ${unstashResult.stderr}`,
|
|
790
|
+
);
|
|
791
|
+
console.error(
|
|
792
|
+
"[beads] Your changes are in 'git stash list' - run 'git stash pop' manually",
|
|
793
|
+
);
|
|
794
|
+
} else {
|
|
795
|
+
console.warn("[beads] Stashed changes restored");
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
725
799
|
if (pullResult.exitCode !== 0) {
|
|
726
800
|
throw new BeadError(
|
|
727
801
|
`Failed to pull: ${pullResult.stderr}`,
|
package/src/index.ts
CHANGED
|
@@ -25,12 +25,14 @@ import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
|
|
|
25
25
|
import { beadsTools, setBeadsWorkingDirectory } from "./beads";
|
|
26
26
|
import {
|
|
27
27
|
agentMailTools,
|
|
28
|
+
setAgentMailProjectDirectory,
|
|
28
29
|
type AgentMailState,
|
|
29
30
|
AGENT_MAIL_URL,
|
|
30
31
|
} from "./agent-mail";
|
|
31
32
|
import { structuredTools } from "./structured";
|
|
32
33
|
import { swarmTools } from "./swarm";
|
|
33
34
|
import { repoCrawlTools } from "./repo-crawl";
|
|
35
|
+
import { skillsTools, setSkillsProjectDirectory } from "./skills";
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* OpenCode Swarm Plugin
|
|
@@ -41,6 +43,7 @@ import { repoCrawlTools } from "./repo-crawl";
|
|
|
41
43
|
* - structured:* - Structured output parsing and validation
|
|
42
44
|
* - swarm:* - Swarm orchestration and task decomposition
|
|
43
45
|
* - repo-crawl:* - GitHub API tools for repository research
|
|
46
|
+
* - skills:* - Agent skills discovery, activation, and execution
|
|
44
47
|
*
|
|
45
48
|
* @param input - Plugin context from OpenCode
|
|
46
49
|
* @returns Plugin hooks including tools, events, and tool execution hooks
|
|
@@ -54,6 +57,15 @@ export const SwarmPlugin: Plugin = async (
|
|
|
54
57
|
// This ensures bd runs in the project directory, not ~/.config/opencode
|
|
55
58
|
setBeadsWorkingDirectory(directory);
|
|
56
59
|
|
|
60
|
+
// Set the project directory for skills discovery
|
|
61
|
+
// Skills are discovered from .opencode/skills/, .claude/skills/, or skills/
|
|
62
|
+
setSkillsProjectDirectory(directory);
|
|
63
|
+
|
|
64
|
+
// Set the project directory for Agent Mail
|
|
65
|
+
// This ensures agentmail_init uses the correct project path by default
|
|
66
|
+
// (prevents using plugin directory when working in a different project)
|
|
67
|
+
setAgentMailProjectDirectory(directory);
|
|
68
|
+
|
|
57
69
|
/** Track active sessions for cleanup */
|
|
58
70
|
let activeAgentMailState: AgentMailState | null = null;
|
|
59
71
|
|
|
@@ -116,6 +128,7 @@ export const SwarmPlugin: Plugin = async (
|
|
|
116
128
|
...structuredTools,
|
|
117
129
|
...swarmTools,
|
|
118
130
|
...repoCrawlTools,
|
|
131
|
+
...skillsTools,
|
|
119
132
|
},
|
|
120
133
|
|
|
121
134
|
/**
|
|
@@ -239,6 +252,11 @@ export {
|
|
|
239
252
|
AgentMailNotInitializedError,
|
|
240
253
|
FileReservationConflictError,
|
|
241
254
|
createAgentMailError,
|
|
255
|
+
setAgentMailProjectDirectory,
|
|
256
|
+
getAgentMailProjectDirectory,
|
|
257
|
+
mcpCallWithAutoInit,
|
|
258
|
+
isProjectNotFoundError,
|
|
259
|
+
isAgentNotFoundError,
|
|
242
260
|
type AgentMailState,
|
|
243
261
|
} from "./agent-mail";
|
|
244
262
|
|
|
@@ -305,6 +323,7 @@ export const allTools = {
|
|
|
305
323
|
...structuredTools,
|
|
306
324
|
...swarmTools,
|
|
307
325
|
...repoCrawlTools,
|
|
326
|
+
...skillsTools,
|
|
308
327
|
} as const;
|
|
309
328
|
|
|
310
329
|
/**
|
|
@@ -387,3 +406,33 @@ export {
|
|
|
387
406
|
* - Graceful rate limit handling
|
|
388
407
|
*/
|
|
389
408
|
export { repoCrawlTools, RepoCrawlError } from "./repo-crawl";
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Re-export skills module
|
|
412
|
+
*
|
|
413
|
+
* Implements Anthropic's Agent Skills specification for OpenCode.
|
|
414
|
+
*
|
|
415
|
+
* Includes:
|
|
416
|
+
* - skillsTools - All skills tools (list, use, execute, read)
|
|
417
|
+
* - discoverSkills, getSkill, listSkills - Discovery functions
|
|
418
|
+
* - parseFrontmatter - YAML frontmatter parser
|
|
419
|
+
* - getSkillsContextForSwarm - Swarm integration helper
|
|
420
|
+
* - findRelevantSkills - Task-based skill matching
|
|
421
|
+
*
|
|
422
|
+
* Types:
|
|
423
|
+
* - Skill, SkillMetadata, SkillRef - Skill data types
|
|
424
|
+
*/
|
|
425
|
+
export {
|
|
426
|
+
skillsTools,
|
|
427
|
+
discoverSkills,
|
|
428
|
+
getSkill,
|
|
429
|
+
listSkills,
|
|
430
|
+
parseFrontmatter,
|
|
431
|
+
setSkillsProjectDirectory,
|
|
432
|
+
invalidateSkillsCache,
|
|
433
|
+
getSkillsContextForSwarm,
|
|
434
|
+
findRelevantSkills,
|
|
435
|
+
type Skill,
|
|
436
|
+
type SkillMetadata,
|
|
437
|
+
type SkillRef,
|
|
438
|
+
} from "./skills";
|
package/src/schemas/bead.ts
CHANGED
|
@@ -38,11 +38,13 @@ export type BeadDependency = z.infer<typeof BeadDependencySchema>;
|
|
|
38
38
|
* ID format:
|
|
39
39
|
* - Standard: `{project}-{hash}` (e.g., `opencode-swarm-plugin-1i8`)
|
|
40
40
|
* - Subtask: `{project}-{hash}.{index}` (e.g., `opencode-swarm-plugin-1i8.1`)
|
|
41
|
+
* - Custom: `{project}-{custom-id}` (e.g., `migrate-egghead-phase-0`)
|
|
42
|
+
* - Custom subtask: `{project}-{custom-id}.{suffix}` (e.g., `migrate-egghead-phase-0.e2e-test`)
|
|
41
43
|
*/
|
|
42
44
|
export const BeadSchema = z.object({
|
|
43
45
|
id: z
|
|
44
46
|
.string()
|
|
45
|
-
.regex(/^[a-z0-9]+(-[a-z0-9]+)+(
|
|
47
|
+
.regex(/^[a-z0-9]+(-[a-z0-9]+)+(\.[\w-]+)?$/, "Invalid bead ID format"),
|
|
46
48
|
title: z.string().min(1, "Title required"),
|
|
47
49
|
description: z.string().optional().default(""),
|
|
48
50
|
status: BeadStatusSchema.default("open"),
|
|
@@ -64,6 +66,12 @@ export const BeadCreateArgsSchema = z.object({
|
|
|
64
66
|
priority: z.number().int().min(0).max(3).default(2),
|
|
65
67
|
description: z.string().optional(),
|
|
66
68
|
parent_id: z.string().optional(),
|
|
69
|
+
/**
|
|
70
|
+
* Custom ID for human-readable bead names.
|
|
71
|
+
* MUST include project prefix (e.g., 'migrate-egghead-phase-0', not just 'phase-0').
|
|
72
|
+
* For subtasks, use dot notation: 'migrate-egghead-phase-0.e2e-test'
|
|
73
|
+
*/
|
|
74
|
+
id: z.string().optional(),
|
|
67
75
|
});
|
|
68
76
|
export type BeadCreateArgs = z.infer<typeof BeadCreateArgsSchema>;
|
|
69
77
|
|
|
@@ -125,12 +133,24 @@ export type BeadTree = z.infer<typeof BeadTreeSchema>;
|
|
|
125
133
|
export const EpicCreateArgsSchema = z.object({
|
|
126
134
|
epic_title: z.string().min(1),
|
|
127
135
|
epic_description: z.string().optional(),
|
|
136
|
+
/**
|
|
137
|
+
* Custom ID for the epic. MUST include project prefix.
|
|
138
|
+
* Example: 'migrate-egghead-phase-0' (not just 'phase-0')
|
|
139
|
+
* If not provided, bd generates a random ID.
|
|
140
|
+
*/
|
|
141
|
+
epic_id: z.string().optional(),
|
|
128
142
|
subtasks: z
|
|
129
143
|
.array(
|
|
130
144
|
z.object({
|
|
131
145
|
title: z.string().min(1),
|
|
132
146
|
priority: z.number().int().min(0).max(3).default(2),
|
|
133
147
|
files: z.array(z.string()).optional().default([]),
|
|
148
|
+
/**
|
|
149
|
+
* Custom ID suffix for subtask. Combined with epic_id using dot notation.
|
|
150
|
+
* Example: epic_id='migrate-egghead-phase-0', id_suffix='e2e-test'
|
|
151
|
+
* → subtask ID: 'migrate-egghead-phase-0.e2e-test'
|
|
152
|
+
*/
|
|
153
|
+
id_suffix: z.string().optional(),
|
|
134
154
|
}),
|
|
135
155
|
)
|
|
136
156
|
.min(1),
|