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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Skill Initializer - Creates a new skill from template
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun scripts/init-skill.ts <skill-name> [--path <path>] [--global]
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* bun scripts/init-skill.ts my-skill
|
|
10
|
+
* bun scripts/init-skill.ts my-skill --path .claude/skills
|
|
11
|
+
* bun scripts/init-skill.ts my-skill --global
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { parseArgs } from "util";
|
|
18
|
+
|
|
19
|
+
const SKILL_TEMPLATE = (name: string, title: string) => `---
|
|
20
|
+
name: ${name}
|
|
21
|
+
description: [TODO: Complete description of what this skill does and WHEN to use it. Be specific about scenarios that trigger this skill.]
|
|
22
|
+
tags:
|
|
23
|
+
- [TODO: add tags]
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# ${title}
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
[TODO: 1-2 sentences explaining what this skill enables]
|
|
31
|
+
|
|
32
|
+
## When to Use This Skill
|
|
33
|
+
|
|
34
|
+
[TODO: List specific scenarios when this skill should be activated:
|
|
35
|
+
- When working on X type of task
|
|
36
|
+
- When files matching Y pattern are involved
|
|
37
|
+
- When the user asks about Z topic]
|
|
38
|
+
|
|
39
|
+
## Instructions
|
|
40
|
+
|
|
41
|
+
[TODO: Add actionable instructions for the agent. Use imperative form:
|
|
42
|
+
- "Read the configuration file first"
|
|
43
|
+
- "Check for existing patterns before creating new ones"
|
|
44
|
+
- "Always validate output before completing"]
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
### Example 1: [TODO: Realistic scenario]
|
|
49
|
+
|
|
50
|
+
**User**: "[TODO: Example user request]"
|
|
51
|
+
|
|
52
|
+
**Process**:
|
|
53
|
+
1. [TODO: Step-by-step process]
|
|
54
|
+
2. [TODO: Next step]
|
|
55
|
+
3. [TODO: Final step]
|
|
56
|
+
|
|
57
|
+
## Resources
|
|
58
|
+
|
|
59
|
+
This skill may include additional resources:
|
|
60
|
+
|
|
61
|
+
### scripts/
|
|
62
|
+
Executable scripts for automation. Run with \`skills_execute\`.
|
|
63
|
+
|
|
64
|
+
### references/
|
|
65
|
+
Documentation loaded on-demand. Access with \`skills_read\`.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
*Delete any unused sections and this line when skill is complete.*
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
const EXAMPLE_SCRIPT = (name: string) => `#!/usr/bin/env bash
|
|
72
|
+
# Example helper script for ${name}
|
|
73
|
+
#
|
|
74
|
+
# This is a placeholder. Replace with actual implementation or delete.
|
|
75
|
+
#
|
|
76
|
+
# Usage: skills_execute(skill: "${name}", script: "example.sh")
|
|
77
|
+
|
|
78
|
+
echo "Hello from ${name} skill!"
|
|
79
|
+
echo "Project directory: $1"
|
|
80
|
+
|
|
81
|
+
# TODO: Add actual script logic
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const REFERENCE_TEMPLATE = (title: string) => `# Reference Documentation for ${title}
|
|
85
|
+
|
|
86
|
+
## Overview
|
|
87
|
+
|
|
88
|
+
[TODO: Detailed reference material for this skill]
|
|
89
|
+
|
|
90
|
+
## API Reference
|
|
91
|
+
|
|
92
|
+
[TODO: If applicable, document APIs, schemas, or interfaces]
|
|
93
|
+
|
|
94
|
+
## Detailed Workflows
|
|
95
|
+
|
|
96
|
+
[TODO: Complex multi-step workflows that don't fit in SKILL.md]
|
|
97
|
+
|
|
98
|
+
## Troubleshooting
|
|
99
|
+
|
|
100
|
+
[TODO: Common issues and solutions]
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
function titleCase(name: string): string {
|
|
104
|
+
return name
|
|
105
|
+
.split("-")
|
|
106
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
107
|
+
.join(" ");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function initSkill(
|
|
111
|
+
name: string,
|
|
112
|
+
basePath: string,
|
|
113
|
+
isGlobal: boolean
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
// Validate name
|
|
116
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
117
|
+
console.error("❌ Error: Skill name must be lowercase with hyphens only");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (name.length > 64) {
|
|
122
|
+
console.error("❌ Error: Skill name must be 64 characters or less");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Determine target directory
|
|
127
|
+
let skillDir: string;
|
|
128
|
+
if (isGlobal) {
|
|
129
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
130
|
+
skillDir = join(home, ".config", "opencode", "skills", name);
|
|
131
|
+
} else {
|
|
132
|
+
skillDir = join(basePath, name);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if exists
|
|
136
|
+
if (existsSync(skillDir)) {
|
|
137
|
+
console.error(`❌ Error: Skill directory already exists: ${skillDir}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const title = titleCase(name);
|
|
142
|
+
const createdFiles: string[] = [];
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Create skill directory
|
|
146
|
+
await mkdir(skillDir, { recursive: true });
|
|
147
|
+
console.log(`✅ Created skill directory: ${skillDir}`);
|
|
148
|
+
|
|
149
|
+
// Create SKILL.md
|
|
150
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
151
|
+
await writeFile(skillPath, SKILL_TEMPLATE(name, title));
|
|
152
|
+
createdFiles.push("SKILL.md");
|
|
153
|
+
console.log("✅ Created SKILL.md");
|
|
154
|
+
|
|
155
|
+
// Create scripts/ directory with example
|
|
156
|
+
const scriptsDir = join(skillDir, "scripts");
|
|
157
|
+
await mkdir(scriptsDir, { recursive: true });
|
|
158
|
+
const scriptPath = join(scriptsDir, "example.sh");
|
|
159
|
+
await writeFile(scriptPath, EXAMPLE_SCRIPT(name), { mode: 0o755 });
|
|
160
|
+
createdFiles.push("scripts/example.sh");
|
|
161
|
+
console.log("✅ Created scripts/example.sh");
|
|
162
|
+
|
|
163
|
+
// Create references/ directory with example
|
|
164
|
+
const refsDir = join(skillDir, "references");
|
|
165
|
+
await mkdir(refsDir, { recursive: true });
|
|
166
|
+
const refPath = join(refsDir, "guide.md");
|
|
167
|
+
await writeFile(refPath, REFERENCE_TEMPLATE(title));
|
|
168
|
+
createdFiles.push("references/guide.md");
|
|
169
|
+
console.log("✅ Created references/guide.md");
|
|
170
|
+
|
|
171
|
+
console.log(`\n✅ Skill '${name}' initialized successfully at ${skillDir}`);
|
|
172
|
+
console.log("\nNext steps:");
|
|
173
|
+
console.log(" 1. Edit SKILL.md to complete TODO placeholders");
|
|
174
|
+
console.log(" 2. Update the description in frontmatter");
|
|
175
|
+
console.log(" 3. Add specific 'When to Use' scenarios");
|
|
176
|
+
console.log(" 4. Delete unused sections and placeholder files");
|
|
177
|
+
console.log(" 5. Test with skills_use to verify it works");
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(
|
|
180
|
+
`❌ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
181
|
+
);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Parse arguments
|
|
187
|
+
const { values, positionals } = parseArgs({
|
|
188
|
+
args: process.argv.slice(2),
|
|
189
|
+
options: {
|
|
190
|
+
path: { type: "string", default: ".opencode/skills" },
|
|
191
|
+
global: { type: "boolean", default: false },
|
|
192
|
+
help: { type: "boolean", short: "h", default: false },
|
|
193
|
+
},
|
|
194
|
+
allowPositionals: true,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (values.help || positionals.length === 0) {
|
|
198
|
+
console.log(`
|
|
199
|
+
Skill Initializer - Creates a new skill from template
|
|
200
|
+
|
|
201
|
+
Usage:
|
|
202
|
+
bun scripts/init-skill.ts <skill-name> [options]
|
|
203
|
+
|
|
204
|
+
Options:
|
|
205
|
+
--path <path> Directory to create skill in (default: .opencode/skills)
|
|
206
|
+
--global Create in global ~/.config/opencode/skills directory
|
|
207
|
+
-h, --help Show this help message
|
|
208
|
+
|
|
209
|
+
Examples:
|
|
210
|
+
bun scripts/init-skill.ts my-skill
|
|
211
|
+
bun scripts/init-skill.ts my-skill --path .claude/skills
|
|
212
|
+
bun scripts/init-skill.ts my-skill --global
|
|
213
|
+
|
|
214
|
+
Skill name requirements:
|
|
215
|
+
- Lowercase letters, digits, and hyphens only
|
|
216
|
+
- Max 64 characters
|
|
217
|
+
`);
|
|
218
|
+
process.exit(values.help ? 0 : 1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const skillName = positionals[0];
|
|
222
|
+
await initSkill(skillName, values.path!, values.global!);
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Skill Validator - Validates skill structure and content
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun scripts/validate-skill.ts <path/to/skill>
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* bun scripts/validate-skill.ts .opencode/skills/my-skill
|
|
10
|
+
* bun scripts/validate-skill.ts global-skills/debugging
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFile, readdir } from "fs/promises";
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { join, basename } from "path";
|
|
16
|
+
import { parseFrontmatter } from "../src/skills.js";
|
|
17
|
+
|
|
18
|
+
interface ValidationResult {
|
|
19
|
+
valid: boolean;
|
|
20
|
+
errors: string[];
|
|
21
|
+
warnings: string[];
|
|
22
|
+
info: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function validateSkill(skillPath: string): Promise<ValidationResult> {
|
|
26
|
+
const result: ValidationResult = {
|
|
27
|
+
valid: true,
|
|
28
|
+
errors: [],
|
|
29
|
+
warnings: [],
|
|
30
|
+
info: [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const skillName = basename(skillPath);
|
|
34
|
+
|
|
35
|
+
// Check directory exists
|
|
36
|
+
if (!existsSync(skillPath)) {
|
|
37
|
+
result.errors.push(`Skill directory does not exist: ${skillPath}`);
|
|
38
|
+
result.valid = false;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check SKILL.md exists
|
|
43
|
+
const skillMdPath = join(skillPath, "SKILL.md");
|
|
44
|
+
if (!existsSync(skillMdPath)) {
|
|
45
|
+
result.errors.push("Missing required SKILL.md file");
|
|
46
|
+
result.valid = false;
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Read and parse SKILL.md
|
|
51
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
52
|
+
|
|
53
|
+
// Check frontmatter
|
|
54
|
+
if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
|
|
55
|
+
result.errors.push("SKILL.md must start with YAML frontmatter (---)");
|
|
56
|
+
result.valid = false;
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { metadata: frontmatter, body } = parseFrontmatter(content);
|
|
61
|
+
if (Object.keys(frontmatter).length === 0) {
|
|
62
|
+
result.errors.push("Invalid YAML frontmatter format");
|
|
63
|
+
result.valid = false;
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate required fields
|
|
68
|
+
if (!frontmatter.name) {
|
|
69
|
+
result.errors.push("Missing required 'name' field in frontmatter");
|
|
70
|
+
result.valid = false;
|
|
71
|
+
} else if (frontmatter.name !== skillName) {
|
|
72
|
+
result.warnings.push(
|
|
73
|
+
`Frontmatter name '${frontmatter.name}' doesn't match directory name '${skillName}'`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!frontmatter.description) {
|
|
78
|
+
result.errors.push("Missing required 'description' field in frontmatter");
|
|
79
|
+
result.valid = false;
|
|
80
|
+
} else {
|
|
81
|
+
const desc = String(frontmatter.description);
|
|
82
|
+
if (desc.includes("[TODO")) {
|
|
83
|
+
result.warnings.push("Description contains TODO placeholder");
|
|
84
|
+
}
|
|
85
|
+
if (desc.length < 20) {
|
|
86
|
+
result.warnings.push("Description is very short (< 20 chars)");
|
|
87
|
+
}
|
|
88
|
+
if (desc.length > 500) {
|
|
89
|
+
result.warnings.push("Description is very long (> 500 chars)");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check for TODO placeholders in body
|
|
94
|
+
const todoCount = (body.match(/\[TODO/g) || []).length;
|
|
95
|
+
if (todoCount > 0) {
|
|
96
|
+
result.warnings.push(`Found ${todoCount} TODO placeholder(s) in body`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check body length (body is already extracted by parseFrontmatter)
|
|
100
|
+
const lineCount = body.split("\n").length;
|
|
101
|
+
if (lineCount > 500) {
|
|
102
|
+
result.warnings.push(
|
|
103
|
+
`SKILL.md body is ${lineCount} lines (recommended < 500)`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for optional directories
|
|
108
|
+
const scriptsDir = join(skillPath, "scripts");
|
|
109
|
+
const refsDir = join(skillPath, "references");
|
|
110
|
+
const assetsDir = join(skillPath, "assets");
|
|
111
|
+
|
|
112
|
+
if (existsSync(scriptsDir)) {
|
|
113
|
+
const scripts = await readdir(scriptsDir);
|
|
114
|
+
result.info.push(`Found ${scripts.length} script(s) in scripts/`);
|
|
115
|
+
|
|
116
|
+
// Check for example placeholders
|
|
117
|
+
if (scripts.includes("example.sh") || scripts.includes("example.py")) {
|
|
118
|
+
result.warnings.push("Contains placeholder example script");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (existsSync(refsDir)) {
|
|
123
|
+
const refs = await readdir(refsDir);
|
|
124
|
+
result.info.push(`Found ${refs.length} reference(s) in references/`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (existsSync(assetsDir)) {
|
|
128
|
+
const assets = await readdir(assetsDir);
|
|
129
|
+
result.info.push(`Found ${assets.length} asset(s) in assets/`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for unwanted files
|
|
133
|
+
const unwantedFiles = [
|
|
134
|
+
"README.md",
|
|
135
|
+
"CHANGELOG.md",
|
|
136
|
+
"INSTALLATION.md",
|
|
137
|
+
"CONTRIBUTING.md",
|
|
138
|
+
];
|
|
139
|
+
for (const file of unwantedFiles) {
|
|
140
|
+
if (existsSync(join(skillPath, file))) {
|
|
141
|
+
result.warnings.push(
|
|
142
|
+
`Found ${file} - skills should only contain SKILL.md and resources`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Main
|
|
151
|
+
const skillPath = process.argv[2];
|
|
152
|
+
|
|
153
|
+
if (!skillPath) {
|
|
154
|
+
console.log(`
|
|
155
|
+
Skill Validator - Validates skill structure and content
|
|
156
|
+
|
|
157
|
+
Usage:
|
|
158
|
+
bun scripts/validate-skill.ts <path/to/skill>
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
bun scripts/validate-skill.ts .opencode/skills/my-skill
|
|
162
|
+
bun scripts/validate-skill.ts global-skills/debugging
|
|
163
|
+
`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`Validating skill: ${skillPath}\n`);
|
|
168
|
+
|
|
169
|
+
const result = await validateSkill(skillPath);
|
|
170
|
+
|
|
171
|
+
// Print results
|
|
172
|
+
if (result.errors.length > 0) {
|
|
173
|
+
console.log("❌ Errors:");
|
|
174
|
+
for (const error of result.errors) {
|
|
175
|
+
console.log(` - ${error}`);
|
|
176
|
+
}
|
|
177
|
+
console.log();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (result.warnings.length > 0) {
|
|
181
|
+
console.log("⚠️ Warnings:");
|
|
182
|
+
for (const warning of result.warnings) {
|
|
183
|
+
console.log(` - ${warning}`);
|
|
184
|
+
}
|
|
185
|
+
console.log();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (result.info.length > 0) {
|
|
189
|
+
console.log("ℹ️ Info:");
|
|
190
|
+
for (const info of result.info) {
|
|
191
|
+
console.log(` - ${info}`);
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (result.valid) {
|
|
197
|
+
console.log("✅ Skill is valid!");
|
|
198
|
+
if (result.warnings.length > 0) {
|
|
199
|
+
console.log(" (but consider addressing warnings above)");
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
console.log("❌ Skill validation failed");
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
import { describe, it, expect, beforeAll } from "vitest";
|
|
12
12
|
import {
|
|
13
13
|
mcpCall,
|
|
14
|
+
mcpCallWithAutoInit,
|
|
14
15
|
sessionStates,
|
|
15
16
|
setState,
|
|
16
17
|
clearState,
|
|
17
18
|
requireState,
|
|
18
19
|
MAX_INBOX_LIMIT,
|
|
19
20
|
AgentMailNotInitializedError,
|
|
21
|
+
isProjectNotFoundError,
|
|
22
|
+
isAgentNotFoundError,
|
|
20
23
|
type AgentMailState,
|
|
21
24
|
} from "./agent-mail";
|
|
22
25
|
|
|
@@ -1318,4 +1321,109 @@ describe("agent-mail integration", () => {
|
|
|
1318
1321
|
clearState(worker2Ctx.sessionID);
|
|
1319
1322
|
});
|
|
1320
1323
|
});
|
|
1324
|
+
|
|
1325
|
+
// ============================================================================
|
|
1326
|
+
// Self-Healing Tests (mcpCallWithAutoInit)
|
|
1327
|
+
// ============================================================================
|
|
1328
|
+
|
|
1329
|
+
describe("self-healing (mcpCallWithAutoInit)", () => {
|
|
1330
|
+
it("detects project not found errors correctly", () => {
|
|
1331
|
+
const projectError = new Error("Project 'migrate-egghead' not found.");
|
|
1332
|
+
const agentError = new Error("Agent 'BlueLake' not found in project");
|
|
1333
|
+
const otherError = new Error("Network timeout");
|
|
1334
|
+
|
|
1335
|
+
expect(isProjectNotFoundError(projectError)).toBe(true);
|
|
1336
|
+
expect(isProjectNotFoundError(agentError)).toBe(false);
|
|
1337
|
+
expect(isProjectNotFoundError(otherError)).toBe(false);
|
|
1338
|
+
|
|
1339
|
+
expect(isAgentNotFoundError(agentError)).toBe(true);
|
|
1340
|
+
expect(isAgentNotFoundError(projectError)).toBe(false);
|
|
1341
|
+
expect(isAgentNotFoundError(otherError)).toBe(false);
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
it("auto-registers project on 'not found' error", async () => {
|
|
1345
|
+
const ctx = createTestContext();
|
|
1346
|
+
|
|
1347
|
+
// First, ensure project exists and register an agent
|
|
1348
|
+
const { state } = await initTestAgent(ctx, `AutoInit_${Date.now()}`);
|
|
1349
|
+
|
|
1350
|
+
// Now use mcpCallWithAutoInit - it should work normally
|
|
1351
|
+
// (no error to recover from, but verifies the wrapper works)
|
|
1352
|
+
await mcpCallWithAutoInit("send_message", {
|
|
1353
|
+
project_key: state.projectKey,
|
|
1354
|
+
agent_name: state.agentName,
|
|
1355
|
+
sender_name: state.agentName,
|
|
1356
|
+
to: [],
|
|
1357
|
+
subject: "Test auto-init wrapper",
|
|
1358
|
+
body_md: "This should work normally",
|
|
1359
|
+
thread_id: "test-thread",
|
|
1360
|
+
importance: "normal",
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
// Verify message was sent by checking inbox
|
|
1364
|
+
const inbox = await mcpCall<Array<{ subject: string }>>("fetch_inbox", {
|
|
1365
|
+
project_key: state.projectKey,
|
|
1366
|
+
agent_name: state.agentName,
|
|
1367
|
+
limit: 5,
|
|
1368
|
+
include_bodies: false,
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
// The message should be in the inbox (sent to empty 'to' = broadcast)
|
|
1372
|
+
// Note: depending on Agent Mail behavior, broadcast might not show in sender's inbox
|
|
1373
|
+
// This test mainly verifies the wrapper doesn't break normal operation
|
|
1374
|
+
|
|
1375
|
+
// Cleanup
|
|
1376
|
+
clearState(ctx.sessionID);
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
it("recovers from simulated project not found by re-registering", async () => {
|
|
1380
|
+
const ctx = createTestContext();
|
|
1381
|
+
|
|
1382
|
+
// Create a fresh project key that doesn't exist yet
|
|
1383
|
+
const freshProjectKey = `/test/fresh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1384
|
+
const agentName = `Recovery_${Date.now()}`;
|
|
1385
|
+
|
|
1386
|
+
// First ensure the project exists (simulating initial setup)
|
|
1387
|
+
await mcpCall("ensure_project", { human_key: freshProjectKey });
|
|
1388
|
+
await mcpCall("register_agent", {
|
|
1389
|
+
project_key: freshProjectKey,
|
|
1390
|
+
program: "opencode-test",
|
|
1391
|
+
model: "test-model",
|
|
1392
|
+
name: agentName,
|
|
1393
|
+
task_description: "Recovery test agent",
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
// Now use mcpCallWithAutoInit for an operation
|
|
1397
|
+
// This should work, and if the project somehow got lost, it would re-register
|
|
1398
|
+
await mcpCallWithAutoInit("send_message", {
|
|
1399
|
+
project_key: freshProjectKey,
|
|
1400
|
+
agent_name: agentName,
|
|
1401
|
+
sender_name: agentName,
|
|
1402
|
+
to: [],
|
|
1403
|
+
subject: "Recovery test",
|
|
1404
|
+
body_md: "Testing self-healing",
|
|
1405
|
+
thread_id: "recovery-test",
|
|
1406
|
+
importance: "normal",
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
// If we got here without error, the wrapper is working
|
|
1410
|
+
// (In a real scenario where the server restarted, it would have re-registered)
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it("passes through non-recoverable errors", async () => {
|
|
1414
|
+
const ctx = createTestContext();
|
|
1415
|
+
const { state } = await initTestAgent(ctx, `ErrorPass_${Date.now()}`);
|
|
1416
|
+
|
|
1417
|
+
// Try to call a non-existent tool - should throw, not retry forever
|
|
1418
|
+
await expect(
|
|
1419
|
+
mcpCallWithAutoInit("nonexistent_tool_xyz", {
|
|
1420
|
+
project_key: state.projectKey,
|
|
1421
|
+
agent_name: state.agentName,
|
|
1422
|
+
}),
|
|
1423
|
+
).rejects.toThrow(/Unknown tool/);
|
|
1424
|
+
|
|
1425
|
+
// Cleanup
|
|
1426
|
+
clearState(ctx.sessionID);
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1321
1429
|
});
|