opencode-swarm-plugin 0.14.0 → 0.16.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/src/swarm.ts CHANGED
@@ -629,7 +629,7 @@ You MUST keep your bead updated as you work:
629
629
 
630
630
  **Never work silently.** Your bead status is how the swarm tracks progress.
631
631
 
632
- ## MANDATORY: Agent Mail Communication
632
+ ## MANDATORY: Swarm Mail Communication
633
633
 
634
634
  You MUST communicate with other agents:
635
635
 
@@ -638,9 +638,9 @@ You MUST communicate with other agents:
638
638
  3. **Announce blockers** immediately - don't spin trying to fix alone
639
639
  4. **Coordinate on shared concerns** - if you see something affecting other agents, say so
640
640
 
641
- Use Agent Mail for all communication:
641
+ Use Swarm Mail for all communication:
642
642
  \`\`\`
643
- agentmail_send(
643
+ swarmmail_send(
644
644
  to: ["coordinator" or specific agent],
645
645
  subject: "Brief subject",
646
646
  body: "Message content",
@@ -652,7 +652,7 @@ agentmail_send(
652
652
 
653
653
  1. **Start**: Your bead is already marked in_progress
654
654
  2. **Progress**: Use swarm_progress to report status updates
655
- 3. **Blocked**: Report immediately via Agent Mail - don't spin
655
+ 3. **Blocked**: Report immediately via Swarm Mail - don't spin
656
656
  4. **Complete**: Use swarm_complete when done - it handles:
657
657
  - Closing your bead with a summary
658
658
  - Releasing file reservations
@@ -674,15 +674,15 @@ Before writing code:
674
674
  1. **Read the files** you're assigned to understand current state
675
675
  2. **Plan your approach** - what changes, in what order?
676
676
  3. **Identify risks** - what could go wrong? What dependencies?
677
- 4. **Communicate your plan** via Agent Mail if non-trivial
677
+ 4. **Communicate your plan** via Swarm Mail if non-trivial
678
678
 
679
679
  Begin work on your subtask now.`;
680
680
 
681
681
  /**
682
- * Streamlined subtask prompt (V2) - still uses Agent Mail and beads
682
+ * Streamlined subtask prompt (V2) - uses Swarm Mail and beads
683
683
  *
684
684
  * This is a cleaner version of SUBTASK_PROMPT that's easier to parse.
685
- * Agents MUST use Agent Mail for communication and beads for tracking.
685
+ * Agents MUST use Swarm Mail for communication and beads for tracking.
686
686
  *
687
687
  * Supports {error_context} placeholder for retry prompts.
688
688
  */
@@ -1858,6 +1858,237 @@ interface UbsScanResult {
1858
1858
  };
1859
1859
  }
1860
1860
 
