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.
@@ -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(2); // 3 strategies - 1 selected = 2 alternatives
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(["file-based", "feature-based", "risk-based"]).toContain(
229
- alt.strategy,
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("reports CASS status in output (queried flag)", async () => {
342
- // Test with CASS disabled
343
- const resultDisabled = await swarm_plan_prompt.execute(
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 parsedDisabled = JSON.parse(resultDisabled);
355
+ const parsed = JSON.parse(result);
352
356
 
353
- expect(parsedDisabled).toHaveProperty("cass_history");
354
- expect(parsedDisabled.cass_history.queried).toBe(false);
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
- // Test with CASS enabled (may or may not be available)
357
- const resultEnabled = await swarm_plan_prompt.execute(
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
- expect(parsedEnabled).toHaveProperty("cass_history");
368
- expect(parsedEnabled.cass_history).toHaveProperty("queried");
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(5);
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("[WORKFLOW]");
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("non-negotiable");
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("Never work silently");
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
+ });