opencode-swarm-plugin 0.14.0 → 0.15.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/.beads/analysis/skill-architecture-meta-skills.md +1562 -0
- package/.beads/issues.jsonl +73 -0
- package/README.md +20 -18
- package/VERIFICATION_QUALITY_PATTERNS.md +565 -0
- package/bin/swarm.ts +5 -5
- package/dist/index.js +425 -12
- package/dist/plugin.js +426 -12
- package/docs/analysis/subagent-coordination-patterns.md +900 -0
- package/docs/analysis-socratic-planner-pattern.md +504 -0
- package/examples/commands/swarm.md +69 -7
- package/global-skills/swarm-coordination/SKILL.md +70 -20
- package/global-skills/swarm-coordination/references/coordinator-patterns.md +1 -1
- package/package.json +1 -1
- package/src/learning.integration.test.ts +310 -0
- package/src/learning.ts +198 -0
- package/src/skills.test.ts +194 -0
- package/src/skills.ts +184 -15
- package/src/swarm.integration.test.ts +4 -4
- package/src/swarm.ts +496 -19
- package/workflow-integration-analysis.md +876 -0
|
@@ -95,30 +95,76 @@ skills_list();
|
|
|
95
95
|
|
|
96
96
|
Synthesize findings into `shared_context` for workers.
|
|
97
97
|
|
|
98
|
-
### Phase 3: Decomposition
|
|
98
|
+
### Phase 3: Decomposition (DELEGATE TO SUBAGENT)
|
|
99
|
+
|
|
100
|
+
> **⚠️ CRITICAL: Context Preservation Pattern**
|
|
101
|
+
>
|
|
102
|
+
> **NEVER do planning inline in the coordinator thread.** Decomposition work (file reading, CASS searching, reasoning about task breakdown) consumes massive amounts of context and will exhaust your token budget on long swarms.
|
|
103
|
+
>
|
|
104
|
+
> **ALWAYS delegate planning to a `swarm/planner` subagent** and receive only the structured BeadTree JSON result back.
|
|
105
|
+
|
|
106
|
+
**❌ Anti-Pattern (Context-Heavy):**
|
|
99
107
|
|
|
100
108
|
```typescript
|
|
101
|
-
//
|
|
102
|
-
const plan = await swarm_plan_prompt({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
// DON'T DO THIS - pollutes main thread context
|
|
110
|
+
const plan = await swarm_plan_prompt({ task, ... });
|
|
111
|
+
// ... agent reasons about decomposition inline ...
|
|
112
|
+
// ... context fills with file contents, analysis ...
|
|
113
|
+
const validation = await swarm_validate_decomposition({ ... });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**✅ Correct Pattern (Context-Lean):**
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// 1. Create planning bead with full context
|
|
120
|
+
await beads_create({
|
|
121
|
+
title: `Plan: ${taskTitle}`,
|
|
122
|
+
type: "task",
|
|
123
|
+
description: `Decompose into subtasks. Context: ${synthesizedContext}`,
|
|
107
124
|
});
|
|
108
125
|
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
|
|
126
|
+
// 2. Delegate to swarm/planner subagent
|
|
127
|
+
const planningResult = await Task({
|
|
128
|
+
subagent_type: "swarm/planner",
|
|
129
|
+
description: `Decompose task: ${taskTitle}`,
|
|
130
|
+
prompt: `
|
|
131
|
+
You are a swarm planner. Generate a BeadTree for this task.
|
|
132
|
+
|
|
133
|
+
## Task
|
|
134
|
+
${taskDescription}
|
|
135
|
+
|
|
136
|
+
## Synthesized Context
|
|
137
|
+
${synthesizedContext}
|
|
138
|
+
|
|
139
|
+
## Instructions
|
|
140
|
+
1. Use swarm_plan_prompt(task="...", max_subtasks=5, query_cass=true)
|
|
141
|
+
2. Reason about decomposition strategy
|
|
142
|
+
3. Generate BeadTree JSON
|
|
143
|
+
4. Validate with swarm_validate_decomposition
|
|
144
|
+
5. Return ONLY the validated BeadTree JSON (no analysis, no file contents)
|
|
145
|
+
|
|
146
|
+
Output format: Valid BeadTree JSON only.
|
|
147
|
+
`,
|
|
112
148
|
});
|
|
113
149
|
|
|
114
|
-
//
|
|
150
|
+
// 3. Parse result (subagent already validated)
|
|
151
|
+
const beadTree = JSON.parse(planningResult);
|
|
152
|
+
|
|
153
|
+
// 4. Create epic + subtasks atomically
|
|
115
154
|
await beads_create_epic({
|
|
116
|
-
epic_title:
|
|
117
|
-
epic_description:
|
|
118
|
-
subtasks:
|
|
155
|
+
epic_title: beadTree.epic.title,
|
|
156
|
+
epic_description: beadTree.epic.description,
|
|
157
|
+
subtasks: beadTree.subtasks,
|
|
119
158
|
});
|
|
120
159
|
```
|
|
121
160
|
|
|
161
|
+
**Why This Matters:**
|
|
162
|
+
|
|
163
|
+
- **Main thread context stays clean** - only receives final JSON, not reasoning
|
|
164
|
+
- **Subagent context is disposable** - gets garbage collected after planning
|
|
165
|
+
- **Scales to long swarms** - coordinator can manage 10+ workers without exhaustion
|
|
166
|
+
- **Faster coordination** - less context = faster responses when monitoring workers
|
|
167
|
+
|
|
122
168
|
### Phase 4: Reserve Files (via Swarm Mail)
|
|
123
169
|
|
|
124
170
|
```typescript
|
|
@@ -263,12 +309,16 @@ One blocker affects multiple subtasks.
|
|
|
263
309
|
|
|
264
310
|
## Anti-Patterns
|
|
265
311
|
|
|
266
|
-
| Anti-Pattern
|
|
267
|
-
|
|
|
268
|
-
| **Mega-Coordinator**
|
|
269
|
-
| **Silent Swarm**
|
|
270
|
-
| **Over-Decomposed**
|
|
271
|
-
| **Under-Specified**
|
|
312
|
+
| Anti-Pattern | Symptom | Fix |
|
|
313
|
+
| ------------------------ | ------------------------------------------ | ------------------------------------ |
|
|
314
|
+
| **Mega-Coordinator** | Coordinator editing files | Coordinator only orchestrates |
|
|
315
|
+
| **Silent Swarm** | No communication, late conflicts | Require updates, check inbox |
|
|
316
|
+
| **Over-Decomposed** | 10 subtasks for 20 lines | 2-5 subtasks max |
|
|
317
|
+
| **Under-Specified** | "Implement backend" | Clear goal, files, criteria |
|
|
318
|
+
| **Inline Planning** ⚠️ | Context pollution, exhaustion on long runs | Delegate planning to subagent |
|
|
319
|
+
| **Heavy File Reading** | Coordinator reading 10+ files | Subagent reads, returns summary only |
|
|
320
|
+
| **Deep CASS Drilling** | Multiple cass_search calls inline | Subagent searches, summarizes |
|
|
321
|
+
| **Manual Decomposition** | Hand-crafting subtasks without validation | Use swarm_plan_prompt + validation |
|
|
272
322
|
|
|
273
323
|
## Shared Context Template
|
|
274
324
|
|
|
@@ -49,7 +49,7 @@ For each subtask:
|
|
|
49
49
|
### 4. Progress Monitoring
|
|
50
50
|
|
|
51
51
|
- Check `beads_query(status="in_progress")` for active work
|
|
52
|
-
- Check `
|
|
52
|
+
- Check `swarmmail_inbox()` for worker messages
|
|
53
53
|
- Intervene on blockers (see Intervention Patterns below)
|
|
54
54
|
|
|
55
55
|
### 5. Completion & Aggregation
|
package/package.json
CHANGED
|
@@ -1427,3 +1427,313 @@ describe("Storage Module", () => {
|
|
|
1427
1427
|
});
|
|
1428
1428
|
});
|
|
1429
1429
|
});
|
|
1430
|
+
|
|
1431
|
+
// ============================================================================
|
|
1432
|
+
// 3-Strike Detection Tests
|
|
1433
|
+
// ============================================================================
|
|
1434
|
+
|
|
1435
|
+
import {
|
|
1436
|
+
InMemoryStrikeStorage,
|
|
1437
|
+
addStrike,
|
|
1438
|
+
getStrikes,
|
|
1439
|
+
isStrikedOut,
|
|
1440
|
+
getArchitecturePrompt,
|
|
1441
|
+
clearStrikes,
|
|
1442
|
+
type StrikeStorage,
|
|
1443
|
+
} from "./learning";
|
|
1444
|
+
|
|
1445
|
+
describe("3-Strike Detection", () => {
|
|
1446
|
+
let storage: StrikeStorage;
|
|
1447
|
+
|
|
1448
|
+
beforeEach(() => {
|
|
1449
|
+
storage = new InMemoryStrikeStorage();
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
describe("addStrike", () => {
|
|
1453
|
+
it("records first strike", async () => {
|
|
1454
|
+
const record = await addStrike(
|
|
1455
|
+
"test-bead-1",
|
|
1456
|
+
"Attempted null check fix",
|
|
1457
|
+
"Still getting undefined errors",
|
|
1458
|
+
storage,
|
|
1459
|
+
);
|
|
1460
|
+
|
|
1461
|
+
expect(record.bead_id).toBe("test-bead-1");
|
|
1462
|
+
expect(record.strike_count).toBe(1);
|
|
1463
|
+
expect(record.failures).toHaveLength(1);
|
|
1464
|
+
expect(record.failures[0].attempt).toBe("Attempted null check fix");
|
|
1465
|
+
expect(record.failures[0].reason).toBe("Still getting undefined errors");
|
|
1466
|
+
expect(record.first_strike_at).toBeDefined();
|
|
1467
|
+
expect(record.last_strike_at).toBeDefined();
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
it("increments strike count on subsequent strikes", async () => {
|
|
1471
|
+
await addStrike("test-bead-2", "Fix 1", "Failed 1", storage);
|
|
1472
|
+
const record2 = await addStrike(
|
|
1473
|
+
"test-bead-2",
|
|
1474
|
+
"Fix 2",
|
|
1475
|
+
"Failed 2",
|
|
1476
|
+
storage,
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
expect(record2.strike_count).toBe(2);
|
|
1480
|
+
expect(record2.failures).toHaveLength(2);
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
it("caps strike count at 3", async () => {
|
|
1484
|
+
await addStrike("test-bead-3", "Fix 1", "Failed 1", storage);
|
|
1485
|
+
await addStrike("test-bead-3", "Fix 2", "Failed 2", storage);
|
|
1486
|
+
await addStrike("test-bead-3", "Fix 3", "Failed 3", storage);
|
|
1487
|
+
const record4 = await addStrike(
|
|
1488
|
+
"test-bead-3",
|
|
1489
|
+
"Fix 4",
|
|
1490
|
+
"Failed 4",
|
|
1491
|
+
storage,
|
|
1492
|
+
);
|
|
1493
|
+
|
|
1494
|
+
expect(record4.strike_count).toBe(3);
|
|
1495
|
+
expect(record4.failures).toHaveLength(4); // Records all attempts
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
it("preserves first_strike_at timestamp", async () => {
|
|
1499
|
+
const record1 = await addStrike(
|
|
1500
|
+
"test-bead-4",
|
|
1501
|
+
"Fix 1",
|
|
1502
|
+
"Failed 1",
|
|
1503
|
+
storage,
|
|
1504
|
+
);
|
|
1505
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1506
|
+
const record2 = await addStrike(
|
|
1507
|
+
"test-bead-4",
|
|
1508
|
+
"Fix 2",
|
|
1509
|
+
"Failed 2",
|
|
1510
|
+
storage,
|
|
1511
|
+
);
|
|
1512
|
+
|
|
1513
|
+
expect(record2.first_strike_at).toBe(record1.first_strike_at);
|
|
1514
|
+
expect(record2.last_strike_at).not.toBe(record1.last_strike_at);
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
describe("getStrikes", () => {
|
|
1519
|
+
it("returns 0 for bead with no strikes", async () => {
|
|
1520
|
+
const count = await getStrikes("no-strikes-bead", storage);
|
|
1521
|
+
expect(count).toBe(0);
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
it("returns correct strike count", async () => {
|
|
1525
|
+
await addStrike("bead-with-strikes", "Fix 1", "Failed 1", storage);
|
|
1526
|
+
await addStrike("bead-with-strikes", "Fix 2", "Failed 2", storage);
|
|
1527
|
+
|
|
1528
|
+
const count = await getStrikes("bead-with-strikes", storage);
|
|
1529
|
+
expect(count).toBe(2);
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
describe("isStrikedOut", () => {
|
|
1534
|
+
it("returns false for bead with < 3 strikes", async () => {
|
|
1535
|
+
await addStrike("bead-safe", "Fix 1", "Failed 1", storage);
|
|
1536
|
+
await addStrike("bead-safe", "Fix 2", "Failed 2", storage);
|
|
1537
|
+
|
|
1538
|
+
const strikedOut = await isStrikedOut("bead-safe", storage);
|
|
1539
|
+
expect(strikedOut).toBe(false);
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
it("returns true for bead with 3 strikes", async () => {
|
|
1543
|
+
await addStrike("bead-danger", "Fix 1", "Failed 1", storage);
|
|
1544
|
+
await addStrike("bead-danger", "Fix 2", "Failed 2", storage);
|
|
1545
|
+
await addStrike("bead-danger", "Fix 3", "Failed 3", storage);
|
|
1546
|
+
|
|
1547
|
+
const strikedOut = await isStrikedOut("bead-danger", storage);
|
|
1548
|
+
expect(strikedOut).toBe(true);
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
it("returns false for bead with no strikes", async () => {
|
|
1552
|
+
const strikedOut = await isStrikedOut("no-record", storage);
|
|
1553
|
+
expect(strikedOut).toBe(false);
|
|
1554
|
+
});
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
describe("getArchitecturePrompt", () => {
|
|
1558
|
+
it("returns empty string for bead with < 3 strikes", async () => {
|
|
1559
|
+
await addStrike("bead-prompt-1", "Fix 1", "Failed 1", storage);
|
|
1560
|
+
|
|
1561
|
+
const prompt = await getArchitecturePrompt("bead-prompt-1", storage);
|
|
1562
|
+
expect(prompt).toBe("");
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
it("returns empty string for bead with no strikes", async () => {
|
|
1566
|
+
const prompt = await getArchitecturePrompt("no-strikes", storage);
|
|
1567
|
+
expect(prompt).toBe("");
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
it("generates architecture review prompt for struck out bead", async () => {
|
|
1571
|
+
await addStrike(
|
|
1572
|
+
"bead-prompt-2",
|
|
1573
|
+
"Added null checks",
|
|
1574
|
+
"Still crashes on undefined",
|
|
1575
|
+
storage,
|
|
1576
|
+
);
|
|
1577
|
+
await addStrike(
|
|
1578
|
+
"bead-prompt-2",
|
|
1579
|
+
"Used optional chaining",
|
|
1580
|
+
"Runtime error persists",
|
|
1581
|
+
storage,
|
|
1582
|
+
);
|
|
1583
|
+
await addStrike(
|
|
1584
|
+
"bead-prompt-2",
|
|
1585
|
+
"Wrapped in try-catch",
|
|
1586
|
+
"Error still happening",
|
|
1587
|
+
storage,
|
|
1588
|
+
);
|
|
1589
|
+
|
|
1590
|
+
const prompt = await getArchitecturePrompt("bead-prompt-2", storage);
|
|
1591
|
+
|
|
1592
|
+
expect(prompt).toContain("Architecture Review Required");
|
|
1593
|
+
expect(prompt).toContain("bead-prompt-2");
|
|
1594
|
+
expect(prompt).toContain("Added null checks");
|
|
1595
|
+
expect(prompt).toContain("Still crashes on undefined");
|
|
1596
|
+
expect(prompt).toContain("Used optional chaining");
|
|
1597
|
+
expect(prompt).toContain("Runtime error persists");
|
|
1598
|
+
expect(prompt).toContain("Wrapped in try-catch");
|
|
1599
|
+
expect(prompt).toContain("Error still happening");
|
|
1600
|
+
expect(prompt).toContain("architectural problem");
|
|
1601
|
+
expect(prompt).toContain("DO NOT attempt Fix #4");
|
|
1602
|
+
expect(prompt).toContain("Refactor architecture");
|
|
1603
|
+
expect(prompt).toContain("Continue with Fix #4");
|
|
1604
|
+
expect(prompt).toContain("Abandon this approach");
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
it("lists all failures in order", async () => {
|
|
1608
|
+
await addStrike(
|
|
1609
|
+
"bead-prompt-3",
|
|
1610
|
+
"First attempt",
|
|
1611
|
+
"First failure",
|
|
1612
|
+
storage,
|
|
1613
|
+
);
|
|
1614
|
+
await addStrike(
|
|
1615
|
+
"bead-prompt-3",
|
|
1616
|
+
"Second attempt",
|
|
1617
|
+
"Second failure",
|
|
1618
|
+
storage,
|
|
1619
|
+
);
|
|
1620
|
+
await addStrike(
|
|
1621
|
+
"bead-prompt-3",
|
|
1622
|
+
"Third attempt",
|
|
1623
|
+
"Third failure",
|
|
1624
|
+
storage,
|
|
1625
|
+
);
|
|
1626
|
+
|
|
1627
|
+
const prompt = await getArchitecturePrompt("bead-prompt-3", storage);
|
|
1628
|
+
|
|
1629
|
+
const lines = prompt.split("\n");
|
|
1630
|
+
const failureLine1 = lines.find((l) => l.includes("First attempt"));
|
|
1631
|
+
const failureLine2 = lines.find((l) => l.includes("Second attempt"));
|
|
1632
|
+
const failureLine3 = lines.find((l) => l.includes("Third attempt"));
|
|
1633
|
+
|
|
1634
|
+
expect(failureLine1).toBeDefined();
|
|
1635
|
+
expect(failureLine2).toBeDefined();
|
|
1636
|
+
expect(failureLine3).toBeDefined();
|
|
1637
|
+
|
|
1638
|
+
// Check ordering
|
|
1639
|
+
const idx1 = lines.indexOf(failureLine1!);
|
|
1640
|
+
const idx2 = lines.indexOf(failureLine2!);
|
|
1641
|
+
const idx3 = lines.indexOf(failureLine3!);
|
|
1642
|
+
|
|
1643
|
+
expect(idx1).toBeLessThan(idx2);
|
|
1644
|
+
expect(idx2).toBeLessThan(idx3);
|
|
1645
|
+
});
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
describe("clearStrikes", () => {
|
|
1649
|
+
it("clears strikes for a bead", async () => {
|
|
1650
|
+
await addStrike("bead-clear", "Fix 1", "Failed 1", storage);
|
|
1651
|
+
await addStrike("bead-clear", "Fix 2", "Failed 2", storage);
|
|
1652
|
+
|
|
1653
|
+
expect(await getStrikes("bead-clear", storage)).toBe(2);
|
|
1654
|
+
|
|
1655
|
+
await clearStrikes("bead-clear", storage);
|
|
1656
|
+
|
|
1657
|
+
expect(await getStrikes("bead-clear", storage)).toBe(0);
|
|
1658
|
+
expect(await isStrikedOut("bead-clear", storage)).toBe(false);
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
it("handles clearing non-existent bead gracefully", async () => {
|
|
1662
|
+
await expect(clearStrikes("no-bead", storage)).resolves.toBeUndefined();
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
describe("InMemoryStrikeStorage", () => {
|
|
1667
|
+
it("stores and retrieves strike records", async () => {
|
|
1668
|
+
const storage = new InMemoryStrikeStorage();
|
|
1669
|
+
const record = await addStrike("bead-1", "Fix", "Failed", storage);
|
|
1670
|
+
|
|
1671
|
+
const retrieved = await storage.get("bead-1");
|
|
1672
|
+
expect(retrieved).not.toBeNull();
|
|
1673
|
+
expect(retrieved!.bead_id).toBe("bead-1");
|
|
1674
|
+
expect(retrieved!.strike_count).toBe(1);
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
it("returns null for non-existent bead", async () => {
|
|
1678
|
+
const storage = new InMemoryStrikeStorage();
|
|
1679
|
+
const retrieved = await storage.get("non-existent");
|
|
1680
|
+
expect(retrieved).toBeNull();
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
it("lists all strike records", async () => {
|
|
1684
|
+
const storage = new InMemoryStrikeStorage();
|
|
1685
|
+
await addStrike("bead-1", "Fix", "Failed", storage);
|
|
1686
|
+
await addStrike("bead-2", "Fix", "Failed", storage);
|
|
1687
|
+
|
|
1688
|
+
const all = await storage.getAll();
|
|
1689
|
+
expect(all).toHaveLength(2);
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
it("clears specific bead strikes", async () => {
|
|
1693
|
+
const storage = new InMemoryStrikeStorage();
|
|
1694
|
+
await addStrike("bead-1", "Fix", "Failed", storage);
|
|
1695
|
+
await addStrike("bead-2", "Fix", "Failed", storage);
|
|
1696
|
+
|
|
1697
|
+
await storage.clear("bead-1");
|
|
1698
|
+
|
|
1699
|
+
expect(await storage.get("bead-1")).toBeNull();
|
|
1700
|
+
expect(await storage.get("bead-2")).not.toBeNull();
|
|
1701
|
+
});
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
describe("3-Strike Rule Integration", () => {
|
|
1705
|
+
it("follows complete workflow from no strikes to architecture review", async () => {
|
|
1706
|
+
const beadId = "integration-bead";
|
|
1707
|
+
|
|
1708
|
+
// Start: No strikes
|
|
1709
|
+
expect(await getStrikes(beadId, storage)).toBe(0);
|
|
1710
|
+
expect(await isStrikedOut(beadId, storage)).toBe(false);
|
|
1711
|
+
expect(await getArchitecturePrompt(beadId, storage)).toBe("");
|
|
1712
|
+
|
|
1713
|
+
// Strike 1
|
|
1714
|
+
await addStrike(beadId, "Tried approach A", "Didn't work", storage);
|
|
1715
|
+
expect(await getStrikes(beadId, storage)).toBe(1);
|
|
1716
|
+
expect(await isStrikedOut(beadId, storage)).toBe(false);
|
|
1717
|
+
|
|
1718
|
+
// Strike 2
|
|
1719
|
+
await addStrike(beadId, "Tried approach B", "Also failed", storage);
|
|
1720
|
+
expect(await getStrikes(beadId, storage)).toBe(2);
|
|
1721
|
+
expect(await isStrikedOut(beadId, storage)).toBe(false);
|
|
1722
|
+
|
|
1723
|
+
// Strike 3 - STRUCK OUT
|
|
1724
|
+
await addStrike(beadId, "Tried approach C", "Still broken", storage);
|
|
1725
|
+
expect(await getStrikes(beadId, storage)).toBe(3);
|
|
1726
|
+
expect(await isStrikedOut(beadId, storage)).toBe(true);
|
|
1727
|
+
|
|
1728
|
+
// Architecture prompt should now be available
|
|
1729
|
+
const prompt = await getArchitecturePrompt(beadId, storage);
|
|
1730
|
+
expect(prompt).not.toBe("");
|
|
1731
|
+
expect(prompt).toContain("Architecture Review Required");
|
|
1732
|
+
|
|
1733
|
+
// Clear strikes (e.g., after human intervention)
|
|
1734
|
+
await clearStrikes(beadId, storage);
|
|
1735
|
+
expect(await getStrikes(beadId, storage)).toBe(0);
|
|
1736
|
+
expect(await isStrikedOut(beadId, storage)).toBe(false);
|
|
1737
|
+
});
|
|
1738
|
+
});
|
|
1739
|
+
});
|
package/src/learning.ts
CHANGED
|
@@ -524,6 +524,203 @@ export class InMemoryFeedbackStorage implements FeedbackStorage {
|
|
|
524
524
|
}
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
// ============================================================================
|
|
528
|
+
// 3-Strike Detection
|
|
529
|
+
// ============================================================================
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Strike record for a bead
|
|
533
|
+
*
|
|
534
|
+
* Tracks consecutive fix failures to detect architectural problems.
|
|
535
|
+
* After 3 strikes, the system should STOP and question the architecture
|
|
536
|
+
* rather than attempting Fix #4.
|
|
537
|
+
*/
|
|
538
|
+
export const StrikeRecordSchema = z.object({
|
|
539
|
+
/** The bead ID */
|
|
540
|
+
bead_id: z.string(),
|
|
541
|
+
/** Number of consecutive failures */
|
|
542
|
+
strike_count: z.number().int().min(0).max(3),
|
|
543
|
+
/** Failure descriptions for each strike */
|
|
544
|
+
failures: z.array(
|
|
545
|
+
z.object({
|
|
546
|
+
/** What fix was attempted */
|
|
547
|
+
attempt: z.string(),
|
|
548
|
+
/** Why it failed */
|
|
549
|
+
reason: z.string(),
|
|
550
|
+
/** When it failed */
|
|
551
|
+
timestamp: z.string(), // ISO-8601
|
|
552
|
+
}),
|
|
553
|
+
),
|
|
554
|
+
/** When strikes were recorded */
|
|
555
|
+
first_strike_at: z.string().optional(), // ISO-8601
|
|
556
|
+
last_strike_at: z.string().optional(), // ISO-8601
|
|
557
|
+
});
|
|
558
|
+
export type StrikeRecord = z.infer<typeof StrikeRecordSchema>;
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Storage interface for strike records
|
|
562
|
+
*/
|
|
563
|
+
export interface StrikeStorage {
|
|
564
|
+
/** Store a strike record */
|
|
565
|
+
store(record: StrikeRecord): Promise<void>;
|
|
566
|
+
/** Get strike record for a bead */
|
|
567
|
+
get(beadId: string): Promise<StrikeRecord | null>;
|
|
568
|
+
/** Get all strike records */
|
|
569
|
+
getAll(): Promise<StrikeRecord[]>;
|
|
570
|
+
/** Clear strikes for a bead */
|
|
571
|
+
clear(beadId: string): Promise<void>;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* In-memory strike storage
|
|
576
|
+
*/
|
|
577
|
+
export class InMemoryStrikeStorage implements StrikeStorage {
|
|
578
|
+
private strikes: Map<string, StrikeRecord> = new Map();
|
|
579
|
+
|
|
580
|
+
async store(record: StrikeRecord): Promise<void> {
|
|
581
|
+
this.strikes.set(record.bead_id, record);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async get(beadId: string): Promise<StrikeRecord | null> {
|
|
585
|
+
return this.strikes.get(beadId) ?? null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async getAll(): Promise<StrikeRecord[]> {
|
|
589
|
+
return Array.from(this.strikes.values());
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async clear(beadId: string): Promise<void> {
|
|
593
|
+
this.strikes.delete(beadId);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Add a strike to a bead's record
|
|
599
|
+
*
|
|
600
|
+
* Records a failure attempt and increments the strike count.
|
|
601
|
+
*
|
|
602
|
+
* @param beadId - Bead ID
|
|
603
|
+
* @param attempt - Description of what was attempted
|
|
604
|
+
* @param reason - Why it failed
|
|
605
|
+
* @param storage - Strike storage (defaults to in-memory)
|
|
606
|
+
* @returns Updated strike record
|
|
607
|
+
*/
|
|
608
|
+
export async function addStrike(
|
|
609
|
+
beadId: string,
|
|
610
|
+
attempt: string,
|
|
611
|
+
reason: string,
|
|
612
|
+
storage: StrikeStorage = new InMemoryStrikeStorage(),
|
|
613
|
+
): Promise<StrikeRecord> {
|
|
614
|
+
const existing = await storage.get(beadId);
|
|
615
|
+
const now = new Date().toISOString();
|
|
616
|
+
|
|
617
|
+
const record: StrikeRecord = existing ?? {
|
|
618
|
+
bead_id: beadId,
|
|
619
|
+
strike_count: 0,
|
|
620
|
+
failures: [],
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
record.strike_count = Math.min(3, record.strike_count + 1);
|
|
624
|
+
record.failures.push({ attempt, reason, timestamp: now });
|
|
625
|
+
record.last_strike_at = now;
|
|
626
|
+
|
|
627
|
+
if (!record.first_strike_at) {
|
|
628
|
+
record.first_strike_at = now;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
await storage.store(record);
|
|
632
|
+
return record;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Get strike count for a bead
|
|
637
|
+
*
|
|
638
|
+
* @param beadId - Bead ID
|
|
639
|
+
* @param storage - Strike storage
|
|
640
|
+
* @returns Strike count (0-3)
|
|
641
|
+
*/
|
|
642
|
+
export async function getStrikes(
|
|
643
|
+
beadId: string,
|
|
644
|
+
storage: StrikeStorage = new InMemoryStrikeStorage(),
|
|
645
|
+
): Promise<number> {
|
|
646
|
+
const record = await storage.get(beadId);
|
|
647
|
+
return record?.strike_count ?? 0;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Check if a bead has struck out (3 strikes)
|
|
652
|
+
*
|
|
653
|
+
* @param beadId - Bead ID
|
|
654
|
+
* @param storage - Strike storage
|
|
655
|
+
* @returns True if bead has 3 strikes
|
|
656
|
+
*/
|
|
657
|
+
export async function isStrikedOut(
|
|
658
|
+
beadId: string,
|
|
659
|
+
storage: StrikeStorage = new InMemoryStrikeStorage(),
|
|
660
|
+
): Promise<boolean> {
|
|
661
|
+
const count = await getStrikes(beadId, storage);
|
|
662
|
+
return count >= 3;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Generate architecture review prompt for a struck-out bead
|
|
667
|
+
*
|
|
668
|
+
* When a bead hits 3 strikes, this generates a prompt that forces
|
|
669
|
+
* the human to question the architecture instead of attempting Fix #4.
|
|
670
|
+
*
|
|
671
|
+
* @param beadId - Bead ID
|
|
672
|
+
* @param storage - Strike storage
|
|
673
|
+
* @returns Architecture review prompt
|
|
674
|
+
*/
|
|
675
|
+
export async function getArchitecturePrompt(
|
|
676
|
+
beadId: string,
|
|
677
|
+
storage: StrikeStorage = new InMemoryStrikeStorage(),
|
|
678
|
+
): Promise<string> {
|
|
679
|
+
const record = await storage.get(beadId);
|
|
680
|
+
|
|
681
|
+
if (!record || record.strike_count < 3) {
|
|
682
|
+
return "";
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const failuresList = record.failures
|
|
686
|
+
.map((f, i) => `${i + 1}. **${f.attempt}** - Failed: ${f.reason}`)
|
|
687
|
+
.join("\n");
|
|
688
|
+
|
|
689
|
+
return `## Architecture Review Required
|
|
690
|
+
|
|
691
|
+
This bead (\`${beadId}\`) has failed 3 consecutive fix attempts:
|
|
692
|
+
|
|
693
|
+
${failuresList}
|
|
694
|
+
|
|
695
|
+
This pattern suggests an **architectural problem**, not a bug.
|
|
696
|
+
|
|
697
|
+
**Questions to consider:**
|
|
698
|
+
- Is the current approach fundamentally sound?
|
|
699
|
+
- Should we refactor the architecture instead?
|
|
700
|
+
- Are we fixing symptoms instead of root cause?
|
|
701
|
+
|
|
702
|
+
**Options:**
|
|
703
|
+
1. **Refactor architecture** (describe new approach)
|
|
704
|
+
2. **Continue with Fix #4** (explain why this time is different)
|
|
705
|
+
3. **Abandon this approach entirely**
|
|
706
|
+
|
|
707
|
+
**DO NOT attempt Fix #4 without answering these questions.**
|
|
708
|
+
`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Clear strikes for a bead (e.g., after successful fix)
|
|
713
|
+
*
|
|
714
|
+
* @param beadId - Bead ID
|
|
715
|
+
* @param storage - Strike storage
|
|
716
|
+
*/
|
|
717
|
+
export async function clearStrikes(
|
|
718
|
+
beadId: string,
|
|
719
|
+
storage: StrikeStorage = new InMemoryStrikeStorage(),
|
|
720
|
+
): Promise<void> {
|
|
721
|
+
await storage.clear(beadId);
|
|
722
|
+
}
|
|
723
|
+
|
|
527
724
|
// ============================================================================
|
|
528
725
|
// Error Accumulator
|
|
529
726
|
// ============================================================================
|
|
@@ -772,4 +969,5 @@ export const learningSchemas = {
|
|
|
772
969
|
DecompositionStrategySchema,
|
|
773
970
|
ErrorTypeSchema,
|
|
774
971
|
ErrorEntrySchema,
|
|
972
|
+
StrikeRecordSchema,
|
|
775
973
|
};
|