opencode-swarm-plugin 0.31.7 → 0.32.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.
Files changed (48) hide show
  1. package/.turbo/turbo-build.log +10 -9
  2. package/.turbo/turbo-test.log +319 -317
  3. package/CHANGELOG.md +134 -0
  4. package/README.md +7 -4
  5. package/bin/swarm.ts +388 -128
  6. package/dist/compaction-hook.d.ts +1 -1
  7. package/dist/compaction-hook.d.ts.map +1 -1
  8. package/dist/hive.d.ts.map +1 -1
  9. package/dist/index.d.ts +0 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +123 -134
  12. package/dist/memory-tools.d.ts.map +1 -1
  13. package/dist/memory.d.ts +5 -4
  14. package/dist/memory.d.ts.map +1 -1
  15. package/dist/plugin.js +118 -131
  16. package/dist/swarm-orchestrate.d.ts +29 -5
  17. package/dist/swarm-orchestrate.d.ts.map +1 -1
  18. package/dist/swarm-prompts.d.ts +7 -0
  19. package/dist/swarm-prompts.d.ts.map +1 -1
  20. package/dist/swarm.d.ts +0 -2
  21. package/dist/swarm.d.ts.map +1 -1
  22. package/evals/lib/{data-loader.test.ts → data-loader.evalite-test.ts} +7 -6
  23. package/evals/lib/data-loader.ts +1 -1
  24. package/evals/scorers/{outcome-scorers.test.ts → outcome-scorers.evalite-test.ts} +1 -1
  25. package/examples/plugin-wrapper-template.ts +19 -4
  26. package/global-skills/swarm-coordination/SKILL.md +118 -8
  27. package/package.json +2 -2
  28. package/src/compaction-hook.ts +5 -3
  29. package/src/hive.integration.test.ts +83 -1
  30. package/src/hive.ts +37 -12
  31. package/src/mandate-storage.integration.test.ts +601 -0
  32. package/src/memory-tools.ts +6 -4
  33. package/src/memory.integration.test.ts +117 -49
  34. package/src/memory.test.ts +41 -217
  35. package/src/memory.ts +12 -8
  36. package/src/repo-crawl.integration.test.ts +441 -0
  37. package/src/skills.integration.test.ts +1056 -0
  38. package/src/structured.integration.test.ts +817 -0
  39. package/src/swarm-deferred.integration.test.ts +157 -0
  40. package/src/swarm-deferred.test.ts +38 -0
  41. package/src/swarm-mail.integration.test.ts +15 -19
  42. package/src/swarm-orchestrate.integration.test.ts +282 -0
  43. package/src/swarm-orchestrate.ts +96 -201
  44. package/src/swarm-prompts.test.ts +92 -0
  45. package/src/swarm-prompts.ts +69 -0
  46. package/src/swarm-review.integration.test.ts +290 -0
  47. package/src/swarm.integration.test.ts +23 -20
  48. package/src/tool-adapter.integration.test.ts +1221 -0
@@ -44,6 +44,7 @@ import {
44
44
  getAgent,
45
45
  createEvent,
46
46
  appendEvent,
47
+ getSwarmMailLibSQL,
47
48
  } from "swarm-mail";
