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/.beads/analysis/skill-architecture-meta-skills.md +1562 -0
- package/.beads/issues.jsonl +79 -0
- package/README.md +20 -18
- package/VERIFICATION_QUALITY_PATTERNS.md +565 -0
- package/bin/swarm.ts +5 -5
- package/dist/index.js +1318 -28
- package/dist/plugin.js +1218 -14
- package/docs/analysis/subagent-coordination-patterns.md +900 -0
- package/docs/analysis-socratic-planner-pattern.md +504 -0
- package/examples/commands/swarm.md +112 -7
- package/global-skills/swarm-coordination/SKILL.md +118 -20
- package/global-skills/swarm-coordination/references/coordinator-patterns.md +1 -1
- package/package.json +1 -1
- package/src/index.ts +78 -0
- package/src/learning.integration.test.ts +310 -0
- package/src/learning.ts +198 -0
- package/src/mandate-promotion.test.ts +473 -0
- package/src/mandate-promotion.ts +239 -0
- package/src/mandate-storage.test.ts +578 -0
- package/src/mandate-storage.ts +786 -0
- package/src/mandates.ts +540 -0
- package/src/schemas/index.ts +27 -0
- package/src/schemas/mandate.ts +232 -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
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandate schemas for voting system
|
|
3
|
+
*
|
|
4
|
+
* Agents file and vote on ideas, tips, lore, snippets, and feature requests.
|
|
5
|
+
* High-consensus items become "mandates" that influence future behavior.
|
|
6
|
+
*
|
|
7
|
+
* Vote decay and scoring patterns match learning.ts (90-day half-life).
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Core Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Content types for mandate entries
|
|
17
|
+
*/
|
|
18
|
+
export const MandateContentTypeSchema = z.enum([
|
|
19
|
+
"idea",
|
|
20
|
+
"tip",
|
|
21
|
+
"lore",
|
|
22
|
+
"snippet",
|
|
23
|
+
"feature_request",
|
|
24
|
+
]);
|
|
25
|
+
export type MandateContentType = z.infer<typeof MandateContentTypeSchema>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Mandate status lifecycle
|
|
29
|
+
*
|
|
30
|
+
* - candidate: New entry, collecting votes
|
|
31
|
+
* - established: Has some consensus but not enough for mandate status
|
|
32
|
+
* - mandate: High consensus (net_votes >= 5 AND vote_ratio >= 0.7)
|
|
33
|
+
* - rejected: Strong negative consensus or explicitly rejected
|
|
34
|
+
*/
|
|
35
|
+
export const MandateStatusSchema = z.enum([
|
|
36
|
+
"candidate",
|
|
37
|
+
"established",
|
|
38
|
+
"mandate",
|
|
39
|
+
"rejected",
|
|
40
|
+
]);
|
|
41
|
+
export type MandateStatus = z.infer<typeof MandateStatusSchema>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Vote type
|
|
45
|
+
*/
|
|
46
|
+
export const VoteTypeSchema = z.enum(["upvote", "downvote"]);
|
|
47
|
+
export type VoteType = z.infer<typeof VoteTypeSchema>;
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Entry Schema
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A mandate entry represents a proposal from an agent
|
|
55
|
+
*
|
|
56
|
+
* Entries can be ideas, tips, lore, code snippets, or feature requests.
|
|
57
|
+
* Other agents vote on entries to reach consensus.
|
|
58
|
+
*/
|
|
59
|
+
export const MandateEntrySchema = z.object({
|
|
60
|
+
/** Unique ID for this entry */
|
|
61
|
+
id: z.string(),
|
|
62
|
+
/** The actual content of the mandate */
|
|
63
|
+
content: z.string().min(1, "Content required"),
|
|
64
|
+
/** Type of content */
|
|
65
|
+
content_type: MandateContentTypeSchema,
|
|
66
|
+
/** Agent that created this entry */
|
|
67
|
+
author_agent: z.string(),
|
|
68
|
+
/** When this entry was created (ISO-8601) */
|
|
69
|
+
created_at: z.string().datetime({ offset: true }),
|
|
70
|
+
/** Current status */
|
|
71
|
+
status: MandateStatusSchema.default("candidate"),
|
|
72
|
+
/** Optional tags for categorization and search */
|
|
73
|
+
tags: z.array(z.string()).default([]),
|
|
74
|
+
/** Optional metadata (e.g., code language for snippets) */
|
|
75
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
76
|
+
});
|
|
77
|
+
export type MandateEntry = z.infer<typeof MandateEntrySchema>;
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Vote Schema
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A vote on a mandate entry
|
|
85
|
+
*
|
|
86
|
+
* Each agent can vote once per entry (upvote or downvote).
|
|
87
|
+
* Votes decay with 90-day half-life matching learning.ts patterns.
|
|
88
|
+
*/
|
|
89
|
+
export const VoteSchema = z.object({
|
|
90
|
+
/** Unique ID for this vote */
|
|
91
|
+
id: z.string(),
|
|
92
|
+
/** The mandate entry this vote applies to */
|
|
93
|
+
mandate_id: z.string(),
|
|
94
|
+
/** Agent that cast this vote */
|
|
95
|
+
agent_name: z.string(),
|
|
96
|
+
/** Type of vote */
|
|
97
|
+
vote_type: VoteTypeSchema,
|
|
98
|
+
/** When this vote was cast (ISO-8601) */
|
|
99
|
+
timestamp: z.string().datetime({ offset: true }),
|
|
100
|
+
/** Raw vote weight before decay (default: 1.0) */
|
|
101
|
+
weight: z.number().min(0).max(1).default(1.0),
|
|
102
|
+
});
|
|
103
|
+
export type Vote = z.infer<typeof VoteSchema>;
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Score Schema
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Calculated score for a mandate entry
|
|
111
|
+
*
|
|
112
|
+
* Scores are recalculated periodically with decay applied.
|
|
113
|
+
* Uses same decay formula as learning.ts (90-day half-life).
|
|
114
|
+
*/
|
|
115
|
+
export const MandateScoreSchema = z.object({
|
|
116
|
+
/** The mandate entry this score applies to */
|
|
117
|
+
mandate_id: z.string(),
|
|
118
|
+
/** Net votes (upvotes - downvotes) with decay applied */
|
|
119
|
+
net_votes: z.number(),
|
|
120
|
+
/** Vote ratio: upvotes / (upvotes + downvotes) */
|
|
121
|
+
vote_ratio: z.number().min(0).max(1),
|
|
122
|
+
/** Final decayed score for ranking */
|
|
123
|
+
decayed_score: z.number(),
|
|
124
|
+
/** When this score was last calculated (ISO-8601) */
|
|
125
|
+
last_calculated: z.string().datetime({ offset: true }),
|
|
126
|
+
/** Raw vote counts (before decay) */
|
|
127
|
+
raw_upvotes: z.number().int().min(0),
|
|
128
|
+
raw_downvotes: z.number().int().min(0),
|
|
129
|
+
/** Decayed vote counts */
|
|
130
|
+
decayed_upvotes: z.number().min(0),
|
|
131
|
+
decayed_downvotes: z.number().min(0),
|
|
132
|
+
});
|
|
133
|
+
export type MandateScore = z.infer<typeof MandateScoreSchema>;
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Decay Configuration
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Configuration for mandate decay calculation
|
|
141
|
+
*
|
|
142
|
+
* Matches learning.ts decay patterns.
|
|
143
|
+
*/
|
|
144
|
+
export interface MandateDecayConfig {
|
|
145
|
+
/** Half-life for vote decay in days */
|
|
146
|
+
halfLifeDays: number;
|
|
147
|
+
/** Net votes threshold for mandate status */
|
|
148
|
+
mandateNetVotesThreshold: number;
|
|
149
|
+
/** Vote ratio threshold for mandate status */
|
|
150
|
+
mandateVoteRatioThreshold: number;
|
|
151
|
+
/** Net votes threshold for established status */
|
|
152
|
+
establishedNetVotesThreshold: number;
|
|
153
|
+
/** Negative net votes threshold for rejected status */
|
|
154
|
+
rejectedNetVotesThreshold: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const DEFAULT_MANDATE_DECAY_CONFIG: MandateDecayConfig = {
|
|
158
|
+
halfLifeDays: 90,
|
|
159
|
+
mandateNetVotesThreshold: 5,
|
|
160
|
+
mandateVoteRatioThreshold: 0.7,
|
|
161
|
+
establishedNetVotesThreshold: 2,
|
|
162
|
+
rejectedNetVotesThreshold: -3,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// API Schemas
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Arguments for creating a mandate entry
|
|
171
|
+
*/
|
|
172
|
+
export const CreateMandateArgsSchema = z.object({
|
|
173
|
+
content: z.string().min(1, "Content required"),
|
|
174
|
+
content_type: MandateContentTypeSchema,
|
|
175
|
+
tags: z.array(z.string()).default([]),
|
|
176
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
177
|
+
});
|
|
178
|
+
export type CreateMandateArgs = z.infer<typeof CreateMandateArgsSchema>;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Arguments for casting a vote
|
|
182
|
+
*/
|
|
183
|
+
export const CastVoteArgsSchema = z.object({
|
|
184
|
+
mandate_id: z.string(),
|
|
185
|
+
vote_type: VoteTypeSchema,
|
|
186
|
+
weight: z.number().min(0).max(1).default(1.0),
|
|
187
|
+
});
|
|
188
|
+
export type CastVoteArgs = z.infer<typeof CastVoteArgsSchema>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Arguments for querying mandates
|
|
192
|
+
*/
|
|
193
|
+
export const QueryMandatesArgsSchema = z.object({
|
|
194
|
+
status: MandateStatusSchema.optional(),
|
|
195
|
+
content_type: MandateContentTypeSchema.optional(),
|
|
196
|
+
tags: z.array(z.string()).optional(),
|
|
197
|
+
author_agent: z.string().optional(),
|
|
198
|
+
limit: z.number().int().positive().default(20),
|
|
199
|
+
min_score: z.number().optional(),
|
|
200
|
+
});
|
|
201
|
+
export type QueryMandatesArgs = z.infer<typeof QueryMandatesArgsSchema>;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Result of score calculation
|
|
205
|
+
*/
|
|
206
|
+
export const ScoreCalculationResultSchema = z.object({
|
|
207
|
+
mandate_id: z.string(),
|
|
208
|
+
previous_status: MandateStatusSchema,
|
|
209
|
+
new_status: MandateStatusSchema,
|
|
210
|
+
score: MandateScoreSchema,
|
|
211
|
+
status_changed: z.boolean(),
|
|
212
|
+
});
|
|
213
|
+
export type ScoreCalculationResult = z.infer<
|
|
214
|
+
typeof ScoreCalculationResultSchema
|
|
215
|
+
>;
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Exports
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
export const mandateSchemas = {
|
|
222
|
+
MandateContentTypeSchema,
|
|
223
|
+
MandateStatusSchema,
|
|
224
|
+
VoteTypeSchema,
|
|
225
|
+
MandateEntrySchema,
|
|
226
|
+
VoteSchema,
|
|
227
|
+
MandateScoreSchema,
|
|
228
|
+
CreateMandateArgsSchema,
|
|
229
|
+
CastVoteArgsSchema,
|
|
230
|
+
QueryMandatesArgsSchema,
|
|
231
|
+
ScoreCalculationResultSchema,
|
|
232
|
+
};
|
package/src/skills.test.ts
CHANGED
|
@@ -357,6 +357,200 @@ describe("ES module compatibility", () => {
|
|
|
357
357
|
});
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// Tests: CSO Validation
|
|
362
|
+
// ============================================================================
|
|
363
|
+
|
|
364
|
+
import { validateCSOCompliance } from "./skills";
|
|
365
|
+
|
|
366
|
+
describe("validateCSOCompliance", () => {
|
|
367
|
+
describe("description validation", () => {
|
|
368
|
+
it("passes for CSO-compliant description with 'Use when'", () => {
|
|
369
|
+
const warnings = validateCSOCompliance(
|
|
370
|
+
"testing-async",
|
|
371
|
+
"Use when tests have race conditions - replaces arbitrary timeouts with condition polling",
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(warnings.critical).toHaveLength(0);
|
|
375
|
+
expect(warnings.suggestions).toHaveLength(0);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("warns when missing 'Use when...' pattern", () => {
|
|
379
|
+
const warnings = validateCSOCompliance(
|
|
380
|
+
"testing-async",
|
|
381
|
+
"For async testing patterns",
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
expect(warnings.critical).toContain(
|
|
385
|
+
"Description should include 'Use when...' to focus on triggering conditions",
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("warns for first-person voice", () => {
|
|
390
|
+
const warnings = validateCSOCompliance(
|
|
391
|
+
"testing-async",
|
|
392
|
+
"I can help you with async tests when I detect race conditions",
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
expect(warnings.critical.some((w) => w.includes("first-person"))).toBe(
|
|
396
|
+
true,
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("warns for second-person voice", () => {
|
|
401
|
+
const warnings = validateCSOCompliance(
|
|
402
|
+
"testing-async",
|
|
403
|
+
"Use when you need to test async code and your tests have race conditions",
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect(warnings.critical.some((w) => w.includes("second-person"))).toBe(
|
|
407
|
+
true,
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("rejects description > 1024 chars", () => {
|
|
412
|
+
const longDesc = "a".repeat(1025);
|
|
413
|
+
const warnings = validateCSOCompliance("test", longDesc);
|
|
414
|
+
|
|
415
|
+
expect(
|
|
416
|
+
warnings.critical.some(
|
|
417
|
+
(w) => w.includes("1025") && w.includes("max 1024"),
|
|
418
|
+
),
|
|
419
|
+
).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("suggests improvement for description > 500 chars", () => {
|
|
423
|
+
const mediumDesc = "Use when testing. " + "a".repeat(490);
|
|
424
|
+
const warnings = validateCSOCompliance("test", mediumDesc);
|
|
425
|
+
|
|
426
|
+
expect(warnings.critical).toHaveLength(0); // Not critical
|
|
427
|
+
expect(warnings.suggestions.some((w) => w.includes("aim for <500"))).toBe(
|
|
428
|
+
true,
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("accepts description < 500 chars with no length warnings", () => {
|
|
433
|
+
const shortDesc =
|
|
434
|
+
"Use when tests have race conditions - replaces timeouts";
|
|
435
|
+
const warnings = validateCSOCompliance("testing-async", shortDesc);
|
|
436
|
+
|
|
437
|
+
const hasLengthWarning =
|
|
438
|
+
warnings.critical.some((w) => w.includes("chars")) ||
|
|
439
|
+
warnings.suggestions.some((w) => w.includes("chars"));
|
|
440
|
+
|
|
441
|
+
expect(hasLengthWarning).toBe(false);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe("name validation", () => {
|
|
446
|
+
it("accepts gerund-based names", () => {
|
|
447
|
+
const warnings = validateCSOCompliance(
|
|
448
|
+
"testing-async",
|
|
449
|
+
"Use when testing async code",
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const hasNameWarning = warnings.suggestions.some((w) =>
|
|
453
|
+
w.includes("verb-first"),
|
|
454
|
+
);
|
|
455
|
+
expect(hasNameWarning).toBe(false);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("accepts verb-first names", () => {
|
|
459
|
+
const warnings = validateCSOCompliance(
|
|
460
|
+
"validate-schemas",
|
|
461
|
+
"Use when validating schemas",
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const hasNameWarning = warnings.suggestions.some((w) =>
|
|
465
|
+
w.includes("verb-first"),
|
|
466
|
+
);
|
|
467
|
+
expect(hasNameWarning).toBe(false);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("accepts action verbs", () => {
|
|
471
|
+
const actionVerbs = [
|
|
472
|
+
"test-runner",
|
|
473
|
+
"debug-tools",
|
|
474
|
+
"scan-code",
|
|
475
|
+
"check-types",
|
|
476
|
+
"build-artifacts",
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
for (const name of actionVerbs) {
|
|
480
|
+
const warnings = validateCSOCompliance(name, "Use when testing");
|
|
481
|
+
const hasNameWarning = warnings.suggestions.some((w) =>
|
|
482
|
+
w.includes("verb-first"),
|
|
483
|
+
);
|
|
484
|
+
expect(hasNameWarning).toBe(false);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("suggests verb-first for noun-first names", () => {
|
|
489
|
+
const warnings = validateCSOCompliance(
|
|
490
|
+
"async-test",
|
|
491
|
+
"Use when testing async",
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
expect(
|
|
495
|
+
warnings.suggestions.some((w) =>
|
|
496
|
+
w.includes("doesn't follow verb-first"),
|
|
497
|
+
),
|
|
498
|
+
).toBe(true);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("warns for name > 64 chars", () => {
|
|
502
|
+
const longName = "a".repeat(65);
|
|
503
|
+
const warnings = validateCSOCompliance(longName, "Use when testing");
|
|
504
|
+
|
|
505
|
+
expect(warnings.critical.some((w) => w.includes("64 character"))).toBe(
|
|
506
|
+
true,
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("warns for invalid name format", () => {
|
|
511
|
+
const warnings = validateCSOCompliance(
|
|
512
|
+
"Invalid_Name",
|
|
513
|
+
"Use when testing",
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
expect(
|
|
517
|
+
warnings.critical.some((w) =>
|
|
518
|
+
w.includes("lowercase letters, numbers, and hyphens"),
|
|
519
|
+
),
|
|
520
|
+
).toBe(true);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
describe("comprehensive examples", () => {
|
|
525
|
+
it("perfect CSO compliance", () => {
|
|
526
|
+
const warnings = validateCSOCompliance(
|
|
527
|
+
"testing-race-conditions",
|
|
528
|
+
"Use when tests have race conditions - replaces arbitrary timeouts with condition polling and retry logic",
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(warnings.critical).toHaveLength(0);
|
|
532
|
+
expect(warnings.suggestions).toHaveLength(0);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("multiple critical issues", () => {
|
|
536
|
+
const warnings = validateCSOCompliance(
|
|
537
|
+
"BadName_123",
|
|
538
|
+
"I can help you test async code when you need to avoid race conditions. " +
|
|
539
|
+
"a".repeat(1000),
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
expect(warnings.critical.length).toBeGreaterThan(2);
|
|
543
|
+
expect(warnings.critical.some((w) => w.includes("first-person"))).toBe(
|
|
544
|
+
true,
|
|
545
|
+
);
|
|
546
|
+
expect(warnings.critical.some((w) => w.includes("second-person"))).toBe(
|
|
547
|
+
true,
|
|
548
|
+
);
|
|
549
|
+
expect(warnings.critical.some((w) => w.includes("lowercase"))).toBe(true);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
360
554
|
// ============================================================================
|
|
361
555
|
// Tests: Edge Cases
|
|
362
556
|
// ============================================================================
|
package/src/skills.ts
CHANGED
|
@@ -628,6 +628,167 @@ Use this to access supplementary skill resources.`,
|
|
|
628
628
|
*/
|
|
629
629
|
const DEFAULT_SKILLS_DIR = ".opencode/skills";
|
|
630
630
|
|
|
631
|
+
// =============================================================================
|
|
632
|
+
// CSO (Claude Search Optimization) Validation
|
|
633
|
+
// =============================================================================
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* CSO validation warnings for skill metadata
|
|
637
|
+
*/
|
|
638
|
+
export interface CSOValidationWarnings {
|
|
639
|
+
/** Critical warnings (strong indicators of poor discoverability) */
|
|
640
|
+
critical: string[];
|
|
641
|
+
/** Suggestions for improvement */
|
|
642
|
+
suggestions: string[];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Validate skill metadata against Claude Search Optimization best practices
|
|
647
|
+
*
|
|
648
|
+
* Checks:
|
|
649
|
+
* - 'Use when...' format in description
|
|
650
|
+
* - Description length (warn > 500, max 1024)
|
|
651
|
+
* - Third-person voice (no 'I', 'you')
|
|
652
|
+
* - Name conventions (verb-first, gerunds, hyphens)
|
|
653
|
+
*
|
|
654
|
+
* @returns Warnings object with critical issues and suggestions
|
|
655
|
+
*/
|
|
656
|
+
export function validateCSOCompliance(
|
|
657
|
+
name: string,
|
|
658
|
+
description: string,
|
|
659
|
+
): CSOValidationWarnings {
|
|
660
|
+
const warnings: CSOValidationWarnings = {
|
|
661
|
+
critical: [],
|
|
662
|
+
suggestions: [],
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// Description: Check for 'Use when...' pattern
|
|
666
|
+
const hasUseWhen = /\buse when\b/i.test(description);
|
|
667
|
+
if (!hasUseWhen) {
|
|
668
|
+
warnings.critical.push(
|
|
669
|
+
"Description should include 'Use when...' to focus on triggering conditions",
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Description: Length checks
|
|
674
|
+
if (description.length > 1024) {
|
|
675
|
+
warnings.critical.push(
|
|
676
|
+
`Description is ${description.length} chars (max 1024) - will be rejected`,
|
|
677
|
+
);
|
|
678
|
+
} else if (description.length > 500) {
|
|
679
|
+
warnings.suggestions.push(
|
|
680
|
+
`Description is ${description.length} chars (aim for <500 for optimal discoverability)`,
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Description: Third-person check (no 'I', 'you')
|
|
685
|
+
const firstPersonPattern = /\b(I|I'm|I'll|my|mine|myself)\b/i;
|
|
686
|
+
const secondPersonPattern = /\b(you|you're|you'll|your|yours|yourself)\b/i;
|
|
687
|
+
|
|
688
|
+
if (firstPersonPattern.test(description)) {
|
|
689
|
+
warnings.critical.push(
|
|
690
|
+
"Description uses first-person ('I', 'my') - skills are injected into system prompt, use third-person only",
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (secondPersonPattern.test(description)) {
|
|
695
|
+
warnings.critical.push(
|
|
696
|
+
"Description uses second-person ('you', 'your') - use third-person voice (e.g., 'Handles X' not 'You can handle X')",
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Name: Check for verb-first/gerund patterns
|
|
701
|
+
const nameWords = name.split("-");
|
|
702
|
+
const firstWord = nameWords[0];
|
|
703
|
+
|
|
704
|
+
// Common gerund endings: -ing
|
|
705
|
+
// Common verb forms: -ing, -ize, -ify, -ate
|
|
706
|
+
const isGerund = /ing$/.test(firstWord);
|
|
707
|
+
const isVerbForm = /(ing|ize|ify|ate)$/.test(firstWord);
|
|
708
|
+
|
|
709
|
+
if (!isGerund && !isVerbForm) {
|
|
710
|
+
// Check if it's a common action verb
|
|
711
|
+
const actionVerbs = [
|
|
712
|
+
"test",
|
|
713
|
+
"debug",
|
|
714
|
+
"fix",
|
|
715
|
+
"scan",
|
|
716
|
+
"check",
|
|
717
|
+
"validate",
|
|
718
|
+
"create",
|
|
719
|
+
"build",
|
|
720
|
+
"deploy",
|
|
721
|
+
"run",
|
|
722
|
+
"load",
|
|
723
|
+
"fetch",
|
|
724
|
+
"parse",
|
|
725
|
+
];
|
|
726
|
+
const startsWithAction = actionVerbs.includes(firstWord);
|
|
727
|
+
|
|
728
|
+
if (!startsWithAction) {
|
|
729
|
+
warnings.suggestions.push(
|
|
730
|
+
`Name '${name}' doesn't follow verb-first pattern. Consider gerunds (e.g., 'testing-skills' not 'test-skill') or action verbs for better clarity`,
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Name: Check length
|
|
736
|
+
if (name.length > 64) {
|
|
737
|
+
warnings.critical.push(
|
|
738
|
+
`Name exceeds 64 character limit (${name.length} chars)`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Name: Validate format (already enforced by schema, but good to document)
|
|
743
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
744
|
+
warnings.critical.push(
|
|
745
|
+
"Name must be lowercase letters, numbers, and hyphens only",
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return warnings;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Format CSO warnings into a readable message for tool output
|
|
754
|
+
*/
|
|
755
|
+
function formatCSOWarnings(warnings: CSOValidationWarnings): string | null {
|
|
756
|
+
if (warnings.critical.length === 0 && warnings.suggestions.length === 0) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const parts: string[] = [];
|
|
761
|
+
|
|
762
|
+
if (warnings.critical.length > 0) {
|
|
763
|
+
parts.push("**CSO Critical Issues:**");
|
|
764
|
+
for (const warning of warnings.critical) {
|
|
765
|
+
parts.push(` ⚠️ ${warning}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (warnings.suggestions.length > 0) {
|
|
770
|
+
parts.push("\n**CSO Suggestions:**");
|
|
771
|
+
for (const suggestion of warnings.suggestions) {
|
|
772
|
+
parts.push(` 💡 ${suggestion}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
parts.push("\n**CSO Guide:**");
|
|
777
|
+
parts.push(
|
|
778
|
+
" • Start description with 'Use when...' (focus on triggering conditions)",
|
|
779
|
+
);
|
|
780
|
+
parts.push(" • Keep description <500 chars (max 1024)");
|
|
781
|
+
parts.push(" • Use third-person voice only (injected into system prompt)");
|
|
782
|
+
parts.push(
|
|
783
|
+
" • Name: verb-first or gerunds (e.g., 'testing-async' not 'async-test')",
|
|
784
|
+
);
|
|
785
|
+
parts.push(
|
|
786
|
+
"\n Example: 'Use when tests have race conditions - replaces arbitrary timeouts with condition polling'",
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
return parts.join("\n");
|
|
790
|
+
}
|
|
791
|
+
|
|
631
792
|
/**
|
|
632
793
|
* Quote a YAML scalar if it contains special characters
|
|
633
794
|
* Uses double quotes and escapes internal quotes/newlines
|
|
@@ -749,6 +910,9 @@ Good skills have:
|
|
|
749
910
|
return `Skill '${args.name}' already exists at ${existing.path}. Use skills_update to modify it.`;
|
|
750
911
|
}
|
|
751
912
|
|
|
913
|
+
// Validate CSO compliance (advisory warnings only)
|
|
914
|
+
const csoWarnings = validateCSOCompliance(args.name, args.description);
|
|
915
|
+
|
|
752
916
|
// Determine target directory
|
|
753
917
|
let skillDir: string;
|
|
754
918
|
if (args.directory === "global") {
|
|
@@ -778,21 +942,26 @@ Good skills have:
|
|
|
778
942
|
// Invalidate cache so new skill is discoverable
|
|
779
943
|
invalidateSkillsCache();
|
|
780
944
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
);
|
|
945
|
+
// Build response with CSO warnings if present
|
|
946
|
+
const response: Record<string, unknown> = {
|
|
947
|
+
success: true,
|
|
948
|
+
skill: args.name,
|
|
949
|
+
path: skillPath,
|
|
950
|
+
message: `Created skill '${args.name}'. It's now discoverable via skills_list.`,
|
|
951
|
+
next_steps: [
|
|
952
|
+
"Test with skills_use to verify instructions are clear",
|
|
953
|
+
"Add examples.md or reference.md for supplementary content",
|
|
954
|
+
"Add scripts/ directory for executable helpers",
|
|
955
|
+
],
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// Add CSO warnings if any
|
|
959
|
+
const warningsMessage = formatCSOWarnings(csoWarnings);
|
|
960
|
+
if (warningsMessage) {
|
|
961
|
+
response.cso_warnings = warningsMessage;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return JSON.stringify(response, null, 2);
|
|
796
965
|
} catch (error) {
|
|
797
966
|
return `Failed to create skill: ${error instanceof Error ? error.message : String(error)}`;
|
|
798
967
|
}
|
|
@@ -1231,10 +1231,10 @@ describe("Graceful Degradation", () => {
|
|
|
1231
1231
|
mockContext,
|
|
1232
1232
|
);
|
|
1233
1233
|
|
|
1234
|
-
// Check that
|
|
1234
|
+
// Check that swarm-mail discipline is in the prompt
|
|
1235
1235
|
expect(result).toContain("MANDATORY");
|
|
1236
|
-
expect(result).toContain("
|
|
1237
|
-
expect(result).toContain("
|
|
1236
|
+
expect(result).toContain("Swarm Mail");
|
|
1237
|
+
expect(result).toContain("swarmmail_send");
|
|
1238
1238
|
expect(result).toContain("Report progress");
|
|
1239
1239
|
});
|
|
1240
1240
|
});
|
|
@@ -1243,7 +1243,7 @@ describe("Graceful Degradation", () => {
|
|
|
1243
1243
|
// Coordinator-Centric Swarm Tools (V2)
|
|
1244
1244
|
// ============================================================================
|
|
1245
1245
|
|
|
1246
|
-
describe("Swarm Prompt V2 (with
|
|
1246
|
+
describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
|
|
1247
1247
|
describe("formatSubtaskPromptV2", () => {
|
|
1248
1248
|
it("generates correct prompt with all fields", () => {
|
|
1249
1249
|
const result = formatSubtaskPromptV2({
|