opencode-swarm-plugin 0.1.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/.local_version +1 -0
- package/.beads/README.md +81 -0
- package/.beads/config.yaml +62 -0
- package/.beads/issues.jsonl +549 -0
- package/.beads/metadata.json +4 -0
- package/.gitattributes +3 -0
- package/Dockerfile +30 -0
- package/README.md +312 -0
- package/bun.lock +212 -0
- package/dist/index.js +14627 -0
- package/dist/plugin.js +14562 -0
- package/docker/agent-mail/Dockerfile +23 -0
- package/docker/agent-mail/__pycache__/server.cpython-314.pyc +0 -0
- package/docker/agent-mail/requirements.txt +3 -0
- package/docker/agent-mail/server.py +879 -0
- package/docker-compose.yml +45 -0
- package/package.json +52 -0
- package/scripts/docker-entrypoint.sh +54 -0
- package/src/agent-mail.integration.test.ts +1321 -0
- package/src/agent-mail.ts +665 -0
- package/src/anti-patterns.ts +430 -0
- package/src/beads.integration.test.ts +688 -0
- package/src/beads.ts +603 -0
- package/src/index.ts +267 -0
- package/src/learning.integration.test.ts +1104 -0
- package/src/learning.ts +438 -0
- package/src/pattern-maturity.ts +487 -0
- package/src/plugin.ts +11 -0
- package/src/schemas/bead.ts +152 -0
- package/src/schemas/evaluation.ts +133 -0
- package/src/schemas/index.test.ts +199 -0
- package/src/schemas/index.ts +77 -0
- package/src/schemas/task.ts +129 -0
- package/src/structured.ts +708 -0
- package/src/swarm.integration.test.ts +763 -0
- package/src/swarm.ts +1411 -0
- package/tsconfig.json +28 -0
- package/vitest.integration.config.ts +13 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-Pattern Learning Module
|
|
3
|
+
*
|
|
4
|
+
* Tracks failed decomposition patterns and auto-inverts them to anti-patterns.
|
|
5
|
+
* When a pattern consistently fails, it gets flagged as something to avoid.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/Dicklesworthstone/cass_memory_system/blob/main/src/curate.ts#L95-L117
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Schemas
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Pattern kind - whether this is a positive pattern or an anti-pattern
|
|
17
|
+
*/
|
|
18
|
+
export const PatternKindSchema = z.enum(["pattern", "anti_pattern"]);
|
|
19
|
+
export type PatternKind = z.infer<typeof PatternKindSchema>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A decomposition pattern that has been observed
|
|
23
|
+
*
|
|
24
|
+
* Patterns are extracted from successful/failed decompositions and
|
|
25
|
+
* tracked over time to learn what works and what doesn't.
|
|
26
|
+
*/
|
|
27
|
+
export const DecompositionPatternSchema = z.object({
|
|
28
|
+
/** Unique ID for this pattern */
|
|
29
|
+
id: z.string(),
|
|
30
|
+
/** Human-readable description of the pattern */
|
|
31
|
+
content: z.string(),
|
|
32
|
+
/** Whether this is a positive pattern or anti-pattern */
|
|
33
|
+
kind: PatternKindSchema,
|
|
34
|
+
/** Whether this pattern should be avoided (true for anti-patterns) */
|
|
35
|
+
is_negative: z.boolean(),
|
|
36
|
+
/** Number of times this pattern succeeded */
|
|
37
|
+
success_count: z.number().int().min(0).default(0),
|
|
38
|
+
/** Number of times this pattern failed */
|
|
39
|
+
failure_count: z.number().int().min(0).default(0),
|
|
40
|
+
/** When this pattern was first observed */
|
|
41
|
+
created_at: z.string(), // ISO-8601
|
|
42
|
+
/** When this pattern was last updated */
|
|
43
|
+
updated_at: z.string(), // ISO-8601
|
|
44
|
+
/** Context about why this pattern was created/inverted */
|
|
45
|
+
reason: z.string().optional(),
|
|
46
|
+
/** Tags for categorization (e.g., "file-splitting", "dependency-ordering") */
|
|
47
|
+
tags: z.array(z.string()).default([]),
|
|
48
|
+
/** Example bead IDs where this pattern was observed */
|
|
49
|
+
example_beads: z.array(z.string()).default([]),
|
|
50
|
+
});
|
|
51
|
+
export type DecompositionPattern = z.infer<typeof DecompositionPatternSchema>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Result of pattern inversion
|
|
55
|
+
*/
|
|
56
|
+
export const PatternInversionResultSchema = z.object({
|
|
57
|
+
/** The original pattern */
|
|
58
|
+
original: DecompositionPatternSchema,
|
|
59
|
+
/** The inverted anti-pattern */
|
|
60
|
+
inverted: DecompositionPatternSchema,
|
|
61
|
+
/** Why the inversion happened */
|
|
62
|
+
reason: z.string(),
|
|
63
|
+
});
|
|
64
|
+
export type PatternInversionResult = z.infer<
|
|
65
|
+
typeof PatternInversionResultSchema
|
|
66
|
+
>;
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Configuration
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Configuration for anti-pattern detection
|
|
74
|
+
*/
|
|
75
|
+
export interface AntiPatternConfig {
|
|
76
|
+
/** Minimum observations before considering inversion */
|
|
77
|
+
minObservations: number;
|
|
78
|
+
/** Failure ratio threshold for inversion (0-1) */
|
|
79
|
+
failureRatioThreshold: number;
|
|
80
|
+
/** Prefix for anti-pattern content */
|
|
81
|
+
antiPatternPrefix: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const DEFAULT_ANTI_PATTERN_CONFIG: AntiPatternConfig = {
|
|
85
|
+
minObservations: 3,
|
|
86
|
+
failureRatioThreshold: 0.6, // 60% failure rate triggers inversion
|
|
87
|
+
antiPatternPrefix: "AVOID: ",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Core Functions
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a pattern should be inverted to an anti-pattern
|
|
96
|
+
*
|
|
97
|
+
* A pattern is inverted when:
|
|
98
|
+
* 1. It has enough observations (minObservations)
|
|
99
|
+
* 2. Its failure ratio exceeds the threshold
|
|
100
|
+
*
|
|
101
|
+
* @param pattern - The pattern to check
|
|
102
|
+
* @param config - Anti-pattern configuration
|
|
103
|
+
* @returns Whether the pattern should be inverted
|
|
104
|
+
*/
|
|
105
|
+
export function shouldInvertPattern(
|
|
106
|
+
pattern: DecompositionPattern,
|
|
107
|
+
config: AntiPatternConfig = DEFAULT_ANTI_PATTERN_CONFIG,
|
|
108
|
+
): boolean {
|
|
109
|
+
// Already an anti-pattern
|
|
110
|
+
if (pattern.kind === "anti_pattern") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const total = pattern.success_count + pattern.failure_count;
|
|
115
|
+
|
|
116
|
+
// Not enough observations
|
|
117
|
+
if (total < config.minObservations) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const failureRatio = pattern.failure_count / total;
|
|
122
|
+
return failureRatio >= config.failureRatioThreshold;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Invert a pattern to an anti-pattern
|
|
127
|
+
*
|
|
128
|
+
* Creates a new anti-pattern from a failing pattern.
|
|
129
|
+
* The content is prefixed with "AVOID: " and the kind is changed.
|
|
130
|
+
*
|
|
131
|
+
* @param pattern - The pattern to invert
|
|
132
|
+
* @param reason - Why the inversion is happening
|
|
133
|
+
* @param config - Anti-pattern configuration
|
|
134
|
+
* @returns The inverted anti-pattern
|
|
135
|
+
*/
|
|
136
|
+
export function invertToAntiPattern(
|
|
137
|
+
pattern: DecompositionPattern,
|
|
138
|
+
reason: string,
|
|
139
|
+
config: AntiPatternConfig = DEFAULT_ANTI_PATTERN_CONFIG,
|
|
140
|
+
): PatternInversionResult {
|
|
141
|
+
// Clean the content (remove any existing prefix)
|
|
142
|
+
const cleaned = pattern.content
|
|
143
|
+
.replace(/^AVOID:\s*/i, "")
|
|
144
|
+
.replace(/^DO NOT:\s*/i, "")
|
|
145
|
+
.replace(/^NEVER:\s*/i, "");
|
|
146
|
+
|
|
147
|
+
const inverted: DecompositionPattern = {
|
|
148
|
+
...pattern,
|
|
149
|
+
id: `anti-${pattern.id}`,
|
|
150
|
+
content: `${config.antiPatternPrefix}${cleaned}. ${reason}`,
|
|
151
|
+
kind: "anti_pattern",
|
|
152
|
+
is_negative: true,
|
|
153
|
+
reason,
|
|
154
|
+
updated_at: new Date().toISOString(),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
original: pattern,
|
|
159
|
+
inverted,
|
|
160
|
+
reason,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Record a pattern observation (success or failure)
|
|
166
|
+
*
|
|
167
|
+
* Updates the pattern's success/failure counts and checks if
|
|
168
|
+
* it should be inverted to an anti-pattern.
|
|
169
|
+
*
|
|
170
|
+
* @param pattern - The pattern to update
|
|
171
|
+
* @param success - Whether this observation was successful
|
|
172
|
+
* @param beadId - Optional bead ID to record as example
|
|
173
|
+
* @param config - Anti-pattern configuration
|
|
174
|
+
* @returns Updated pattern and optional inversion result
|
|
175
|
+
*/
|
|
176
|
+
export function recordPatternObservation(
|
|
177
|
+
pattern: DecompositionPattern,
|
|
178
|
+
success: boolean,
|
|
179
|
+
beadId?: string,
|
|
180
|
+
config: AntiPatternConfig = DEFAULT_ANTI_PATTERN_CONFIG,
|
|
181
|
+
): { pattern: DecompositionPattern; inversion?: PatternInversionResult } {
|
|
182
|
+
// Update counts
|
|
183
|
+
const updated: DecompositionPattern = {
|
|
184
|
+
...pattern,
|
|
185
|
+
success_count: success ? pattern.success_count + 1 : pattern.success_count,
|
|
186
|
+
failure_count: success ? pattern.failure_count : pattern.failure_count + 1,
|
|
187
|
+
updated_at: new Date().toISOString(),
|
|
188
|
+
example_beads: beadId
|
|
189
|
+
? [...pattern.example_beads.slice(-9), beadId] // Keep last 10
|
|
190
|
+
: pattern.example_beads,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Check if should invert
|
|
194
|
+
if (shouldInvertPattern(updated, config)) {
|
|
195
|
+
const total = updated.success_count + updated.failure_count;
|
|
196
|
+
const failureRatio = updated.failure_count / total;
|
|
197
|
+
const reason = `Failed ${updated.failure_count}/${total} times (${Math.round(failureRatio * 100)}% failure rate)`;
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
pattern: updated,
|
|
201
|
+
inversion: invertToAntiPattern(updated, reason, config),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { pattern: updated };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Extract patterns from a decomposition description
|
|
210
|
+
*
|
|
211
|
+
* Looks for common decomposition strategies in the text.
|
|
212
|
+
*
|
|
213
|
+
* @param description - Decomposition description or reasoning
|
|
214
|
+
* @returns Extracted pattern descriptions
|
|
215
|
+
*/
|
|
216
|
+
export function extractPatternsFromDescription(description: string): string[] {
|
|
217
|
+
const patterns: string[] = [];
|
|
218
|
+
|
|
219
|
+
// Common decomposition strategies to detect
|
|
220
|
+
const strategyPatterns = [
|
|
221
|
+
{
|
|
222
|
+
regex: /split(?:ting)?\s+by\s+file\s+type/i,
|
|
223
|
+
pattern: "Split by file type",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
regex: /split(?:ting)?\s+by\s+component/i,
|
|
227
|
+
pattern: "Split by component",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
regex: /split(?:ting)?\s+by\s+layer/i,
|
|
231
|
+
pattern: "Split by layer (UI/logic/data)",
|
|
232
|
+
},
|
|
233
|
+
{ regex: /split(?:ting)?\s+by\s+feature/i, pattern: "Split by feature" },
|
|
234
|
+
{
|
|
235
|
+
regex: /one\s+file\s+per\s+(?:sub)?task/i,
|
|
236
|
+
pattern: "One file per subtask",
|
|
237
|
+
},
|
|
238
|
+
{ regex: /shared\s+types?\s+first/i, pattern: "Handle shared types first" },
|
|
239
|
+
{ regex: /api\s+(?:routes?)?\s+separate/i, pattern: "Separate API routes" },
|
|
240
|
+
{
|
|
241
|
+
regex: /tests?\s+(?:with|alongside)\s+(?:code|implementation)/i,
|
|
242
|
+
pattern: "Tests alongside implementation",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
regex: /tests?\s+(?:in\s+)?separate\s+(?:sub)?task/i,
|
|
246
|
+
pattern: "Tests in separate subtask",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
regex: /parallel(?:ize)?\s+(?:all|everything)/i,
|
|
250
|
+
pattern: "Maximize parallelization",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
regex: /sequential\s+(?:order|execution)/i,
|
|
254
|
+
pattern: "Sequential execution order",
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
regex: /dependency\s+(?:chain|order)/i,
|
|
258
|
+
pattern: "Respect dependency chain",
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
for (const { regex, pattern } of strategyPatterns) {
|
|
263
|
+
if (regex.test(description)) {
|
|
264
|
+
patterns.push(pattern);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return patterns;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Create a new pattern from a description
|
|
273
|
+
*
|
|
274
|
+
* @param content - Pattern description
|
|
275
|
+
* @param tags - Optional tags for categorization
|
|
276
|
+
* @returns New pattern
|
|
277
|
+
*/
|
|
278
|
+
export function createPattern(
|
|
279
|
+
content: string,
|
|
280
|
+
tags: string[] = [],
|
|
281
|
+
): DecompositionPattern {
|
|
282
|
+
const now = new Date().toISOString();
|
|
283
|
+
return {
|
|
284
|
+
id: `pattern-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
285
|
+
content,
|
|
286
|
+
kind: "pattern",
|
|
287
|
+
is_negative: false,
|
|
288
|
+
success_count: 0,
|
|
289
|
+
failure_count: 0,
|
|
290
|
+
created_at: now,
|
|
291
|
+
updated_at: now,
|
|
292
|
+
tags,
|
|
293
|
+
example_beads: [],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Format anti-patterns for inclusion in decomposition prompts
|
|
299
|
+
*
|
|
300
|
+
* @param patterns - Anti-patterns to format
|
|
301
|
+
* @returns Formatted string for prompt inclusion
|
|
302
|
+
*/
|
|
303
|
+
export function formatAntiPatternsForPrompt(
|
|
304
|
+
patterns: DecompositionPattern[],
|
|
305
|
+
): string {
|
|
306
|
+
const antiPatterns = patterns.filter((p) => p.kind === "anti_pattern");
|
|
307
|
+
|
|
308
|
+
if (antiPatterns.length === 0) {
|
|
309
|
+
return "";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const lines = [
|
|
313
|
+
"## Anti-Patterns to Avoid",
|
|
314
|
+
"",
|
|
315
|
+
"Based on past failures, avoid these decomposition strategies:",
|
|
316
|
+
"",
|
|
317
|
+
...antiPatterns.map((p) => `- ${p.content}`),
|
|
318
|
+
"",
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
return lines.join("\n");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Format successful patterns for inclusion in decomposition prompts
|
|
326
|
+
*
|
|
327
|
+
* @param patterns - Patterns to format
|
|
328
|
+
* @param minSuccessRate - Minimum success rate to include (0-1)
|
|
329
|
+
* @returns Formatted string for prompt inclusion
|
|
330
|
+
*/
|
|
331
|
+
export function formatSuccessfulPatternsForPrompt(
|
|
332
|
+
patterns: DecompositionPattern[],
|
|
333
|
+
minSuccessRate: number = 0.7,
|
|
334
|
+
): string {
|
|
335
|
+
const successful = patterns.filter((p) => {
|
|
336
|
+
if (p.kind === "anti_pattern") return false;
|
|
337
|
+
const total = p.success_count + p.failure_count;
|
|
338
|
+
if (total < 2) return false;
|
|
339
|
+
return p.success_count / total >= minSuccessRate;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (successful.length === 0) {
|
|
343
|
+
return "";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const lines = [
|
|
347
|
+
"## Successful Patterns",
|
|
348
|
+
"",
|
|
349
|
+
"These decomposition strategies have worked well in the past:",
|
|
350
|
+
"",
|
|
351
|
+
...successful.map((p) => {
|
|
352
|
+
const total = p.success_count + p.failure_count;
|
|
353
|
+
const rate = Math.round((p.success_count / total) * 100);
|
|
354
|
+
return `- ${p.content} (${rate}% success rate)`;
|
|
355
|
+
}),
|
|
356
|
+
"",
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
return lines.join("\n");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ============================================================================
|
|
363
|
+
// Storage Interface
|
|
364
|
+
// ============================================================================
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Storage interface for decomposition patterns
|
|
368
|
+
*/
|
|
369
|
+
export interface PatternStorage {
|
|
370
|
+
/** Store or update a pattern */
|
|
371
|
+
store(pattern: DecompositionPattern): Promise<void>;
|
|
372
|
+
/** Get a pattern by ID */
|
|
373
|
+
get(id: string): Promise<DecompositionPattern | null>;
|
|
374
|
+
/** Get all patterns */
|
|
375
|
+
getAll(): Promise<DecompositionPattern[]>;
|
|
376
|
+
/** Get all anti-patterns */
|
|
377
|
+
getAntiPatterns(): Promise<DecompositionPattern[]>;
|
|
378
|
+
/** Get patterns by tag */
|
|
379
|
+
getByTag(tag: string): Promise<DecompositionPattern[]>;
|
|
380
|
+
/** Find patterns matching content */
|
|
381
|
+
findByContent(content: string): Promise<DecompositionPattern[]>;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* In-memory pattern storage (for testing and short-lived sessions)
|
|
386
|
+
*/
|
|
387
|
+
export class InMemoryPatternStorage implements PatternStorage {
|
|
388
|
+
private patterns: Map<string, DecompositionPattern> = new Map();
|
|
389
|
+
|
|
390
|
+
async store(pattern: DecompositionPattern): Promise<void> {
|
|
391
|
+
this.patterns.set(pattern.id, pattern);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async get(id: string): Promise<DecompositionPattern | null> {
|
|
395
|
+
return this.patterns.get(id) ?? null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async getAll(): Promise<DecompositionPattern[]> {
|
|
399
|
+
return Array.from(this.patterns.values());
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async getAntiPatterns(): Promise<DecompositionPattern[]> {
|
|
403
|
+
return Array.from(this.patterns.values()).filter(
|
|
404
|
+
(p) => p.kind === "anti_pattern",
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async getByTag(tag: string): Promise<DecompositionPattern[]> {
|
|
409
|
+
return Array.from(this.patterns.values()).filter((p) =>
|
|
410
|
+
p.tags.includes(tag),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async findByContent(content: string): Promise<DecompositionPattern[]> {
|
|
415
|
+
const lower = content.toLowerCase();
|
|
416
|
+
return Array.from(this.patterns.values()).filter((p) =>
|
|
417
|
+
p.content.toLowerCase().includes(lower),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ============================================================================
|
|
423
|
+
// Exports
|
|
424
|
+
// ============================================================================
|
|
425
|
+
|
|
426
|
+
export const antiPatternSchemas = {
|
|
427
|
+
PatternKindSchema,
|
|
428
|
+
DecompositionPatternSchema,
|
|
429
|
+
PatternInversionResultSchema,
|
|
430
|
+
};
|