opencode-swarm-plugin 0.23.6 → 0.25.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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +53 -0
- package/README.md +155 -3
- package/bin/swarm.ts +635 -234
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +620 -89
- package/dist/plugin.js +548 -89
- package/dist/schemas/bead-events.d.ts +698 -0
- package/dist/schemas/bead-events.d.ts.map +1 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/swarm-decompose.d.ts +74 -0
- package/dist/swarm-decompose.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm.d.ts +27 -0
- package/dist/swarm.d.ts.map +1 -1
- package/docs/testing/context-recovery-test.md +470 -0
- package/examples/commands/swarm.md +92 -20
- package/global-skills/swarm-coordination/SKILL.md +380 -10
- package/package.json +1 -1
- package/src/schemas/bead-events.test.ts +341 -0
- package/src/schemas/bead-events.ts +583 -0
- package/src/schemas/index.ts +51 -0
- package/src/skills.ts +10 -3
- package/src/swarm-decompose.ts +337 -0
- package/src/swarm-orchestrate.ts +15 -51
- package/src/swarm-prompts.ts +144 -42
- package/src/swarm.integration.test.ts +581 -31
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
swarm_plan_prompt,
|
|
21
21
|
formatSubtaskPromptV2,
|
|
22
22
|
SUBTASK_PROMPT_V2,
|
|
23
|
+
swarm_checkpoint,
|
|
24
|
+
swarm_recover,
|
|
23
25
|
} from "./swarm";
|
|
24
26
|
import { mcpCall, setState, clearState, AGENT_MAIL_URL } from "./agent-mail";
|
|
25
27
|
|
|
@@ -219,15 +221,18 @@ describe("swarm_select_strategy", () => {
|
|
|
219
221
|
|
|
220
222
|
expect(parsed).toHaveProperty("alternatives");
|
|
221
223
|
expect(parsed.alternatives).toBeInstanceOf(Array);
|
|
222
|
-
expect(parsed.alternatives.length).toBe(
|
|
224
|
+
expect(parsed.alternatives.length).toBe(3); // 4 strategies - 1 selected = 3 alternatives
|
|
223
225
|
|
|
224
226
|
for (const alt of parsed.alternatives) {
|
|
225
227
|
expect(alt).toHaveProperty("strategy");
|
|
226
228
|
expect(alt).toHaveProperty("description");
|
|
227
229
|
expect(alt).toHaveProperty("score");
|
|
228
|
-
expect([
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
expect([
|
|
231
|
+
"file-based",
|
|
232
|
+
"feature-based",
|
|
233
|
+
"risk-based",
|
|
234
|
+
"research-based",
|
|
235
|
+
]).toContain(alt.strategy);
|
|
231
236
|
expect(typeof alt.score).toBe("number");
|
|
232
237
|
}
|
|
233
238
|
});
|
|
@@ -338,38 +343,30 @@ describe("swarm_plan_prompt", () => {
|
|
|
338
343
|
expect(parsed.schema_hint).toHaveProperty("subtasks");
|
|
339
344
|
});
|
|
340
345
|
|
|
341
|
-
it("
|
|
342
|
-
// Test
|
|
343
|
-
const
|
|
346
|
+
it("includes strategy and skills info in output", async () => {
|
|
347
|
+
// Test swarm_plan_prompt output structure
|
|
348
|
+
const result = await swarm_plan_prompt.execute(
|
|
344
349
|
{
|
|
345
350
|
task: "Add feature",
|
|
346
351
|
max_subtasks: 3,
|
|
347
|
-
query_cass: false,
|
|
348
352
|
},
|
|
349
353
|
mockContext,
|
|
350
354
|
);
|
|
351
|
-
const
|
|
355
|
+
const parsed = JSON.parse(result);
|
|
352
356
|
|
|
353
|
-
|
|
354
|
-
expect(
|
|
357
|
+
// Should have strategy info
|
|
358
|
+
expect(parsed).toHaveProperty("strategy");
|
|
359
|
+
expect(parsed.strategy).toHaveProperty("selected");
|
|
360
|
+
expect(parsed.strategy).toHaveProperty("reasoning");
|
|
361
|
+
expect(parsed.strategy).toHaveProperty("guidelines");
|
|
362
|
+
expect(parsed.strategy).toHaveProperty("anti_patterns");
|
|
355
363
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
task: "Add feature",
|
|
360
|
-
max_subtasks: 3,
|
|
361
|
-
query_cass: true,
|
|
362
|
-
},
|
|
363
|
-
mockContext,
|
|
364
|
-
);
|
|
365
|
-
const parsedEnabled = JSON.parse(resultEnabled);
|
|
364
|
+
// Should have skills info
|
|
365
|
+
expect(parsed).toHaveProperty("skills");
|
|
366
|
+
expect(parsed.skills).toHaveProperty("included");
|
|
366
367
|
|
|
367
|
-
|
|
368
|
-
expect(
|
|
369
|
-
// If CASS is unavailable, queried will be false with reason
|
|
370
|
-
if (!parsedEnabled.cass_history.queried) {
|
|
371
|
-
expect(parsedEnabled.cass_history).toHaveProperty("reason");
|
|
372
|
-
}
|
|
368
|
+
// Should have memory query instruction
|
|
369
|
+
expect(parsed).toHaveProperty("memory_query");
|
|
373
370
|
});
|
|
374
371
|
|
|
375
372
|
it("includes context in prompt when provided", async () => {
|
|
@@ -1088,11 +1085,12 @@ describe("Tool Availability", () => {
|
|
|
1088
1085
|
|
|
1089
1086
|
it("checks all tools at once", async () => {
|
|
1090
1087
|
const availability = await checkAllTools();
|
|
1091
|
-
expect(availability.size).toBe(
|
|
1088
|
+
expect(availability.size).toBe(6); // semantic-memory, cass, ubs, beads, swarm-mail, agent-mail
|
|
1092
1089
|
expect(availability.has("semantic-memory")).toBe(true);
|
|
1093
1090
|
expect(availability.has("cass")).toBe(true);
|
|
1094
1091
|
expect(availability.has("ubs")).toBe(true);
|
|
1095
1092
|
expect(availability.has("beads")).toBe(true);
|
|
1093
|
+
expect(availability.has("swarm-mail")).toBe(true);
|
|
1096
1094
|
expect(availability.has("agent-mail")).toBe(true);
|
|
1097
1095
|
});
|
|
1098
1096
|
|
|
@@ -1325,7 +1323,7 @@ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
|
|
|
1325
1323
|
expect(SUBTASK_PROMPT_V2).toContain("[CONTEXT]");
|
|
1326
1324
|
expect(SUBTASK_PROMPT_V2).toContain("{shared_context}");
|
|
1327
1325
|
|
|
1328
|
-
expect(SUBTASK_PROMPT_V2).toContain("[
|
|
1326
|
+
expect(SUBTASK_PROMPT_V2).toContain("[MANDATORY SURVIVAL CHECKLIST]");
|
|
1329
1327
|
});
|
|
1330
1328
|
|
|
1331
1329
|
it("DOES contain Swarm Mail instructions (MANDATORY)", () => {
|
|
@@ -1337,7 +1335,7 @@ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
|
|
|
1337
1335
|
expect(SUBTASK_PROMPT_V2).toContain("swarmmail_reserve");
|
|
1338
1336
|
expect(SUBTASK_PROMPT_V2).toContain("swarmmail_release");
|
|
1339
1337
|
expect(SUBTASK_PROMPT_V2).toContain("thread_id");
|
|
1340
|
-
expect(SUBTASK_PROMPT_V2).toContain("
|
|
1338
|
+
expect(SUBTASK_PROMPT_V2).toContain("NON-NEGOTIABLE");
|
|
1341
1339
|
});
|
|
1342
1340
|
|
|
1343
1341
|
it("DOES contain beads instructions", () => {
|
|
@@ -1350,11 +1348,74 @@ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
|
|
|
1350
1348
|
});
|
|
1351
1349
|
|
|
1352
1350
|
it("instructs agents to communicate via swarmmail", () => {
|
|
1353
|
-
expect(SUBTASK_PROMPT_V2).toContain("
|
|
1351
|
+
expect(SUBTASK_PROMPT_V2).toContain("don't work silently");
|
|
1354
1352
|
expect(SUBTASK_PROMPT_V2).toContain("progress");
|
|
1355
1353
|
expect(SUBTASK_PROMPT_V2).toContain("coordinator");
|
|
1356
1354
|
expect(SUBTASK_PROMPT_V2).toContain("CRITICAL");
|
|
1357
1355
|
});
|
|
1356
|
+
|
|
1357
|
+
it("contains survival checklist: semantic-memory_find", () => {
|
|
1358
|
+
// Step 2: Query past learnings BEFORE starting work
|
|
1359
|
+
expect(SUBTASK_PROMPT_V2).toContain("semantic-memory_find");
|
|
1360
|
+
expect(SUBTASK_PROMPT_V2).toContain("Query Past Learnings");
|
|
1361
|
+
expect(SUBTASK_PROMPT_V2).toContain("BEFORE starting work");
|
|
1362
|
+
expect(SUBTASK_PROMPT_V2).toContain("Past learnings save time");
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it("contains survival checklist: skills discovery and loading", () => {
|
|
1366
|
+
// Step 3: Load relevant skills if available
|
|
1367
|
+
expect(SUBTASK_PROMPT_V2).toContain("skills_list");
|
|
1368
|
+
expect(SUBTASK_PROMPT_V2).toContain("skills_use");
|
|
1369
|
+
expect(SUBTASK_PROMPT_V2).toContain("Load Relevant Skills");
|
|
1370
|
+
expect(SUBTASK_PROMPT_V2).toContain("Common skill triggers");
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
it("contains survival checklist: worker reserves files (not coordinator)", () => {
|
|
1374
|
+
// Step 4: Worker reserves their own files
|
|
1375
|
+
expect(SUBTASK_PROMPT_V2).toContain("swarmmail_reserve");
|
|
1376
|
+
expect(SUBTASK_PROMPT_V2).toContain("Reserve Your Files");
|
|
1377
|
+
expect(SUBTASK_PROMPT_V2).toContain("YOU reserve, not coordinator");
|
|
1378
|
+
expect(SUBTASK_PROMPT_V2).toContain("Workers reserve their own files");
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
it("contains survival checklist: swarm_progress at milestones", () => {
|
|
1382
|
+
// Step 6: Report progress at 25/50/75%
|
|
1383
|
+
expect(SUBTASK_PROMPT_V2).toContain("swarm_progress");
|
|
1384
|
+
expect(SUBTASK_PROMPT_V2).toContain("Report Progress at Milestones");
|
|
1385
|
+
expect(SUBTASK_PROMPT_V2).toContain("progress_percent");
|
|
1386
|
+
expect(SUBTASK_PROMPT_V2).toContain("25%, 50%, 75%");
|
|
1387
|
+
expect(SUBTASK_PROMPT_V2).toContain("auto-checkpoint");
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
it("contains survival checklist: swarm_checkpoint before risky ops", () => {
|
|
1391
|
+
// Step 7: Manual checkpoint before risky operations
|
|
1392
|
+
expect(SUBTASK_PROMPT_V2).toContain("swarm_checkpoint");
|
|
1393
|
+
expect(SUBTASK_PROMPT_V2).toContain("Manual Checkpoint BEFORE Risky Operations");
|
|
1394
|
+
expect(SUBTASK_PROMPT_V2).toContain("Large refactors");
|
|
1395
|
+
expect(SUBTASK_PROMPT_V2).toContain("preserve context");
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
it("contains survival checklist: semantic-memory_store for learnings", () => {
|
|
1399
|
+
// Step 8: Store discoveries and learnings
|
|
1400
|
+
expect(SUBTASK_PROMPT_V2).toContain("semantic-memory_store");
|
|
1401
|
+
expect(SUBTASK_PROMPT_V2).toContain("Store Learnings");
|
|
1402
|
+
expect(SUBTASK_PROMPT_V2).toContain("Tricky bugs you solved");
|
|
1403
|
+
expect(SUBTASK_PROMPT_V2).toContain("Store the WHY, not just the WHAT");
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
it("does NOT mention coordinator reserving files", () => {
|
|
1407
|
+
// Coordinator no longer reserves files - workers do it themselves
|
|
1408
|
+
const lowerPrompt = SUBTASK_PROMPT_V2.toLowerCase();
|
|
1409
|
+
expect(lowerPrompt).not.toContain("coordinator reserves");
|
|
1410
|
+
expect(lowerPrompt).not.toContain("coordinator will reserve");
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it("enforces swarm_complete over manual beads_close", () => {
|
|
1414
|
+
// Step 9: Use swarm_complete, not beads_close
|
|
1415
|
+
expect(SUBTASK_PROMPT_V2).toContain("swarm_complete");
|
|
1416
|
+
expect(SUBTASK_PROMPT_V2).toContain("DO NOT manually close the bead");
|
|
1417
|
+
expect(SUBTASK_PROMPT_V2).toContain("Use swarm_complete");
|
|
1418
|
+
});
|
|
1358
1419
|
});
|
|
1359
1420
|
|
|
1360
1421
|
describe("swarm_complete automatic memory capture", () => {
|
|
@@ -1481,3 +1542,492 @@ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
|
|
|
1481
1542
|
);
|
|
1482
1543
|
});
|
|
1483
1544
|
});
|
|
1545
|
+
|
|
1546
|
+
// ============================================================================
|
|
1547
|
+
// Checkpoint/Recovery Flow Integration Tests
|
|
1548
|
+
// ============================================================================
|
|
1549
|
+
|
|
1550
|
+
describe("Checkpoint/Recovery Flow (integration)", () => {
|
|
1551
|
+
describe("swarm_checkpoint", () => {
|
|
1552
|
+
it("creates swarm_checkpointed event and updates swarm_contexts table", async () => {
|
|
1553
|
+
const uniqueProjectKey = `${TEST_PROJECT_PATH}-checkpoint-${Date.now()}`;
|
|
1554
|
+
const sessionID = `checkpoint-session-${Date.now()}`;
|
|
1555
|
+
|
|
1556
|
+
// Initialize swarm-mail database directly (no Agent Mail needed)
|
|
1557
|
+
const { getDatabase, closeDatabase } = await import("swarm-mail");
|
|
1558
|
+
const db = await getDatabase(uniqueProjectKey);
|
|
1559
|
+
|
|
1560
|
+
try {
|
|
1561
|
+
const ctx = {
|
|
1562
|
+
...mockContext,
|
|
1563
|
+
sessionID,
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
const epicId = "bd-test-epic-123";
|
|
1567
|
+
const beadId = "bd-test-epic-123.1";
|
|
1568
|
+
const agentName = "TestAgent";
|
|
1569
|
+
|
|
1570
|
+
// Execute checkpoint
|
|
1571
|
+
const result = await swarm_checkpoint.execute(
|
|
1572
|
+
{
|
|
1573
|
+
project_key: uniqueProjectKey,
|
|
1574
|
+
agent_name: agentName,
|
|
1575
|
+
bead_id: beadId,
|
|
1576
|
+
epic_id: epicId,
|
|
1577
|
+
files_modified: ["src/test.ts", "src/test2.ts"],
|
|
1578
|
+
progress_percent: 50,
|
|
1579
|
+
directives: {
|
|
1580
|
+
shared_context: "Testing checkpoint functionality",
|
|
1581
|
+
skills_to_load: ["testing-patterns"],
|
|
1582
|
+
coordinator_notes: "Mid-task checkpoint",
|
|
1583
|
+
},
|
|
1584
|
+
},
|
|
1585
|
+
ctx,
|
|
1586
|
+
);
|
|
1587
|
+
|
|
1588
|
+
const parsed = JSON.parse(result);
|
|
1589
|
+
|
|
1590
|
+
// Verify checkpoint was created
|
|
1591
|
+
expect(parsed.success).toBe(true);
|
|
1592
|
+
expect(parsed.bead_id).toBe(beadId);
|
|
1593
|
+
expect(parsed.epic_id).toBe(epicId);
|
|
1594
|
+
expect(parsed.files_tracked).toBe(2);
|
|
1595
|
+
expect(parsed.summary).toContain("50%");
|
|
1596
|
+
expect(parsed).toHaveProperty("checkpoint_timestamp");
|
|
1597
|
+
|
|
1598
|
+
// Verify swarm_contexts table was updated
|
|
1599
|
+
const dbResult = await db.query<{
|
|
1600
|
+
id: string;
|
|
1601
|
+
epic_id: string;
|
|
1602
|
+
bead_id: string;
|
|
1603
|
+
strategy: string;
|
|
1604
|
+
files: string;
|
|
1605
|
+
recovery: string;
|
|
1606
|
+
}>(
|
|
1607
|
+
`SELECT id, epic_id, bead_id, strategy, files, recovery
|
|
1608
|
+
FROM swarm_contexts
|
|
1609
|
+
WHERE bead_id = $1`,
|
|
1610
|
+
[beadId],
|
|
1611
|
+
);
|
|
1612
|
+
|
|
1613
|
+
expect(dbResult.rows.length).toBe(1);
|
|
1614
|
+
const row = dbResult.rows[0];
|
|
1615
|
+
expect(row.epic_id).toBe(epicId);
|
|
1616
|
+
expect(row.bead_id).toBe(beadId);
|
|
1617
|
+
expect(row.strategy).toBe("file-based");
|
|
1618
|
+
|
|
1619
|
+
// PGLite auto-parses JSON columns, so we get objects directly
|
|
1620
|
+
const files =
|
|
1621
|
+
typeof row.files === "string" ? JSON.parse(row.files) : row.files;
|
|
1622
|
+
expect(files).toEqual(["src/test.ts", "src/test2.ts"]);
|
|
1623
|
+
|
|
1624
|
+
const recovery =
|
|
1625
|
+
typeof row.recovery === "string"
|
|
1626
|
+
? JSON.parse(row.recovery)
|
|
1627
|
+
: row.recovery;
|
|
1628
|
+
expect(recovery.progress_percent).toBe(50);
|
|
1629
|
+
expect(recovery.files_modified).toEqual(["src/test.ts", "src/test2.ts"]);
|
|
1630
|
+
expect(recovery).toHaveProperty("last_checkpoint");
|
|
1631
|
+
} finally {
|
|
1632
|
+
await closeDatabase(uniqueProjectKey);
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
it("handles checkpoint with error_context", async () => {
|
|
1637
|
+
const uniqueProjectKey = `${TEST_PROJECT_PATH}-checkpoint-error-${Date.now()}`;
|
|
1638
|
+
const sessionID = `checkpoint-error-session-${Date.now()}`;
|
|
1639
|
+
|
|
1640
|
+
const { getDatabase, closeDatabase } = await import("swarm-mail");
|
|
1641
|
+
const db = await getDatabase(uniqueProjectKey);
|
|
1642
|
+
|
|
1643
|
+
try {
|
|
1644
|
+
const ctx = {
|
|
1645
|
+
...mockContext,
|
|
1646
|
+
sessionID,
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1649
|
+
const result = await swarm_checkpoint.execute(
|
|
1650
|
+
{
|
|
1651
|
+
project_key: uniqueProjectKey,
|
|
1652
|
+
agent_name: "TestAgent",
|
|
1653
|
+
bead_id: "bd-error-test.1",
|
|
1654
|
+
epic_id: "bd-error-test",
|
|
1655
|
+
files_modified: ["src/buggy.ts"],
|
|
1656
|
+
progress_percent: 75,
|
|
1657
|
+
error_context:
|
|
1658
|
+
"Hit type error on line 42, need to add explicit types",
|
|
1659
|
+
},
|
|
1660
|
+
ctx,
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
const parsed = JSON.parse(result);
|
|
1664
|
+
expect(parsed.success).toBe(true);
|
|
1665
|
+
|
|
1666
|
+
// Verify error_context was stored
|
|
1667
|
+
const dbResult = await db.query<{ recovery: string }>(
|
|
1668
|
+
`SELECT recovery FROM swarm_contexts WHERE bead_id = $1`,
|
|
1669
|
+
["bd-error-test.1"],
|
|
1670
|
+
);
|
|
1671
|
+
|
|
1672
|
+
const recoveryRaw = dbResult.rows[0].recovery;
|
|
1673
|
+
const recovery =
|
|
1674
|
+
typeof recoveryRaw === "string" ? JSON.parse(recoveryRaw) : recoveryRaw;
|
|
1675
|
+
expect(recovery.error_context).toBe(
|
|
1676
|
+
"Hit type error on line 42, need to add explicit types",
|
|
1677
|
+
);
|
|
1678
|
+
} finally {
|
|
1679
|
+
await closeDatabase(uniqueProjectKey);
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
describe("swarm_recover", () => {
|
|
1685
|
+
it("retrieves checkpoint data from swarm_contexts table", async () => {
|
|
1686
|
+
const uniqueProjectKey = `${TEST_PROJECT_PATH}-recover-${Date.now()}`;
|
|
1687
|
+
const sessionID = `recover-session-${Date.now()}`;
|
|
1688
|
+
|
|
1689
|
+
const { getDatabase, closeDatabase } = await import("swarm-mail");
|
|
1690
|
+
const db = await getDatabase(uniqueProjectKey);
|
|
1691
|
+
|
|
1692
|
+
try {
|
|
1693
|
+
const ctx = {
|
|
1694
|
+
...mockContext,
|
|
1695
|
+
sessionID,
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
const epicId = "bd-recover-epic-456";
|
|
1699
|
+
const beadId = "bd-recover-epic-456.1";
|
|
1700
|
+
const agentName = "TestAgent";
|
|
1701
|
+
|
|
1702
|
+
// First create a checkpoint
|
|
1703
|
+
await swarm_checkpoint.execute(
|
|
1704
|
+
{
|
|
1705
|
+
project_key: uniqueProjectKey,
|
|
1706
|
+
agent_name: agentName,
|
|
1707
|
+
bead_id: beadId,
|
|
1708
|
+
epic_id: epicId,
|
|
1709
|
+
files_modified: ["src/auth.ts", "src/middleware.ts"],
|
|
1710
|
+
progress_percent: 75,
|
|
1711
|
+
directives: {
|
|
1712
|
+
shared_context: "OAuth implementation in progress",
|
|
1713
|
+
skills_to_load: ["testing-patterns", "swarm-coordination"],
|
|
1714
|
+
},
|
|
1715
|
+
},
|
|
1716
|
+
ctx,
|
|
1717
|
+
);
|
|
1718
|
+
|
|
1719
|
+
// Now recover it
|
|
1720
|
+
const result = await swarm_recover.execute(
|
|
1721
|
+
{
|
|
1722
|
+
project_key: uniqueProjectKey,
|
|
1723
|
+
epic_id: epicId,
|
|
1724
|
+
},
|
|
1725
|
+
ctx,
|
|
1726
|
+
);
|
|
1727
|
+
|
|
1728
|
+
const parsed = JSON.parse(result);
|
|
1729
|
+
|
|
1730
|
+
// Verify recovery succeeded
|
|
1731
|
+
expect(parsed.found).toBe(true);
|
|
1732
|
+
expect(parsed).toHaveProperty("context");
|
|
1733
|
+
expect(parsed).toHaveProperty("summary");
|
|
1734
|
+
expect(parsed).toHaveProperty("age_seconds");
|
|
1735
|
+
|
|
1736
|
+
const { context } = parsed;
|
|
1737
|
+
expect(context.epic_id).toBe(epicId);
|
|
1738
|
+
expect(context.bead_id).toBe(beadId);
|
|
1739
|
+
expect(context.strategy).toBe("file-based");
|
|
1740
|
+
expect(context.files).toEqual(["src/auth.ts", "src/middleware.ts"]);
|
|
1741
|
+
expect(context.recovery.progress_percent).toBe(75);
|
|
1742
|
+
expect(context.directives.shared_context).toBe(
|
|
1743
|
+
"OAuth implementation in progress",
|
|
1744
|
+
);
|
|
1745
|
+
expect(context.directives.skills_to_load).toEqual([
|
|
1746
|
+
"testing-patterns",
|
|
1747
|
+
"swarm-coordination",
|
|
1748
|
+
]);
|
|
1749
|
+
} finally {
|
|
1750
|
+
await closeDatabase(uniqueProjectKey);
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
it("returns found:false when no checkpoint exists", async () => {
|
|
1755
|
+
const uniqueProjectKey = `${TEST_PROJECT_PATH}-recover-notfound-${Date.now()}`;
|
|
1756
|
+
const sessionID = `recover-notfound-session-${Date.now()}`;
|
|
1757
|
+
|
|
1758
|
+
const { getDatabase, closeDatabase } = await import("swarm-mail");
|
|
1759
|
+
await getDatabase(uniqueProjectKey);
|
|
1760
|
+
|
|
1761
|
+
try {
|
|
1762
|
+
const ctx = {
|
|
1763
|
+
...mockContext,
|
|
1764
|
+
sessionID,
|
|
1765
|
+
};
|
|
1766
|
+
|
|
1767
|
+
// Try to recover non-existent checkpoint
|
|
1768
|
+
const result = await swarm_recover.execute(
|
|
1769
|
+
{
|
|
1770
|
+
project_key: uniqueProjectKey,
|
|
1771
|
+
epic_id: "bd-nonexistent-epic",
|
|
1772
|
+
},
|
|
1773
|
+
ctx,
|
|
1774
|
+
);
|
|
1775
|
+
|
|
1776
|
+
const parsed = JSON.parse(result);
|
|
1777
|
+
|
|
1778
|
+
expect(parsed.found).toBe(false);
|
|
1779
|
+
expect(parsed.message).toContain("No checkpoint found");
|
|
1780
|
+
expect(parsed.epic_id).toBe("bd-nonexistent-epic");
|
|
1781
|
+
} finally {
|
|
1782
|
+
await closeDatabase(uniqueProjectKey);
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
describe("Auto-checkpoint at progress milestones", () => {
|
|
1788
|
+
it("creates checkpoint at 25% progress", async () => {
|
|
1789
|
+
const uniqueProjectKey = `${TEST_PROJECT_PATH}-auto25-${Date.now()}`;
|
|
1790
|
+
const sessionID = `auto25-session-${Date.now()}`;
|
|
1791
|
+
|
|
1792
|
+
const { getDatabase, closeDatabase } = await import("swarm-mail");
|
|
1793
|
+
const db = await getDatabase(uniqueProjectKey);
|
|
1794
|
+
|
|
1795
|
+
try {
|
|
1796
|
+
const ctx = {
|
|
1797
|
+
...mockContext,
|
|
1798
|
+
sessionID,
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
const beadId = "bd-auto-test.1";
|
|
1802
|
+
const agentName = "TestAgent";
|
|
1803
|
+
|
|
1804
|
+
// Report progress at 25% - should trigger auto-checkpoint
|
|
1805
|
+
const result = await swarm_progress.execute(
|
|
1806
|
+
{
|
|
1807
|
+
project_key: uniqueProjectKey,
|
|
1808
|
+
agent_name: agentName,
|
|
1809
|
+
bead_id: beadId,
|
|
1810
|
+
status: "in_progress",
|
|
1811
|
+
progress_percent: 25,
|
|
1812
|
+
message: "Quarter done",
|
|
1813
|
+
files_touched: ["src/component.tsx"],
|
|
1814
|
+
},
|
|
1815
|
+
ctx,
|
|
1816
|
+
);
|
|
1817
|
+
|
|
1818
|
+
// Verify checkpoint was created (indicated in response)
|
|
1819
|
+
expect(result).toContain("Progress reported");
|
|
1820
|
+
expect(result).toContain("25%");
|
|
1821
|
+
expect(result).toContain("[checkpoint created]");
|
|
1822
|
+
|
|
1823
|
+
// Verify checkpoint exists in database
|
|
1824
|
+
const dbResult = await db.query<{ recovery: string }>(
|
|
1825
|
+
`SELECT recovery FROM swarm_contexts WHERE bead_id = $1`,
|
|
1826
|
+
[beadId],
|
|
1827
|
+
);
|
|
1828
|
+
|
|
1829
|
+
expect(dbResult.rows.length).toBe(1);
|
|
1830
|
+
const recoveryRaw = dbResult.rows[0].recovery;
|
|
1831
|
+
const recovery =
|
|
1832
|
+
typeof recoveryRaw === "string" ? JSON.parse(recoveryRaw) : recoveryRaw;
|
|
1833
|
+
expect(recovery.progress_percent).toBe(25);
|
|
1834
|
+
expect(recovery.files_modified).toEqual(["src/component.tsx"]);
|
|
1835
|
+
} finally {
|
|
1836
|
+
await closeDatabase(uniqueProjectKey);
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
it("creates checkpoint at 50% progress", async () => {
|
|
1841
|
+
const uniqueProjectKey = `${TEST_PROJECT_PATH}-auto50-${Date.now()}`;
|
|
1842
|
+
const sessionID = `auto50-session-${Date.now()}`;
|
|
1843
|
+
|
|
1844
|
+
const { getDatabase, closeDatabase } = await import("swarm-mail");
|
|
1845
|
+
const db = await getDatabase(uniqueProjectKey);
|
|
1846
|
+
|
|
1847
|
+
try {
|
|
1848
|
+
const ctx = {
|
|
1849
|
+
...mockContext,
|
|
1850
|
+
sessionID,
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1853
|
+
const beadId = "bd-auto50-test.1";
|
|
1854
|
+
const agentName = "TestAgent";
|
|
1855
|
+
|
|
1856
|
+
// Report progress at 50%
|
|
1857
|
+
const result = await swarm_progress.execute(
|
|
1858
|
+
{
|
|
1859
|
+
project_key: uniqueProjectKey,
|
|
1860
|
+
agent_name: agentName,
|
|
1861
|
+
bead_id: beadId,
|
|
1862
|
+
status: "in_progress",
|
|
1863
|
+
progress_percent: 50,
|
|
1864
|
+
message: "Halfway there",
|
|
1865
|
+
files_touched: ["src/api.ts", "src/types.ts"],
|
|
1866
|
+
},
|
|
1867
|
+
ctx,
|
|
1868
|
+
);
|
|
1869
|
+
|
|
1870
|
+
expect(result).toContain("[checkpoint created]");
|
|
1871
|
+
|
|
1872
|
+
// Verify checkpoint
|
|
1873
|
+
const dbResult = await db.query<{ recovery: string }>(
|
|
1874
|
+
`SELECT recovery FROM swarm_contexts WHERE bead_id = $1`,
|
|
1875
|
+
[beadId],
|
|
1876
|
+
);
|
|
1877
|
+
|
|
1878
|
+
const recoveryRaw50 = dbResult.rows[0].recovery;
|
|
1879
|
+
const recovery =
|
|
1880
|
+
typeof recoveryRaw50 === "string"
|
|
1881
|
+
? JSON.parse(recoveryRaw50)
|
|
1882
|
+
: recoveryRaw50;
|
|
1883
|
+
expect(recovery.progress_percent).toBe(50);
|
|
1884
|
+
} finally {
|
|
1885
|
+
await closeDatabase(uniqueProjectKey);
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
it("creates checkpoint at 75% progress", async () => {
|
|
1890
|
+
const uniqueProjectKey = `${TEST_PROJECT_PATH}-auto75-${Date.now()}`;
|
|
1891
|
+
const sessionID = `auto75-session-${Date.now()}`;
|
|
1892
|
+
|
|
1893
|
+
const { getDatabase, closeDatabase } = await import("swarm-mail");
|
|
1894
|
+
const db = await getDatabase(uniqueProjectKey);
|
|
1895
|
+
|
|
1896
|
+
try {
|
|
1897
|
+
const ctx = {
|
|
1898
|
+
...mockContext,
|
|
1899
|
+
sessionID,
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
const beadId = "bd-auto75-test.1";
|
|
1903
|
+
const agentName = "TestAgent";
|
|
1904
|
+
|
|
1905
|
+
// Report progress at 75%
|
|
1906
|
+
const result = await swarm_progress.execute(
|
|
1907
|
+
{
|
|
1908
|
+
project_key: uniqueProjectKey,
|
|
1909
|
+
agent_name: agentName,
|
|
1910
|
+
bead_id: beadId,
|
|
1911
|
+
status: "in_progress",
|
|
1912
|
+
progress_percent: 75,
|
|
1913
|
+
message: "Almost done",
|
|
1914
|
+
files_touched: ["src/final.ts"],
|
|
1915
|
+
},
|
|
1916
|
+
ctx,
|
|
1917
|
+
);
|
|
1918
|
+
|
|
1919
|
+
expect(result).toContain("[checkpoint created]");
|
|
1920
|
+
|
|
1921
|
+
// Verify checkpoint
|
|
1922
|
+
const dbResult = await db.query<{ recovery: string }>(
|
|
1923
|
+
`SELECT recovery FROM swarm_contexts WHERE bead_id = $1`,
|
|
1924
|
+
[beadId],
|
|
1925
|
+
);
|
|
1926
|
+
|
|
1927
|
+
const recoveryRaw75 = dbResult.rows[0].recovery;
|
|
1928
|
+
const recovery =
|
|
1929
|
+
typeof recoveryRaw75 === "string"
|
|
1930
|
+
? JSON.parse(recoveryRaw75)
|
|
1931
|
+
: recoveryRaw75;
|
|
1932
|
+
expect(recovery.progress_percent).toBe(75);
|
|
1933
|
+
} finally {
|
|
1934
|
+
await closeDatabase(uniqueProjectKey);
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
it("does NOT create checkpoint at non-milestone progress", async () => {
|
|
1939
|
+
const uniqueProjectKey = `${TEST_PROJECT_PATH}-auto-nomilestone-${Date.now()}`;
|
|
1940
|
+
const sessionID = `auto-nomilestone-session-${Date.now()}`;
|
|
1941
|
+
|
|
1942
|
+
const { getDatabase, closeDatabase } = await import("swarm-mail");
|
|
1943
|
+
const db = await getDatabase(uniqueProjectKey);
|
|
1944
|
+
|
|
1945
|
+
try {
|
|
1946
|
+
const ctx = {
|
|
1947
|
+
...mockContext,
|
|
1948
|
+
sessionID,
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
const beadId = "bd-auto-nomilestone.1";
|
|
1952
|
+
const agentName = "TestAgent";
|
|
1953
|
+
|
|
1954
|
+
// Report progress at 30% (not a milestone)
|
|
1955
|
+
const result = await swarm_progress.execute(
|
|
1956
|
+
{
|
|
1957
|
+
project_key: uniqueProjectKey,
|
|
1958
|
+
agent_name: agentName,
|
|
1959
|
+
bead_id: beadId,
|
|
1960
|
+
status: "in_progress",
|
|
1961
|
+
progress_percent: 30,
|
|
1962
|
+
message: "Not a milestone",
|
|
1963
|
+
files_touched: ["src/random.ts"],
|
|
1964
|
+
},
|
|
1965
|
+
ctx,
|
|
1966
|
+
);
|
|
1967
|
+
|
|
1968
|
+
// Should NOT contain checkpoint indicator
|
|
1969
|
+
expect(result).not.toContain("[checkpoint created]");
|
|
1970
|
+
expect(result).toContain("30%");
|
|
1971
|
+
|
|
1972
|
+
// Verify NO checkpoint was created
|
|
1973
|
+
const dbResult = await db.query(
|
|
1974
|
+
`SELECT * FROM swarm_contexts WHERE bead_id = $1`,
|
|
1975
|
+
[beadId],
|
|
1976
|
+
);
|
|
1977
|
+
|
|
1978
|
+
expect(dbResult.rows.length).toBe(0);
|
|
1979
|
+
} finally {
|
|
1980
|
+
await closeDatabase(uniqueProjectKey);
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
it("checkpoint includes message from progress report", async () => {
|
|
1985
|
+
const uniqueProjectKey = `${TEST_PROJECT_PATH}-auto-message-${Date.now()}`;
|
|
1986
|
+
const sessionID = `auto-message-session-${Date.now()}`;
|
|
1987
|
+
|
|
1988
|
+
const { getDatabase, closeDatabase } = await import("swarm-mail");
|
|
1989
|
+
const db = await getDatabase(uniqueProjectKey);
|
|
1990
|
+
|
|
1991
|
+
try {
|
|
1992
|
+
const ctx = {
|
|
1993
|
+
...mockContext,
|
|
1994
|
+
sessionID,
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
const beadId = "bd-auto-message.1";
|
|
1998
|
+
const testMessage =
|
|
1999
|
+
"Implemented auth service, working on JWT tokens";
|
|
2000
|
+
const agentName = "TestAgent";
|
|
2001
|
+
|
|
2002
|
+
// Report progress with message
|
|
2003
|
+
await swarm_progress.execute(
|
|
2004
|
+
{
|
|
2005
|
+
project_key: uniqueProjectKey,
|
|
2006
|
+
agent_name: agentName,
|
|
2007
|
+
bead_id: beadId,
|
|
2008
|
+
status: "in_progress",
|
|
2009
|
+
progress_percent: 50,
|
|
2010
|
+
message: testMessage,
|
|
2011
|
+
files_touched: ["src/auth.ts"],
|
|
2012
|
+
},
|
|
2013
|
+
ctx,
|
|
2014
|
+
);
|
|
2015
|
+
|
|
2016
|
+
// Verify message was stored in checkpoint
|
|
2017
|
+
const dbResult = await db.query<{ recovery: string }>(
|
|
2018
|
+
`SELECT recovery FROM swarm_contexts WHERE bead_id = $1`,
|
|
2019
|
+
[beadId],
|
|
2020
|
+
);
|
|
2021
|
+
|
|
2022
|
+
const recoveryRawMsg = dbResult.rows[0].recovery;
|
|
2023
|
+
const recovery =
|
|
2024
|
+
typeof recoveryRawMsg === "string"
|
|
2025
|
+
? JSON.parse(recoveryRawMsg)
|
|
2026
|
+
: recoveryRawMsg;
|
|
2027
|
+
expect(recovery.last_message).toBe(testMessage);
|
|
2028
|
+
} finally {
|
|
2029
|
+
await closeDatabase(uniqueProjectKey);
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
});
|
|
2033
|
+
});
|