opencode-swarm 7.20.2 → 7.21.1
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/dist/cli/index.js +1 -1
- package/dist/hooks/skill-propagation-gate.d.ts +113 -0
- package/dist/hooks/skill-scoring.d.ts +127 -0
- package/dist/hooks/skill-usage-log.d.ts +109 -0
- package/dist/index.js +1476 -759
- package/dist/tools/update-task-status.d.ts +4 -5
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var package_default;
|
|
|
34
34
|
var init_package = __esm(() => {
|
|
35
35
|
package_default = {
|
|
36
36
|
name: "opencode-swarm",
|
|
37
|
-
version: "7.
|
|
37
|
+
version: "7.21.1",
|
|
38
38
|
description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
39
39
|
main: "dist/index.js",
|
|
40
40
|
types: "dist/index.d.ts",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill propagation gate — warns when the architect delegates to a
|
|
3
|
+
* skill-capable agent without providing a SKILLS field.
|
|
4
|
+
*
|
|
5
|
+
* Two integration points:
|
|
6
|
+
*
|
|
7
|
+
* 1. `tool.execute.before` — when the architect delegates via the Task
|
|
8
|
+
* tool to a skill-capable agent (coder, reviewer, test_engineer, sme,
|
|
9
|
+
* docs, designer) and the SKILLS field is missing or "none" while
|
|
10
|
+
* skills exist in the project, appends a warning event to
|
|
11
|
+
* `.swarm/events.jsonl`. This is a SOFT WARNING — it NEVER blocks
|
|
12
|
+
* tool execution. Also records skill delegation entries to
|
|
13
|
+
* `.swarm/skill-usage.jsonl` for auditability.
|
|
14
|
+
*
|
|
15
|
+
* 2. `experimental.chat.messages.transform` — scans reviewer output
|
|
16
|
+
* for SKILL_COMPLIANCE verdicts and records compliance outcomes
|
|
17
|
+
* to `.swarm/skill-usage.jsonl`.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import type { MessageWithParts } from './knowledge-types.js';
|
|
21
|
+
import { computeSkillRelevanceScore } from './skill-scoring.js';
|
|
22
|
+
import { appendSkillUsageEntry, readSkillUsageEntries, readSkillUsageEntriesTail } from './skill-usage-log.js';
|
|
23
|
+
/** Agents that should receive skill context in delegations. */
|
|
24
|
+
export declare const SKILL_CAPABLE_AGENTS: Set<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Maximum number of session-scoped skill-usage entries to process for
|
|
27
|
+
* skill scoring. When a session accumulates more entries than this
|
|
28
|
+
* limit, scoring is skipped to prevent unbounded file reads from
|
|
29
|
+
* stalling every delegated Task call.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MAX_SCORING_SESSION_ENTRIES = 500;
|
|
32
|
+
export interface SkillGateInput {
|
|
33
|
+
tool: unknown;
|
|
34
|
+
agent?: unknown;
|
|
35
|
+
sessionID?: unknown;
|
|
36
|
+
args?: unknown;
|
|
37
|
+
}
|
|
38
|
+
export interface SkillPropagationConfig {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare const _internals: {
|
|
42
|
+
readdirSync: typeof fs.readdirSync;
|
|
43
|
+
existsSync: typeof fs.existsSync;
|
|
44
|
+
statSync: typeof fs.statSync;
|
|
45
|
+
mkdirSync: typeof fs.mkdirSync;
|
|
46
|
+
appendFileSync: typeof fs.appendFileSync;
|
|
47
|
+
skillPropagationGateBefore: typeof skillPropagationGateBefore;
|
|
48
|
+
skillPropagationTransformScan: typeof skillPropagationTransformScan;
|
|
49
|
+
SKILL_CAPABLE_AGENTS: Set<string>;
|
|
50
|
+
MAX_SCORING_SESSION_ENTRIES: number;
|
|
51
|
+
writeWarnEvent: typeof writeWarnEvent;
|
|
52
|
+
discoverAvailableSkills: typeof discoverAvailableSkills;
|
|
53
|
+
parseDelegationArgs: typeof parseDelegationArgs;
|
|
54
|
+
appendSkillUsageEntry: typeof appendSkillUsageEntry;
|
|
55
|
+
readSkillUsageEntries: typeof readSkillUsageEntries;
|
|
56
|
+
readSkillUsageEntriesTail: typeof readSkillUsageEntriesTail;
|
|
57
|
+
parseSkillPaths: typeof parseSkillPaths;
|
|
58
|
+
extractTaskIdFromPrompt: typeof extractTaskIdFromPrompt;
|
|
59
|
+
computeSkillRelevanceScore: typeof computeSkillRelevanceScore;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Scans project for available skill SKILL.md files.
|
|
63
|
+
* Returns array of relative skill paths (e.g., '.claude/skills/writing-tests/SKILL.md').
|
|
64
|
+
* Best-effort: returns empty array on any error.
|
|
65
|
+
*/
|
|
66
|
+
export declare function discoverAvailableSkills(directory: string): string[];
|
|
67
|
+
/**
|
|
68
|
+
* Parse delegation args to extract target agent name and SKILLS field.
|
|
69
|
+
* Returns { targetAgent, skillsField } or null if not parseable.
|
|
70
|
+
*
|
|
71
|
+
* The args for Task tool are a JSON object with a "subagent_type" field
|
|
72
|
+
* (e.g., "mega_coder") which is the authoritative target agent name, and
|
|
73
|
+
* a "prompt" field containing the delegation text. The SKILLS: line from
|
|
74
|
+
* the prompt (if present) provides the skills field value.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseDelegationArgs(args: unknown): {
|
|
77
|
+
targetAgent: string;
|
|
78
|
+
skillsField: string;
|
|
79
|
+
} | null;
|
|
80
|
+
/** Write a warning event to .swarm/events.jsonl (sync, best-effort). */
|
|
81
|
+
export declare function writeWarnEvent(directory: string, record: Record<string, unknown>): void;
|
|
82
|
+
/**
|
|
83
|
+
* Parse a SKILLS or SKILLS_USED_BY_CODER field value into individual skill paths.
|
|
84
|
+
* Handles comma-separated paths and strips surrounding whitespace.
|
|
85
|
+
* Returns empty array for empty, "none", or unparseable values.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseSkillPaths(fieldValue: string): string[];
|
|
88
|
+
/**
|
|
89
|
+
* Extract a task ID from a delegation prompt string.
|
|
90
|
+
* Looks for patterns like "taskId: <id>" or "TASK: <id>" (case-insensitive).
|
|
91
|
+
* Returns "unknown" when no pattern is found.
|
|
92
|
+
*/
|
|
93
|
+
export declare function extractTaskIdFromPrompt(prompt: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Pre-tool gate. When the architect delegates via Task tool to a skill-capable
|
|
96
|
+
* agent and the SKILLS field is missing or 'none' while skills exist in the
|
|
97
|
+
* project, logs a warning event to events.jsonl. NEVER blocks execution.
|
|
98
|
+
*
|
|
99
|
+
* Also records skill delegation entries to `.swarm/skill-usage.jsonl` when
|
|
100
|
+
* the architect delegates to a skill-capable agent with a non-empty, non-"none"
|
|
101
|
+
* SKILLS field.
|
|
102
|
+
*/
|
|
103
|
+
export declare function skillPropagationGateBefore(directory: string, input: SkillGateInput, config: SkillPropagationConfig): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Chat messages transform hook. Scans reviewer output for SKILL_COMPLIANCE
|
|
106
|
+
* verdicts and records compliance outcomes to `.swarm/skill-usage.jsonl`.
|
|
107
|
+
* Also scans architect messages for skill delegation patterns.
|
|
108
|
+
*
|
|
109
|
+
* Best-effort: never throws; never mutates the messages array.
|
|
110
|
+
*/
|
|
111
|
+
export declare function skillPropagationTransformScan(directory: string, output: {
|
|
112
|
+
messages?: MessageWithParts[];
|
|
113
|
+
}, sessionID?: string): Promise<void>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill scoring — computes skill relevance scores based on historical
|
|
3
|
+
* usage data from `.swarm/skill-usage.jsonl`.
|
|
4
|
+
*
|
|
5
|
+
* All public functions are pure over their inputs. File I/O goes through
|
|
6
|
+
* `readSkillUsageEntries` (imported from `skill-usage-log.ts`).
|
|
7
|
+
* An `_internals` DI seam mirrors the pattern in `skill-usage-log.ts`
|
|
8
|
+
* and `skill-propagation-gate.ts` for testability.
|
|
9
|
+
*/
|
|
10
|
+
import { type SkillUsageEntry } from './skill-usage-log.js';
|
|
11
|
+
/** Per-skill ranking entry returned by `rankSkillsForContext`. */
|
|
12
|
+
export interface SkillRankEntry {
|
|
13
|
+
/** Repo-relative path to the skill file. */
|
|
14
|
+
skillPath: string;
|
|
15
|
+
/** Composite relevance score in [0, 1]. */
|
|
16
|
+
score: number;
|
|
17
|
+
/** Total number of recorded usages for this skill. */
|
|
18
|
+
usageCount: number;
|
|
19
|
+
/** Ratio of compliant verdicts to total verdicts in [0, 1]. */
|
|
20
|
+
complianceRate: number;
|
|
21
|
+
}
|
|
22
|
+
/** Aggregate statistics for a single skill. */
|
|
23
|
+
export interface SkillStats {
|
|
24
|
+
/** Total number of recorded usages. */
|
|
25
|
+
totalUsage: number;
|
|
26
|
+
/** Ratio of compliant verdicts to total verdicts in [0, 1]. */
|
|
27
|
+
complianceRate: number;
|
|
28
|
+
/** ISO 8601 timestamp of the most recent usage, or empty string. */
|
|
29
|
+
lastUsed: string;
|
|
30
|
+
/** Top agents by usage count, sorted descending. */
|
|
31
|
+
topAgents: Array<{
|
|
32
|
+
agent: string;
|
|
33
|
+
count: number;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare const _internals: {
|
|
37
|
+
computeSkillRelevanceScore: typeof computeSkillRelevanceScore;
|
|
38
|
+
rankSkillsForContext: typeof rankSkillsForContext;
|
|
39
|
+
getSkillStats: typeof getSkillStats;
|
|
40
|
+
formatSkillIndexWithContext: typeof formatSkillIndexWithContext;
|
|
41
|
+
extractSkillName: typeof extractSkillName;
|
|
42
|
+
computeRecencyScore: typeof computeRecencyScore;
|
|
43
|
+
computeContextMatchScore: typeof computeContextMatchScore;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Extracts a human-readable skill name from its file path.
|
|
47
|
+
* E.g. `.claude/skills/writing-tests/SKILL.md` → `writing-tests`
|
|
48
|
+
*/
|
|
49
|
+
declare function extractSkillName(skillPath: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Computes a recency score in [0, 1] based on how recently the skill was used.
|
|
52
|
+
* Full score (1.0) for usage within 24 hours, linearly decaying to 0 over 30 days.
|
|
53
|
+
*/
|
|
54
|
+
declare function computeRecencyScore(lastUsedTimestamp: string): number;
|
|
55
|
+
/**
|
|
56
|
+
* Computes a keyword-overlap context match score between a task description and a skill.
|
|
57
|
+
*
|
|
58
|
+
* Extracts keywords from both the task description and the skill's path + name,
|
|
59
|
+
* then returns the ratio of matching keywords to the total task keywords.
|
|
60
|
+
* Returns 0 when the task description has no extractable keywords.
|
|
61
|
+
*
|
|
62
|
+
* @param taskDescription - Free-text description of the current task.
|
|
63
|
+
* @param skillPath - Repo-relative path to the skill file.
|
|
64
|
+
* @returns Context match score in [0, 1].
|
|
65
|
+
*/
|
|
66
|
+
declare function computeContextMatchScore(taskDescription: string, skillPath: string): number;
|
|
67
|
+
/**
|
|
68
|
+
* Compute a composite relevance score for a skill based on its usage history
|
|
69
|
+
* and keyword overlap with the task description.
|
|
70
|
+
*
|
|
71
|
+
* Spec compliance: FR-002 requires scoring "based on historical usage data using the skill
|
|
72
|
+
* usage log and task descriptions." This function uses the usage log (frequency, compliance,
|
|
73
|
+
* recency, taskID diversity) and the current task description (keyword overlap with skill
|
|
74
|
+
* name/path) to produce context-aware rankings.
|
|
75
|
+
*
|
|
76
|
+
* Formula (all components clamped to [0, 1]):
|
|
77
|
+
* frequencyScore = min(1.0, usageCount / FREQUENCY_CAP) * FREQUENCY_WEIGHT
|
|
78
|
+
* complianceScore = (compliantCount / max(1, totalWithVerdict)) * COMPLIANCE_WEIGHT
|
|
79
|
+
* recencyScore = linearDecay(lastUsed) * RECENCY_WEIGHT
|
|
80
|
+
* taskDiversityScore = (distinctTaskIDs / max(1, usageHistory.length)) * TASK_DIVERSITY_WEIGHT
|
|
81
|
+
* contextScore = keywordOverlap(taskDescription, skillPath) * CONTEXT_WEIGHT
|
|
82
|
+
* total = frequencyScore + complianceScore + recencyScore + taskDiversityScore + contextScore
|
|
83
|
+
*
|
|
84
|
+
* The context component ensures different task descriptions produce different
|
|
85
|
+
* rankings: keywords from the task description are matched against the skill's
|
|
86
|
+
* file path and name via simple set intersection.
|
|
87
|
+
*
|
|
88
|
+
* @param skillPath - Repo-relative path to the skill file.
|
|
89
|
+
* @param taskDescription - Free-text description of the current task.
|
|
90
|
+
* @param usageHistory - Pre-loaded usage entries for this skill.
|
|
91
|
+
* @returns Composite score in [0, 1]. Returns contextScore only when history is empty.
|
|
92
|
+
*/
|
|
93
|
+
export declare function computeSkillRelevanceScore(skillPath: string, taskDescription: string, usageHistory: SkillUsageEntry[]): number;
|
|
94
|
+
/**
|
|
95
|
+
* Rank an array of skill paths by relevance to a task context.
|
|
96
|
+
*
|
|
97
|
+
* Reads `.swarm/skill-usage.jsonl` for historical data, computes composite
|
|
98
|
+
* scores for each skill, and returns entries sorted by score descending.
|
|
99
|
+
*
|
|
100
|
+
* @param skills - Array of repo-relative skill paths.
|
|
101
|
+
* @param taskContext - Free-text description of the task.
|
|
102
|
+
* @param directory - Project root directory (used to locate `.swarm/`).
|
|
103
|
+
* @returns Sorted array of `SkillRankEntry`, highest score first.
|
|
104
|
+
*/
|
|
105
|
+
export declare function rankSkillsForContext(skills: string[], taskContext: string, directory: string): SkillRankEntry[];
|
|
106
|
+
/**
|
|
107
|
+
* Read usage log and compute aggregate statistics for a single skill.
|
|
108
|
+
*
|
|
109
|
+
* @param skillPath - Repo-relative path to the skill file.
|
|
110
|
+
* @param directory - Project root directory.
|
|
111
|
+
* @returns `SkillStats` with aggregate metrics. Returns zeros/empty when log is missing.
|
|
112
|
+
*/
|
|
113
|
+
export declare function getSkillStats(skillPath: string, directory: string): SkillStats;
|
|
114
|
+
/**
|
|
115
|
+
* Produce a formatted skill index string with contextual usage stats.
|
|
116
|
+
*
|
|
117
|
+
* Each line looks like:
|
|
118
|
+
* `engineering-conventions: Engineering invariants (used: 12, compliance: 95%) → coder, reviewer`
|
|
119
|
+
*
|
|
120
|
+
* Falls back to a simple index without stats if the usage log does not exist.
|
|
121
|
+
*
|
|
122
|
+
* @param skills - Array of repo-relative skill paths.
|
|
123
|
+
* @param directory - Project root directory.
|
|
124
|
+
* @returns Multi-line formatted string, one line per skill.
|
|
125
|
+
*/
|
|
126
|
+
export declare function formatSkillIndexWithContext(skills: string[], directory: string): string;
|
|
127
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill usage log — tracks skill delegations and compliance outcomes.
|
|
3
|
+
*
|
|
4
|
+
* Writes one JSONL line per skill-usage event to `.swarm/skill-usage.jsonl`.
|
|
5
|
+
* Follows the same append-only JSONL pattern as knowledge-application.jsonl.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
/** Single entry in the skill-usage audit log. */
|
|
9
|
+
export interface SkillUsageEntry {
|
|
10
|
+
/** Auto-generated unique identifier (UUID v4). */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Repo-relative path to the skill file. */
|
|
13
|
+
skillPath: string;
|
|
14
|
+
/** Name of the agent receiving the skill. */
|
|
15
|
+
agentName: string;
|
|
16
|
+
/** Plan task ID the skill was loaded for. */
|
|
17
|
+
taskID: string;
|
|
18
|
+
/** ISO 8601 timestamp of the event. */
|
|
19
|
+
timestamp: string;
|
|
20
|
+
/** Compliance outcome — 'compliant' | 'violation' | 'partial' | 'not_checked' | custom. */
|
|
21
|
+
complianceVerdict: string;
|
|
22
|
+
/** Optional free-text notes from the reviewer. */
|
|
23
|
+
reviewerNotes?: string;
|
|
24
|
+
/** Session identifier. */
|
|
25
|
+
sessionID: string;
|
|
26
|
+
}
|
|
27
|
+
/** Filter options for reading skill-usage entries. */
|
|
28
|
+
export interface SkillUsageFilterOptions {
|
|
29
|
+
/** Filter entries by session ID (exact match). */
|
|
30
|
+
sessionID?: string;
|
|
31
|
+
/** Filter entries by skill path (exact match). */
|
|
32
|
+
skillPath?: string;
|
|
33
|
+
/** Filter entries by agent name (exact match). */
|
|
34
|
+
agentName?: string;
|
|
35
|
+
/** Filter entries by plan task ID (exact match). */
|
|
36
|
+
taskID?: string;
|
|
37
|
+
/** Filter entries to timestamps within this ISO 8601 range (inclusive). */
|
|
38
|
+
dateRange?: {
|
|
39
|
+
start: string;
|
|
40
|
+
end: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Return value from prune operations. */
|
|
44
|
+
export interface PruneResult {
|
|
45
|
+
/** Number of entries removed. */
|
|
46
|
+
pruned: number;
|
|
47
|
+
/** Number of entries remaining in the log. */
|
|
48
|
+
remaining: number;
|
|
49
|
+
/** Error message when the write/rename step fails; absent on success. */
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Test-only dependency-injection seam. Tests override these without
|
|
54
|
+
* `mock.module` (which leaks across files in Bun's shared test-runner).
|
|
55
|
+
* Restore in `afterEach`.
|
|
56
|
+
*/
|
|
57
|
+
export declare const _internals: {
|
|
58
|
+
generateId: () => string;
|
|
59
|
+
appendFileSync: typeof fs.appendFileSync;
|
|
60
|
+
readFileSync: typeof fs.readFileSync;
|
|
61
|
+
writeFileSync: typeof fs.writeFileSync;
|
|
62
|
+
renameSync: typeof fs.renameSync;
|
|
63
|
+
mkdirSync: typeof fs.mkdirSync;
|
|
64
|
+
existsSync: typeof fs.existsSync;
|
|
65
|
+
statSync: any;
|
|
66
|
+
openSync: typeof fs.openSync;
|
|
67
|
+
readSync: typeof fs.readSync;
|
|
68
|
+
closeSync: typeof fs.closeSync;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Validate and append a single skill-usage entry to the JSONL log.
|
|
72
|
+
*
|
|
73
|
+
* The `id` field is auto-generated; callers provide all other fields.
|
|
74
|
+
* Uses synchronous I/O for consistency with the JSONL append pattern.
|
|
75
|
+
*/
|
|
76
|
+
export declare function appendSkillUsageEntry(directory: string, entry: Omit<SkillUsageEntry, 'id'>): void;
|
|
77
|
+
/**
|
|
78
|
+
* Read and parse skill-usage entries from the JSONL log, optionally filtered.
|
|
79
|
+
*
|
|
80
|
+
* Malformed lines are silently skipped (no throw). Returns an empty array
|
|
81
|
+
* if the log file does not exist.
|
|
82
|
+
*/
|
|
83
|
+
export declare function readSkillUsageEntries(directory: string, options?: SkillUsageFilterOptions): SkillUsageEntry[];
|
|
84
|
+
/** Default maximum bytes to read from the end of the log file. */
|
|
85
|
+
export declare const TAIL_BYTES_DEFAULT: number;
|
|
86
|
+
/**
|
|
87
|
+
* Read the last `maxBytes` of the skill-usage JSONL log and parse matching
|
|
88
|
+
* entries. Much faster than `readSkillUsageEntries` for large logs because
|
|
89
|
+
* it reads only a bounded number of bytes from the end of the file instead
|
|
90
|
+
* of loading the entire file into memory.
|
|
91
|
+
*
|
|
92
|
+
* Uses low-level `openSync` / `readSync` / `closeSync` to seek to the last
|
|
93
|
+
* `maxBytes` of the file. Skips the first (potentially partial) line that
|
|
94
|
+
* results from starting mid-file. Best-effort: returns an empty array on any
|
|
95
|
+
* I/O or parse error.
|
|
96
|
+
*/
|
|
97
|
+
export declare function readSkillUsageEntriesTail(directory: string, filters: {
|
|
98
|
+
sessionID?: string;
|
|
99
|
+
}, maxBytes?: number): SkillUsageEntry[];
|
|
100
|
+
/**
|
|
101
|
+
* Prune the skill-usage log, keeping at most `maxEntriesPerSkill` entries
|
|
102
|
+
* per unique skillPath. Oldest entries beyond the limit are removed.
|
|
103
|
+
*
|
|
104
|
+
* Writes atomically (temp file + rename). No-op if the log file doesn't
|
|
105
|
+
* exist or all skills are within their limits.
|
|
106
|
+
*
|
|
107
|
+
* @returns Stats about how many entries were pruned and how many remain.
|
|
108
|
+
*/
|
|
109
|
+
export declare function pruneSkillUsageLog(directory: string, maxEntriesPerSkill?: number): PruneResult;
|