48
49
  import {
49
50
  addStrike,
@@ -386,104 +387,9 @@ interface VerificationGateResult {
386
387
  blockers: string[];
387
388
  }
388
389
 
389
- /**
390
- * UBS scan result schema
391
- */
392
- interface UbsScanResult {
393
- exitCode: number;
394
- bugs: Array<{
395
- file: string;
396
- line: number;
397
- severity: string;
398
- message: string;
399
- category: string;
400
- }>;
401
- summary: {
402
- total: number;
403
- critical: number;
404
- high: number;
405
- medium: number;
406
- low: number;
407
- };
408
- }
409
-
410
- /**
411
- * Run UBS scan on files before completion
412
- *
413
- * @param files - Files to scan
414
- * @returns Scan result or null if UBS not available
415
- */
416
- async function runUbsScan(files: string[]): Promise<UbsScanResult | null> {
417
- if (files.length === 0) {
418
- return null;
419
- }
420
-
421
- // Check if UBS is available first
422
- const ubsAvailable = await isToolAvailable("ubs");
423
- if (!ubsAvailable) {
424
- warnMissingTool("ubs");
425
- return null;
426
- }
427
-
428
- try {
429
- // Run UBS scan with JSON output
430
- const result = await Bun.$`ubs scan ${files.join(" ")} --json`
431
- .quiet()
432
- .nothrow();
433
-
434
- const output = result.stdout.toString();
435
- if (!output.trim()) {
436
- return {
437
- exitCode: result.exitCode,
438
- bugs: [],
439
- summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
440
- };
441
- }
442
-
443
- try {
444
- const parsed = JSON.parse(output);
445
-
446
- // Basic validation of structure
447
- if (typeof parsed !== "object" || parsed === null) {
448
- throw new Error("UBS output is not an object");
449
- }
450
- if (!Array.isArray(parsed.bugs)) {
451
- console.warn("[swarm] UBS output missing bugs array, using empty");
452
- }
453
- if (typeof parsed.summary !== "object" || parsed.summary === null) {
454
- console.warn("[swarm] UBS output missing summary object, using empty");
455
- }
456
-
457
- return {
458
- exitCode: result.exitCode,
459
- bugs: Array.isArray(parsed.bugs) ? parsed.bugs : [],
460
- summary: parsed.summary || {
461
- total: 0,
462
- critical: 0,
463
- high: 0,
464
- medium: 0,
465
- low: 0,
466
- },
467
- };
468
- } catch (error) {
469
- // UBS output wasn't JSON - this is an error condition
470
- console.error(
471
- `[swarm] CRITICAL: UBS scan failed to parse JSON output because output is malformed:`,
472
- error,
473
- );
474
- console.error(
475
- `[swarm] Raw output: ${output}. Try: Run 'ubs doctor' to check installation, verify UBS version with 'ubs --version' (need v1.0.0+), or check if UBS supports --json flag.`,
476
- );
477
- return {
478
- exitCode: result.exitCode,
479
- bugs: [],
480
- summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
481
- };
482
- }
483
- } catch {
484
- return null;
485
- }
486
- }
390
+ // NOTE: UBS scan (runUbsScan, UbsScanResult) removed in v0.31
391
+ // It was slowing down completion without proportional value.
392
+ // Run UBS manually if needed: ubs scan <files>
487
393
 
488
394
  /**
489
395
  * Run typecheck verification
@@ -608,51 +514,22 @@ async function runTestVerification(
608
514
  * Run the full Verification Gate
609
515
  *
610
516
  * Implements the Gate Function (IDENTIFY → RUN → READ → VERIFY → CLAIM):
611
- * 1. UBS scan (already exists)
612
- * 2. Typecheck
613
- * 3. Tests for touched files
517
+ * 1. Typecheck
518
+ * 2. Tests for touched files
519
+ *
520
+ * NOTE: UBS scan was removed in v0.31 - it was slowing down completion
521
+ * without providing proportional value. Run UBS manually if needed.
614
522
  *
615
523
  * All steps must pass (or be skipped with valid reason) to proceed.
616
524
  */
617
525
  async function runVerificationGate(
618
526
  filesTouched: string[],
619
- skipUbs: boolean = false,
527
+ _skipUbs: boolean = false, // Kept for backward compatibility, now ignored
620
528
  ): Promise<VerificationGateResult> {
621
529
  const steps: VerificationStep[] = [];
622
530
  const blockers: string[] = [];
623
531
 
624
- // Step 1: UBS scan
625
- if (!skipUbs && filesTouched.length > 0) {
626
- const ubsResult = await runUbsScan(filesTouched);
627
- if (ubsResult) {
628
- const ubsStep: VerificationStep = {
629
- name: "ubs_scan",
630
- command: `ubs scan ${filesTouched.join(" ")}`,
631
- passed: ubsResult.summary.critical === 0,
632
- exitCode: ubsResult.exitCode,
633
- };
634
-
635
- if (!ubsStep.passed) {
636
- ubsStep.error = `Found ${ubsResult.summary.critical} critical bugs`;
637
- blockers.push(
638
- `UBS found ${ubsResult.summary.critical} critical bug(s). Try: Run 'ubs scan ${filesTouched.join(" ")}' to see details, fix critical bugs in reported files, or use skip_ubs_scan=true to bypass (not recommended).`,
639
- );
640
- }
641
-
642
- steps.push(ubsStep);
643
- } else {
644
- steps.push({
645
- name: "ubs_scan",
646
- command: "ubs scan",
647
- passed: true,
648
- exitCode: 0,
649
- skipped: true,
650
- skipReason: "UBS not available",
651
- });
652
- }
653
- }
654
-
655
- // Step 2: Typecheck
532
+ // Step 1: Typecheck (UBS scan removed in v0.31)
656
533
  const typecheckStep = await runTypecheckVerification();
657
534
  steps.push(typecheckStep);
658
535
  if (!typecheckStep.passed && !typecheckStep.skipped) {
@@ -1224,11 +1101,39 @@ export const swarm_broadcast = tool({
1224
1101
  * 4. VERIFY: All checks must pass
1225
1102
  * 5. ONLY THEN: Close the cell
1226
1103
  *
1227
- * Closes cell, releases reservations, notifies coordinator.
1104
+ * Closes cell, releases reservations, notifies coordinator, and resolves
1105
+ * a DurableDeferred keyed by bead_id for cross-agent task completion signaling.
1106
+ *
1107
+ * ## DurableDeferred Integration
1108
+ *
1109
+ * When a coordinator spawns workers, it can create a deferred BEFORE spawning:
1110
+ *
1111
+ * ```typescript
1112
+ * const swarmMail = await getSwarmMailLibSQL(projectPath);
1113
+ * const db = await swarmMail.getDatabase();
1114
+ *
1115
+ * // Create deferred keyed by bead_id
1116
+ * const deferredUrl = `deferred:${beadId}`;
1117
+ * await db.query(
1118
+ * `INSERT INTO deferred (url, resolved, expires_at, created_at) VALUES (?, 0, ?, ?)`,
1119
+ * [deferredUrl, Date.now() + 3600000, Date.now()]
1120
+ * );
1121
+ *
1122
+ * // Spawn worker (swarm_spawn_subtask...)
1123
+ *
1124
+ * // Await completion
1125
+ * const result = await db.query<{ value: string }>(
1126
+ * `SELECT value FROM deferred WHERE url = ? AND resolved = 1`,
1127
+ * [deferredUrl]
1128
+ * );
1129
+ * ```
1130
+ *
1131
+ * When the worker calls swarm_complete, it resolves the deferred automatically.
1132
+ * Coordinator can await without polling.
1228
1133
  */
1229
1134
  export const swarm_complete = tool({
1230
1135
  description:
1231
- "Mark subtask complete with Verification Gate. Runs UBS scan, typecheck, and tests before allowing completion.",
1136
+ "Mark subtask complete with Verification Gate. Runs typecheck and tests before allowing completion.",
1232
1137
  args: {
1233
1138
  project_key: tool.schema.string().describe("Project path"),
1234
1139
  agent_name: tool.schema.string().describe("Your Agent Mail name"),
@@ -1241,16 +1146,12 @@ export const swarm_complete = tool({
1241
1146
  files_touched: tool.schema
1242
1147
  .array(tool.schema.string())
1243
1148
  .optional()
1244
- .describe("Files modified - will be verified (UBS, typecheck, tests)"),
1245
- skip_ubs_scan: tool.schema
1246
- .boolean()
1247
- .optional()
1248
- .describe("Skip UBS bug scan (default: false)"),
1149
+ .describe("Files modified - will be verified (typecheck, tests)"),
1249
1150
  skip_verification: tool.schema
1250
1151
  .boolean()
1251
1152
  .optional()
1252
1153
  .describe(
1253
- "Skip ALL verification (UBS, typecheck, tests). Use sparingly! (default: false)",
1154
+ "Skip ALL verification (typecheck, tests). Use sparingly! (default: false)",
1254
1155
  ),
1255
1156
  planned_files: tool.schema
1256
1157
  .array(tool.schema.string())
@@ -1397,7 +1298,7 @@ Continuing with completion, but this should be fixed for future subtasks.`;
1397
1298
  if (!args.skip_verification && args.files_touched?.length) {
1398
1299
  verificationResult = await runVerificationGate(
1399
1300
  args.files_touched,
1400
- args.skip_ubs_scan ?? false,
1301
+ false,
1401
1302
  );
1402
1303
 
1403
1304
  // Block completion if verification failed
@@ -1431,39 +1332,9 @@ Continuing with completion, but this should be fixed for future subtasks.`;
1431
1332
  }
1432
1333
  }
1433
1334
 
1434
- // Legacy UBS-only path for backward compatibility (when no files_touched)
1435
- let ubsResult: UbsScanResult | null = null;
1436
- if (
1437
- !args.skip_verification &&
1438
- !verificationResult &&
1439
- args.files_touched?.length &&
1440
- !args.skip_ubs_scan
1441
- ) {
1442
- ubsResult = await runUbsScan(args.files_touched);
1443
-
1444
- // Block completion if critical bugs found
1445
- if (ubsResult && ubsResult.summary.critical > 0) {
1446
- return JSON.stringify(
1447
- {
1448
- success: false,
1449
- error: `UBS found ${ubsResult.summary.critical} critical bug(s) that must be fixed before completing`,
1450
- ubs_scan: {
1451
- critical_count: ubsResult.summary.critical,
1452
- bugs: ubsResult.bugs.filter((b) => b.severity === "critical"),
1453
- },
1454
- hint: `Fix these critical bugs: ${ubsResult.bugs
1455
- .filter((b) => b.severity === "critical")
1456
- .map((b) => `${b.file}:${b.line} - ${b.message}`)
1457
- .slice(0, 3)
1458
- .join(
1459
- "; ",
1460
- )}. Try: Run 'ubs scan ${args.files_touched?.join(" ") || "."} --json' for full report, fix reported issues, or use skip_ubs_scan=true to bypass (not recommended).`,
1461
- },
1462
- null,
1463
- 2,
1464
- );
1465
- }
1466
- }
1335
+ // NOTE: Legacy UBS-only path removed in v0.31
1336
+ // UBS scan was slowing down completion without proportional value.
1337
+ // Run UBS manually if needed: ubs scan <files>
1467
1338
 
1468
1339
  // Contract Validation - check files_touched against WorkerHandoff contract
1469
1340
  let contractValidation: { valid: boolean; violations: string[] } | null = null;
@@ -1570,6 +1441,47 @@ This will be recorded as a negative learning signal.`;
1570
1441
  );
1571
1442
  }
1572
1443
 
1444
+ // Resolve DurableDeferred for cross-agent task completion signaling
1445
+ // This allows coordinator to await worker completion without polling
1446
+ let deferredResolved = false;
1447
+ let deferredError: string | undefined;
1448
+ try {
1449
+ const swarmMail = await getSwarmMailLibSQL(args.project_key);
1450
+ const db = await swarmMail.getDatabase();
1451
+
1452
+ // Resolve deferred keyed by bead_id
1453
+ // Coordinator should have created this deferred before spawning worker
1454
+ const deferredUrl = `deferred:${args.bead_id}`;
1455
+
1456
+ // Check if deferred exists before resolving
1457
+ const checkResult = await db.query<{ url: string; resolved: number }>(
1458
+ `SELECT url, resolved FROM deferred WHERE url = ? AND resolved = 0`,
1459
+ [deferredUrl],
1460
+ );
1461
+
1462
+ if (checkResult.rows.length > 0) {
1463
+ // Resolve with completion payload
1464
+ await db.query(
1465
+ `UPDATE deferred SET resolved = 1, value = ? WHERE url = ? AND resolved = 0`,
1466
+ [JSON.stringify({ completed: true, summary: args.summary }), deferredUrl],
1467
+ );
1468
+
1469
+ deferredResolved = true;
1470
+ } else {
1471
+ // Deferred doesn't exist - worker was likely not spawned via swarm pattern
1472
+ // This is non-fatal - just log for debugging
1473
+ console.info(
1474
+ `[swarm_complete] No deferred found for ${args.bead_id} - task may not be part of active swarm`,
1475
+ );
1476
+ }
1477
+ } catch (error) {
1478
+ // Non-fatal - deferred resolution is optional for backward compatibility
1479
+ deferredError = error instanceof Error ? error.message : String(error);
1480
+ console.warn(
1481
+ `[swarm_complete] Failed to resolve deferred (non-fatal): ${deferredError}`,
1482
+ );
1483
+ }
1484
+
1573
1485
  // Sync cell to .hive/issues.jsonl (auto-sync on complete)
1574
1486
  // This ensures the worker's completed work persists before process exits
1575
1487
  let syncSuccess = false;
@@ -1737,6 +1649,8 @@ This will be recorded as a negative learning signal.`;
1737
1649
  sync_error: syncError,
1738
1650
  message_sent: messageSent,
1739
1651
  message_error: messageError,
1652
+ deferred_resolved: deferredResolved,
1653
+ deferred_error: deferredError,
1740
1654
  agent_registration: {
1741
1655
  verified: agentRegistered,
1742
1656
  warning: registrationWarning || undefined,
@@ -1755,21 +1669,6 @@ This will be recorded as a negative learning signal.`;
1755
1669
  : args.skip_verification
1756
1670
  ? { skipped: true, reason: "skip_verification=true" }
1757
1671
  : { skipped: true, reason: "no files_touched provided" },
1758
- ubs_scan: ubsResult
1759
- ? {
1760
- ran: true,
1761
- bugs_found: ubsResult.summary.total,
1762
- summary: ubsResult.summary,
1763
- warnings: ubsResult.bugs.filter((b) => b.severity !== "critical"),
1764
- }
1765
- : verificationResult
1766
- ? { ran: true, included_in_verification_gate: true }
1767
- : {
1768
- ran: false,
1769
- reason: args.skip_ubs_scan
1770
- ? "skipped"
1771
- : "no files or ubs unavailable",
1772
- },
1773
1672
  learning_prompt: `## Reflection
1774
1673
 
1775
1674
  Did you learn anything reusable during this subtask? Consider:
@@ -1820,9 +1719,7 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1820
1719
  // Determine which step failed
1821
1720
  let failedStep = "unknown";
1822
1721
  if (errorMessage.includes("verification")) {
1823
- failedStep = "Verification Gate (UBS/typecheck/tests)";
1824
- } else if (errorMessage.includes("UBS") || errorMessage.includes("ubs")) {
1825
- failedStep = "UBS scan";
1722
+ failedStep = "Verification Gate (typecheck/tests)";
1826
1723
  } else if (errorMessage.includes("evaluation")) {
1827
1724
  failedStep = "Self-evaluation parsing";
1828
1725
  } else if (
@@ -1863,10 +1760,9 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1863
1760
  errorStack
1864
1761
  ? `### Stack Trace\n\`\`\`\n${errorStack.slice(0, 1000)}\n\`\`\`\n`
1865
1762
  : "",
1866
- `### Context`,
1763
+ `### Context`,
1867
1764
  `- **Summary**: ${args.summary}`,
1868
1765
  `- **Files touched**: ${args.files_touched?.length ? args.files_touched.join(", ") : "none"}`,
1869
- `- **Skip UBS**: ${args.skip_ubs_scan ?? false}`,
1870
1766
  `- **Skip verification**: ${args.skip_verification ?? false}`,
1871
1767
  "",
1872
1768
  `### Recovery Actions`,
@@ -1912,10 +1808,9 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1912
1808
  coordinator_notified: notificationSent,
1913
1809
  stack_trace: errorStack?.slice(0, 500),
1914
1810
  hint: "Check the error message above. Common issues: bead not found, session not initialized.",
1915
- context: {
1811
+ context: {
1916
1812
  summary: args.summary,
1917
1813
  files_touched: args.files_touched || [],
1918
- skip_ubs_scan: args.skip_ubs_scan ?? false,
1919
1814
  skip_verification: args.skip_verification ?? false,
1920
1815
  },
1921
1816
  recovery: {
@@ -1927,7 +1822,6 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1927
1822
  ],
1928
1823
  common_fixes: {
1929
1824
  "Verification Gate": "Use skip_verification=true to bypass (not recommended)",
1930
- "UBS scan": "Use skip_ubs_scan=true to bypass",
1931
1825
  "Cell close": "Check cell status with hive_query(), may need hive_update() first",
1932
1826
  "Self-evaluation": "Check evaluation JSON format matches EvaluationSchema",
1933
1827
  },
@@ -2554,8 +2448,9 @@ export const swarm_recover = tool({
2554
2448
  },
2555
2449
  async execute(args) {
2556
2450
  try {
2557
- const { getDatabase } = await import("swarm-mail");
2558
- const db = await getDatabase(args.project_key);
2451
+ const { getSwarmMailLibSQL } = await import("swarm-mail");
2452
+ const swarmMail = await getSwarmMailLibSQL(args.project_key);
2453
+ const db = await swarmMail.getDatabase();
2559
2454
 
2560
2455
  // Query most recent checkpoint for this epic
2561
2456
  const result = await db.query<{
@@ -174,3 +174,95 @@ describe("formatSubtaskPromptV2", () => {
174
174
  expect(result).toContain("semantic-memory_find");
175
175
  });
176
176
  });
177
+
178
+ describe("swarm_spawn_subtask tool", () => {
179
+ test("returns post_completion_instructions field in JSON response", async () => {
180
+ const { swarm_spawn_subtask } = await import("./swarm-prompts");
181
+
182
+ const result = await swarm_spawn_subtask.execute({
183
+ bead_id: "test-project-abc123-task1",
184
+ epic_id: "test-project-abc123-epic1",
185
+ subtask_title: "Implement feature X",
186
+ subtask_description: "Add feature X to the system",
187
+ files: ["src/feature.ts", "src/feature.test.ts"],
188
+ shared_context: "Epic context here",
189
+ project_path: "/Users/joel/Code/project",
190
+ });
191
+
192
+ const parsed = JSON.parse(result);
193
+ expect(parsed).toHaveProperty("post_completion_instructions");
194
+ expect(typeof parsed.post_completion_instructions).toBe("string");
195
+ });
196
+
197
+ test("post_completion_instructions contains mandatory review steps", async () => {
198
+ const { swarm_spawn_subtask } = await import("./swarm-prompts");
199
+
200
+ const result = await swarm_spawn_subtask.execute({
201
+ bead_id: "test-project-abc123-task1",
202
+ epic_id: "test-project-abc123-epic1",
203
+ subtask_title: "Implement feature X",
204
+ files: ["src/feature.ts"],
205
+ project_path: "/Users/joel/Code/project",
206
+ });
207
+
208
+ const parsed = JSON.parse(result);
209
+ const instructions = parsed.post_completion_instructions;
210
+
211
+ // Should contain all 5 steps
212
+ expect(instructions).toContain("Step 1: Check Swarm Mail");
213
+ expect(instructions).toContain("swarmmail_inbox()");
214
+ expect(instructions).toContain("Step 2: Review the Work");
215
+ expect(instructions).toContain("swarm_review");
216
+ expect(instructions).toContain("Step 3: Evaluate Against Criteria");
217
+ expect(instructions).toContain("Step 4: Send Feedback");
218
+ expect(instructions).toContain("swarm_review_feedback");
219
+ expect(instructions).toContain("Step 5: ONLY THEN Continue");
220
+ });
221
+
222
+ test("post_completion_instructions substitutes placeholders", async () => {
223
+ const { swarm_spawn_subtask } = await import("./swarm-prompts");
224
+
225
+ const result = await swarm_spawn_subtask.execute({
226
+ bead_id: "test-project-abc123-task1",
227
+ epic_id: "test-project-abc123-epic1",
228
+ subtask_title: "Implement feature X",
229
+ files: ["src/feature.ts", "src/feature.test.ts"],
230
+ project_path: "/Users/joel/Code/project",
231
+ });
232
+
233
+ const parsed = JSON.parse(result);
234
+ const instructions = parsed.post_completion_instructions;
235
+
236
+ // Placeholders should be replaced
237
+ expect(instructions).toContain("/Users/joel/Code/project");
238
+ expect(instructions).toContain("test-project-abc123-epic1");
239
+ expect(instructions).toContain("test-project-abc123-task1");
240
+ expect(instructions).toContain('"src/feature.ts"');
241
+ expect(instructions).toContain('"src/feature.test.ts"');
242
+
243
+ // Placeholders should NOT remain
244
+ expect(instructions).not.toContain("{project_key}");
245
+ expect(instructions).not.toContain("{epic_id}");
246
+ expect(instructions).not.toContain("{task_id}");
247
+ expect(instructions).not.toContain("{files_touched}");
248
+ });
249
+
250
+ test("post_completion_instructions emphasizes mandatory nature", async () => {
251
+ const { swarm_spawn_subtask } = await import("./swarm-prompts");
252
+
253
+ const result = await swarm_spawn_subtask.execute({
254
+ bead_id: "test-project-abc123-task1",
255
+ epic_id: "test-project-abc123-epic1",
256
+ subtask_title: "Implement feature X",
257
+ files: ["src/feature.ts"],
258
+ project_path: "/Users/joel/Code/project",
259
+ });
260
+
261
+ const parsed = JSON.parse(result);
262
+ const instructions = parsed.post_completion_instructions;
263
+
264
+ // Should have strong language
265
+ expect(instructions).toMatch(/⚠️|MANDATORY|NON-NEGOTIABLE|DO NOT skip/i);
266
+ expect(instructions).toContain("DO THIS IMMEDIATELY");
267
+ });
268
+ });
@@ -551,6 +551,65 @@ Other cell operations:
551
551
 
552
552
  Begin now.`;
553
553
 
554
+ /**
555
+ * Coordinator post-worker checklist - MANDATORY review loop
556
+ *
557
+ * This checklist is returned to coordinators after spawning a worker.
558
+ * It ensures coordinators REVIEW worker output before spawning the next worker.
559
+ */
560
+ export const COORDINATOR_POST_WORKER_CHECKLIST = `
561
+ ## ⚠️ MANDATORY: Post-Worker Review (DO THIS IMMEDIATELY)
562
+
563
+ **A worker just returned. Before doing ANYTHING else, complete this checklist:**
564
+
565
+ ### Step 1: Check Swarm Mail
566
+ \`\`\`
567
+ swarmmail_inbox()
568
+ swarmmail_read_message(message_id=N) // Read any messages from the worker
569
+ \`\`\`
570
+
571
+ ### Step 2: Review the Work
572
+ \`\`\`
573
+ swarm_review(
574
+ project_key="{project_key}",
575
+ epic_id="{epic_id}",
576
+ task_id="{task_id}",
577
+ files_touched=[{files_touched}]
578
+ )
579
+ \`\`\`
580
+
581
+ This generates a review prompt with:
582
+ - Epic context (what we're trying to achieve)
583
+ - Subtask requirements
584
+ - Git diff of changes
585
+ - Dependency status
586
+
587
+ ### Step 3: Evaluate Against Criteria
588
+ - Does the work fulfill the subtask requirements?
589
+ - Does it serve the overall epic goal?
590
+ - Does it enable downstream tasks?
591
+ - Type safety, no obvious bugs?
592
+
593
+ ### Step 4: Send Feedback
594
+ \`\`\`
595
+ swarm_review_feedback(
596
+ project_key="{project_key}",
597
+ task_id="{task_id}",
598
+ worker_id="{worker_id}",
599
+ status="approved", // or "needs_changes"
600
+ summary="<brief summary>",
601
+ issues="[]" // or "[{file, line, issue, suggestion}]"
602
+ )
603
+ \`\`\`
604
+
605
+ ### Step 5: ONLY THEN Continue
606
+ - If approved: Close the cell, spawn next worker
607
+ - If needs_changes: Worker gets feedback, retries (max 3 attempts)
608
+ - If 3 failures: Mark blocked, escalate to human
609
+
610
+ **⚠️ DO NOT spawn the next worker until review is complete.**
611
+ `;
612
+
554
613
  /**
555
614
  * Prompt for self-evaluation before completing a subtask.
556
615
  *
@@ -852,6 +911,15 @@ export const swarm_spawn_subtask = tool({
852
911
 
853
912
  const selectedModel = selectWorkerModel(subtask, config);
854
913
 
914
+ // Generate post-completion instructions for coordinator
915
+ const filesJoined = args.files.map(f => `"${f}"`).join(", ");
916
+ const postCompletionInstructions = COORDINATOR_POST_WORKER_CHECKLIST
917
+ .replace(/{project_key}/g, args.project_path || "$PWD")
918
+ .replace(/{epic_id}/g, args.epic_id)
919
+ .replace(/{task_id}/g, args.bead_id)
920
+ .replace(/{files_touched}/g, filesJoined)
921
+ .replace(/{worker_id}/g, "worker"); // Will be filled by actual worker name
922
+
855
923
  return JSON.stringify(
856
924
  {
857
925
  prompt,
@@ -861,6 +929,7 @@ export const swarm_spawn_subtask = tool({
861
929
  project_path: args.project_path,
862
930
  recovery_context: args.recovery_context,
863
931
  recommended_model: selectedModel,
932
+ post_completion_instructions: postCompletionInstructions,
864
933
  },
865
934
  null,
866
935
  2,