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.
@@ -0,0 +1,487 @@
1
+ /**
2
+ * Pattern Maturity Module
3
+ *
4
+ * Tracks decomposition pattern maturity states through lifecycle:
5
+ * candidate → established → proven (or deprecated)
6
+ *
7
+ * Patterns start as candidates until they accumulate enough feedback.
8
+ * Strong positive feedback promotes to proven, strong negative deprecates.
9
+ *
10
+ * @see https://github.com/Dicklesworthstone/cass_memory_system/blob/main/src/scoring.ts#L73-L98
11
+ */
12
+ import { z } from "zod";
13
+ import { calculateDecayedValue } from "./learning";
14
+
15
+ // ============================================================================
16
+ // Schemas
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Maturity state for a decomposition pattern
21
+ *
22
+ * - candidate: Not enough feedback to judge (< minFeedback events)
23
+ * - established: Enough feedback, neither proven nor deprecated
24
+ * - proven: Strong positive signal (high helpful, low harmful ratio)
25
+ * - deprecated: Strong negative signal (high harmful ratio)
26
+ */
27
+ export const MaturityStateSchema = z.enum([
28
+ "candidate",
29
+ "established",
30
+ "proven",
31
+ "deprecated",
32
+ ]);
33
+ export type MaturityState = z.infer<typeof MaturityStateSchema>;
34
+
35
+ /**
36
+ * Pattern maturity tracking
37
+ *
38
+ * Tracks feedback counts and state transitions for a decomposition pattern.
39
+ */
40
+ export const PatternMaturitySchema = z.object({
41
+ /** Unique identifier for the pattern */
42
+ pattern_id: z.string(),
43
+ /** Current maturity state */
44
+ state: MaturityStateSchema,
45
+ /** Number of helpful feedback events */
46
+ helpful_count: z.number().int().min(0),
47
+ /** Number of harmful feedback events */
48
+ harmful_count: z.number().int().min(0),
49
+ /** When the pattern was last validated (ISO-8601) */
50
+ last_validated: z.string(),
51
+ /** When the pattern was promoted to proven (ISO-8601) */
52
+ promoted_at: z.string().optional(),
53
+ /** When the pattern was deprecated (ISO-8601) */
54
+ deprecated_at: z.string().optional(),
55
+ });
56
+ export type PatternMaturity = z.infer<typeof PatternMaturitySchema>;
57
+
58
+ /**
59
+ * Feedback event for maturity tracking
60
+ */
61
+ export const MaturityFeedbackSchema = z.object({
62
+ /** Pattern this feedback applies to */
63
+ pattern_id: z.string(),
64
+ /** Whether the pattern was helpful or harmful */
65
+ type: z.enum(["helpful", "harmful"]),
66
+ /** When this feedback was recorded (ISO-8601) */
67
+ timestamp: z.string(),
68
+ /** Raw weight before decay (0-1) */
69
+ weight: z.number().min(0).max(1).default(1),
70
+ });
71
+ export type MaturityFeedback = z.infer<typeof MaturityFeedbackSchema>;
72
+
73
+ // ============================================================================
74
+ // Configuration
75
+ // ============================================================================
76
+
77
+ /**
78
+ * Configuration for maturity calculations
79
+ */
80
+ export interface MaturityConfig {
81
+ /** Minimum feedback events before leaving candidate state */
82
+ minFeedback: number;
83
+ /** Minimum decayed helpful score to reach proven state */
84
+ minHelpful: number;
85
+ /** Maximum harmful ratio to reach/maintain proven state */
86
+ maxHarmful: number;
87
+ /** Harmful ratio threshold for deprecation */
88
+ deprecationThreshold: number;
89
+ /** Half-life for decay in days */
90
+ halfLifeDays: number;
91
+ }
92
+
93
+ export const DEFAULT_MATURITY_CONFIG: MaturityConfig = {
94
+ minFeedback: 3,
95
+ minHelpful: 5,
96
+ maxHarmful: 0.15, // 15% harmful is acceptable for proven
97
+ deprecationThreshold: 0.3, // 30% harmful triggers deprecation
98
+ halfLifeDays: 90,
99
+ };
100
+
101
+ // ============================================================================
102
+ // Core Functions
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Calculate decayed feedback counts
107
+ *
108
+ * Applies half-life decay to each feedback event based on age.
109
+ *
110
+ * @param feedbackEvents - Raw feedback events
111
+ * @param config - Maturity configuration
112
+ * @param now - Current timestamp for decay calculation
113
+ * @returns Decayed helpful and harmful totals
114
+ */
115
+ export function calculateDecayedCounts(
116
+ feedbackEvents: MaturityFeedback[],
117
+ config: MaturityConfig = DEFAULT_MATURITY_CONFIG,
118
+ now: Date = new Date(),
119
+ ): { decayedHelpful: number; decayedHarmful: number } {
120
+ let decayedHelpful = 0;
121
+ let decayedHarmful = 0;
122
+
123
+ for (const event of feedbackEvents) {
124
+ const decay = calculateDecayedValue(
125
+ event.timestamp,
126
+ now,
127
+ config.halfLifeDays,
128
+ );
129
+ const value = event.weight * decay;
130
+
131
+ if (event.type === "helpful") {
132
+ decayedHelpful += value;
133
+ } else {
134
+ decayedHarmful += value;
135
+ }
136
+ }
137
+
138
+ return { decayedHelpful, decayedHarmful };
139
+ }
140
+
141
+ /**
142
+ * Calculate maturity state from feedback events
143
+ *
144
+ * State determination logic:
145
+ * 1. "deprecated" if harmful ratio > 0.3 AND total >= minFeedback
146
+ * 2. "candidate" if total < minFeedback (not enough data)
147
+ * 3. "proven" if decayedHelpful >= minHelpful AND harmfulRatio < maxHarmful
148
+ * 4. "established" otherwise (enough data but not yet proven)
149
+ *
150
+ * @param feedbackEvents - Feedback events for this pattern
151
+ * @param config - Maturity configuration
152
+ * @param now - Current timestamp for decay calculation
153
+ * @returns Calculated maturity state
154
+ */
155
+ export function calculateMaturityState(
156
+ feedbackEvents: MaturityFeedback[],
157
+ config: MaturityConfig = DEFAULT_MATURITY_CONFIG,
158
+ now: Date = new Date(),
159
+ ): MaturityState {
160
+ const { decayedHelpful, decayedHarmful } = calculateDecayedCounts(
161
+ feedbackEvents,
162
+ config,
163
+ now,
164
+ );
165
+
166
+ const total = decayedHelpful + decayedHarmful;
167
+ const epsilon = 0.01; // Float comparison tolerance
168
+ const safeTotal = total > epsilon ? total : 0;
169
+ const harmfulRatio = safeTotal > 0 ? decayedHarmful / safeTotal : 0;
170
+
171
+ // Deprecated: high harmful ratio with enough feedback
172
+ if (
173
+ harmfulRatio > config.deprecationThreshold &&
174
+ safeTotal >= config.minFeedback - epsilon
175
+ ) {
176
+ return "deprecated";
177
+ }
178
+
179
+ // Candidate: not enough feedback yet
180
+ if (safeTotal < config.minFeedback - epsilon) {
181
+ return "candidate";
182
+ }
183
+
184
+ // Proven: strong positive signal
185
+ if (
186
+ decayedHelpful >= config.minHelpful - epsilon &&
187
+ harmfulRatio < config.maxHarmful
188
+ ) {
189
+ return "proven";
190
+ }
191
+
192
+ // Established: enough data but not proven
193
+ return "established";
194
+ }
195
+
196
+ /**
197
+ * Create initial pattern maturity record
198
+ *
199
+ * @param patternId - Unique pattern identifier
200
+ * @returns New PatternMaturity in candidate state
201
+ */
202
+ export function createPatternMaturity(patternId: string): PatternMaturity {
203
+ return {
204
+ pattern_id: patternId,
205
+ state: "candidate",
206
+ helpful_count: 0,
207
+ harmful_count: 0,
208
+ last_validated: new Date().toISOString(),
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Update pattern maturity with new feedback
214
+ *
215
+ * Records feedback, updates counts, and recalculates state.
216
+ *
217
+ * @param maturity - Current maturity record
218
+ * @param feedbackEvents - All feedback events for this pattern
219
+ * @param config - Maturity configuration
220
+ * @returns Updated maturity record
221
+ */
222
+ export function updatePatternMaturity(
223
+ maturity: PatternMaturity,
224
+ feedbackEvents: MaturityFeedback[],
225
+ config: MaturityConfig = DEFAULT_MATURITY_CONFIG,
226
+ ): PatternMaturity {
227
+ const now = new Date();
228
+ const newState = calculateMaturityState(feedbackEvents, config, now);
229
+
230
+ // Count raw feedback (not decayed)
231
+ const helpfulCount = feedbackEvents.filter(
232
+ (e) => e.type === "helpful",
233
+ ).length;
234
+ const harmfulCount = feedbackEvents.filter(
235
+ (e) => e.type === "harmful",
236
+ ).length;
237
+
238
+ const updated: PatternMaturity = {
239
+ ...maturity,
240
+ state: newState,
241
+ helpful_count: helpfulCount,
242
+ harmful_count: harmfulCount,
243
+ last_validated: now.toISOString(),
244
+ };
245
+
246
+ // Track state transitions
247
+ if (newState === "proven" && maturity.state !== "proven") {
248
+ updated.promoted_at = now.toISOString();
249
+ }
250
+ if (newState === "deprecated" && maturity.state !== "deprecated") {
251
+ updated.deprecated_at = now.toISOString();
252
+ }
253
+
254
+ return updated;
255
+ }
256
+
257
+ /**
258
+ * Promote a pattern to proven state
259
+ *
260
+ * Manually promotes a pattern regardless of feedback counts.
261
+ * Use when external validation confirms pattern effectiveness.
262
+ *
263
+ * @param maturity - Current maturity record
264
+ * @returns Updated maturity record with proven state
265
+ */
266
+ export function promotePattern(maturity: PatternMaturity): PatternMaturity {
267
+ if (maturity.state === "deprecated") {
268
+ throw new Error("Cannot promote a deprecated pattern");
269
+ }
270
+
271
+ if (maturity.state === "proven") {
272
+ return maturity; // Already proven
273
+ }
274
+
275
+ const now = new Date().toISOString();
276
+ return {
277
+ ...maturity,
278
+ state: "proven",
279
+ promoted_at: now,
280
+ last_validated: now,
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Deprecate a pattern
286
+ *
287
+ * Manually deprecates a pattern regardless of feedback counts.
288
+ * Use when external validation shows pattern is harmful.
289
+ *
290
+ * @param maturity - Current maturity record
291
+ * @param reason - Optional reason for deprecation
292
+ * @returns Updated maturity record with deprecated state
293
+ */
294
+ export function deprecatePattern(
295
+ maturity: PatternMaturity,
296
+ _reason?: string,
297
+ ): PatternMaturity {
298
+ if (maturity.state === "deprecated") {
299
+ return maturity; // Already deprecated
300
+ }
301
+
302
+ const now = new Date().toISOString();
303
+ return {
304
+ ...maturity,
305
+ state: "deprecated",
306
+ deprecated_at: now,
307
+ last_validated: now,
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Get maturity score multiplier for pattern ranking
313
+ *
314
+ * Higher maturity patterns should be weighted more heavily.
315
+ *
316
+ * @param state - Maturity state
317
+ * @returns Score multiplier (0-1.5)
318
+ */
319
+ export function getMaturityMultiplier(state: MaturityState): number {
320
+ const multipliers: Record<MaturityState, number> = {
321
+ candidate: 0.5,
322
+ established: 1.0,
323
+ proven: 1.5,
324
+ deprecated: 0,
325
+ };
326
+ return multipliers[state];
327
+ }
328
+
329
+ /**
330
+ * Format maturity state for inclusion in prompts
331
+ *
332
+ * Shows pattern reliability to help agents make informed decisions.
333
+ *
334
+ * @param maturity - Pattern maturity record
335
+ * @returns Formatted string describing pattern reliability
336
+ */
337
+ export function formatMaturityForPrompt(maturity: PatternMaturity): string {
338
+ const total = maturity.helpful_count + maturity.harmful_count;
339
+ const harmfulRatio =
340
+ total > 0 ? Math.round((maturity.harmful_count / total) * 100) : 0;
341
+ const helpfulRatio =
342
+ total > 0 ? Math.round((maturity.helpful_count / total) * 100) : 0;
343
+
344
+ switch (maturity.state) {
345
+ case "candidate":
346
+ return `[CANDIDATE - ${total} observations, needs more data]`;
347
+ case "established":
348
+ return `[ESTABLISHED - ${helpfulRatio}% helpful, ${harmfulRatio}% harmful from ${total} observations]`;
349
+ case "proven":
350
+ return `[PROVEN - ${helpfulRatio}% helpful from ${total} observations]`;
351
+ case "deprecated":
352
+ return `[DEPRECATED - ${harmfulRatio}% harmful, avoid using]`;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Format multiple patterns with maturity for prompt inclusion
358
+ *
359
+ * Groups patterns by maturity state for clear presentation.
360
+ *
361
+ * @param patterns - Map of pattern content to maturity record
362
+ * @returns Formatted string for prompt inclusion
363
+ */
364
+ export function formatPatternsWithMaturityForPrompt(
365
+ patterns: Map<string, PatternMaturity>,
366
+ ): string {
367
+ const proven: string[] = [];
368
+ const established: string[] = [];
369
+ const candidates: string[] = [];
370
+ const deprecated: string[] = [];
371
+
372
+ for (const [content, maturity] of patterns) {
373
+ const formatted = `- ${content} ${formatMaturityForPrompt(maturity)}`;
374
+ switch (maturity.state) {
375
+ case "proven":
376
+ proven.push(formatted);
377
+ break;
378
+ case "established":
379
+ established.push(formatted);
380
+ break;
381
+ case "candidate":
382
+ candidates.push(formatted);
383
+ break;
384
+ case "deprecated":
385
+ deprecated.push(formatted);
386
+ break;
387
+ }
388
+ }
389
+
390
+ const sections: string[] = [];
391
+
392
+ if (proven.length > 0) {
393
+ sections.push(
394
+ "## Proven Patterns\n\nThese patterns consistently work well:\n\n" +
395
+ proven.join("\n"),
396
+ );
397
+ }
398
+
399
+ if (established.length > 0) {
400
+ sections.push(
401
+ "## Established Patterns\n\nThese patterns have track records:\n\n" +
402
+ established.join("\n"),
403
+ );
404
+ }
405
+
406
+ if (candidates.length > 0) {
407
+ sections.push(
408
+ "## Candidate Patterns\n\nThese patterns need more validation:\n\n" +
409
+ candidates.join("\n"),
410
+ );
411
+ }
412
+
413
+ if (deprecated.length > 0) {
414
+ sections.push(
415
+ "## Deprecated Patterns\n\nAVOID these patterns - they have poor track records:\n\n" +
416
+ deprecated.join("\n"),
417
+ );
418
+ }
419
+
420
+ return sections.join("\n\n");
421
+ }
422
+
423
+ // ============================================================================
424
+ // Storage
425
+ // ============================================================================
426
+
427
+ /**
428
+ * Storage interface for pattern maturity records
429
+ */
430
+ export interface MaturityStorage {
431
+ /** Store or update a maturity record */
432
+ store(maturity: PatternMaturity): Promise<void>;
433
+ /** Get maturity record by pattern ID */
434
+ get(patternId: string): Promise<PatternMaturity | null>;
435
+ /** Get all maturity records */
436
+ getAll(): Promise<PatternMaturity[]>;
437
+ /** Get patterns by state */
438
+ getByState(state: MaturityState): Promise<PatternMaturity[]>;
439
+ /** Store a feedback event */
440
+ storeFeedback(feedback: MaturityFeedback): Promise<void>;
441
+ /** Get all feedback for a pattern */
442
+ getFeedback(patternId: string): Promise<MaturityFeedback[]>;
443
+ }
444
+
445
+ /**
446
+ * In-memory maturity storage (for testing and short-lived sessions)
447
+ */
448
+ export class InMemoryMaturityStorage implements MaturityStorage {
449
+ private maturities: Map<string, PatternMaturity> = new Map();
450
+ private feedback: MaturityFeedback[] = [];
451
+
452
+ async store(maturity: PatternMaturity): Promise<void> {
453
+ this.maturities.set(maturity.pattern_id, maturity);
454
+ }
455
+
456
+ async get(patternId: string): Promise<PatternMaturity | null> {
457
+ return this.maturities.get(patternId) ?? null;
458
+ }
459
+
460
+ async getAll(): Promise<PatternMaturity[]> {
461
+ return Array.from(this.maturities.values());
462
+ }
463
+
464
+ async getByState(state: MaturityState): Promise<PatternMaturity[]> {
465
+ return Array.from(this.maturities.values()).filter(
466
+ (m) => m.state === state,
467
+ );
468
+ }
469
+
470
+ async storeFeedback(feedback: MaturityFeedback): Promise<void> {
471
+ this.feedback.push(feedback);
472
+ }
473
+
474
+ async getFeedback(patternId: string): Promise<MaturityFeedback[]> {
475
+ return this.feedback.filter((f) => f.pattern_id === patternId);
476
+ }
477
+ }
478
+
479
+ // ============================================================================
480
+ // Exports
481
+ // ============================================================================
482
+
483
+ export const maturitySchemas = {
484
+ MaturityStateSchema,
485
+ PatternMaturitySchema,
486
+ MaturityFeedbackSchema,
487
+ };
package/src/plugin.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * OpenCode Plugin Entry Point
3
+ *
4
+ * This file ONLY exports the plugin function.
5
+ * The plugin loader iterates over all exports and calls them as functions,
6
+ * so we cannot export anything else here (classes, constants, types, etc.)
7
+ */
8
+ import { SwarmPlugin } from "./index";
9
+
10
+ // Only export the plugin function - nothing else!
11
+ export { SwarmPlugin };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Bead schemas for type-safe beads operations
3
+ *
4
+ * These schemas validate all data from the `bd` CLI to ensure
5
+ * type safety and catch malformed responses early.
6
+ */
7
+ import { z } from "zod";
8
+
9
+ /** Valid bead statuses */
10
+ export const BeadStatusSchema = z.enum([
11
+ "open",
12
+ "in_progress",
13
+ "blocked",
14
+ "closed",
15
+ ]);
16
+ export type BeadStatus = z.infer<typeof BeadStatusSchema>;
17
+
18
+ /** Valid bead types */
19
+ export const BeadTypeSchema = z.enum([
20
+ "bug",
21
+ "feature",
22
+ "task",
23
+ "epic",
24
+ "chore",
25
+ ]);
26
+ export type BeadType = z.infer<typeof BeadTypeSchema>;
27
+
28
+ /** Dependency relationship between beads */
29
+ export const BeadDependencySchema = z.object({
30
+ id: z.string(),
31
+ type: z.enum(["blocks", "blocked-by", "related", "discovered-from"]),
32
+ });
33
+ export type BeadDependency = z.infer<typeof BeadDependencySchema>;
34
+
35
+ /**
36
+ * Core bead schema - validates bd CLI JSON output
37
+ *
38
+ * ID format:
39
+ * - Standard: `{project}-{hash}` (e.g., `opencode-swarm-plugin-1i8`)
40
+ * - Subtask: `{project}-{hash}.{index}` (e.g., `opencode-swarm-plugin-1i8.1`)
41
+ */
42
+ export const BeadSchema = z.object({
43
+ id: z
44
+ .string()
45
+ .regex(/^[a-z0-9-]+-[a-z0-9]+(\.\d+)?$/, "Invalid bead ID format"),
46
+ title: z.string().min(1, "Title required"),
47
+ description: z.string().optional().default(""),
48
+ status: BeadStatusSchema.default("open"),
49
+ priority: z.number().int().min(0).max(3).default(2),
50
+ issue_type: BeadTypeSchema.default("task"),
51
+ created_at: z.string(), // ISO-8601
52
+ updated_at: z.string().optional(),
53
+ closed_at: z.string().optional(),
54
+ parent_id: z.string().optional(),
55
+ dependencies: z.array(BeadDependencySchema).optional().default([]),
56
+ metadata: z.record(z.string(), z.unknown()).optional(),
57
+ });
58
+ export type Bead = z.infer<typeof BeadSchema>;
59
+
60
+ /** Arguments for creating a bead */
61
+ export const BeadCreateArgsSchema = z.object({
62
+ title: z.string().min(1, "Title required"),
63
+ type: BeadTypeSchema.default("task"),
64
+ priority: z.number().int().min(0).max(3).default(2),
65
+ description: z.string().optional(),
66
+ parent_id: z.string().optional(),
67
+ });
68
+ export type BeadCreateArgs = z.infer<typeof BeadCreateArgsSchema>;
69
+
70
+ /** Arguments for updating a bead */
71
+ export const BeadUpdateArgsSchema = z.object({
72
+ id: z.string(),
73
+ status: BeadStatusSchema.optional(),
74
+ description: z.string().optional(),
75
+ priority: z.number().int().min(0).max(3).optional(),
76
+ });
77
+ export type BeadUpdateArgs = z.infer<typeof BeadUpdateArgsSchema>;
78
+
79
+ /** Arguments for closing a bead */
80
+ export const BeadCloseArgsSchema = z.object({
81
+ id: z.string(),
82
+ reason: z.string().min(1, "Reason required"),
83
+ });
84
+ export type BeadCloseArgs = z.infer<typeof BeadCloseArgsSchema>;
85
+
86
+ /** Arguments for querying beads */
87
+ export const BeadQueryArgsSchema = z.object({
88
+ status: BeadStatusSchema.optional(),
89
+ type: BeadTypeSchema.optional(),
90
+ ready: z.boolean().optional(),
91
+ limit: z.number().int().positive().default(20),
92
+ });
93
+ export type BeadQueryArgs = z.infer<typeof BeadQueryArgsSchema>;
94
+
95
+ /**
96
+ * Subtask specification for epic decomposition
97
+ *
98
+ * Used when creating an epic with subtasks in one operation.
99
+ * The `files` array is used for Agent Mail file reservations.
100
+ */
101
+ export const SubtaskSpecSchema = z.object({
102
+ title: z.string().min(1),
103
+ description: z.string().optional().default(""),
104
+ files: z.array(z.string()).default([]),
105
+ dependencies: z.array(z.number().int().min(0)).default([]), // Indices of other subtasks
106
+ estimated_complexity: z.number().int().min(1).max(5).default(3),
107
+ });
108
+ export type SubtaskSpec = z.infer<typeof SubtaskSpecSchema>;
109
+
110
+ /**
111
+ * Bead tree for swarm decomposition
112
+ *
113
+ * Represents an epic with its subtasks, ready for spawning agents.
114
+ */
115
+ export const BeadTreeSchema = z.object({
116
+ epic: z.object({
117
+ title: z.string().min(1),
118
+ description: z.string().optional().default(""),
119
+ }),
120
+ subtasks: z.array(SubtaskSpecSchema).min(1).max(10),
121
+ });
122
+ export type BeadTree = z.infer<typeof BeadTreeSchema>;
123
+
124
+ /** Arguments for creating an epic with subtasks */
125
+ export const EpicCreateArgsSchema = z.object({
126
+ epic_title: z.string().min(1),
127
+ epic_description: z.string().optional(),
128
+ subtasks: z
129
+ .array(
130
+ z.object({
131
+ title: z.string().min(1),
132
+ priority: z.number().int().min(0).max(3).default(2),
133
+ files: z.array(z.string()).optional().default([]),
134
+ }),
135
+ )
136
+ .min(1)
137
+ .max(10),
138
+ });
139
+ export type EpicCreateArgs = z.infer<typeof EpicCreateArgsSchema>;
140
+
141
+ /**
142
+ * Result of epic creation
143
+ *
144
+ * Contains the created epic and all subtasks with their IDs.
145
+ */
146
+ export const EpicCreateResultSchema = z.object({
147
+ success: z.boolean(),
148
+ epic: BeadSchema,
149
+ subtasks: z.array(BeadSchema),
150
+ rollback_hint: z.string().optional(),
151
+ });
152
+ export type EpicCreateResult = z.infer<typeof EpicCreateResultSchema>;