1861
+ // ============================================================================
1862
+ // Verification Gate
1863
+ // ============================================================================
1864
+
1865
+ /**
1866
+ * Verification Gate result - tracks each verification step
1867
+ *
1868
+ * Based on the Gate Function from superpowers:
1869
+ * 1. IDENTIFY: What command proves this claim?
1870
+ * 2. RUN: Execute the FULL command (fresh, complete)
1871
+ * 3. READ: Full output, check exit code, count failures
1872
+ * 4. VERIFY: Does output confirm the claim?
1873
+ * 5. ONLY THEN: Make the claim
1874
+ */
1875
+ interface VerificationStep {
1876
+ name: string;
1877
+ command: string;
1878
+ passed: boolean;
1879
+ exitCode: number;
1880
+ output?: string;
1881
+ error?: string;
1882
+ skipped?: boolean;
1883
+ skipReason?: string;
1884
+ }
1885
+
1886
+ interface VerificationGateResult {
1887
+ passed: boolean;
1888
+ steps: VerificationStep[];
1889
+ summary: string;
1890
+ blockers: string[];
1891
+ }
1892
+
1893
+ /**
1894
+ * Run typecheck verification
1895
+ *
1896
+ * Attempts to run TypeScript type checking on the project.
1897
+ * Falls back gracefully if tsc is not available.
1898
+ */
1899
+ async function runTypecheckVerification(): Promise<VerificationStep> {
1900
+ const step: VerificationStep = {
1901
+ name: "typecheck",
1902
+ command: "tsc --noEmit",
1903
+ passed: false,
1904
+ exitCode: -1,
1905
+ };
1906
+
1907
+ try {
1908
+ // Check if tsconfig.json exists in current directory
1909
+ const tsconfigExists = await Bun.file("tsconfig.json").exists();
1910
+ if (!tsconfigExists) {
1911
+ step.skipped = true;
1912
+ step.skipReason = "No tsconfig.json found";
1913
+ step.passed = true; // Don't block if no TypeScript
1914
+ return step;
1915
+ }
1916
+
1917
+ const result = await Bun.$`tsc --noEmit`.quiet().nothrow();
1918
+ step.exitCode = result.exitCode;
1919
+ step.passed = result.exitCode === 0;
1920
+
1921
+ if (!step.passed) {
1922
+ step.error = result.stderr.toString().slice(0, 1000); // Truncate for context
1923
+ step.output = result.stdout.toString().slice(0, 1000);
1924
+ }
1925
+ } catch (error) {
1926
+ step.skipped = true;
1927
+ step.skipReason = `tsc not available: ${error instanceof Error ? error.message : String(error)}`;
1928
+ step.passed = true; // Don't block if tsc unavailable
1929
+ }
1930
+
1931
+ return step;
1932
+ }
1933
+
1934
+ /**
1935
+ * Run test verification for specific files
1936
+ *
1937
+ * Attempts to find and run tests related to the touched files.
1938
+ * Uses common test patterns (*.test.ts, *.spec.ts, __tests__/).
1939
+ */
1940
+ async function runTestVerification(
1941
+ filesTouched: string[],
1942
+ ): Promise<VerificationStep> {
1943
+ const step: VerificationStep = {
1944
+ name: "tests",
1945
+ command: "bun test <related-files>",
1946
+ passed: false,
1947
+ exitCode: -1,
1948
+ };
1949
+
1950
+ if (filesTouched.length === 0) {
1951
+ step.skipped = true;
1952
+ step.skipReason = "No files touched";
1953
+ step.passed = true;
1954
+ return step;
1955
+ }
1956
+
1957
+ // Find test files related to touched files
1958
+ const testPatterns: string[] = [];
1959
+ for (const file of filesTouched) {
1960
+ // Skip if already a test file
1961
+ if (file.includes(".test.") || file.includes(".spec.")) {
1962
+ testPatterns.push(file);
1963
+ continue;
1964
+ }
1965
+
1966
+ // Look for corresponding test file
1967
+ const baseName = file.replace(/\.(ts|tsx|js|jsx)$/, "");
1968
+ testPatterns.push(`${baseName}.test.ts`);
1969
+ testPatterns.push(`${baseName}.test.tsx`);
1970
+ testPatterns.push(`${baseName}.spec.ts`);
1971
+ }
1972
+
1973
+ // Check if any test files exist
1974
+ const existingTests: string[] = [];
1975
+ for (const pattern of testPatterns) {
1976
+ try {
1977
+ const exists = await Bun.file(pattern).exists();
1978
+ if (exists) {
1979
+ existingTests.push(pattern);
1980
+ }
1981
+ } catch {
1982
+ // File doesn't exist, skip
1983
+ }
1984
+ }
1985
+
1986
+ if (existingTests.length === 0) {
1987
+ step.skipped = true;
1988
+ step.skipReason = "No related test files found";
1989
+ step.passed = true;
1990
+ return step;
1991
+ }
1992
+
1993
+ try {
1994
+ step.command = `bun test ${existingTests.join(" ")}`;
1995
+ const result = await Bun.$`bun test ${existingTests}`.quiet().nothrow();
1996
+ step.exitCode = result.exitCode;
1997
+ step.passed = result.exitCode === 0;
1998
+
1999
+ if (!step.passed) {
2000
+ step.error = result.stderr.toString().slice(0, 1000);
2001
+ step.output = result.stdout.toString().slice(0, 1000);
2002
+ }
2003
+ } catch (error) {
2004
+ step.skipped = true;
2005
+ step.skipReason = `Test runner failed: ${error instanceof Error ? error.message : String(error)}`;
2006
+ step.passed = true; // Don't block if test runner unavailable
2007
+ }
2008
+
2009
+ return step;
2010
+ }
2011
+
2012
+ /**
2013
+ * Run the full Verification Gate
2014
+ *
2015
+ * Implements the Gate Function (IDENTIFY → RUN → READ → VERIFY → CLAIM):
2016
+ * 1. UBS scan (already exists)
2017
+ * 2. Typecheck
2018
+ * 3. Tests for touched files
2019
+ *
2020
+ * All steps must pass (or be skipped with valid reason) to proceed.
2021
+ */
2022
+ async function runVerificationGate(
2023
+ filesTouched: string[],
2024
+ skipUbs: boolean = false,
2025
+ ): Promise<VerificationGateResult> {
2026
+ const steps: VerificationStep[] = [];
2027
+ const blockers: string[] = [];
2028
+
2029
+ // Step 1: UBS scan
2030
+ if (!skipUbs && filesTouched.length > 0) {
2031
+ const ubsResult = await runUbsScan(filesTouched);
2032
+ if (ubsResult) {
2033
+ const ubsStep: VerificationStep = {
2034
+ name: "ubs_scan",
2035
+ command: `ubs scan ${filesTouched.join(" ")}`,
2036
+ passed: ubsResult.summary.critical === 0,
2037
+ exitCode: ubsResult.exitCode,
2038
+ };
2039
+
2040
+ if (!ubsStep.passed) {
2041
+ ubsStep.error = `Found ${ubsResult.summary.critical} critical bugs`;
2042
+ blockers.push(`UBS: ${ubsResult.summary.critical} critical bugs found`);
2043
+ }
2044
+
2045
+ steps.push(ubsStep);
2046
+ } else {
2047
+ steps.push({
2048
+ name: "ubs_scan",
2049
+ command: "ubs scan",
2050
+ passed: true,
2051
+ exitCode: 0,
2052
+ skipped: true,
2053
+ skipReason: "UBS not available",
2054
+ });
2055
+ }
2056
+ }
2057
+
2058
+ // Step 2: Typecheck
2059
+ const typecheckStep = await runTypecheckVerification();
2060
+ steps.push(typecheckStep);
2061
+ if (!typecheckStep.passed && !typecheckStep.skipped) {
2062
+ blockers.push(
2063
+ `Typecheck: ${typecheckStep.error?.slice(0, 100) || "failed"}`,
2064
+ );
2065
+ }
2066
+
2067
+ // Step 3: Tests
2068
+ const testStep = await runTestVerification(filesTouched);
2069
+ steps.push(testStep);
2070
+ if (!testStep.passed && !testStep.skipped) {
2071
+ blockers.push(`Tests: ${testStep.error?.slice(0, 100) || "failed"}`);
2072
+ }
2073
+
2074
+ // Build summary
2075
+ const passedCount = steps.filter((s) => s.passed).length;
2076
+ const skippedCount = steps.filter((s) => s.skipped).length;
2077
+ const failedCount = steps.filter((s) => !s.passed && !s.skipped).length;
2078
+
2079
+ const summary =
2080
+ failedCount === 0
2081
+ ? `Verification passed: ${passedCount} checks passed, ${skippedCount} skipped`
2082
+ : `Verification FAILED: ${failedCount} checks failed, ${passedCount} passed, ${skippedCount} skipped`;
2083
+
2084
+ return {
2085
+ passed: failedCount === 0,
2086
+ steps,
2087
+ summary,
2088
+ blockers,
2089
+ };
2090
+ }
2091
+
1861
2092
  /**
1862
2093
  * Run UBS scan on files before completion
1863
2094
  *
@@ -2025,12 +2256,18 @@ export const swarm_broadcast = tool({
2025
2256
  /**
2026
2257
  * Mark a subtask as complete
2027
2258
  *
2259
+ * Implements the Verification Gate (from superpowers):
2260
+ * 1. IDENTIFY: What commands prove this claim?
2261
+ * 2. RUN: Execute verification (UBS, typecheck, tests)
2262
+ * 3. READ: Check exit codes and output
2263
+ * 4. VERIFY: All checks must pass
2264
+ * 5. ONLY THEN: Close the bead
2265
+ *
2028
2266
  * Closes bead, releases reservations, notifies coordinator.
2029
- * Optionally runs UBS scan on modified files before completion.
2030
2267
  */
2031
2268
  export const swarm_complete = tool({
2032
2269
  description:
2033
- "Mark subtask complete, release reservations, notify coordinator. Runs UBS bug scan if files_touched provided.",
2270
+ "Mark subtask complete with Verification Gate. Runs UBS scan, typecheck, and tests before allowing completion.",
2034
2271
  args: {
2035
2272
  project_key: tool.schema.string().describe("Project path"),
2036
2273
  agent_name: tool.schema.string().describe("Your Agent Mail name"),
@@ -2043,18 +2280,62 @@ export const swarm_complete = tool({
2043
2280
  files_touched: tool.schema
2044
2281
  .array(tool.schema.string())
2045
2282
  .optional()
2046
- .describe("Files modified - will be scanned by UBS for bugs"),
2283
+ .describe("Files modified - will be verified (UBS, typecheck, tests)"),
2047
2284
  skip_ubs_scan: tool.schema
2048
2285
  .boolean()
2049
2286
  .optional()
2050
2287
  .describe("Skip UBS bug scan (default: false)"),
2288
+ skip_verification: tool.schema
2289
+ .boolean()
2290
+ .optional()
2291
+ .describe(
2292
+ "Skip ALL verification (UBS, typecheck, tests). Use sparingly! (default: false)",
2293
+ ),
2051
2294
  },
2052
2295
  async execute(args) {
2053
- // Run UBS scan on modified files if provided
2296
+ // Run Verification Gate unless explicitly skipped
2297
+ let verificationResult: VerificationGateResult | null = null;
2298
+
2299
+ if (!args.skip_verification && args.files_touched?.length) {
2300
+ verificationResult = await runVerificationGate(
2301
+ args.files_touched,
2302
+ args.skip_ubs_scan ?? false,
2303
+ );
2304
+
2305
+ // Block completion if verification failed
2306
+ if (!verificationResult.passed) {
2307
+ return JSON.stringify(
2308
+ {
2309
+ success: false,
2310
+ error: "Verification Gate FAILED - fix issues before completing",
2311
+ verification: {
2312
+ passed: false,
2313
+ summary: verificationResult.summary,
2314
+ blockers: verificationResult.blockers,
2315
+ steps: verificationResult.steps.map((s) => ({
2316
+ name: s.name,
2317
+ passed: s.passed,
2318
+ skipped: s.skipped,
2319
+ skipReason: s.skipReason,
2320
+ error: s.error?.slice(0, 200),
2321
+ })),
2322
+ },
2323
+ hint: "Fix the failing checks and try again. Use skip_verification=true only as last resort.",
2324
+ gate_function:
2325
+ "IDENTIFY → RUN → READ → VERIFY → CLAIM (you are at VERIFY, claim blocked)",
2326
+ },
2327
+ null,
2328
+ 2,
2329
+ );
2330
+ }
2331
+ }
2332
+
2333
+ // Legacy UBS-only path for backward compatibility (when no files_touched)
2054
2334
  let ubsResult: UbsScanResult | null = null;
2055
2335
  if (
2056
- args.files_touched &&
2057
- args.files_touched.length > 0 &&
2336
+ !args.skip_verification &&
2337
+ !verificationResult &&
2338
+ args.files_touched?.length &&
2058
2339
  !args.skip_ubs_scan
2059
2340
  ) {
2060
2341
  ubsResult = await runUbsScan(args.files_touched);
@@ -2176,6 +2457,20 @@ export const swarm_complete = tool({
2176
2457
  closed: true,
2177
2458
  reservations_released: true,
2178
2459
  message_sent: true,
2460
+ verification_gate: verificationResult
2461
+ ? {
2462
+ passed: true,
2463
+ summary: verificationResult.summary,
2464
+ steps: verificationResult.steps.map((s) => ({
2465
+ name: s.name,
2466
+ passed: s.passed,
2467
+ skipped: s.skipped,
2468
+ skipReason: s.skipReason,
2469
+ })),
2470
+ }
2471
+ : args.skip_verification
2472
+ ? { skipped: true, reason: "skip_verification=true" }
2473
+ : { skipped: true, reason: "no files_touched provided" },
2179
2474
  ubs_scan: ubsResult
2180
2475
  ? {
2181
2476
  ran: true,
@@ -2183,12 +2478,14 @@ export const swarm_complete = tool({
2183
2478
  summary: ubsResult.summary,
2184
2479
  warnings: ubsResult.bugs.filter((b) => b.severity !== "critical"),
2185
2480
  }
2186
- : {
2187
- ran: false,
2188
- reason: args.skip_ubs_scan
2189
- ? "skipped"
2190
- : "no files or ubs unavailable",
2191
- },
2481
+ : verificationResult
2482
+ ? { ran: true, included_in_verification_gate: true }
2483
+ : {
2484
+ ran: false,
2485
+ reason: args.skip_ubs_scan
2486
+ ? "skipped"
2487
+ : "no files or ubs unavailable",
2488
+ },
2192
2489
  learning_prompt: `## Reflection
2193
2490
 
2194
2491
  Did you learn anything reusable during this subtask? Consider:
@@ -3157,3 +3454,183 @@ export const swarmTools = {
3157
3454
  swarm_get_error_context: swarm_get_error_context,
3158
3455
  swarm_resolve_error: swarm_resolve_error,
3159
3456
  };
3457
+
3458
+ // ============================================================================
3459
+ // 3-Strike Detection
3460
+ // ============================================================================
3461
+
3462
+ /**
3463
+ * Global strike storage for tracking consecutive fix failures
3464
+ */
3465
+ import {
3466
+ InMemoryStrikeStorage,
3467
+ addStrike,
3468
+ getStrikes,
3469
+ isStrikedOut,
3470
+ getArchitecturePrompt,
3471
+ clearStrikes,
3472
+ type StrikeStorage,
3473
+ } from "./learning";
3474
+
3475
+ const globalStrikeStorage: StrikeStorage = new InMemoryStrikeStorage();
3476
+
3477
+ /**
3478
+ * Check if a bead has struck out (3 consecutive failures)
3479
+ *
3480
+ * The 3-Strike Rule:
3481
+ * IF 3+ fixes have failed:
3482
+ * STOP → Question the architecture
3483
+ * DON'T attempt Fix #4
3484
+ * Discuss with human partner
3485
+ *
3486
+ * This is NOT a failed hypothesis.
3487
+ * This is a WRONG ARCHITECTURE.
3488
+ *
3489
+ * Use this tool to:
3490
+ * - Check strike count before attempting a fix
3491
+ * - Get architecture review prompt if struck out
3492
+ * - Record a strike when a fix fails
3493
+ * - Clear strikes when a fix succeeds
3494
+ */
3495
+ export const swarm_check_strikes = tool({
3496
+ description:
3497
+ "Check 3-strike status for a bead. Records failures, detects architectural problems, generates architecture review prompts.",
3498
+ args: {
3499
+ bead_id: tool.schema.string().describe("Bead ID to check"),
3500
+ action: tool.schema
3501
+ .enum(["check", "add_strike", "clear", "get_prompt"])
3502
+ .describe(
3503
+ "Action: check count, add strike, clear strikes, or get prompt",
3504
+ ),
3505
+ attempt: tool.schema
3506
+ .string()
3507
+ .optional()
3508
+ .describe("Description of fix attempt (required for add_strike)"),
3509
+ reason: tool.schema
3510
+ .string()
3511
+ .optional()
3512
+ .describe("Why the fix failed (required for add_strike)"),
3513
+ },
3514
+ async execute(args) {
3515
+ switch (args.action) {
3516
+ case "check": {
3517
+ const count = await getStrikes(args.bead_id, globalStrikeStorage);
3518
+ const strikedOut = await isStrikedOut(
3519
+ args.bead_id,
3520
+ globalStrikeStorage,
3521
+ );
3522
+
3523
+ return JSON.stringify(
3524
+ {
3525
+ bead_id: args.bead_id,
3526
+ strike_count: count,
3527
+ is_striked_out: strikedOut,
3528
+ message: strikedOut
3529
+ ? "⚠️ STRUCK OUT: 3 strikes reached. Use get_prompt action for architecture review."
3530
+ : count === 0
3531
+ ? "No strikes. Clear to proceed."
3532
+ : `${count} strike${count > 1 ? "s" : ""}. ${3 - count} remaining before architecture review required.`,
3533
+ next_action: strikedOut
3534
+ ? "Call with action=get_prompt to get architecture review questions"
3535
+ : "Continue with fix attempt",
3536
+ },
3537
+ null,
3538
+ 2,
3539
+ );
3540
+ }
3541
+
3542
+ case "add_strike": {
3543
+ if (!args.attempt || !args.reason) {
3544
+ return JSON.stringify(
3545
+ {
3546
+ error: "add_strike requires 'attempt' and 'reason' parameters",
3547
+ },
3548
+ null,
3549
+ 2,
3550
+ );
3551
+ }
3552
+
3553
+ const record = await addStrike(
3554
+ args.bead_id,
3555
+ args.attempt,
3556
+ args.reason,
3557
+ globalStrikeStorage,
3558
+ );
3559
+
3560
+ const strikedOut = record.strike_count >= 3;
3561
+
3562
+ return JSON.stringify(
3563
+ {
3564
+ bead_id: args.bead_id,
3565
+ strike_count: record.strike_count,
3566
+ is_striked_out: strikedOut,
3567
+ failures: record.failures,
3568
+ message: strikedOut
3569
+ ? "⚠️ STRUCK OUT: 3 strikes reached. STOP and question the architecture."
3570
+ : `Strike ${record.strike_count} recorded. ${3 - record.strike_count} remaining.`,
3571
+ warning: strikedOut
3572
+ ? "DO NOT attempt Fix #4. Call with action=get_prompt for architecture review."
3573
+ : undefined,
3574
+ },
3575
+ null,
3576
+ 2,
3577
+ );
3578
+ }
3579
+
3580
+ case "clear": {
3581
+ await clearStrikes(args.bead_id, globalStrikeStorage);
3582
+
3583
+ return JSON.stringify(
3584
+ {
3585
+ bead_id: args.bead_id,
3586
+ strike_count: 0,
3587
+ is_striked_out: false,
3588
+ message: "Strikes cleared. Fresh start.",
3589
+ },
3590
+ null,
3591
+ 2,
3592
+ );
3593
+ }
3594
+
3595
+ case "get_prompt": {
3596
+ const prompt = await getArchitecturePrompt(
3597
+ args.bead_id,
3598
+ globalStrikeStorage,
3599
+ );
3600
+
3601
+ if (!prompt) {
3602
+ return JSON.stringify(
3603
+ {
3604
+ bead_id: args.bead_id,
3605
+ has_prompt: false,
3606
+ message: "No architecture prompt (not struck out yet)",
3607
+ },
3608
+ null,
3609
+ 2,
3610
+ );
3611
+ }
3612
+
3613
+ return JSON.stringify(
3614
+ {
3615
+ bead_id: args.bead_id,
3616
+ has_prompt: true,
3617
+ architecture_review_prompt: prompt,
3618
+ message:
3619
+ "Architecture review required. Present this prompt to the human partner.",
3620
+ },
3621
+ null,
3622
+ 2,
3623
+ );
3624
+ }
3625
+
3626
+ default:
3627
+ return JSON.stringify(
3628
+ {
3629
+ error: `Unknown action: ${args.action}`,
3630
+ },
3631
+ null,
3632
+ 2,
3633
+ );
3634
+ }
3635
+ },
3636
+ });