skillops-cli 0.0.0-stage → 1.2.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/.github/agents/address-review.agent.md +14 -0
- package/.github/agents/docs-sync.agent.md +18 -0
- package/.github/agents/pr-brief.agent.md +24 -0
- package/.github/agents/skill-auditor.agent.md +22 -0
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/SKILL_CONTRACT.md +146 -0
- package/catalog/ai-engineering/agent-harness-auditor/SKILL.md +44 -0
- package/catalog/ai-engineering/agent-harness-auditor/evals/benchmark.json +22 -0
- package/catalog/ai-engineering/agent-harness-auditor/evals/evals.json +40 -0
- package/catalog/ai-engineering/agent-harness-auditor/scripts/harness_linter.py +101 -0
- package/catalog/ai-engineering/context-pruner-advisor/SKILL.md +44 -0
- package/catalog/ai-engineering/context-pruner-advisor/evals/benchmark.json +22 -0
- package/catalog/ai-engineering/context-pruner-advisor/evals/evals.json +40 -0
- package/catalog/ai-engineering/context-pruner-advisor/scripts/context_pruner.py +114 -0
- package/catalog/ai-engineering/mcp-architect/SKILL.md +45 -0
- package/catalog/ai-engineering/mcp-architect/evals/benchmark.json +22 -0
- package/catalog/ai-engineering/mcp-architect/evals/evals.json +54 -0
- package/catalog/ai-engineering/mcp-architect/scripts/validate_mcp_server.py +156 -0
- package/catalog/ai-engineering/prompt-red-teaming/SKILL.md +43 -0
- package/catalog/ai-engineering/prompt-red-teaming/evals/benchmark.json +22 -0
- package/catalog/ai-engineering/prompt-red-teaming/evals/evals.json +40 -0
- package/catalog/ai-engineering/prompt-red-teaming/scripts/injection_scanner.py +79 -0
- package/catalog/arch-quality/ast-blast-radius/SKILL.md +41 -0
- package/catalog/arch-quality/ast-blast-radius/evals/benchmark.json +22 -0
- package/catalog/arch-quality/ast-blast-radius/evals/evals.json +40 -0
- package/catalog/arch-quality/ast-blast-radius/scripts/analyze_call_graph.py +114 -0
- package/catalog/arch-quality/bug-reproducer-tdd/SKILL.md +42 -0
- package/catalog/arch-quality/bug-reproducer-tdd/evals/benchmark.json +22 -0
- package/catalog/arch-quality/bug-reproducer-tdd/evals/evals.json +40 -0
- package/catalog/arch-quality/bug-reproducer-tdd/scripts/repro_harness_check.py +129 -0
- package/catalog/arch-quality/security-sast-gate/SKILL.md +42 -0
- package/catalog/arch-quality/security-sast-gate/evals/benchmark.json +22 -0
- package/catalog/arch-quality/security-sast-gate/evals/evals.json +40 -0
- package/catalog/arch-quality/security-sast-gate/scripts/owasp_ast_scanner.py +153 -0
- package/catalog/python-backend/concurrency-race-detector/SKILL.md +43 -0
- package/catalog/python-backend/concurrency-race-detector/evals/benchmark.json +22 -0
- package/catalog/python-backend/concurrency-race-detector/evals/evals.json +40 -0
- package/catalog/python-backend/concurrency-race-detector/scripts/event_loop_blocking_scanner.py +127 -0
- package/catalog/python-backend/fastapi-production/SKILL.md +44 -0
- package/catalog/python-backend/fastapi-production/evals/benchmark.json +22 -0
- package/catalog/python-backend/fastapi-production/evals/evals.json +54 -0
- package/catalog/python-backend/fastapi-production/scripts/audit_fastapi_routes.py +169 -0
- package/catalog/python-backend/postgres-query-doctor/SKILL.md +44 -0
- package/catalog/python-backend/postgres-query-doctor/evals/benchmark.json +22 -0
- package/catalog/python-backend/postgres-query-doctor/evals/evals.json +54 -0
- package/catalog/python-backend/postgres-query-doctor/scripts/analyze_sql_safety.py +130 -0
- package/catalog/subagents/blast-radius-auditor/SUBAGENT.md +26 -0
- package/catalog/subagents/context-scout/SUBAGENT.md +23 -0
- package/catalog/subagents/red-team-breaker/SUBAGENT.md +22 -0
- package/catalog/subagents/tdd-reproducer-agent/SUBAGENT.md +23 -0
- package/catalog/subagents/token-budget-governor/SUBAGENT.md +26 -0
- package/dist/cli.js +4578 -0
- package/dist/index.d.ts +2039 -0
- package/dist/index.js +4129 -0
- package/docs/BENCHMARKS.md +82 -0
- package/package.json +84 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,4129 @@
|
|
|
1
|
+
// src/models.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var SKILLOPS_VERSION = "1.2.1";
|
|
6
|
+
var SKILL_NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
var SEMVER_REGEX = /^\d+\.\d+\.\d+$/;
|
|
8
|
+
var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
9
|
+
var WORD_REGEX = /\S+/g;
|
|
10
|
+
var PUNCT_CODE_REGEX = /[{}[\]()<>:;,.=`~@#$%^&*|\\/]/g;
|
|
11
|
+
function countWords(text) {
|
|
12
|
+
const matches = text.match(WORD_REGEX);
|
|
13
|
+
return matches ? matches.length : 0;
|
|
14
|
+
}
|
|
15
|
+
function estimateTokenCount(text) {
|
|
16
|
+
const stripped = text.trim();
|
|
17
|
+
if (!stripped) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const words = countWords(stripped);
|
|
21
|
+
const punctBonus = Math.floor((stripped.match(PUNCT_CODE_REGEX)?.length ?? 0) / 4);
|
|
22
|
+
return Math.max(1, Math.ceil(words * 1.33) + punctBonus);
|
|
23
|
+
}
|
|
24
|
+
function splitFrontmatter(rawText, sourceLabel) {
|
|
25
|
+
const match = FRONTMATTER_REGEX.exec(rawText);
|
|
26
|
+
if (!match) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Document at '${sourceLabel}' is missing valid YAML frontmatter delimited by '---'.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const yamlText = match[1] ?? "";
|
|
32
|
+
const body = match[2] ?? "";
|
|
33
|
+
const parsed = YAML.parse(yamlText);
|
|
34
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
35
|
+
throw new Error(`YAML frontmatter in '${sourceLabel}' must evaluate to a key-value mapping.`);
|
|
36
|
+
}
|
|
37
|
+
return [parsed, body];
|
|
38
|
+
}
|
|
39
|
+
var ExitCode = {
|
|
40
|
+
SUCCESS: 0,
|
|
41
|
+
ERROR: 1,
|
|
42
|
+
COLLISION_ERROR: 2,
|
|
43
|
+
BUDGET_OR_CONTRACT_ERROR: 3,
|
|
44
|
+
EVAL_REGRESSION_ERROR: 4
|
|
45
|
+
};
|
|
46
|
+
var CatalogTrackSchema = z.enum([
|
|
47
|
+
"ai-engineering",
|
|
48
|
+
"python-backend",
|
|
49
|
+
"arch-quality",
|
|
50
|
+
"token-economy"
|
|
51
|
+
]);
|
|
52
|
+
var CollisionSeveritySchema = z.enum([
|
|
53
|
+
"SAFE",
|
|
54
|
+
"WARNING",
|
|
55
|
+
"HIGH_RISK",
|
|
56
|
+
"BLOCKING_ERROR"
|
|
57
|
+
]);
|
|
58
|
+
var RuntimeTargetSchema = z.enum([
|
|
59
|
+
"claude",
|
|
60
|
+
"antigravity",
|
|
61
|
+
"cursor",
|
|
62
|
+
"codex",
|
|
63
|
+
"copilot",
|
|
64
|
+
"windsurf",
|
|
65
|
+
"cline",
|
|
66
|
+
"roocode",
|
|
67
|
+
"kilocode",
|
|
68
|
+
"zed",
|
|
69
|
+
"continue",
|
|
70
|
+
"aider",
|
|
71
|
+
"openhands",
|
|
72
|
+
"amp",
|
|
73
|
+
"goose",
|
|
74
|
+
"opencode",
|
|
75
|
+
"kiro",
|
|
76
|
+
"trae",
|
|
77
|
+
"amazonq",
|
|
78
|
+
"junie"
|
|
79
|
+
]);
|
|
80
|
+
var CORE_RUNTIME_TARGETS = [
|
|
81
|
+
"claude",
|
|
82
|
+
"antigravity",
|
|
83
|
+
"cursor",
|
|
84
|
+
"codex",
|
|
85
|
+
"copilot"
|
|
86
|
+
];
|
|
87
|
+
var ALL_RUNTIME_TARGETS = [
|
|
88
|
+
"claude",
|
|
89
|
+
"antigravity",
|
|
90
|
+
"cursor",
|
|
91
|
+
"codex",
|
|
92
|
+
"copilot",
|
|
93
|
+
"windsurf",
|
|
94
|
+
"cline",
|
|
95
|
+
"roocode",
|
|
96
|
+
"kilocode",
|
|
97
|
+
"zed",
|
|
98
|
+
"continue",
|
|
99
|
+
"aider",
|
|
100
|
+
"openhands",
|
|
101
|
+
"amp",
|
|
102
|
+
"goose",
|
|
103
|
+
"opencode",
|
|
104
|
+
"kiro",
|
|
105
|
+
"trae",
|
|
106
|
+
"amazonq",
|
|
107
|
+
"junie"
|
|
108
|
+
];
|
|
109
|
+
var RUNTIME_HARNESS_SPECS = {
|
|
110
|
+
claude: {
|
|
111
|
+
id: "claude",
|
|
112
|
+
displayName: "Anthropic Claude Code",
|
|
113
|
+
vendor: "Anthropic",
|
|
114
|
+
category: "cli-agent",
|
|
115
|
+
skillFormat: "progressive-skill-dir",
|
|
116
|
+
skillsDir: ".claude/skills",
|
|
117
|
+
subagentsDir: ".claude/agents",
|
|
118
|
+
detectionPaths: [".claude", "CLAUDE.md", ".mcp.json"],
|
|
119
|
+
alwaysOnFiles: ["CLAUDE.md", ".claude/CLAUDE.md"],
|
|
120
|
+
mcpConfigPath: ".mcp.json",
|
|
121
|
+
supportsMcp: true
|
|
122
|
+
},
|
|
123
|
+
antigravity: {
|
|
124
|
+
id: "antigravity",
|
|
125
|
+
displayName: "Google Antigravity / Gemini CLI",
|
|
126
|
+
vendor: "Google DeepMind",
|
|
127
|
+
category: "ai-ide",
|
|
128
|
+
skillFormat: "progressive-skill-dir",
|
|
129
|
+
skillsDir: ".agent/skills",
|
|
130
|
+
subagentsDir: ".agent/agents",
|
|
131
|
+
detectionPaths: [".agent", ".gemini", "GEMINI.md"],
|
|
132
|
+
alwaysOnFiles: ["GEMINI.md", ".gemini/GEMINI.md"],
|
|
133
|
+
mcpConfigPath: ".gemini/settings.json",
|
|
134
|
+
supportsMcp: true
|
|
135
|
+
},
|
|
136
|
+
cursor: {
|
|
137
|
+
id: "cursor",
|
|
138
|
+
displayName: "Cursor AI IDE",
|
|
139
|
+
vendor: "Anysphere",
|
|
140
|
+
category: "ai-ide",
|
|
141
|
+
skillFormat: "mdc-rule",
|
|
142
|
+
skillsDir: ".cursor/rules",
|
|
143
|
+
subagentsDir: ".cursor/rules",
|
|
144
|
+
detectionPaths: [".cursor", ".cursorrules"],
|
|
145
|
+
alwaysOnFiles: [".cursorrules"],
|
|
146
|
+
mcpConfigPath: ".cursor/mcp.json",
|
|
147
|
+
supportsMcp: true
|
|
148
|
+
},
|
|
149
|
+
codex: {
|
|
150
|
+
id: "codex",
|
|
151
|
+
displayName: "OpenAI Codex CLI",
|
|
152
|
+
vendor: "OpenAI",
|
|
153
|
+
category: "cli-agent",
|
|
154
|
+
skillFormat: "progressive-skill-dir",
|
|
155
|
+
skillsDir: ".codex/skills",
|
|
156
|
+
subagentsDir: ".codex/agents",
|
|
157
|
+
detectionPaths: [".codex", "AGENTS.md"],
|
|
158
|
+
alwaysOnFiles: ["AGENTS.md"],
|
|
159
|
+
mcpConfigPath: ".codex/config.toml",
|
|
160
|
+
supportsMcp: true
|
|
161
|
+
},
|
|
162
|
+
copilot: {
|
|
163
|
+
id: "copilot",
|
|
164
|
+
displayName: "GitHub Copilot",
|
|
165
|
+
vendor: "GitHub / Microsoft",
|
|
166
|
+
category: "ide-extension",
|
|
167
|
+
skillFormat: "copilot-instructions",
|
|
168
|
+
skillsDir: ".github/instructions",
|
|
169
|
+
subagentsDir: ".github/agents",
|
|
170
|
+
detectionPaths: [
|
|
171
|
+
".github/copilot-instructions.md",
|
|
172
|
+
".github/instructions",
|
|
173
|
+
".github/prompts",
|
|
174
|
+
".github/agents"
|
|
175
|
+
],
|
|
176
|
+
alwaysOnFiles: [".github/copilot-instructions.md"],
|
|
177
|
+
mcpConfigPath: ".vscode/mcp.json",
|
|
178
|
+
supportsMcp: true
|
|
179
|
+
},
|
|
180
|
+
windsurf: {
|
|
181
|
+
id: "windsurf",
|
|
182
|
+
displayName: "Windsurf AI IDE",
|
|
183
|
+
vendor: "Codeium / Windsurf",
|
|
184
|
+
category: "ai-ide",
|
|
185
|
+
skillFormat: "markdown-rule",
|
|
186
|
+
skillsDir: ".windsurf/rules",
|
|
187
|
+
subagentsDir: ".windsurf/workflows",
|
|
188
|
+
detectionPaths: [".windsurf", ".windsurfrules"],
|
|
189
|
+
alwaysOnFiles: [".windsurfrules"],
|
|
190
|
+
mcpConfigPath: null,
|
|
191
|
+
supportsMcp: false
|
|
192
|
+
},
|
|
193
|
+
cline: {
|
|
194
|
+
id: "cline",
|
|
195
|
+
displayName: "Cline Autonomous Coding Agent",
|
|
196
|
+
vendor: "Cline",
|
|
197
|
+
category: "ide-extension",
|
|
198
|
+
skillFormat: "markdown-rule",
|
|
199
|
+
skillsDir: ".clinerules",
|
|
200
|
+
subagentsDir: ".clinerules/workflows",
|
|
201
|
+
detectionPaths: [".clinerules", ".cline"],
|
|
202
|
+
alwaysOnFiles: [".clinerules"],
|
|
203
|
+
mcpConfigPath: null,
|
|
204
|
+
supportsMcp: false
|
|
205
|
+
},
|
|
206
|
+
roocode: {
|
|
207
|
+
id: "roocode",
|
|
208
|
+
displayName: "Roo Code",
|
|
209
|
+
vendor: "Roo Veterinary",
|
|
210
|
+
category: "ide-extension",
|
|
211
|
+
skillFormat: "markdown-rule",
|
|
212
|
+
skillsDir: ".roo/rules",
|
|
213
|
+
subagentsDir: ".roo/rules-architect",
|
|
214
|
+
detectionPaths: [".roo", ".roorules", ".roomodes"],
|
|
215
|
+
alwaysOnFiles: [".roorules"],
|
|
216
|
+
mcpConfigPath: ".roo/mcp.json",
|
|
217
|
+
supportsMcp: true
|
|
218
|
+
},
|
|
219
|
+
kilocode: {
|
|
220
|
+
id: "kilocode",
|
|
221
|
+
displayName: "Kilo Code",
|
|
222
|
+
vendor: "Kilo Org",
|
|
223
|
+
category: "ide-extension",
|
|
224
|
+
skillFormat: "markdown-rule",
|
|
225
|
+
skillsDir: ".kilocode/rules",
|
|
226
|
+
subagentsDir: ".kilocode/workflows",
|
|
227
|
+
detectionPaths: [".kilocode", ".kilocoderules", ".kilocodemodes"],
|
|
228
|
+
alwaysOnFiles: [".kilocoderules"],
|
|
229
|
+
mcpConfigPath: ".kilocode/mcp.json",
|
|
230
|
+
supportsMcp: true
|
|
231
|
+
},
|
|
232
|
+
zed: {
|
|
233
|
+
id: "zed",
|
|
234
|
+
displayName: "Zed AI",
|
|
235
|
+
vendor: "Zed Industries",
|
|
236
|
+
category: "ai-ide",
|
|
237
|
+
skillFormat: "markdown-rule",
|
|
238
|
+
skillsDir: ".zed/rules",
|
|
239
|
+
subagentsDir: ".zed/rules",
|
|
240
|
+
detectionPaths: [".zed", ".rules"],
|
|
241
|
+
alwaysOnFiles: [".rules"],
|
|
242
|
+
mcpConfigPath: ".zed/settings.json",
|
|
243
|
+
supportsMcp: true
|
|
244
|
+
},
|
|
245
|
+
continue: {
|
|
246
|
+
id: "continue",
|
|
247
|
+
displayName: "Continue.dev",
|
|
248
|
+
vendor: "Continue",
|
|
249
|
+
category: "ide-extension",
|
|
250
|
+
skillFormat: "markdown-rule",
|
|
251
|
+
skillsDir: ".continue/rules",
|
|
252
|
+
subagentsDir: ".continue/prompts",
|
|
253
|
+
detectionPaths: [".continue", ".continuerules"],
|
|
254
|
+
alwaysOnFiles: [".continuerules"],
|
|
255
|
+
mcpConfigPath: ".continue/mcpServers/skillops.yaml",
|
|
256
|
+
supportsMcp: true
|
|
257
|
+
},
|
|
258
|
+
aider: {
|
|
259
|
+
id: "aider",
|
|
260
|
+
displayName: "Aider AI Pair Programmer",
|
|
261
|
+
vendor: "Aider",
|
|
262
|
+
category: "cli-agent",
|
|
263
|
+
skillFormat: "markdown-rule",
|
|
264
|
+
skillsDir: ".aider/conventions",
|
|
265
|
+
subagentsDir: ".aider/conventions",
|
|
266
|
+
detectionPaths: [".aider", ".aider.conf.yml", "CONVENTIONS.md"],
|
|
267
|
+
alwaysOnFiles: ["CONVENTIONS.md"],
|
|
268
|
+
mcpConfigPath: null,
|
|
269
|
+
supportsMcp: false
|
|
270
|
+
},
|
|
271
|
+
openhands: {
|
|
272
|
+
id: "openhands",
|
|
273
|
+
displayName: "OpenHands (All Hands AI)",
|
|
274
|
+
vendor: "All Hands AI",
|
|
275
|
+
category: "autonomous-swe",
|
|
276
|
+
skillFormat: "openhands-microagent",
|
|
277
|
+
skillsDir: ".openhands/microagents",
|
|
278
|
+
subagentsDir: ".openhands/microagents",
|
|
279
|
+
detectionPaths: [".openhands"],
|
|
280
|
+
alwaysOnFiles: [".openhands/microagents/repo.md"],
|
|
281
|
+
mcpConfigPath: null,
|
|
282
|
+
supportsMcp: false
|
|
283
|
+
},
|
|
284
|
+
amp: {
|
|
285
|
+
id: "amp",
|
|
286
|
+
displayName: "Sourcegraph Amp",
|
|
287
|
+
vendor: "Sourcegraph",
|
|
288
|
+
category: "cli-agent",
|
|
289
|
+
skillFormat: "progressive-skill-dir",
|
|
290
|
+
skillsDir: ".amp/skills",
|
|
291
|
+
subagentsDir: ".amp/agents",
|
|
292
|
+
detectionPaths: [".amp", "AGENT.md"],
|
|
293
|
+
alwaysOnFiles: ["AGENT.md"],
|
|
294
|
+
mcpConfigPath: ".amp/settings.json",
|
|
295
|
+
supportsMcp: true
|
|
296
|
+
},
|
|
297
|
+
goose: {
|
|
298
|
+
id: "goose",
|
|
299
|
+
displayName: "Block Goose",
|
|
300
|
+
vendor: "Block",
|
|
301
|
+
category: "cli-agent",
|
|
302
|
+
skillFormat: "progressive-skill-dir",
|
|
303
|
+
skillsDir: ".goose/skills",
|
|
304
|
+
subagentsDir: ".goose/agents",
|
|
305
|
+
detectionPaths: [".goose", ".goosehints"],
|
|
306
|
+
alwaysOnFiles: [".goosehints"],
|
|
307
|
+
mcpConfigPath: null,
|
|
308
|
+
supportsMcp: false
|
|
309
|
+
},
|
|
310
|
+
opencode: {
|
|
311
|
+
id: "opencode",
|
|
312
|
+
displayName: "OpenCode CLI",
|
|
313
|
+
vendor: "SST / OpenCode",
|
|
314
|
+
category: "cli-agent",
|
|
315
|
+
skillFormat: "progressive-skill-dir",
|
|
316
|
+
skillsDir: ".opencode/skills",
|
|
317
|
+
subagentsDir: ".opencode/agent",
|
|
318
|
+
detectionPaths: [".opencode", "opencode.json"],
|
|
319
|
+
alwaysOnFiles: [],
|
|
320
|
+
mcpConfigPath: "opencode.json",
|
|
321
|
+
supportsMcp: true
|
|
322
|
+
},
|
|
323
|
+
kiro: {
|
|
324
|
+
id: "kiro",
|
|
325
|
+
displayName: "AWS Kiro IDE",
|
|
326
|
+
vendor: "Amazon Web Services",
|
|
327
|
+
category: "ai-ide",
|
|
328
|
+
skillFormat: "kiro-steering",
|
|
329
|
+
skillsDir: ".kiro/steering",
|
|
330
|
+
subagentsDir: ".kiro/agents",
|
|
331
|
+
detectionPaths: [".kiro"],
|
|
332
|
+
alwaysOnFiles: [
|
|
333
|
+
".kiro/steering/product.md",
|
|
334
|
+
".kiro/steering/tech.md",
|
|
335
|
+
".kiro/steering/structure.md"
|
|
336
|
+
],
|
|
337
|
+
mcpConfigPath: ".kiro/settings/mcp.json",
|
|
338
|
+
supportsMcp: true
|
|
339
|
+
},
|
|
340
|
+
trae: {
|
|
341
|
+
id: "trae",
|
|
342
|
+
displayName: "Trae AI IDE",
|
|
343
|
+
vendor: "ByteDance / Trae",
|
|
344
|
+
category: "ai-ide",
|
|
345
|
+
skillFormat: "markdown-rule",
|
|
346
|
+
skillsDir: ".trae/rules",
|
|
347
|
+
subagentsDir: ".trae/agents",
|
|
348
|
+
detectionPaths: [".trae"],
|
|
349
|
+
alwaysOnFiles: [".trae/rules/project_rules.md"],
|
|
350
|
+
mcpConfigPath: null,
|
|
351
|
+
supportsMcp: false
|
|
352
|
+
},
|
|
353
|
+
amazonq: {
|
|
354
|
+
id: "amazonq",
|
|
355
|
+
displayName: "Amazon Q Developer",
|
|
356
|
+
vendor: "Amazon Web Services",
|
|
357
|
+
category: "ide-extension",
|
|
358
|
+
skillFormat: "markdown-rule",
|
|
359
|
+
skillsDir: ".amazonq/rules",
|
|
360
|
+
subagentsDir: ".amazonq/cli-agents",
|
|
361
|
+
detectionPaths: [".amazonq"],
|
|
362
|
+
alwaysOnFiles: [],
|
|
363
|
+
mcpConfigPath: ".amazonq/mcp.json",
|
|
364
|
+
supportsMcp: true
|
|
365
|
+
},
|
|
366
|
+
junie: {
|
|
367
|
+
id: "junie",
|
|
368
|
+
displayName: "JetBrains Junie / AI Assistant",
|
|
369
|
+
vendor: "JetBrains",
|
|
370
|
+
category: "ide-extension",
|
|
371
|
+
skillFormat: "markdown-rule",
|
|
372
|
+
skillsDir: ".junie/skills",
|
|
373
|
+
subagentsDir: ".junie/agents",
|
|
374
|
+
detectionPaths: [".junie", ".aiassistant"],
|
|
375
|
+
alwaysOnFiles: [".junie/guidelines.md"],
|
|
376
|
+
mcpConfigPath: null,
|
|
377
|
+
supportsMcp: false
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
function detectInstalledHarnesses(repoPath) {
|
|
381
|
+
const normalizedRoot = repoPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
382
|
+
const detected = [];
|
|
383
|
+
for (const target of ALL_RUNTIME_TARGETS) {
|
|
384
|
+
const spec = RUNTIME_HARNESS_SPECS[target];
|
|
385
|
+
const hasMatch = spec.detectionPaths.some(
|
|
386
|
+
(relPath) => fs.existsSync(`${normalizedRoot}/${relPath}`)
|
|
387
|
+
);
|
|
388
|
+
if (hasMatch) {
|
|
389
|
+
detected.push(target);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return detected;
|
|
393
|
+
}
|
|
394
|
+
function resolveRuntimeTargets(rawSpec, repoPath, fallbackTargets = CORE_RUNTIME_TARGETS) {
|
|
395
|
+
if (!rawSpec) {
|
|
396
|
+
return [...fallbackTargets];
|
|
397
|
+
}
|
|
398
|
+
const tokens = (Array.isArray(rawSpec) ? rawSpec : [rawSpec]).flatMap((item) => String(item).split(",")).map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
399
|
+
if (tokens.length === 0) {
|
|
400
|
+
return [...fallbackTargets];
|
|
401
|
+
}
|
|
402
|
+
const resolved = [];
|
|
403
|
+
const pushUnique = (target) => {
|
|
404
|
+
if (!resolved.includes(target)) {
|
|
405
|
+
resolved.push(target);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
for (const token of tokens) {
|
|
409
|
+
if (token === "all" || token === "*" || token === "top20") {
|
|
410
|
+
for (const t of ALL_RUNTIME_TARGETS) {
|
|
411
|
+
pushUnique(t);
|
|
412
|
+
}
|
|
413
|
+
} else if (token === "core" || token === "default") {
|
|
414
|
+
for (const t of CORE_RUNTIME_TARGETS) {
|
|
415
|
+
pushUnique(t);
|
|
416
|
+
}
|
|
417
|
+
} else if (token === "auto") {
|
|
418
|
+
const detected = repoPath ? detectInstalledHarnesses(repoPath) : [];
|
|
419
|
+
const autoList = detected.length > 0 ? detected : fallbackTargets;
|
|
420
|
+
for (const t of autoList) {
|
|
421
|
+
pushUnique(t);
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
pushUnique(RuntimeTargetSchema.parse(token));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return resolved;
|
|
428
|
+
}
|
|
429
|
+
var RoutingSignalsSchema = z.object({
|
|
430
|
+
languages: z.array(z.string()).default([]),
|
|
431
|
+
files_any: z.array(z.string()).default([]),
|
|
432
|
+
dependencies_any: z.array(z.string()).default([]),
|
|
433
|
+
negative_triggers: z.array(z.string()).default([])
|
|
434
|
+
}).strict();
|
|
435
|
+
var ScriptEntrypointSchema = z.object({
|
|
436
|
+
path: z.string().min(1).transform((val) => val.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")).refine(
|
|
437
|
+
(val) => val.startsWith("scripts/") && (val.endsWith(".py") || val.endsWith(".mjs") || val.endsWith(".js") || val.endsWith(".ts")),
|
|
438
|
+
{
|
|
439
|
+
message: "Script entrypoint path must reside under 'scripts/' (.py, .mjs, .js, or .ts)."
|
|
440
|
+
}
|
|
441
|
+
),
|
|
442
|
+
description: z.string().min(10)
|
|
443
|
+
}).strict();
|
|
444
|
+
var SkillEntrypointsSchema = z.object({
|
|
445
|
+
scripts: z.array(ScriptEntrypointSchema).min(1)
|
|
446
|
+
}).strict();
|
|
447
|
+
var SkillManifestSchema = z.object({
|
|
448
|
+
name: z.string().regex(SKILL_NAME_REGEX, {
|
|
449
|
+
message: "Skill name must be lowercase kebab-case (e.g., 'mcp-architect')."
|
|
450
|
+
}),
|
|
451
|
+
description: z.string().transform((val) => val.trim().replace(/\s+/g, " ")).superRefine((cleaned, ctx) => {
|
|
452
|
+
if (!cleaned.startsWith("Use when")) {
|
|
453
|
+
ctx.addIssue({
|
|
454
|
+
code: z.ZodIssueCode.custom,
|
|
455
|
+
message: "Skill description must start with 'Use when' per SKILL_CONTRACT.md \xA74.2."
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
const words = countWords(cleaned);
|
|
459
|
+
if (words < 40 || words > 110) {
|
|
460
|
+
ctx.addIssue({
|
|
461
|
+
code: z.ZodIssueCode.custom,
|
|
462
|
+
message: `Skill description must contain between 40 and 110 words per INV-SKILL-01 (got ${words} words).`
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const tokens = estimateTokenCount(cleaned);
|
|
466
|
+
if (tokens > 160) {
|
|
467
|
+
ctx.addIssue({
|
|
468
|
+
code: z.ZodIssueCode.custom,
|
|
469
|
+
message: `Skill description exceeds Level 1 budget of 160 tokens per INV-SKILL-01 (estimated ${tokens} tokens).`
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}),
|
|
473
|
+
version: z.string().regex(SEMVER_REGEX).default("1.0.0"),
|
|
474
|
+
track: CatalogTrackSchema,
|
|
475
|
+
tags: z.array(z.string().min(1)).min(1),
|
|
476
|
+
routing_signals: RoutingSignalsSchema.default({
|
|
477
|
+
languages: [],
|
|
478
|
+
files_any: [],
|
|
479
|
+
dependencies_any: [],
|
|
480
|
+
negative_triggers: []
|
|
481
|
+
}),
|
|
482
|
+
entrypoints: SkillEntrypointsSchema,
|
|
483
|
+
body_lines: z.number().int().min(0).max(250).default(0),
|
|
484
|
+
body_tokens: z.number().int().min(0).max(1800).default(0)
|
|
485
|
+
}).strict();
|
|
486
|
+
function createSkillManifest(input) {
|
|
487
|
+
const parsed = SkillManifestSchema.parse(input);
|
|
488
|
+
return {
|
|
489
|
+
...parsed,
|
|
490
|
+
description_words: countWords(parsed.description),
|
|
491
|
+
description_tokens: estimateTokenCount(parsed.description)
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function parseSkillMdFile(filePath) {
|
|
495
|
+
const rawText = fs.readFileSync(filePath, "utf-8");
|
|
496
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
497
|
+
const totalLines = rawText.split(/\r?\n/).length;
|
|
498
|
+
const effectiveTotalLines = rawText.endsWith("\n") ? totalLines - 1 : totalLines;
|
|
499
|
+
if (effectiveTotalLines > 250) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`SKILL.md at '${normalizedPath}' has ${effectiveTotalLines} lines, exceeding INV-SKILL-01 maximum of 250 lines.`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const [frontmatter, body] = splitFrontmatter(rawText, normalizedPath);
|
|
505
|
+
const bodyLines = body ? body.endsWith("\n") ? body.split(/\r?\n/).length - 1 : body.split(/\r?\n/).length : 0;
|
|
506
|
+
const bodyTokens = estimateTokenCount(body);
|
|
507
|
+
return createSkillManifest({
|
|
508
|
+
...frontmatter,
|
|
509
|
+
body_lines: bodyLines,
|
|
510
|
+
body_tokens: bodyTokens
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
var FORBIDDEN_WILDCARD_TOOLS = /* @__PURE__ */ new Set(["*", "all", "any", "**"]);
|
|
514
|
+
var SubagentManifestSchema = z.object({
|
|
515
|
+
name: z.string().regex(SKILL_NAME_REGEX, {
|
|
516
|
+
message: "Subagent name must be lowercase kebab-case (e.g., 'context-scout')."
|
|
517
|
+
}),
|
|
518
|
+
description: z.string().transform((val) => val.trim().replace(/\s+/g, " ")).superRefine((cleaned, ctx) => {
|
|
519
|
+
const words = countWords(cleaned);
|
|
520
|
+
if (words < 10 || words > 110) {
|
|
521
|
+
ctx.addIssue({
|
|
522
|
+
code: z.ZodIssueCode.custom,
|
|
523
|
+
message: `Subagent description must contain between 10 and 110 words (got ${words} words).`
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const tokens = estimateTokenCount(cleaned);
|
|
527
|
+
if (tokens > 160) {
|
|
528
|
+
ctx.addIssue({
|
|
529
|
+
code: z.ZodIssueCode.custom,
|
|
530
|
+
message: `Subagent description exceeds Level 1 budget of 160 tokens (estimated ${tokens} tokens).`
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}),
|
|
534
|
+
tools: z.array(z.string().min(1)).min(1, { message: "Subagent must declare an explicit non-empty tool allowlist per INV-SKILL-04." }).superRefine((tools, ctx) => {
|
|
535
|
+
for (const tool of tools) {
|
|
536
|
+
if (FORBIDDEN_WILDCARD_TOOLS.has(tool.trim().toLowerCase())) {
|
|
537
|
+
ctx.addIssue({
|
|
538
|
+
code: z.ZodIssueCode.custom,
|
|
539
|
+
message: `Subagent tool allowlist cannot contain wildcard '${tool}' per INV-SKILL-04 least-privilege guarantee.`
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}),
|
|
544
|
+
user_invocable: z.boolean().default(true),
|
|
545
|
+
context_isolation: z.boolean().default(true),
|
|
546
|
+
max_output_tokens: z.number().int().min(50).max(400).default(400),
|
|
547
|
+
model_tier: z.enum(["inherit", "fast", "reasoning"]).default("inherit"),
|
|
548
|
+
tags: z.array(z.string().min(1)).default([]),
|
|
549
|
+
system_prompt_lines: z.number().int().min(1).max(180).default(1),
|
|
550
|
+
system_prompt_tokens: z.number().int().min(1).max(1e3).default(1)
|
|
551
|
+
}).strict();
|
|
552
|
+
function createSubagentManifest(input) {
|
|
553
|
+
const normalized = { ...input };
|
|
554
|
+
if ("user-invocable" in normalized && !("user_invocable" in normalized)) {
|
|
555
|
+
normalized.user_invocable = normalized["user-invocable"];
|
|
556
|
+
delete normalized["user-invocable"];
|
|
557
|
+
}
|
|
558
|
+
if ("context-isolation" in normalized && !("context_isolation" in normalized)) {
|
|
559
|
+
normalized.context_isolation = normalized["context-isolation"];
|
|
560
|
+
delete normalized["context-isolation"];
|
|
561
|
+
}
|
|
562
|
+
if ("max-output-tokens" in normalized && !("max_output_tokens" in normalized)) {
|
|
563
|
+
normalized.max_output_tokens = normalized["max-output-tokens"];
|
|
564
|
+
delete normalized["max-output-tokens"];
|
|
565
|
+
}
|
|
566
|
+
if ("model-tier" in normalized && !("model_tier" in normalized)) {
|
|
567
|
+
normalized.model_tier = normalized["model-tier"];
|
|
568
|
+
delete normalized["model-tier"];
|
|
569
|
+
}
|
|
570
|
+
const parsed = SubagentManifestSchema.parse(normalized);
|
|
571
|
+
return {
|
|
572
|
+
...parsed,
|
|
573
|
+
description_tokens: estimateTokenCount(parsed.description)
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function parseSubagentMdFile(filePath) {
|
|
577
|
+
const rawText = fs.readFileSync(filePath, "utf-8");
|
|
578
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
579
|
+
const [frontmatter, body] = splitFrontmatter(rawText, normalizedPath);
|
|
580
|
+
const trimmedBody = body.trim();
|
|
581
|
+
const promptLines = trimmedBody ? trimmedBody.split(/\r?\n/).length : 1;
|
|
582
|
+
const promptTokens = Math.max(1, estimateTokenCount(trimmedBody));
|
|
583
|
+
const manifest = createSubagentManifest({
|
|
584
|
+
...frontmatter,
|
|
585
|
+
system_prompt_lines: promptLines,
|
|
586
|
+
system_prompt_tokens: promptTokens
|
|
587
|
+
});
|
|
588
|
+
return { manifest, systemPromptBody: trimmedBody };
|
|
589
|
+
}
|
|
590
|
+
function classifyCollisionSeverity(similarity) {
|
|
591
|
+
if (similarity >= 0.85) {
|
|
592
|
+
return "BLOCKING_ERROR";
|
|
593
|
+
}
|
|
594
|
+
if (similarity >= 0.72) {
|
|
595
|
+
return "HIGH_RISK";
|
|
596
|
+
}
|
|
597
|
+
if (similarity >= 0.58) {
|
|
598
|
+
return "WARNING";
|
|
599
|
+
}
|
|
600
|
+
return "SAFE";
|
|
601
|
+
}
|
|
602
|
+
var CollisionPairSchema = z.object({
|
|
603
|
+
skill_a: z.string().min(1),
|
|
604
|
+
skill_b: z.string().min(1),
|
|
605
|
+
similarity: z.number().min(0).max(1),
|
|
606
|
+
severity: CollisionSeveritySchema,
|
|
607
|
+
shared_terms: z.array(z.string()).default([])
|
|
608
|
+
}).strict();
|
|
609
|
+
function createCollisionPair(skillA, skillB, similarity, sharedTerms = []) {
|
|
610
|
+
const [orderedA, orderedB] = skillA <= skillB ? [skillA, skillB] : [skillB, skillA];
|
|
611
|
+
const clamped = Math.min(1, Math.max(0, Number(similarity.toFixed(4))));
|
|
612
|
+
return CollisionPairSchema.parse({
|
|
613
|
+
skill_a: orderedA,
|
|
614
|
+
skill_b: orderedB,
|
|
615
|
+
similarity: clamped,
|
|
616
|
+
severity: classifyCollisionSeverity(clamped),
|
|
617
|
+
shared_terms: [...new Set(sharedTerms)].sort()
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
var CollisionReportSchema = z.object({
|
|
621
|
+
catalog_path: z.string(),
|
|
622
|
+
skills_audited: z.number().int().min(0),
|
|
623
|
+
subagents_audited: z.number().int().min(0).default(0),
|
|
624
|
+
total_l1_tokens: z.number().int().min(0),
|
|
625
|
+
max_similarity: z.number().min(0).max(1).default(0),
|
|
626
|
+
pairs: z.array(CollisionPairSchema).default([]),
|
|
627
|
+
budget_errors: z.array(z.string()).default([])
|
|
628
|
+
}).strict();
|
|
629
|
+
function createCollisionReport(input) {
|
|
630
|
+
const parsed = CollisionReportSchema.parse(input);
|
|
631
|
+
const blocking = parsed.pairs.filter((p) => p.severity === "BLOCKING_ERROR");
|
|
632
|
+
const highRisk = parsed.pairs.filter((p) => p.severity === "HIGH_RISK");
|
|
633
|
+
const warnings = parsed.pairs.filter((p) => p.severity === "WARNING");
|
|
634
|
+
return {
|
|
635
|
+
...parsed,
|
|
636
|
+
blocking_collisions: blocking,
|
|
637
|
+
high_risk_collisions: highRisk,
|
|
638
|
+
warning_collisions: warnings,
|
|
639
|
+
passed: parsed.budget_errors.length === 0 && blocking.length === 0
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
var TokenFileCategorySchema = z.enum([
|
|
643
|
+
"always_on_instructions",
|
|
644
|
+
"always_on_cursor_rule",
|
|
645
|
+
"mcp_schema_tax",
|
|
646
|
+
"skill_l1_metadata",
|
|
647
|
+
"skill_l2_body",
|
|
648
|
+
"skill_l3_script",
|
|
649
|
+
"subagent_definition"
|
|
650
|
+
]);
|
|
651
|
+
var TokenFileEntrySchema = z.object({
|
|
652
|
+
path: z.string().min(1),
|
|
653
|
+
category: TokenFileCategorySchema,
|
|
654
|
+
always_loaded_on_startup: z.boolean(),
|
|
655
|
+
lines: z.number().int().min(0),
|
|
656
|
+
estimated_tokens: z.number().int().min(0),
|
|
657
|
+
notes: z.string().default("")
|
|
658
|
+
}).strict();
|
|
659
|
+
var TokenWasteHotspotSchema = z.object({
|
|
660
|
+
path: z.string().min(1),
|
|
661
|
+
severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]),
|
|
662
|
+
wasted_tokens: z.number().int().min(0),
|
|
663
|
+
issue: z.string().min(1),
|
|
664
|
+
recommendation: z.string().min(1)
|
|
665
|
+
}).strict();
|
|
666
|
+
var TokenEconomyReportSchema = z.object({
|
|
667
|
+
repo_path: z.string().min(1),
|
|
668
|
+
catalog_path: z.string().min(1),
|
|
669
|
+
startup_token_tax: z.number().int().min(0),
|
|
670
|
+
startup_tax_budget: z.number().int().min(100).default(2500),
|
|
671
|
+
monolithic_equivalent_tokens: z.number().int().min(0),
|
|
672
|
+
progressive_disclosure_l1_tokens: z.number().int().min(0),
|
|
673
|
+
zero_token_l3_scripts_count: z.number().int().min(0),
|
|
674
|
+
context_isolated_subagents_count: z.number().int().min(0),
|
|
675
|
+
estimated_tokens_saved: z.number().int().min(0),
|
|
676
|
+
token_savings_pct: z.number().min(0).max(100),
|
|
677
|
+
inspected_files: z.array(TokenFileEntrySchema).default([]),
|
|
678
|
+
hotspots: z.array(TokenWasteHotspotSchema).default([]),
|
|
679
|
+
passed_budget: z.boolean()
|
|
680
|
+
}).strict();
|
|
681
|
+
var RepoProfileSchema = z.object({
|
|
682
|
+
repo_path: z.string().min(1),
|
|
683
|
+
languages: z.array(z.string()).default([]),
|
|
684
|
+
frameworks: z.array(z.string()).default([]),
|
|
685
|
+
databases: z.array(z.string()).default([]),
|
|
686
|
+
dependencies: z.array(z.string()).default([]),
|
|
687
|
+
ci_systems: z.array(z.string()).default([]),
|
|
688
|
+
detected_files: z.array(z.string()).default([]),
|
|
689
|
+
signals: z.array(z.string()).default([]),
|
|
690
|
+
detected_harnesses: z.array(RuntimeTargetSchema).default([]),
|
|
691
|
+
python_312_plus: z.boolean().default(false)
|
|
692
|
+
}).strict();
|
|
693
|
+
var ScoredSkillSchema = z.object({
|
|
694
|
+
name: z.string().min(1),
|
|
695
|
+
track: CatalogTrackSchema,
|
|
696
|
+
skill_dir: z.string().min(1),
|
|
697
|
+
relevance_score: z.number().min(0).max(100),
|
|
698
|
+
l1_tokens: z.number().int().min(0),
|
|
699
|
+
matched_signals: z.array(z.string()).default([])
|
|
700
|
+
}).strict();
|
|
701
|
+
var SkippedSkillSchema = z.object({
|
|
702
|
+
name: z.string().min(1),
|
|
703
|
+
track: CatalogTrackSchema,
|
|
704
|
+
relevance_score: z.number().min(0).max(100),
|
|
705
|
+
reason: z.string().min(1)
|
|
706
|
+
}).strict();
|
|
707
|
+
var RoutedBundleSchema = z.object({
|
|
708
|
+
repo_profile: RepoProfileSchema,
|
|
709
|
+
catalog_path: z.string().min(1),
|
|
710
|
+
max_skills: z.number().int().min(1).max(12).default(12),
|
|
711
|
+
max_l1_tokens: z.number().int().min(40).default(1500),
|
|
712
|
+
max_similarity: z.number().min(0.1).max(0.99).default(0.78),
|
|
713
|
+
selected_skills: z.array(ScoredSkillSchema).default([]),
|
|
714
|
+
recommended_subagents: z.array(z.string()).default([]),
|
|
715
|
+
skipped_skills: z.array(SkippedSkillSchema).default([]),
|
|
716
|
+
total_l1_tokens: z.number().int().min(0).default(0),
|
|
717
|
+
max_pairwise_similarity: z.number().min(0).max(1).default(0),
|
|
718
|
+
passed_invariants: z.boolean().default(true)
|
|
719
|
+
}).strict();
|
|
720
|
+
var SyncedArtifactSchema = z.object({
|
|
721
|
+
target: RuntimeTargetSchema,
|
|
722
|
+
skill_name: z.string().min(1),
|
|
723
|
+
relative_path: z.string().min(1),
|
|
724
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
725
|
+
action: z.enum(["created", "updated", "unchanged", "removed", "planned", "conflict"])
|
|
726
|
+
}).strict();
|
|
727
|
+
var SyncLockEntrySchema = z.object({
|
|
728
|
+
target: RuntimeTargetSchema,
|
|
729
|
+
skill_name: z.string().min(1),
|
|
730
|
+
relative_path: z.string().min(1),
|
|
731
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
732
|
+
}).strict();
|
|
733
|
+
var SyncLockfileSchema = z.object({
|
|
734
|
+
schema_version: z.string().default("1.0"),
|
|
735
|
+
skillops_version: z.string().min(1),
|
|
736
|
+
targets: z.array(RuntimeTargetSchema).default([]),
|
|
737
|
+
selected_skills: z.array(z.string()).default([]),
|
|
738
|
+
selected_subagents: z.array(z.string()).default([]),
|
|
739
|
+
total_l1_tokens: z.number().int().min(0).default(0),
|
|
740
|
+
artifacts: z.record(z.string(), SyncLockEntrySchema).default({})
|
|
741
|
+
}).strict();
|
|
742
|
+
var SyncReportSchema = z.object({
|
|
743
|
+
repo_path: z.string().min(1),
|
|
744
|
+
catalog_path: z.string().min(1),
|
|
745
|
+
targets: z.array(RuntimeTargetSchema).default([]),
|
|
746
|
+
dry_run: z.boolean().default(false),
|
|
747
|
+
force: z.boolean().default(false),
|
|
748
|
+
routed_bundle: RoutedBundleSchema,
|
|
749
|
+
artifacts: z.array(SyncedArtifactSchema).default([]),
|
|
750
|
+
drift_conflicts: z.array(z.string()).default([]),
|
|
751
|
+
lockfile_path: z.string().min(1),
|
|
752
|
+
passed: z.boolean().default(true)
|
|
753
|
+
}).strict();
|
|
754
|
+
var SkillOpsTokenEconomyConfigSchema = z.object({
|
|
755
|
+
startup_tax_budget: z.number().int().min(100).default(2500),
|
|
756
|
+
max_active_skills: z.number().int().min(1).max(12).default(12),
|
|
757
|
+
max_l1_tokens: z.number().int().min(40).default(1500),
|
|
758
|
+
max_similarity: z.number().min(0.1).max(0.99).default(0.78)
|
|
759
|
+
}).strict();
|
|
760
|
+
var SkillOpsDetectedStackConfigSchema = z.object({
|
|
761
|
+
languages: z.array(z.string()).default([]),
|
|
762
|
+
frameworks: z.array(z.string()).default([])
|
|
763
|
+
}).strict();
|
|
764
|
+
var SkillOpsConfigSchema = z.object({
|
|
765
|
+
version: z.string().min(1).default("1.0"),
|
|
766
|
+
catalog_dir: z.string().min(1).default("catalog"),
|
|
767
|
+
token_economy: SkillOpsTokenEconomyConfigSchema.default({
|
|
768
|
+
startup_tax_budget: 2500,
|
|
769
|
+
max_active_skills: 12,
|
|
770
|
+
max_l1_tokens: 1500,
|
|
771
|
+
max_similarity: 0.78
|
|
772
|
+
}),
|
|
773
|
+
detected_stack: SkillOpsDetectedStackConfigSchema.default({
|
|
774
|
+
languages: [],
|
|
775
|
+
frameworks: []
|
|
776
|
+
}),
|
|
777
|
+
targets: z.array(RuntimeTargetSchema).min(1).default([...CORE_RUNTIME_TARGETS])
|
|
778
|
+
}).strict();
|
|
779
|
+
function loadSkillOpsConfig(repoPath) {
|
|
780
|
+
const configPath = `${repoPath.replace(/\\/g, "/").replace(/\/+$/, "")}/.skillops.yml`;
|
|
781
|
+
if (!fs.existsSync(configPath) || !fs.statSync(configPath).isFile()) {
|
|
782
|
+
return {
|
|
783
|
+
config: null,
|
|
784
|
+
configPath,
|
|
785
|
+
exists: false,
|
|
786
|
+
error: null
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
const rawYaml = fs.readFileSync(configPath, "utf-8");
|
|
791
|
+
const parsedYaml = YAML.parse(rawYaml);
|
|
792
|
+
const validated = SkillOpsConfigSchema.parse(parsedYaml);
|
|
793
|
+
return {
|
|
794
|
+
config: validated,
|
|
795
|
+
configPath,
|
|
796
|
+
exists: true,
|
|
797
|
+
error: null
|
|
798
|
+
};
|
|
799
|
+
} catch (err) {
|
|
800
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
801
|
+
return {
|
|
802
|
+
config: null,
|
|
803
|
+
configPath,
|
|
804
|
+
exists: true,
|
|
805
|
+
error: message
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/auditor/parser.ts
|
|
811
|
+
import fs2 from "fs";
|
|
812
|
+
import path from "path";
|
|
813
|
+
function walkForFilesNamed(rootDir, targetFilename) {
|
|
814
|
+
if (!fs2.existsSync(rootDir)) {
|
|
815
|
+
return [];
|
|
816
|
+
}
|
|
817
|
+
const results = [];
|
|
818
|
+
const entries = fs2.readdirSync(rootDir, { withFileTypes: true });
|
|
819
|
+
for (const entry of entries) {
|
|
820
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
821
|
+
if (entry.isDirectory()) {
|
|
822
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
results.push(...walkForFilesNamed(fullPath, targetFilename));
|
|
826
|
+
} else if (entry.isFile() && entry.name === targetFilename) {
|
|
827
|
+
results.push(fullPath);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return results.sort();
|
|
831
|
+
}
|
|
832
|
+
function discoverSkillDirectories(catalogDir) {
|
|
833
|
+
const skillMdFiles = walkForFilesNamed(catalogDir, "SKILL.md");
|
|
834
|
+
return skillMdFiles.map((file) => path.dirname(file)).sort();
|
|
835
|
+
}
|
|
836
|
+
function parseSkillDirectory(skillDir, options = {}) {
|
|
837
|
+
const requireLayout = options.requireLayout ?? true;
|
|
838
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
839
|
+
if (!fs2.existsSync(skillMdPath)) {
|
|
840
|
+
throw new Error(`Missing SKILL.md in '${skillDir.replace(/\\/g, "/")}'.`);
|
|
841
|
+
}
|
|
842
|
+
const rawText = fs2.readFileSync(skillMdPath, "utf-8");
|
|
843
|
+
const lines = rawText.split(/\r?\n/);
|
|
844
|
+
const totalLines = rawText.endsWith("\n") ? lines.length - 1 : lines.length;
|
|
845
|
+
const [, markdownBody] = splitFrontmatter(rawText, skillMdPath.replace(/\\/g, "/"));
|
|
846
|
+
const manifest = parseSkillMdFile(skillMdPath);
|
|
847
|
+
const declaredScripts = [];
|
|
848
|
+
const missingDeclaredScripts = [];
|
|
849
|
+
const declaredRelSet = /* @__PURE__ */ new Set();
|
|
850
|
+
for (const scriptEntry of manifest.entrypoints.scripts) {
|
|
851
|
+
declaredRelSet.add(scriptEntry.path);
|
|
852
|
+
const fullScriptPath = path.join(skillDir, scriptEntry.path);
|
|
853
|
+
if (fs2.existsSync(fullScriptPath) && fs2.statSync(fullScriptPath).isFile()) {
|
|
854
|
+
declaredScripts.push(fullScriptPath);
|
|
855
|
+
} else {
|
|
856
|
+
missingDeclaredScripts.push(scriptEntry.path);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (requireLayout && missingDeclaredScripts.length > 0) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
`Skill '${manifest.name}' declares missing Level 3 script(s): ${missingDeclaredScripts.join(", ")}`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
const scriptsDir = path.join(skillDir, "scripts");
|
|
865
|
+
const extraScripts = [];
|
|
866
|
+
if (fs2.existsSync(scriptsDir) && fs2.statSync(scriptsDir).isDirectory()) {
|
|
867
|
+
for (const entry of fs2.readdirSync(scriptsDir, { withFileTypes: true })) {
|
|
868
|
+
if (entry.isFile() && (entry.name.endsWith(".py") || entry.name.endsWith(".mjs") || entry.name.endsWith(".js") || entry.name.endsWith(".ts"))) {
|
|
869
|
+
const rel = `scripts/${entry.name}`;
|
|
870
|
+
if (!declaredRelSet.has(rel)) {
|
|
871
|
+
extraScripts.push(path.join(scriptsDir, entry.name));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const referencesDir = path.join(skillDir, "references");
|
|
877
|
+
const referenceFiles = [];
|
|
878
|
+
if (fs2.existsSync(referencesDir) && fs2.statSync(referencesDir).isDirectory()) {
|
|
879
|
+
for (const entry of fs2.readdirSync(referencesDir, { withFileTypes: true })) {
|
|
880
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
881
|
+
referenceFiles.push(path.join(referencesDir, entry.name));
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const candidateEvalsPath = path.join(skillDir, "evals", "evals.json");
|
|
886
|
+
const evalsPath = fs2.existsSync(candidateEvalsPath) && fs2.statSync(candidateEvalsPath).isFile() ? candidateEvalsPath : null;
|
|
887
|
+
if (requireLayout && !evalsPath) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
`Skill '${manifest.name}' is missing required 'evals/evals.json' suite per SKILL_CONTRACT.md.`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
skillDir,
|
|
894
|
+
skillMdPath,
|
|
895
|
+
manifest,
|
|
896
|
+
rawText,
|
|
897
|
+
markdownBody,
|
|
898
|
+
totalLines,
|
|
899
|
+
declaredScripts: declaredScripts.sort(),
|
|
900
|
+
missingDeclaredScripts: missingDeclaredScripts.sort(),
|
|
901
|
+
extraScripts: extraScripts.sort(),
|
|
902
|
+
referenceFiles: referenceFiles.sort(),
|
|
903
|
+
evalsPath
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function discoverSubagentFiles(repoOrCatalogRoot) {
|
|
907
|
+
const results = /* @__PURE__ */ new Set();
|
|
908
|
+
const candidates = [
|
|
909
|
+
path.join(repoOrCatalogRoot, "subagents"),
|
|
910
|
+
path.join(repoOrCatalogRoot, "catalog", "subagents"),
|
|
911
|
+
path.join(repoOrCatalogRoot, ".github", "agents"),
|
|
912
|
+
path.join(path.dirname(repoOrCatalogRoot), ".github", "agents")
|
|
913
|
+
];
|
|
914
|
+
for (const dir of candidates) {
|
|
915
|
+
if (!fs2.existsSync(dir) || !fs2.statSync(dir).isDirectory()) {
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
const stack = [dir];
|
|
919
|
+
while (stack.length > 0) {
|
|
920
|
+
const current = stack.pop();
|
|
921
|
+
for (const entry of fs2.readdirSync(current, { withFileTypes: true })) {
|
|
922
|
+
const full = path.join(current, entry.name);
|
|
923
|
+
if (entry.isDirectory()) {
|
|
924
|
+
stack.push(full);
|
|
925
|
+
} else if (entry.isFile() && (entry.name === "SUBAGENT.md" || entry.name === "AGENT.md" || entry.name.endsWith(".agent.md"))) {
|
|
926
|
+
results.add(path.resolve(full));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return [...results].sort();
|
|
932
|
+
}
|
|
933
|
+
function parseSubagentFile(filePath) {
|
|
934
|
+
const rawText = fs2.readFileSync(filePath, "utf-8");
|
|
935
|
+
const { manifest, systemPromptBody } = parseSubagentMdFile(filePath);
|
|
936
|
+
return {
|
|
937
|
+
filePath,
|
|
938
|
+
manifest,
|
|
939
|
+
systemPromptBody,
|
|
940
|
+
rawText
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/auditor/token_budget.ts
|
|
945
|
+
function auditSkillTokenBudget(skillDir, options = {}) {
|
|
946
|
+
const requireLayout = options.requireLayout ?? false;
|
|
947
|
+
try {
|
|
948
|
+
const parsed = parseSkillDirectory(skillDir, { requireLayout });
|
|
949
|
+
const warnings = [];
|
|
950
|
+
const errors = [];
|
|
951
|
+
if (parsed.manifest.routing_signals.negative_triggers.length === 0 && !parsed.manifest.description.toLowerCase().includes("do not trigger")) {
|
|
952
|
+
warnings.push(
|
|
953
|
+
`Skill '${parsed.manifest.name}' has no explicit negative triggers ('Do not trigger for...' or routing_signals.negative_triggers).`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (requireLayout && parsed.missingDeclaredScripts.length > 0) {
|
|
957
|
+
errors.push(
|
|
958
|
+
`Skill '${parsed.manifest.name}' is missing declared script(s): ${parsed.missingDeclaredScripts.join(", ")}`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
skillName: parsed.manifest.name,
|
|
963
|
+
skillDir: skillDir.replace(/\\/g, "/"),
|
|
964
|
+
l1Words: parsed.manifest.description_words,
|
|
965
|
+
l1Tokens: parsed.manifest.description_tokens,
|
|
966
|
+
l2Lines: parsed.manifest.body_lines,
|
|
967
|
+
l2Tokens: parsed.manifest.body_tokens,
|
|
968
|
+
totalLines: parsed.totalLines,
|
|
969
|
+
scriptsCount: parsed.declaredScripts.length,
|
|
970
|
+
passed: errors.length === 0,
|
|
971
|
+
errors,
|
|
972
|
+
warnings
|
|
973
|
+
};
|
|
974
|
+
} catch (err) {
|
|
975
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
976
|
+
return {
|
|
977
|
+
skillName: skillDir.split(/[\\/]/).pop() ?? "unknown",
|
|
978
|
+
skillDir: skillDir.replace(/\\/g, "/"),
|
|
979
|
+
l1Words: 0,
|
|
980
|
+
l1Tokens: 0,
|
|
981
|
+
l2Lines: 0,
|
|
982
|
+
l2Tokens: 0,
|
|
983
|
+
totalLines: 0,
|
|
984
|
+
scriptsCount: 0,
|
|
985
|
+
passed: false,
|
|
986
|
+
errors: [message],
|
|
987
|
+
warnings: []
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function auditSubagentTokenBudget(filePath) {
|
|
992
|
+
try {
|
|
993
|
+
const parsed = parseSubagentFile(filePath);
|
|
994
|
+
return {
|
|
995
|
+
subagentName: parsed.manifest.name,
|
|
996
|
+
filePath: filePath.replace(/\\/g, "/"),
|
|
997
|
+
descriptionTokens: parsed.manifest.description_tokens,
|
|
998
|
+
systemPromptLines: parsed.manifest.system_prompt_lines,
|
|
999
|
+
systemPromptTokens: parsed.manifest.system_prompt_tokens,
|
|
1000
|
+
maxOutputTokens: parsed.manifest.max_output_tokens,
|
|
1001
|
+
tools: parsed.manifest.tools,
|
|
1002
|
+
passed: true,
|
|
1003
|
+
errors: []
|
|
1004
|
+
};
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1007
|
+
return {
|
|
1008
|
+
subagentName: filePath.split(/[\\/]/).pop() ?? "unknown",
|
|
1009
|
+
filePath: filePath.replace(/\\/g, "/"),
|
|
1010
|
+
descriptionTokens: 0,
|
|
1011
|
+
systemPromptLines: 0,
|
|
1012
|
+
systemPromptTokens: 0,
|
|
1013
|
+
maxOutputTokens: 0,
|
|
1014
|
+
tools: [],
|
|
1015
|
+
passed: false,
|
|
1016
|
+
errors: [message]
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function auditCatalogTokenBudgets(catalogDir, options = {}) {
|
|
1021
|
+
const skillDirs = discoverSkillDirectories(catalogDir);
|
|
1022
|
+
const subagentFiles = discoverSubagentFiles(catalogDir);
|
|
1023
|
+
const skills = skillDirs.map((dir) => auditSkillTokenBudget(dir, options));
|
|
1024
|
+
const subagents = subagentFiles.map((file) => auditSubagentTokenBudget(file));
|
|
1025
|
+
const errors = [];
|
|
1026
|
+
const warnings = [];
|
|
1027
|
+
let totalL1Tokens = 0;
|
|
1028
|
+
let totalSubagentPromptTokens = 0;
|
|
1029
|
+
for (const s of skills) {
|
|
1030
|
+
totalL1Tokens += s.l1Tokens;
|
|
1031
|
+
errors.push(...s.errors);
|
|
1032
|
+
warnings.push(...s.warnings);
|
|
1033
|
+
}
|
|
1034
|
+
for (const sa of subagents) {
|
|
1035
|
+
totalSubagentPromptTokens += sa.systemPromptTokens;
|
|
1036
|
+
errors.push(...sa.errors);
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
catalogPath: catalogDir.replace(/\\/g, "/"),
|
|
1040
|
+
skills,
|
|
1041
|
+
subagents,
|
|
1042
|
+
totalL1Tokens,
|
|
1043
|
+
totalSubagentPromptTokens,
|
|
1044
|
+
errors,
|
|
1045
|
+
warnings,
|
|
1046
|
+
passed: errors.length === 0
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/auditor/collision.ts
|
|
1051
|
+
var ROUTING_STOPWORDS = /* @__PURE__ */ new Set([
|
|
1052
|
+
"a",
|
|
1053
|
+
"about",
|
|
1054
|
+
"above",
|
|
1055
|
+
"after",
|
|
1056
|
+
"again",
|
|
1057
|
+
"against",
|
|
1058
|
+
"all",
|
|
1059
|
+
"also",
|
|
1060
|
+
"am",
|
|
1061
|
+
"an",
|
|
1062
|
+
"and",
|
|
1063
|
+
"any",
|
|
1064
|
+
"are",
|
|
1065
|
+
"as",
|
|
1066
|
+
"at",
|
|
1067
|
+
"be",
|
|
1068
|
+
"because",
|
|
1069
|
+
"been",
|
|
1070
|
+
"before",
|
|
1071
|
+
"being",
|
|
1072
|
+
"below",
|
|
1073
|
+
"between",
|
|
1074
|
+
"both",
|
|
1075
|
+
"but",
|
|
1076
|
+
"by",
|
|
1077
|
+
"can",
|
|
1078
|
+
"could",
|
|
1079
|
+
"did",
|
|
1080
|
+
"do",
|
|
1081
|
+
"does",
|
|
1082
|
+
"doing",
|
|
1083
|
+
"dont",
|
|
1084
|
+
"down",
|
|
1085
|
+
"during",
|
|
1086
|
+
"each",
|
|
1087
|
+
"either",
|
|
1088
|
+
"for",
|
|
1089
|
+
"from",
|
|
1090
|
+
"further",
|
|
1091
|
+
"had",
|
|
1092
|
+
"has",
|
|
1093
|
+
"have",
|
|
1094
|
+
"having",
|
|
1095
|
+
"here",
|
|
1096
|
+
"how",
|
|
1097
|
+
"if",
|
|
1098
|
+
"in",
|
|
1099
|
+
"into",
|
|
1100
|
+
"is",
|
|
1101
|
+
"it",
|
|
1102
|
+
"its",
|
|
1103
|
+
"just",
|
|
1104
|
+
"more",
|
|
1105
|
+
"most",
|
|
1106
|
+
"must",
|
|
1107
|
+
"no",
|
|
1108
|
+
"nor",
|
|
1109
|
+
"not",
|
|
1110
|
+
"now",
|
|
1111
|
+
"of",
|
|
1112
|
+
"off",
|
|
1113
|
+
"on",
|
|
1114
|
+
"once",
|
|
1115
|
+
"only",
|
|
1116
|
+
"or",
|
|
1117
|
+
"other",
|
|
1118
|
+
"our",
|
|
1119
|
+
"out",
|
|
1120
|
+
"over",
|
|
1121
|
+
"own",
|
|
1122
|
+
"same",
|
|
1123
|
+
"should",
|
|
1124
|
+
"so",
|
|
1125
|
+
"some",
|
|
1126
|
+
"such",
|
|
1127
|
+
"than",
|
|
1128
|
+
"that",
|
|
1129
|
+
"the",
|
|
1130
|
+
"their",
|
|
1131
|
+
"them",
|
|
1132
|
+
"then",
|
|
1133
|
+
"there",
|
|
1134
|
+
"these",
|
|
1135
|
+
"they",
|
|
1136
|
+
"this",
|
|
1137
|
+
"those",
|
|
1138
|
+
"through",
|
|
1139
|
+
"to",
|
|
1140
|
+
"too",
|
|
1141
|
+
"trigger",
|
|
1142
|
+
"triggers",
|
|
1143
|
+
"under",
|
|
1144
|
+
"until",
|
|
1145
|
+
"up",
|
|
1146
|
+
"use",
|
|
1147
|
+
"used",
|
|
1148
|
+
"using",
|
|
1149
|
+
"very",
|
|
1150
|
+
"via",
|
|
1151
|
+
"was",
|
|
1152
|
+
"we",
|
|
1153
|
+
"were",
|
|
1154
|
+
"what",
|
|
1155
|
+
"when",
|
|
1156
|
+
"where",
|
|
1157
|
+
"which",
|
|
1158
|
+
"while",
|
|
1159
|
+
"who",
|
|
1160
|
+
"whom",
|
|
1161
|
+
"why",
|
|
1162
|
+
"will",
|
|
1163
|
+
"with",
|
|
1164
|
+
"without",
|
|
1165
|
+
"would",
|
|
1166
|
+
"you",
|
|
1167
|
+
"your"
|
|
1168
|
+
]);
|
|
1169
|
+
var NEGATIVE_CLAUSE_PATTERNS = [
|
|
1170
|
+
/do\s+not\s+trigger\b[\s\S]*$/i,
|
|
1171
|
+
/never\s+trigger\b[\s\S]*$/i,
|
|
1172
|
+
/do\s+not\s+use\b[\s\S]*$/i
|
|
1173
|
+
];
|
|
1174
|
+
function stripNegativeTriggerClauses(description) {
|
|
1175
|
+
let cleaned = description;
|
|
1176
|
+
for (const pattern of NEGATIVE_CLAUSE_PATTERNS) {
|
|
1177
|
+
cleaned = cleaned.replace(pattern, " ");
|
|
1178
|
+
}
|
|
1179
|
+
return cleaned.trim();
|
|
1180
|
+
}
|
|
1181
|
+
function tokenizeRoutingText(text) {
|
|
1182
|
+
const lowered = text.toLowerCase();
|
|
1183
|
+
const matches = lowered.match(/[a-z0-9]{2,}/g) ?? [];
|
|
1184
|
+
return matches.filter((tok) => !ROUTING_STOPWORDS.has(tok));
|
|
1185
|
+
}
|
|
1186
|
+
function addWeightedFeature(features, key, weight) {
|
|
1187
|
+
features.set(key, (features.get(key) ?? 0) + weight);
|
|
1188
|
+
}
|
|
1189
|
+
function extractSkillFeatures(manifest) {
|
|
1190
|
+
const features = /* @__PURE__ */ new Map();
|
|
1191
|
+
for (const tok of tokenizeRoutingText(manifest.name.replace(/-/g, " "))) {
|
|
1192
|
+
addWeightedFeature(features, `word:${tok}`, 3);
|
|
1193
|
+
}
|
|
1194
|
+
for (const tag of manifest.tags) {
|
|
1195
|
+
for (const tok of tokenizeRoutingText(tag.replace(/-/g, " "))) {
|
|
1196
|
+
addWeightedFeature(features, `word:${tok}`, 2.5);
|
|
1197
|
+
addWeightedFeature(features, `tag:${tok}`, 2);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
const positiveDesc = stripNegativeTriggerClauses(manifest.description);
|
|
1201
|
+
const descTokens = tokenizeRoutingText(positiveDesc);
|
|
1202
|
+
for (let i = 0; i < descTokens.length; i++) {
|
|
1203
|
+
const tok = descTokens[i];
|
|
1204
|
+
addWeightedFeature(features, `word:${tok}`, 2);
|
|
1205
|
+
if (i + 1 < descTokens.length) {
|
|
1206
|
+
addWeightedFeature(features, `bi:${tok}_${descTokens[i + 1]}`, 1.2);
|
|
1207
|
+
}
|
|
1208
|
+
if (tok.length >= 4) {
|
|
1209
|
+
for (let c = 0; c <= tok.length - 3; c++) {
|
|
1210
|
+
addWeightedFeature(features, `c3:${tok.slice(c, c + 3)}`, 0.35);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
for (const dep of manifest.routing_signals.dependencies_any) {
|
|
1215
|
+
for (const tok of tokenizeRoutingText(dep.replace(/-/g, " "))) {
|
|
1216
|
+
addWeightedFeature(features, `dep:${tok}`, 1.5);
|
|
1217
|
+
addWeightedFeature(features, `word:${tok}`, 1.5);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
for (const filePat of manifest.routing_signals.files_any) {
|
|
1221
|
+
for (const tok of tokenizeRoutingText(filePat.replace(/[.*\\/_-]/g, " "))) {
|
|
1222
|
+
addWeightedFeature(features, `file:${tok}`, 1.2);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return features;
|
|
1226
|
+
}
|
|
1227
|
+
function computeIdfWeights(featureMaps) {
|
|
1228
|
+
const nDocs = featureMaps.length;
|
|
1229
|
+
const docFreq = /* @__PURE__ */ new Map();
|
|
1230
|
+
for (const fmap of featureMaps) {
|
|
1231
|
+
for (const key of fmap.keys()) {
|
|
1232
|
+
docFreq.set(key, (docFreq.get(key) ?? 0) + 1);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const idf = /* @__PURE__ */ new Map();
|
|
1236
|
+
for (const [key, df] of docFreq.entries()) {
|
|
1237
|
+
idf.set(key, 1 + Math.log((nDocs + 1) / (df + 1)));
|
|
1238
|
+
}
|
|
1239
|
+
return idf;
|
|
1240
|
+
}
|
|
1241
|
+
function computeFeatureCosine(featuresA, featuresB, idfWeights) {
|
|
1242
|
+
let dot = 0;
|
|
1243
|
+
let normA = 0;
|
|
1244
|
+
let normB = 0;
|
|
1245
|
+
const sharedContribs = [];
|
|
1246
|
+
for (const [k, vA] of featuresA.entries()) {
|
|
1247
|
+
const w = idfWeights?.get(k) ?? 1;
|
|
1248
|
+
const weightedA = vA * w;
|
|
1249
|
+
normA += weightedA * weightedA;
|
|
1250
|
+
const vB = featuresB.get(k);
|
|
1251
|
+
if (vB !== void 0) {
|
|
1252
|
+
const weightedB = vB * w;
|
|
1253
|
+
const contrib = weightedA * weightedB;
|
|
1254
|
+
dot += contrib;
|
|
1255
|
+
if (k.startsWith("word:") || k.startsWith("tag:") || k.startsWith("dep:")) {
|
|
1256
|
+
sharedContribs.push([k.slice(k.indexOf(":") + 1), contrib]);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
for (const [k, vB] of featuresB.entries()) {
|
|
1261
|
+
const w = idfWeights?.get(k) ?? 1;
|
|
1262
|
+
const weightedB = vB * w;
|
|
1263
|
+
normB += weightedB * weightedB;
|
|
1264
|
+
}
|
|
1265
|
+
if (normA <= 0 || normB <= 0) {
|
|
1266
|
+
return { similarity: 0, sharedTerms: [] };
|
|
1267
|
+
}
|
|
1268
|
+
const rawCos = dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
1269
|
+
const similarity = Math.min(1, Math.max(0, Number(rawCos.toFixed(4))));
|
|
1270
|
+
sharedContribs.sort((a, b) => b[1] - a[1]);
|
|
1271
|
+
const topTerms = [];
|
|
1272
|
+
for (const [term] of sharedContribs) {
|
|
1273
|
+
if (!topTerms.includes(term)) {
|
|
1274
|
+
topTerms.push(term);
|
|
1275
|
+
if (topTerms.length >= 6) {
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return { similarity, sharedTerms: topTerms };
|
|
1281
|
+
}
|
|
1282
|
+
function computeCollisionMatrix(manifests) {
|
|
1283
|
+
const sorted = [...manifests].sort((a, b) => a.name.localeCompare(b.name));
|
|
1284
|
+
const rawFeatures = sorted.map((m) => extractSkillFeatures(m));
|
|
1285
|
+
const idfWeights = computeIdfWeights(rawFeatures);
|
|
1286
|
+
const unitVectors = rawFeatures.map((fmap) => {
|
|
1287
|
+
let sumSq = 0;
|
|
1288
|
+
const weighted = /* @__PURE__ */ new Map();
|
|
1289
|
+
for (const [k, val] of fmap.entries()) {
|
|
1290
|
+
const wVal = val * (idfWeights.get(k) ?? 1);
|
|
1291
|
+
weighted.set(k, wVal);
|
|
1292
|
+
sumSq += wVal * wVal;
|
|
1293
|
+
}
|
|
1294
|
+
const norm = Math.sqrt(sumSq);
|
|
1295
|
+
if (norm <= 0) {
|
|
1296
|
+
return /* @__PURE__ */ new Map();
|
|
1297
|
+
}
|
|
1298
|
+
const unit = /* @__PURE__ */ new Map();
|
|
1299
|
+
for (const [k, wVal] of weighted.entries()) {
|
|
1300
|
+
unit.set(k, wVal / norm);
|
|
1301
|
+
}
|
|
1302
|
+
return unit;
|
|
1303
|
+
});
|
|
1304
|
+
const pairs = [];
|
|
1305
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
1306
|
+
const vecA = unitVectors[i];
|
|
1307
|
+
const nameA = sorted[i].name;
|
|
1308
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
1309
|
+
const vecB = unitVectors[j];
|
|
1310
|
+
const nameB = sorted[j].name;
|
|
1311
|
+
let dot = 0;
|
|
1312
|
+
const sharedContribs = [];
|
|
1313
|
+
for (const [k, uA] of vecA.entries()) {
|
|
1314
|
+
const uB = vecB.get(k);
|
|
1315
|
+
if (uB !== void 0) {
|
|
1316
|
+
const contrib = uA * uB;
|
|
1317
|
+
dot += contrib;
|
|
1318
|
+
if (k.startsWith("word:") || k.startsWith("tag:") || k.startsWith("dep:")) {
|
|
1319
|
+
sharedContribs.push([k.slice(k.indexOf(":") + 1), contrib]);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
sharedContribs.sort((a, b) => b[1] - a[1]);
|
|
1324
|
+
const sharedTerms = [];
|
|
1325
|
+
for (const [term] of sharedContribs) {
|
|
1326
|
+
if (!sharedTerms.includes(term)) {
|
|
1327
|
+
sharedTerms.push(term);
|
|
1328
|
+
if (sharedTerms.length >= 6) {
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
pairs.push(createCollisionPair(nameA, nameB, dot, sharedTerms));
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return pairs.sort(
|
|
1337
|
+
(a, b) => b.similarity - a.similarity || a.skill_a.localeCompare(b.skill_a) || a.skill_b.localeCompare(b.skill_b)
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
function auditCatalog(catalogDir, options = {}) {
|
|
1341
|
+
const requireLayout = options.requireLayout ?? false;
|
|
1342
|
+
const skillDirs = discoverSkillDirectories(catalogDir);
|
|
1343
|
+
const subagentFiles = discoverSubagentFiles(catalogDir);
|
|
1344
|
+
const manifests = [];
|
|
1345
|
+
const budgetErrors = [];
|
|
1346
|
+
let totalL1Tokens = 0;
|
|
1347
|
+
for (const dir of skillDirs) {
|
|
1348
|
+
try {
|
|
1349
|
+
const parsed = parseSkillDirectory(dir, { requireLayout });
|
|
1350
|
+
manifests.push(parsed.manifest);
|
|
1351
|
+
totalL1Tokens += parsed.manifest.description_tokens;
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
budgetErrors.push(err instanceof Error ? err.message : String(err));
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
let subagentsAudited = 0;
|
|
1357
|
+
for (const subFile of subagentFiles) {
|
|
1358
|
+
try {
|
|
1359
|
+
parseSubagentFile(subFile);
|
|
1360
|
+
subagentsAudited += 1;
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
budgetErrors.push(err instanceof Error ? err.message : String(err));
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const pairs = computeCollisionMatrix(manifests);
|
|
1366
|
+
const maxSimilarity = pairs.length > 0 ? Math.max(...pairs.map((p) => p.similarity)) : 0;
|
|
1367
|
+
return createCollisionReport({
|
|
1368
|
+
catalog_path: catalogDir.replace(/\\/g, "/"),
|
|
1369
|
+
skills_audited: manifests.length,
|
|
1370
|
+
subagents_audited: subagentsAudited,
|
|
1371
|
+
total_l1_tokens: totalL1Tokens,
|
|
1372
|
+
max_similarity: maxSimilarity,
|
|
1373
|
+
pairs,
|
|
1374
|
+
budget_errors: budgetErrors
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/auditor/token_economy.ts
|
|
1379
|
+
import fs3 from "fs";
|
|
1380
|
+
import path2 from "path";
|
|
1381
|
+
import YAML2 from "yaml";
|
|
1382
|
+
var ALWAYS_ON_INSTRUCTION_CANDIDATES = [
|
|
1383
|
+
"CLAUDE.md",
|
|
1384
|
+
".claude/CLAUDE.md",
|
|
1385
|
+
"AGENTS.md",
|
|
1386
|
+
"GEMINI.md",
|
|
1387
|
+
".gemini/GEMINI.md",
|
|
1388
|
+
"AGENT.md",
|
|
1389
|
+
"CONVENTIONS.md",
|
|
1390
|
+
".github/copilot-instructions.md",
|
|
1391
|
+
".cursorrules",
|
|
1392
|
+
".windsurfrules",
|
|
1393
|
+
".clinerules",
|
|
1394
|
+
".roorules",
|
|
1395
|
+
".kilocoderules",
|
|
1396
|
+
".continuerules",
|
|
1397
|
+
".rules",
|
|
1398
|
+
".goosehints",
|
|
1399
|
+
".junie/guidelines.md",
|
|
1400
|
+
".openhands/microagents/repo.md",
|
|
1401
|
+
".trae/rules/project_rules.md",
|
|
1402
|
+
".kiro/steering/product.md",
|
|
1403
|
+
".kiro/steering/tech.md",
|
|
1404
|
+
".kiro/steering/structure.md"
|
|
1405
|
+
];
|
|
1406
|
+
var MCP_CONFIG_CANDIDATES = [
|
|
1407
|
+
".mcp.json",
|
|
1408
|
+
".cursor/mcp.json",
|
|
1409
|
+
".vscode/mcp.json",
|
|
1410
|
+
".gemini/settings.json",
|
|
1411
|
+
".codex/config.toml",
|
|
1412
|
+
".roo/mcp.json",
|
|
1413
|
+
".kilocode/mcp.json",
|
|
1414
|
+
".zed/settings.json",
|
|
1415
|
+
".continue/mcpServers/skillops.yaml",
|
|
1416
|
+
".amp/settings.json",
|
|
1417
|
+
"opencode.json",
|
|
1418
|
+
".kiro/settings/mcp.json",
|
|
1419
|
+
".amazonq/mcp.json"
|
|
1420
|
+
];
|
|
1421
|
+
function countMcpServersInConfig(rawText, relPath) {
|
|
1422
|
+
if (relPath.endsWith(".toml")) {
|
|
1423
|
+
const matches = rawText.match(/^\[mcp_servers\.[^\]]+\]/gm);
|
|
1424
|
+
return matches ? matches.length : 0;
|
|
1425
|
+
}
|
|
1426
|
+
try {
|
|
1427
|
+
const parsed = YAML2.parse(rawText);
|
|
1428
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1429
|
+
return 0;
|
|
1430
|
+
}
|
|
1431
|
+
if (Array.isArray(parsed.mcpServers)) {
|
|
1432
|
+
return parsed.mcpServers.length;
|
|
1433
|
+
}
|
|
1434
|
+
const serversObj = parsed.mcpServers ?? parsed.servers ?? parsed.context_servers ?? parsed["amp.mcpServers"] ?? parsed.mcp;
|
|
1435
|
+
if (serversObj && typeof serversObj === "object" && !Array.isArray(serversObj)) {
|
|
1436
|
+
return Object.keys(serversObj).length;
|
|
1437
|
+
}
|
|
1438
|
+
return 0;
|
|
1439
|
+
} catch {
|
|
1440
|
+
return 1;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
var UNISOLATED_SUBAGENT_EXPLORATION_TOKENS = 1200;
|
|
1444
|
+
var MCP_SERVER_SCHEMA_OVERHEAD_TOKENS = 180;
|
|
1445
|
+
function countLines(text) {
|
|
1446
|
+
if (!text) {
|
|
1447
|
+
return 0;
|
|
1448
|
+
}
|
|
1449
|
+
const lines = text.split(/\r?\n/);
|
|
1450
|
+
return text.endsWith("\n") ? lines.length - 1 : lines.length;
|
|
1451
|
+
}
|
|
1452
|
+
function toPosixRelative(baseDir, targetPath) {
|
|
1453
|
+
const rel = path2.relative(baseDir, targetPath).replace(/\\/g, "/");
|
|
1454
|
+
return rel && !rel.startsWith("..") ? rel : path2.basename(targetPath);
|
|
1455
|
+
}
|
|
1456
|
+
function analyzeRepoTokenEconomy(repoPath, options = {}) {
|
|
1457
|
+
const resolvedRepo = path2.resolve(repoPath);
|
|
1458
|
+
const resolvedCatalog = options.catalogDir ? path2.resolve(options.catalogDir) : path2.join(resolvedRepo, "catalog");
|
|
1459
|
+
const startupTaxBudget = options.startupTaxBudget ?? 2500;
|
|
1460
|
+
const inspectedFiles = [];
|
|
1461
|
+
const hotspots = [];
|
|
1462
|
+
let startupTokenTax = 0;
|
|
1463
|
+
const foundAlwaysOnDocs = [];
|
|
1464
|
+
for (const relCandidate of ALWAYS_ON_INSTRUCTION_CANDIDATES) {
|
|
1465
|
+
const fullPath = path2.join(resolvedRepo, relCandidate);
|
|
1466
|
+
if (fs3.existsSync(fullPath) && fs3.statSync(fullPath).isFile()) {
|
|
1467
|
+
const rawText = fs3.readFileSync(fullPath, "utf-8");
|
|
1468
|
+
const lines = countLines(rawText);
|
|
1469
|
+
const tokens = estimateTokenCount(rawText);
|
|
1470
|
+
startupTokenTax += tokens;
|
|
1471
|
+
foundAlwaysOnDocs.push({ relPath: relCandidate, tokens, rawText });
|
|
1472
|
+
inspectedFiles.push({
|
|
1473
|
+
path: relCandidate,
|
|
1474
|
+
category: "always_on_instructions",
|
|
1475
|
+
always_loaded_on_startup: true,
|
|
1476
|
+
lines,
|
|
1477
|
+
estimated_tokens: tokens,
|
|
1478
|
+
notes: "Always-on root instruction file loaded into every session startup."
|
|
1479
|
+
});
|
|
1480
|
+
if (tokens > 1500) {
|
|
1481
|
+
hotspots.push({
|
|
1482
|
+
path: relCandidate,
|
|
1483
|
+
severity: "CRITICAL",
|
|
1484
|
+
wasted_tokens: tokens - 600,
|
|
1485
|
+
issue: `Oversized always-on instruction file (${tokens} tokens across ${lines} lines) inflates every turn.`,
|
|
1486
|
+
recommendation: "Keep root instruction files <= 600 tokens and extract domain playbooks into on-demand Level 2/3 Skills."
|
|
1487
|
+
});
|
|
1488
|
+
} else if (tokens > 700) {
|
|
1489
|
+
hotspots.push({
|
|
1490
|
+
path: relCandidate,
|
|
1491
|
+
severity: "HIGH",
|
|
1492
|
+
wasted_tokens: tokens - 500,
|
|
1493
|
+
issue: `Large always-on instruction file (${tokens} tokens) increases baseline Startup Token Tax.`,
|
|
1494
|
+
recommendation: "Move specialized architectural checklists and long examples into progressive-disclosure Skills."
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
const claudeDoc = foundAlwaysOnDocs.find((d) => d.relPath === "CLAUDE.md");
|
|
1500
|
+
const agentsDoc = foundAlwaysOnDocs.find((d) => d.relPath === "AGENTS.md");
|
|
1501
|
+
if (claudeDoc && agentsDoc && claudeDoc.tokens > 200 && agentsDoc.tokens > 200 && !claudeDoc.rawText.includes("@AGENTS.md")) {
|
|
1502
|
+
hotspots.push({
|
|
1503
|
+
path: "CLAUDE.md + AGENTS.md",
|
|
1504
|
+
severity: "HIGH",
|
|
1505
|
+
wasted_tokens: Math.min(claudeDoc.tokens, agentsDoc.tokens),
|
|
1506
|
+
issue: "Duplicate root instruction files ('CLAUDE.md' and 'AGENTS.md') both contain full prose without '@AGENTS.md' delegation.",
|
|
1507
|
+
recommendation: "Keep canonical repo instructions in AGENTS.md and make CLAUDE.md a lightweight pointer or run 'npx skillops sync'."
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
const scopedRuleDirs = [
|
|
1511
|
+
{
|
|
1512
|
+
dirPath: path2.join(resolvedRepo, ".cursor", "rules"),
|
|
1513
|
+
relPrefix: ".cursor/rules",
|
|
1514
|
+
isAlwaysOn: (fm) => fm.alwaysApply === true,
|
|
1515
|
+
label: "Cursor"
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
dirPath: path2.join(resolvedRepo, ".windsurf", "rules"),
|
|
1519
|
+
relPrefix: ".windsurf/rules",
|
|
1520
|
+
isAlwaysOn: (fm) => fm.trigger === "always_on" || fm.alwaysApply === true,
|
|
1521
|
+
label: "Windsurf"
|
|
1522
|
+
}
|
|
1523
|
+
];
|
|
1524
|
+
for (const ruleGroup of scopedRuleDirs) {
|
|
1525
|
+
if (fs3.existsSync(ruleGroup.dirPath) && fs3.statSync(ruleGroup.dirPath).isDirectory()) {
|
|
1526
|
+
const entries = fs3.readdirSync(ruleGroup.dirPath, { withFileTypes: true }).filter((e) => e.isFile() && (e.name.endsWith(".mdc") || e.name.endsWith(".md"))).sort((a, b) => a.name.localeCompare(b.name));
|
|
1527
|
+
for (const entry of entries) {
|
|
1528
|
+
const fullPath = path2.join(ruleGroup.dirPath, entry.name);
|
|
1529
|
+
const relPath = `${ruleGroup.relPrefix}/${entry.name}`;
|
|
1530
|
+
const rawText = fs3.readFileSync(fullPath, "utf-8");
|
|
1531
|
+
const lines = countLines(rawText);
|
|
1532
|
+
const tokens = estimateTokenCount(rawText);
|
|
1533
|
+
let alwaysApply = false;
|
|
1534
|
+
try {
|
|
1535
|
+
const [fm] = splitFrontmatter(rawText, relPath);
|
|
1536
|
+
alwaysApply = ruleGroup.isAlwaysOn(fm);
|
|
1537
|
+
} catch {
|
|
1538
|
+
alwaysApply = true;
|
|
1539
|
+
}
|
|
1540
|
+
if (alwaysApply) {
|
|
1541
|
+
startupTokenTax += tokens;
|
|
1542
|
+
}
|
|
1543
|
+
inspectedFiles.push({
|
|
1544
|
+
path: relPath,
|
|
1545
|
+
category: "always_on_cursor_rule",
|
|
1546
|
+
always_loaded_on_startup: alwaysApply,
|
|
1547
|
+
lines,
|
|
1548
|
+
estimated_tokens: tokens,
|
|
1549
|
+
notes: alwaysApply ? `${ruleGroup.label} rule with always-on activation (loaded into every turn).` : `Scoped ${ruleGroup.label} rule (loaded on demand).`
|
|
1550
|
+
});
|
|
1551
|
+
if (alwaysApply && tokens > 250) {
|
|
1552
|
+
hotspots.push({
|
|
1553
|
+
path: relPath,
|
|
1554
|
+
severity: tokens > 600 ? "CRITICAL" : "HIGH",
|
|
1555
|
+
wasted_tokens: Math.max(1, tokens - 80),
|
|
1556
|
+
issue: `Unscoped ${ruleGroup.label} rule '${relPath}' is always-on (${tokens} tokens).`,
|
|
1557
|
+
recommendation: "Set 'alwaysApply: false' / 'trigger: model_decision' and add explicit 'globs' or convert into a Progressive Disclosure Skill."
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
for (const relMcp of MCP_CONFIG_CANDIDATES) {
|
|
1564
|
+
const fullMcp = path2.join(resolvedRepo, relMcp);
|
|
1565
|
+
if (fs3.existsSync(fullMcp) && fs3.statSync(fullMcp).isFile()) {
|
|
1566
|
+
const rawText = fs3.readFileSync(fullMcp, "utf-8");
|
|
1567
|
+
const lines = countLines(rawText);
|
|
1568
|
+
const serverCount = countMcpServersInConfig(rawText, relMcp);
|
|
1569
|
+
const rawTokens = estimateTokenCount(rawText);
|
|
1570
|
+
const estimatedMcpTax = rawTokens + serverCount * MCP_SERVER_SCHEMA_OVERHEAD_TOKENS;
|
|
1571
|
+
startupTokenTax += estimatedMcpTax;
|
|
1572
|
+
inspectedFiles.push({
|
|
1573
|
+
path: relMcp,
|
|
1574
|
+
category: "mcp_schema_tax",
|
|
1575
|
+
always_loaded_on_startup: true,
|
|
1576
|
+
lines,
|
|
1577
|
+
estimated_tokens: estimatedMcpTax,
|
|
1578
|
+
notes: `MCP configuration (${serverCount} server(s)) injecting tool JSON schemas at startup.`
|
|
1579
|
+
});
|
|
1580
|
+
if (serverCount > 3 || estimatedMcpTax > 400) {
|
|
1581
|
+
hotspots.push({
|
|
1582
|
+
path: relMcp,
|
|
1583
|
+
severity: serverCount >= 8 || estimatedMcpTax > 1500 ? "CRITICAL" : "HIGH",
|
|
1584
|
+
wasted_tokens: Math.max(1, estimatedMcpTax - 300),
|
|
1585
|
+
issue: `MCP config '${relMcp}' registers ${serverCount} always-on server(s) (~${estimatedMcpTax} startup schema tokens).`,
|
|
1586
|
+
recommendation: "Replace heavy always-on MCP tool schemas with zero-token Level 3 CLI scripts or disable unused MCP servers."
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
const skillDirs = discoverSkillDirectories(resolvedCatalog);
|
|
1592
|
+
let progressiveDisclosureL1Tokens = 0;
|
|
1593
|
+
let monolithicEquivalentTokens = 0;
|
|
1594
|
+
let zeroTokenL3ScriptsCount = 0;
|
|
1595
|
+
for (const skillDir of skillDirs) {
|
|
1596
|
+
try {
|
|
1597
|
+
const parsed = parseSkillDirectory(skillDir, { requireLayout: false });
|
|
1598
|
+
const relSkillMd = toPosixRelative(resolvedRepo, parsed.skillMdPath);
|
|
1599
|
+
const l1Tokens = parsed.manifest.description_tokens;
|
|
1600
|
+
const l2Tokens = parsed.manifest.body_tokens;
|
|
1601
|
+
progressiveDisclosureL1Tokens += l1Tokens;
|
|
1602
|
+
startupTokenTax += l1Tokens;
|
|
1603
|
+
monolithicEquivalentTokens += l1Tokens + l2Tokens;
|
|
1604
|
+
inspectedFiles.push({
|
|
1605
|
+
path: `${relSkillMd}#L1-frontmatter`,
|
|
1606
|
+
category: "skill_l1_metadata",
|
|
1607
|
+
always_loaded_on_startup: true,
|
|
1608
|
+
lines: Math.max(1, parsed.totalLines - parsed.manifest.body_lines),
|
|
1609
|
+
estimated_tokens: l1Tokens,
|
|
1610
|
+
notes: `Skill '${parsed.manifest.name}' Level 1 routing description (<=120 tokens).`
|
|
1611
|
+
});
|
|
1612
|
+
inspectedFiles.push({
|
|
1613
|
+
path: `${relSkillMd}#L2-body`,
|
|
1614
|
+
category: "skill_l2_body",
|
|
1615
|
+
always_loaded_on_startup: false,
|
|
1616
|
+
lines: parsed.manifest.body_lines,
|
|
1617
|
+
estimated_tokens: l2Tokens,
|
|
1618
|
+
notes: `Skill '${parsed.manifest.name}' Level 2 playbook loaded only when triggered.`
|
|
1619
|
+
});
|
|
1620
|
+
for (const scriptPath of parsed.declaredScripts) {
|
|
1621
|
+
const scriptRaw = fs3.readFileSync(scriptPath, "utf-8");
|
|
1622
|
+
const scriptLines = countLines(scriptRaw);
|
|
1623
|
+
const scriptTokens = estimateTokenCount(scriptRaw);
|
|
1624
|
+
zeroTokenL3ScriptsCount += 1;
|
|
1625
|
+
monolithicEquivalentTokens += scriptTokens;
|
|
1626
|
+
inspectedFiles.push({
|
|
1627
|
+
path: toPosixRelative(resolvedRepo, scriptPath),
|
|
1628
|
+
category: "skill_l3_script",
|
|
1629
|
+
always_loaded_on_startup: false,
|
|
1630
|
+
lines: scriptLines,
|
|
1631
|
+
estimated_tokens: scriptTokens,
|
|
1632
|
+
notes: `Zero-token Level 3 executable script (${scriptTokens} source tokens kept out of prompt context).`
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
} catch {
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const subagentFiles = discoverSubagentFiles(resolvedCatalog);
|
|
1639
|
+
let contextIsolatedSubagentsCount = 0;
|
|
1640
|
+
for (const saFile of subagentFiles) {
|
|
1641
|
+
try {
|
|
1642
|
+
const parsedSa = parseSubagentFile(saFile);
|
|
1643
|
+
contextIsolatedSubagentsCount += 1;
|
|
1644
|
+
const descTokens = parsedSa.manifest.description_tokens;
|
|
1645
|
+
const promptTokens = parsedSa.manifest.system_prompt_tokens;
|
|
1646
|
+
progressiveDisclosureL1Tokens += descTokens;
|
|
1647
|
+
monolithicEquivalentTokens += descTokens + promptTokens + UNISOLATED_SUBAGENT_EXPLORATION_TOKENS;
|
|
1648
|
+
inspectedFiles.push({
|
|
1649
|
+
path: toPosixRelative(resolvedRepo, saFile),
|
|
1650
|
+
category: "subagent_definition",
|
|
1651
|
+
always_loaded_on_startup: false,
|
|
1652
|
+
lines: countLines(parsedSa.rawText),
|
|
1653
|
+
estimated_tokens: promptTokens,
|
|
1654
|
+
notes: `Context-isolated subagent '${parsedSa.manifest.name}' (caps output to <=${parsedSa.manifest.max_output_tokens} tokens).`
|
|
1655
|
+
});
|
|
1656
|
+
} catch {
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
const estimatedTokensSaved = Math.max(
|
|
1660
|
+
0,
|
|
1661
|
+
monolithicEquivalentTokens - progressiveDisclosureL1Tokens
|
|
1662
|
+
);
|
|
1663
|
+
const tokenSavingsPct = monolithicEquivalentTokens > 0 ? Number((estimatedTokensSaved / monolithicEquivalentTokens * 100).toFixed(2)) : 0;
|
|
1664
|
+
return TokenEconomyReportSchema.parse({
|
|
1665
|
+
repo_path: resolvedRepo.replace(/\\/g, "/"),
|
|
1666
|
+
catalog_path: resolvedCatalog.replace(/\\/g, "/"),
|
|
1667
|
+
startup_token_tax: startupTokenTax,
|
|
1668
|
+
startup_tax_budget: startupTaxBudget,
|
|
1669
|
+
monolithic_equivalent_tokens: monolithicEquivalentTokens,
|
|
1670
|
+
progressive_disclosure_l1_tokens: progressiveDisclosureL1Tokens,
|
|
1671
|
+
zero_token_l3_scripts_count: zeroTokenL3ScriptsCount,
|
|
1672
|
+
context_isolated_subagents_count: contextIsolatedSubagentsCount,
|
|
1673
|
+
estimated_tokens_saved: estimatedTokensSaved,
|
|
1674
|
+
token_savings_pct: tokenSavingsPct,
|
|
1675
|
+
inspected_files: inspectedFiles,
|
|
1676
|
+
hotspots,
|
|
1677
|
+
passed_budget: startupTokenTax <= startupTaxBudget
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
function formatTokenEconomyReport(report, format = "table") {
|
|
1681
|
+
if (format === "json") {
|
|
1682
|
+
return JSON.stringify(report, null, 2);
|
|
1683
|
+
}
|
|
1684
|
+
if (format === "github") {
|
|
1685
|
+
const lines2 = [];
|
|
1686
|
+
for (const h of report.hotspots) {
|
|
1687
|
+
const level = h.severity === "CRITICAL" || h.severity === "HIGH" ? "error" : "warning";
|
|
1688
|
+
lines2.push(
|
|
1689
|
+
`::${level} file=${h.path},title=SkillOps Token Hotspot (${h.severity})::${h.issue} \u2014 ${h.recommendation}`
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
if (!report.passed_budget) {
|
|
1693
|
+
lines2.push(
|
|
1694
|
+
`::error title=SkillOps Startup Token Tax Exceeded::Startup Token Tax ${report.startup_token_tax} exceeds budget ${report.startup_tax_budget}.`
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
lines2.push(
|
|
1698
|
+
`::notice title=SkillOps Token Economy::Startup Tax ${report.startup_token_tax}/${report.startup_tax_budget} tokens (${report.token_savings_pct.toFixed(2)}% saved via Progressive Disclosure).`
|
|
1699
|
+
);
|
|
1700
|
+
return `${lines2.join("\n")}
|
|
1701
|
+
`;
|
|
1702
|
+
}
|
|
1703
|
+
if (format === "markdown") {
|
|
1704
|
+
const lines2 = [
|
|
1705
|
+
"## SkillOps Token Economy & Startup Context Tax Report",
|
|
1706
|
+
"",
|
|
1707
|
+
`- **Startup Token Tax**: \`${report.startup_token_tax}\` / \`${report.startup_tax_budget}\` tokens (${report.passed_budget ? "PASS" : "OVER BUDGET"})`,
|
|
1708
|
+
`- **Monolithic Equivalent Tokens**: \`${report.monolithic_equivalent_tokens}\` tokens`,
|
|
1709
|
+
`- **Progressive Disclosure L1 Tokens**: \`${report.progressive_disclosure_l1_tokens}\` tokens`,
|
|
1710
|
+
`- **Estimated Tokens Saved**: \`${report.estimated_tokens_saved}\` tokens (**${report.token_savings_pct.toFixed(2)}%** reduction)`,
|
|
1711
|
+
`- **Zero-Token Level 3 Scripts**: \`${report.zero_token_l3_scripts_count}\``,
|
|
1712
|
+
`- **Context-Isolated Subagents**: \`${report.context_isolated_subagents_count}\``,
|
|
1713
|
+
"",
|
|
1714
|
+
"| Category | File Path | Startup Loaded | Lines | Est. Tokens |",
|
|
1715
|
+
"| :--- | :--- | :---: | ---: | ---: |",
|
|
1716
|
+
...report.inspected_files.map(
|
|
1717
|
+
(f) => `| \`${f.category}\` | \`${f.path}\` | ${f.always_loaded_on_startup ? "YES" : "NO"} | ${f.lines} | ${f.estimated_tokens} |`
|
|
1718
|
+
)
|
|
1719
|
+
];
|
|
1720
|
+
if (report.hotspots.length > 0) {
|
|
1721
|
+
lines2.push(
|
|
1722
|
+
"",
|
|
1723
|
+
"### Detected Token Waste Hotspots",
|
|
1724
|
+
"",
|
|
1725
|
+
"| Severity | Path | Wasted Tokens | Issue | Recommendation |",
|
|
1726
|
+
"| :---: | :--- | ---: | :--- | :--- |",
|
|
1727
|
+
...report.hotspots.map(
|
|
1728
|
+
(h) => `| **${h.severity}** | \`${h.path}\` | ${h.wasted_tokens} | ${h.issue} | ${h.recommendation} |`
|
|
1729
|
+
)
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
return `${lines2.join("\n")}
|
|
1733
|
+
`;
|
|
1734
|
+
}
|
|
1735
|
+
const lines = [
|
|
1736
|
+
"=== SkillOps Token Economy & Startup Context Tax Analyzer ===",
|
|
1737
|
+
`Startup Token Tax : ${report.startup_token_tax} / ${report.startup_tax_budget} tokens [${report.passed_budget ? "PASS" : "OVER BUDGET"}]`,
|
|
1738
|
+
`Monolithic Equivalent : ${report.monolithic_equivalent_tokens} tokens`,
|
|
1739
|
+
`Progressive L1 Footprint : ${report.progressive_disclosure_l1_tokens} tokens`,
|
|
1740
|
+
`Token Savings : ${report.estimated_tokens_saved} tokens (${report.token_savings_pct.toFixed(2)}% saved)`,
|
|
1741
|
+
`Level 3 Zero-Token Tools : ${report.zero_token_l3_scripts_count} scripts`,
|
|
1742
|
+
`Context-Isolated Agents : ${report.context_isolated_subagents_count} subagents`
|
|
1743
|
+
];
|
|
1744
|
+
if (report.hotspots.length > 0) {
|
|
1745
|
+
lines.push("", "Token Waste Hotspots:");
|
|
1746
|
+
for (const h of report.hotspots) {
|
|
1747
|
+
lines.push(
|
|
1748
|
+
` [${h.severity}] ${h.path} (~${h.wasted_tokens} wasted tokens): ${h.issue}`,
|
|
1749
|
+
` -> Fix: ${h.recommendation}`
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return `${lines.join("\n")}
|
|
1754
|
+
`;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// src/auditor/doctor.ts
|
|
1758
|
+
import { spawnSync } from "child_process";
|
|
1759
|
+
import crypto from "crypto";
|
|
1760
|
+
import fs4 from "fs";
|
|
1761
|
+
import path3 from "path";
|
|
1762
|
+
function computeFileSha256(content) {
|
|
1763
|
+
return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
1764
|
+
}
|
|
1765
|
+
function detectPythonVersion() {
|
|
1766
|
+
for (const cmd of ["python3", "python"]) {
|
|
1767
|
+
try {
|
|
1768
|
+
const res = spawnSync(cmd, ["--version"], {
|
|
1769
|
+
encoding: "utf-8",
|
|
1770
|
+
timeout: 3e3
|
|
1771
|
+
});
|
|
1772
|
+
if (res.status === 0) {
|
|
1773
|
+
const out = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
|
|
1774
|
+
const match = /Python\s+(\d+\.\d+(?:\.\d+)?)/i.exec(out);
|
|
1775
|
+
if (match?.[1]) {
|
|
1776
|
+
return match[1];
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
} catch {
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return null;
|
|
1783
|
+
}
|
|
1784
|
+
function isVersionAtLeast(versionStr, minMajor, minMinor = 0) {
|
|
1785
|
+
const cleaned = versionStr.replace(/^v/i, "").trim();
|
|
1786
|
+
const parts = cleaned.split(".").map((p) => Number.parseInt(p, 10));
|
|
1787
|
+
const major = parts[0] ?? 0;
|
|
1788
|
+
const minor = parts[1] ?? 0;
|
|
1789
|
+
if (major > minMajor) {
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
if (major === minMajor && minor >= minMinor) {
|
|
1793
|
+
return true;
|
|
1794
|
+
}
|
|
1795
|
+
return false;
|
|
1796
|
+
}
|
|
1797
|
+
function diagnoseRepository(repoPath, options = {}) {
|
|
1798
|
+
const resolvedRepo = path3.resolve(repoPath);
|
|
1799
|
+
const repoLabel = path3.basename(resolvedRepo) || ".";
|
|
1800
|
+
const checks = [];
|
|
1801
|
+
const remediation = [];
|
|
1802
|
+
const nodeVersion = process.version;
|
|
1803
|
+
const nodeOk = isVersionAtLeast(nodeVersion, 20, 0);
|
|
1804
|
+
checks.push({
|
|
1805
|
+
id: "node_runtime",
|
|
1806
|
+
title: "Node.js Runtime (>= 20.0.0)",
|
|
1807
|
+
status: nodeOk ? "pass" : "fail",
|
|
1808
|
+
summary: nodeOk ? `Node.js ${nodeVersion} satisfies engine requirement (>= 20.0.0).` : `Node.js ${nodeVersion} is below minimum required version 20.0.0.`
|
|
1809
|
+
});
|
|
1810
|
+
if (!nodeOk) {
|
|
1811
|
+
remediation.push("Upgrade Node.js to v20 LTS or newer.");
|
|
1812
|
+
}
|
|
1813
|
+
const pythonVersion = detectPythonVersion();
|
|
1814
|
+
const pythonOk = pythonVersion !== null && isVersionAtLeast(pythonVersion, 3, 12);
|
|
1815
|
+
checks.push({
|
|
1816
|
+
id: "python_runtime",
|
|
1817
|
+
title: "Python Runtime (>= 3.12 for Level 3 Scripts)",
|
|
1818
|
+
status: pythonOk ? "pass" : "warn",
|
|
1819
|
+
summary: pythonVersion === null ? "Python runtime not detected on PATH (optional unless executing Python Level 3 scripts)." : pythonOk ? `Python ${pythonVersion} detected and ready for Level 3 zero-token scripts.` : `Python ${pythonVersion} detected, but >= 3.12 is recommended for Level 3 scripts.`
|
|
1820
|
+
});
|
|
1821
|
+
if (!pythonOk) {
|
|
1822
|
+
remediation.push(
|
|
1823
|
+
"Install Python 3.12+ on PATH to execute Python-based Level 3 deterministic scripts."
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
const detectedHarnesses = detectInstalledHarnesses(resolvedRepo);
|
|
1827
|
+
checks.push({
|
|
1828
|
+
id: "detected_harnesses",
|
|
1829
|
+
title: "Detected AI Coding Harnesses (Top 20)",
|
|
1830
|
+
status: "pass",
|
|
1831
|
+
summary: detectedHarnesses.length > 0 ? `Detected ${detectedHarnesses.length} AI coding harness(es): ${detectedHarnesses.join(", ")}.` : "No repo-local AI harness folders detected yet (ready to sync all 20 agents)."
|
|
1832
|
+
});
|
|
1833
|
+
const loadedCfg = loadSkillOpsConfig(resolvedRepo);
|
|
1834
|
+
const configPresent = loadedCfg.exists;
|
|
1835
|
+
let configValid = true;
|
|
1836
|
+
let configError;
|
|
1837
|
+
let configuredStartupBudget = options.startupTaxBudget;
|
|
1838
|
+
if (configPresent) {
|
|
1839
|
+
if (loadedCfg.error) {
|
|
1840
|
+
configValid = false;
|
|
1841
|
+
configError = loadedCfg.error;
|
|
1842
|
+
checks.push({
|
|
1843
|
+
id: "skillops_config",
|
|
1844
|
+
title: "Declarative Config (.skillops.yml)",
|
|
1845
|
+
status: "fail",
|
|
1846
|
+
summary: `Invalid .skillops.yml: ${configError}`
|
|
1847
|
+
});
|
|
1848
|
+
remediation.push(
|
|
1849
|
+
"Fix schema errors in .skillops.yml or run 'npx skillops-cli@latest init' to regenerate it."
|
|
1850
|
+
);
|
|
1851
|
+
} else {
|
|
1852
|
+
if (configuredStartupBudget === void 0 && loadedCfg.config?.token_economy.startup_tax_budget !== void 0) {
|
|
1853
|
+
configuredStartupBudget = loadedCfg.config.token_economy.startup_tax_budget;
|
|
1854
|
+
}
|
|
1855
|
+
checks.push({
|
|
1856
|
+
id: "skillops_config",
|
|
1857
|
+
title: "Declarative Config (.skillops.yml)",
|
|
1858
|
+
status: "pass",
|
|
1859
|
+
summary: ".skillops.yml present and conforms to SkillOpsConfigSchema."
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
checks.push({
|
|
1864
|
+
id: "skillops_config",
|
|
1865
|
+
title: "Declarative Config (.skillops.yml)",
|
|
1866
|
+
status: "pass",
|
|
1867
|
+
summary: "No .skillops.yml found (using built-in SkillOps defaults)."
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
const lockfilePath = path3.join(resolvedRepo, ".skillops-lock.json");
|
|
1871
|
+
const lockfilePresent = fs4.existsSync(lockfilePath) && fs4.statSync(lockfilePath).isFile();
|
|
1872
|
+
let lockfileValid = true;
|
|
1873
|
+
const artifacts = [];
|
|
1874
|
+
if (lockfilePresent) {
|
|
1875
|
+
try {
|
|
1876
|
+
const raw = JSON.parse(fs4.readFileSync(lockfilePath, "utf-8"));
|
|
1877
|
+
const lockfile = SyncLockfileSchema.parse(raw);
|
|
1878
|
+
for (const entry of Object.values(lockfile.artifacts)) {
|
|
1879
|
+
const absPath = path3.join(resolvedRepo, entry.relative_path);
|
|
1880
|
+
if (!fs4.existsSync(absPath) || !fs4.statSync(absPath).isFile()) {
|
|
1881
|
+
artifacts.push({
|
|
1882
|
+
relative_path: entry.relative_path,
|
|
1883
|
+
target: entry.target,
|
|
1884
|
+
item_name: entry.skill_name,
|
|
1885
|
+
expected_sha256: entry.sha256,
|
|
1886
|
+
actual_sha256: null,
|
|
1887
|
+
status: "missing"
|
|
1888
|
+
});
|
|
1889
|
+
} else {
|
|
1890
|
+
const content = fs4.readFileSync(absPath, "utf-8");
|
|
1891
|
+
const actualSha = computeFileSha256(content);
|
|
1892
|
+
artifacts.push({
|
|
1893
|
+
relative_path: entry.relative_path,
|
|
1894
|
+
target: entry.target,
|
|
1895
|
+
item_name: entry.skill_name,
|
|
1896
|
+
expected_sha256: entry.sha256,
|
|
1897
|
+
actual_sha256: actualSha,
|
|
1898
|
+
status: actualSha === entry.sha256 ? "healthy" : "drifted"
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
} catch (err) {
|
|
1903
|
+
lockfileValid = false;
|
|
1904
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1905
|
+
checks.push({
|
|
1906
|
+
id: "lockfile_integrity",
|
|
1907
|
+
title: "Lockfile SHA-256 Integrity (.skillops-lock.json)",
|
|
1908
|
+
status: "fail",
|
|
1909
|
+
summary: `Corrupted .skillops-lock.json: ${msg}`
|
|
1910
|
+
});
|
|
1911
|
+
remediation.push(
|
|
1912
|
+
"Run 'npx skillops-cli@latest sync --force' to rebuild .skillops-lock.json."
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
const healthyCount = artifacts.filter((a) => a.status === "healthy").length;
|
|
1917
|
+
const driftedCount = artifacts.filter((a) => a.status === "drifted").length;
|
|
1918
|
+
const missingCount = artifacts.filter((a) => a.status === "missing").length;
|
|
1919
|
+
if (lockfilePresent && lockfileValid) {
|
|
1920
|
+
if (driftedCount > 0 || missingCount > 0) {
|
|
1921
|
+
checks.push({
|
|
1922
|
+
id: "lockfile_integrity",
|
|
1923
|
+
title: "Lockfile SHA-256 Integrity (.skillops-lock.json)",
|
|
1924
|
+
status: "fail",
|
|
1925
|
+
summary: `${healthyCount}/${artifacts.length} healthy, ${driftedCount} drifted, ${missingCount} missing artifact(s).`
|
|
1926
|
+
});
|
|
1927
|
+
remediation.push(
|
|
1928
|
+
"Run 'npx skillops-cli@latest sync --force' to restore drifted/missing artifacts or update .skillops-lock.json."
|
|
1929
|
+
);
|
|
1930
|
+
} else {
|
|
1931
|
+
checks.push({
|
|
1932
|
+
id: "lockfile_integrity",
|
|
1933
|
+
title: "Lockfile SHA-256 Integrity (.skillops-lock.json)",
|
|
1934
|
+
status: "pass",
|
|
1935
|
+
summary: `All ${artifacts.length} tracked artifact(s) match their SHA-256 lockfile signatures.`
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
} else if (!lockfilePresent) {
|
|
1939
|
+
checks.push({
|
|
1940
|
+
id: "lockfile_integrity",
|
|
1941
|
+
title: "Lockfile SHA-256 Integrity (.skillops-lock.json)",
|
|
1942
|
+
status: "pass",
|
|
1943
|
+
summary: "No .skillops-lock.json present (no synced runtime artifacts tracked yet)."
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
const effectiveBudget = configuredStartupBudget ?? 2500;
|
|
1947
|
+
const tokenReport = analyzeRepoTokenEconomy(resolvedRepo, {
|
|
1948
|
+
catalogDir: options.catalogDir,
|
|
1949
|
+
startupTaxBudget: effectiveBudget
|
|
1950
|
+
});
|
|
1951
|
+
checks.push({
|
|
1952
|
+
id: "startup_token_tax",
|
|
1953
|
+
title: "Startup Token Tax Budget",
|
|
1954
|
+
status: tokenReport.passed_budget ? "pass" : "warn",
|
|
1955
|
+
summary: `${tokenReport.startup_token_tax}/${tokenReport.startup_tax_budget} tokens (${tokenReport.passed_budget ? "within budget" : "exceeds budget"}, ${tokenReport.token_savings_pct}% saved via Progressive Disclosure).`
|
|
1956
|
+
});
|
|
1957
|
+
if (!tokenReport.passed_budget) {
|
|
1958
|
+
remediation.push(
|
|
1959
|
+
`Reduce always-on startup context (${tokenReport.startup_token_tax} > ${tokenReport.startup_tax_budget} tokens) by moving instructions into on-demand SKILL.md bodies.`
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
const hasFail = checks.some((c) => c.status === "fail");
|
|
1963
|
+
const hasWarn = checks.some((c) => c.status === "warn");
|
|
1964
|
+
const overallStatus = hasFail ? "fail" : hasWarn ? "warn" : "pass";
|
|
1965
|
+
return {
|
|
1966
|
+
status: overallStatus,
|
|
1967
|
+
repo_path: repoLabel,
|
|
1968
|
+
node_version: nodeVersion,
|
|
1969
|
+
python_version: pythonVersion,
|
|
1970
|
+
detected_harnesses: detectedHarnesses,
|
|
1971
|
+
config_present: configPresent,
|
|
1972
|
+
config_valid: configValid,
|
|
1973
|
+
...configError ? { config_error: configError } : {},
|
|
1974
|
+
lockfile_present: lockfilePresent,
|
|
1975
|
+
lockfile_valid: lockfileValid,
|
|
1976
|
+
tracked_artifacts_count: artifacts.length,
|
|
1977
|
+
healthy_artifacts_count: healthyCount,
|
|
1978
|
+
drifted_artifacts_count: driftedCount,
|
|
1979
|
+
missing_artifacts_count: missingCount,
|
|
1980
|
+
artifacts,
|
|
1981
|
+
startup_tax_tokens: tokenReport.startup_token_tax,
|
|
1982
|
+
startup_tax_budget: tokenReport.startup_tax_budget,
|
|
1983
|
+
startup_tax_within_budget: tokenReport.passed_budget,
|
|
1984
|
+
checks,
|
|
1985
|
+
remediation
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
function inspectHarnesses(repoPath) {
|
|
1989
|
+
const resolvedRepo = path3.resolve(repoPath);
|
|
1990
|
+
const repoLabel = path3.basename(resolvedRepo) || ".";
|
|
1991
|
+
const detectedHarnesses = detectInstalledHarnesses(resolvedRepo);
|
|
1992
|
+
const detectedSet = new Set(detectedHarnesses);
|
|
1993
|
+
const harnesses = ALL_RUNTIME_TARGETS.map((target) => ({
|
|
1994
|
+
...RUNTIME_HARNESS_SPECS[target],
|
|
1995
|
+
detected: detectedSet.has(target)
|
|
1996
|
+
}));
|
|
1997
|
+
return {
|
|
1998
|
+
repo_path: repoLabel,
|
|
1999
|
+
total_supported: harnesses.length,
|
|
2000
|
+
detected_count: detectedHarnesses.length,
|
|
2001
|
+
detected_harnesses: detectedHarnesses,
|
|
2002
|
+
harnesses
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
function formatHarnessInspectionReport(report, format = "table") {
|
|
2006
|
+
if (format === "json") {
|
|
2007
|
+
return `${JSON.stringify(report, null, 2)}
|
|
2008
|
+
`;
|
|
2009
|
+
}
|
|
2010
|
+
if (format === "markdown") {
|
|
2011
|
+
const lines2 = [
|
|
2012
|
+
`## SkillOps Universal Top-20 AI Coding Agent Harnesses (\`${report.repo_path}\`)`,
|
|
2013
|
+
"",
|
|
2014
|
+
`- **Supported AI Coding Harnesses:** \`${report.total_supported}\``,
|
|
2015
|
+
`- **Detected in Repository:** \`${report.detected_count}\` (${report.detected_harnesses.length > 0 ? report.detected_harnesses.join(", ") : "none"})`,
|
|
2016
|
+
"",
|
|
2017
|
+
"| Target ID | AI Coding Agent / IDE | Vendor | Category | Skills Path | Subagents Path | MCP Config | Detected |",
|
|
2018
|
+
"| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :---: |",
|
|
2019
|
+
...report.harnesses.map(
|
|
2020
|
+
(h) => `| \`${h.id}\` | **${h.displayName}** | ${h.vendor} | \`${h.category}\` | \`${h.skillsDir}\` | \`${h.subagentsDir}\` | ${h.mcpConfigPath ? `\`${h.mcpConfigPath}\`` : "\u2014"} | ${h.detected ? "YES" : "NO"} |`
|
|
2021
|
+
),
|
|
2022
|
+
""
|
|
2023
|
+
];
|
|
2024
|
+
return lines2.join("\n");
|
|
2025
|
+
}
|
|
2026
|
+
const lines = [
|
|
2027
|
+
`=== SkillOps Universal Top-20 AI Coding Harnesses (${report.repo_path}) ===`,
|
|
2028
|
+
`Supported Harnesses : ${report.total_supported}`,
|
|
2029
|
+
`Detected in Repo : ${report.detected_count} (${report.detected_harnesses.length > 0 ? report.detected_harnesses.join(", ") : "none"})`,
|
|
2030
|
+
"-".repeat(92),
|
|
2031
|
+
...report.harnesses.map(
|
|
2032
|
+
(h) => ` [${h.detected ? "DETECTED" : "READY "}] ${h.id.padEnd(11)} | ${h.displayName.padEnd(32)} | skills=${h.skillsDir} | mcp=${h.mcpConfigPath ?? "n/a"}`
|
|
2033
|
+
),
|
|
2034
|
+
""
|
|
2035
|
+
];
|
|
2036
|
+
return lines.join("\n");
|
|
2037
|
+
}
|
|
2038
|
+
function formatDoctorReport(report, format = "table") {
|
|
2039
|
+
if (format === "json") {
|
|
2040
|
+
return JSON.stringify(report, null, 2);
|
|
2041
|
+
}
|
|
2042
|
+
if (format === "github") {
|
|
2043
|
+
const lines2 = [];
|
|
2044
|
+
for (const art of report.artifacts) {
|
|
2045
|
+
if (art.status !== "healthy") {
|
|
2046
|
+
lines2.push(
|
|
2047
|
+
`::error file=${art.relative_path},title=SkillOps Lockfile ${art.status.toUpperCase()}::Artifact '${art.relative_path}' (${art.target} :: ${art.item_name}) is ${art.status}.`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
for (const check of report.checks) {
|
|
2052
|
+
if (check.status === "fail") {
|
|
2053
|
+
lines2.push(`::error title=SkillOps Doctor (${check.title})::${check.summary}`);
|
|
2054
|
+
} else if (check.status === "warn") {
|
|
2055
|
+
lines2.push(`::warning title=SkillOps Doctor (${check.title})::${check.summary}`);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
lines2.push(
|
|
2059
|
+
`::notice title=SkillOps Doctor::Overall status ${report.status.toUpperCase()} (${report.healthy_artifacts_count}/${report.tracked_artifacts_count} artifacts healthy, Startup Tax ${report.startup_tax_tokens}/${report.startup_tax_budget}t).`
|
|
2060
|
+
);
|
|
2061
|
+
return lines2.join("\n");
|
|
2062
|
+
}
|
|
2063
|
+
if (format === "markdown") {
|
|
2064
|
+
const lines2 = [
|
|
2065
|
+
`# SkillOps Doctor & Lockfile Status Report (\`${report.repo_path}\`)`,
|
|
2066
|
+
"",
|
|
2067
|
+
`- **Overall Status:** \`${report.status.toUpperCase()}\``,
|
|
2068
|
+
`- **Node.js Runtime:** \`${report.node_version}\``,
|
|
2069
|
+
`- **Python Runtime:** \`${report.python_version ?? "not detected"}\``,
|
|
2070
|
+
`- **Detected AI Harnesses:** \`${report.detected_harnesses.length > 0 ? report.detected_harnesses.join(", ") : "none"}\``,
|
|
2071
|
+
`- **Tracked Artifacts:** \`${report.healthy_artifacts_count}/${report.tracked_artifacts_count}\` healthy (\`${report.drifted_artifacts_count}\` drifted, \`${report.missing_artifacts_count}\` missing)`,
|
|
2072
|
+
`- **Startup Token Tax:** \`${report.startup_tax_tokens} / ${report.startup_tax_budget}\` tokens`,
|
|
2073
|
+
"",
|
|
2074
|
+
"## Diagnostic Checks",
|
|
2075
|
+
"",
|
|
2076
|
+
"| Check | Status | Summary |",
|
|
2077
|
+
"| :--- | :---: | :--- |",
|
|
2078
|
+
...report.checks.map(
|
|
2079
|
+
(c) => `| **${c.title}** | \`${c.status.toUpperCase()}\` | ${c.summary} |`
|
|
2080
|
+
)
|
|
2081
|
+
];
|
|
2082
|
+
if (report.artifacts.length > 0) {
|
|
2083
|
+
lines2.push(
|
|
2084
|
+
"",
|
|
2085
|
+
"## Lockfile Artifact Status (`.skillops-lock.json`)",
|
|
2086
|
+
"",
|
|
2087
|
+
"| Path | Runtime | Item | Status |",
|
|
2088
|
+
"| :--- | :--- | :--- | :---: |",
|
|
2089
|
+
...report.artifacts.map(
|
|
2090
|
+
(a) => `| \`${a.relative_path}\` | \`${a.target}\` | \`${a.item_name}\` | \`${a.status.toUpperCase()}\` |`
|
|
2091
|
+
)
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
if (report.remediation.length > 0) {
|
|
2095
|
+
lines2.push(
|
|
2096
|
+
"",
|
|
2097
|
+
"## Recommended Remediation",
|
|
2098
|
+
"",
|
|
2099
|
+
...report.remediation.map((r) => `- ${r}`)
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
return lines2.join("\n");
|
|
2103
|
+
}
|
|
2104
|
+
const lines = [
|
|
2105
|
+
`SkillOps Doctor & Lockfile Diagnostics (${report.repo_path})`,
|
|
2106
|
+
"=".repeat(64),
|
|
2107
|
+
`Overall Status : ${report.status.toUpperCase()}`,
|
|
2108
|
+
`Node.js Runtime : ${report.node_version}`,
|
|
2109
|
+
`Python Runtime : ${report.python_version ?? "not detected"}`,
|
|
2110
|
+
`Detected Harnesses : ${report.detected_harnesses.length > 0 ? report.detected_harnesses.join(", ") : "none"}`,
|
|
2111
|
+
`Declarative Config : ${!report.config_present ? "not present (defaults)" : report.config_valid ? "valid (.skillops.yml)" : "INVALID (.skillops.yml)"}`,
|
|
2112
|
+
`Lockfile Status : ${!report.lockfile_present ? "not present (0 tracked artifacts)" : !report.lockfile_valid ? "CORRUPTED (.skillops-lock.json)" : `${report.healthy_artifacts_count}/${report.tracked_artifacts_count} healthy, ${report.drifted_artifacts_count} drifted, ${report.missing_artifacts_count} missing`}`,
|
|
2113
|
+
`Startup Token Tax : ${report.startup_tax_tokens}/${report.startup_tax_budget} tokens (${report.startup_tax_within_budget ? "PASS" : "WARN"})`,
|
|
2114
|
+
"-".repeat(64),
|
|
2115
|
+
"Diagnostic Checks:"
|
|
2116
|
+
];
|
|
2117
|
+
for (const check of report.checks) {
|
|
2118
|
+
const badge = check.status === "pass" ? "[PASS]" : check.status === "warn" ? "[WARN]" : "[FAIL]";
|
|
2119
|
+
lines.push(` ${badge} ${check.title}: ${check.summary}`);
|
|
2120
|
+
}
|
|
2121
|
+
if (report.artifacts.length > 0) {
|
|
2122
|
+
lines.push("-".repeat(64), "Tracked Lockfile Artifacts:");
|
|
2123
|
+
for (const art of report.artifacts) {
|
|
2124
|
+
const badge = art.status === "healthy" ? "[HEALTHY]" : art.status === "drifted" ? "[DRIFTED]" : "[MISSING]";
|
|
2125
|
+
lines.push(` ${badge} ${art.relative_path} (${art.target} :: ${art.item_name})`);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (report.remediation.length > 0) {
|
|
2129
|
+
lines.push("-".repeat(64), "Remediation:");
|
|
2130
|
+
for (const rem of report.remediation) {
|
|
2131
|
+
lines.push(` - ${rem}`);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return lines.join("\n");
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// src/router/fingerprinter.ts
|
|
2138
|
+
import fs5 from "fs";
|
|
2139
|
+
import path4 from "path";
|
|
2140
|
+
var PACKAGE_NAME_PREFIX_REGEX = /^(@[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+|[a-zA-Z0-9._-]+)/;
|
|
2141
|
+
var FRAMEWORK_MAP = {
|
|
2142
|
+
fastapi: "fastapi",
|
|
2143
|
+
pydantic: "pydantic",
|
|
2144
|
+
"pydantic-settings": "pydantic",
|
|
2145
|
+
sqlalchemy: "sqlalchemy",
|
|
2146
|
+
alembic: "sqlalchemy",
|
|
2147
|
+
pytest: "pytest",
|
|
2148
|
+
django: "django",
|
|
2149
|
+
flask: "flask",
|
|
2150
|
+
celery: "celery",
|
|
2151
|
+
mcp: "mcp",
|
|
2152
|
+
"@modelcontextprotocol/sdk": "mcp",
|
|
2153
|
+
typer: "typer",
|
|
2154
|
+
rich: "rich",
|
|
2155
|
+
next: "nextjs",
|
|
2156
|
+
react: "react",
|
|
2157
|
+
vue: "vue",
|
|
2158
|
+
express: "express",
|
|
2159
|
+
fastify: "fastify",
|
|
2160
|
+
nestjs: "nestjs",
|
|
2161
|
+
"@nestjs/core": "nestjs",
|
|
2162
|
+
zod: "zod",
|
|
2163
|
+
vitest: "vitest",
|
|
2164
|
+
tsup: "tsup"
|
|
2165
|
+
};
|
|
2166
|
+
var DATABASE_MAP = {
|
|
2167
|
+
psycopg: "postgresql",
|
|
2168
|
+
psycopg2: "postgresql",
|
|
2169
|
+
"psycopg2-binary": "postgresql",
|
|
2170
|
+
asyncpg: "postgresql",
|
|
2171
|
+
pg: "postgresql",
|
|
2172
|
+
postgres: "postgresql",
|
|
2173
|
+
pgvector: "postgresql",
|
|
2174
|
+
prisma: "postgresql",
|
|
2175
|
+
redis: "redis",
|
|
2176
|
+
ioredis: "redis",
|
|
2177
|
+
pymongo: "mongodb",
|
|
2178
|
+
mongoose: "mongodb",
|
|
2179
|
+
aiosqlite: "sqlite",
|
|
2180
|
+
"better-sqlite3": "sqlite"
|
|
2181
|
+
};
|
|
2182
|
+
function normalizePackageName(rawSpec) {
|
|
2183
|
+
const cleaned = rawSpec.trim().replace(/^["']|["']$/g, "");
|
|
2184
|
+
if (!cleaned || cleaned.startsWith("#") || cleaned.startsWith("-")) {
|
|
2185
|
+
return "";
|
|
2186
|
+
}
|
|
2187
|
+
const match = PACKAGE_NAME_PREFIX_REGEX.exec(cleaned);
|
|
2188
|
+
if (!match?.[1]) {
|
|
2189
|
+
return "";
|
|
2190
|
+
}
|
|
2191
|
+
return match[1].toLowerCase().replace(/_/g, "-");
|
|
2192
|
+
}
|
|
2193
|
+
var RepoFingerprinter = class {
|
|
2194
|
+
repoPath;
|
|
2195
|
+
constructor(repoPath) {
|
|
2196
|
+
this.repoPath = path4.resolve(repoPath);
|
|
2197
|
+
}
|
|
2198
|
+
fingerprint() {
|
|
2199
|
+
const languages = /* @__PURE__ */ new Set();
|
|
2200
|
+
const frameworks = /* @__PURE__ */ new Set();
|
|
2201
|
+
const databases = /* @__PURE__ */ new Set();
|
|
2202
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
2203
|
+
const ciSystems = /* @__PURE__ */ new Set();
|
|
2204
|
+
const detectedFiles = /* @__PURE__ */ new Set();
|
|
2205
|
+
const signals = /* @__PURE__ */ new Set();
|
|
2206
|
+
let python312Plus = false;
|
|
2207
|
+
const recordDep = (rawDep) => {
|
|
2208
|
+
const pkg = normalizePackageName(rawDep);
|
|
2209
|
+
if (!pkg) {
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
dependencies.add(pkg);
|
|
2213
|
+
signals.add(pkg);
|
|
2214
|
+
const fw = FRAMEWORK_MAP[pkg];
|
|
2215
|
+
if (fw) {
|
|
2216
|
+
frameworks.add(fw);
|
|
2217
|
+
signals.add(fw);
|
|
2218
|
+
}
|
|
2219
|
+
const db = DATABASE_MAP[pkg];
|
|
2220
|
+
if (db) {
|
|
2221
|
+
databases.add(db);
|
|
2222
|
+
signals.add(db);
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
const pyprojectPath = path4.join(this.repoPath, "pyproject.toml");
|
|
2226
|
+
if (fs5.existsSync(pyprojectPath) && fs5.statSync(pyprojectPath).isFile()) {
|
|
2227
|
+
detectedFiles.add("pyproject.toml");
|
|
2228
|
+
languages.add("python");
|
|
2229
|
+
signals.add("python");
|
|
2230
|
+
signals.add("pyproject.toml");
|
|
2231
|
+
const content = fs5.readFileSync(pyprojectPath, "utf-8");
|
|
2232
|
+
if (/requires-python\s*=\s*["'][^"']*3\.(1[2-9]|[2-9]\d)/.test(content)) {
|
|
2233
|
+
python312Plus = true;
|
|
2234
|
+
signals.add("python3.12");
|
|
2235
|
+
}
|
|
2236
|
+
const quotedSpecs = content.match(/["']([a-zA-Z0-9._-]+(?:\[[a-zA-Z0-9,_-]+\])?\s*(?:[><=~!][^"']+)?)["']/g) ?? [];
|
|
2237
|
+
for (const spec of quotedSpecs) {
|
|
2238
|
+
recordDep(spec);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
if (fs5.existsSync(this.repoPath)) {
|
|
2242
|
+
for (const entry of fs5.readdirSync(this.repoPath, { withFileTypes: true })) {
|
|
2243
|
+
if (entry.isFile() && /^requirements.*\.txt$/i.test(entry.name)) {
|
|
2244
|
+
detectedFiles.add(entry.name);
|
|
2245
|
+
languages.add("python");
|
|
2246
|
+
signals.add("python");
|
|
2247
|
+
const lines = fs5.readFileSync(path4.join(this.repoPath, entry.name), "utf-8").split(/\r?\n/);
|
|
2248
|
+
for (const line of lines) {
|
|
2249
|
+
recordDep(line);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
const pkgJsonPath = path4.join(this.repoPath, "package.json");
|
|
2255
|
+
if (fs5.existsSync(pkgJsonPath) && fs5.statSync(pkgJsonPath).isFile()) {
|
|
2256
|
+
detectedFiles.add("package.json");
|
|
2257
|
+
languages.add("javascript");
|
|
2258
|
+
signals.add("javascript");
|
|
2259
|
+
signals.add("nodejs");
|
|
2260
|
+
signals.add("package.json");
|
|
2261
|
+
try {
|
|
2262
|
+
const pkgJson = JSON.parse(fs5.readFileSync(pkgJsonPath, "utf-8"));
|
|
2263
|
+
const depSections = ["dependencies", "devDependencies", "peerDependencies"];
|
|
2264
|
+
for (const section of depSections) {
|
|
2265
|
+
const obj = pkgJson[section];
|
|
2266
|
+
if (obj && typeof obj === "object") {
|
|
2267
|
+
for (const depName of Object.keys(obj)) {
|
|
2268
|
+
recordDep(depName);
|
|
2269
|
+
if (depName === "typescript" || depName.startsWith("@types/")) {
|
|
2270
|
+
languages.add("typescript");
|
|
2271
|
+
signals.add("typescript");
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
} catch {
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
const tsconfigPath = path4.join(this.repoPath, "tsconfig.json");
|
|
2280
|
+
if (fs5.existsSync(tsconfigPath) && fs5.statSync(tsconfigPath).isFile()) {
|
|
2281
|
+
detectedFiles.add("tsconfig.json");
|
|
2282
|
+
languages.add("typescript");
|
|
2283
|
+
signals.add("typescript");
|
|
2284
|
+
signals.add("tsconfig.json");
|
|
2285
|
+
}
|
|
2286
|
+
if (fs5.existsSync(path4.join(this.repoPath, "Cargo.toml"))) {
|
|
2287
|
+
detectedFiles.add("Cargo.toml");
|
|
2288
|
+
languages.add("rust");
|
|
2289
|
+
signals.add("rust");
|
|
2290
|
+
}
|
|
2291
|
+
if (fs5.existsSync(path4.join(this.repoPath, "go.mod"))) {
|
|
2292
|
+
detectedFiles.add("go.mod");
|
|
2293
|
+
languages.add("go");
|
|
2294
|
+
signals.add("go");
|
|
2295
|
+
}
|
|
2296
|
+
if (fs5.existsSync(path4.join(this.repoPath, "Dockerfile"))) {
|
|
2297
|
+
detectedFiles.add("Dockerfile");
|
|
2298
|
+
signals.add("docker");
|
|
2299
|
+
}
|
|
2300
|
+
const ghWorkflowsDir = path4.join(this.repoPath, ".github", "workflows");
|
|
2301
|
+
if (fs5.existsSync(ghWorkflowsDir) && fs5.statSync(ghWorkflowsDir).isDirectory()) {
|
|
2302
|
+
const wfFiles = fs5.readdirSync(ghWorkflowsDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
2303
|
+
if (wfFiles.length > 0) {
|
|
2304
|
+
ciSystems.add("github-actions");
|
|
2305
|
+
signals.add("github-actions");
|
|
2306
|
+
for (const wf of wfFiles) {
|
|
2307
|
+
detectedFiles.add(`.github/workflows/${wf}`);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
const scanDir = (dir, depth) => {
|
|
2312
|
+
if (depth > 3 || !fs5.existsSync(dir)) {
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
|
|
2316
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === ".venv" || entry.name === "__pycache__") {
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
const full = path4.join(dir, entry.name);
|
|
2320
|
+
if (entry.isDirectory()) {
|
|
2321
|
+
scanDir(full, depth + 1);
|
|
2322
|
+
} else if (entry.isFile()) {
|
|
2323
|
+
const rel = path4.relative(this.repoPath, full).replace(/\\/g, "/");
|
|
2324
|
+
if (entry.name.endsWith(".py")) {
|
|
2325
|
+
languages.add("python");
|
|
2326
|
+
signals.add("python");
|
|
2327
|
+
detectedFiles.add(rel);
|
|
2328
|
+
const pyText = fs5.readFileSync(full, "utf-8");
|
|
2329
|
+
if (/^\s*type\s+[A-Za-z_][A-Za-z0-9_]*\s*=/m.test(pyText)) {
|
|
2330
|
+
python312Plus = true;
|
|
2331
|
+
signals.add("python3.12");
|
|
2332
|
+
}
|
|
2333
|
+
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
2334
|
+
languages.add("typescript");
|
|
2335
|
+
signals.add("typescript");
|
|
2336
|
+
detectedFiles.add(rel);
|
|
2337
|
+
} else if (entry.name.endsWith(".sql")) {
|
|
2338
|
+
databases.add("postgresql");
|
|
2339
|
+
signals.add("sql");
|
|
2340
|
+
signals.add("postgresql");
|
|
2341
|
+
detectedFiles.add(rel);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
scanDir(this.repoPath, 0);
|
|
2347
|
+
const detectedHarnesses = detectInstalledHarnesses(this.repoPath);
|
|
2348
|
+
return RepoProfileSchema.parse({
|
|
2349
|
+
repo_path: this.repoPath.replace(/\\/g, "/"),
|
|
2350
|
+
languages: [...languages].sort(),
|
|
2351
|
+
frameworks: [...frameworks].sort(),
|
|
2352
|
+
databases: [...databases].sort(),
|
|
2353
|
+
dependencies: [...dependencies].sort(),
|
|
2354
|
+
ci_systems: [...ciSystems].sort(),
|
|
2355
|
+
detected_files: [...detectedFiles].sort(),
|
|
2356
|
+
signals: [...signals].sort(),
|
|
2357
|
+
detected_harnesses: detectedHarnesses,
|
|
2358
|
+
python_312_plus: python312Plus
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
};
|
|
2362
|
+
function fingerprintRepository(repoPath) {
|
|
2363
|
+
return new RepoFingerprinter(repoPath).fingerprint();
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// src/router/bundle_compiler.ts
|
|
2367
|
+
import path5 from "path";
|
|
2368
|
+
function scoreSkillAgainstProfile(parsedSkill, profile, taskPrompt = "") {
|
|
2369
|
+
const manifest = parsedSkill.manifest;
|
|
2370
|
+
const repoSignalSet = new Set(
|
|
2371
|
+
[
|
|
2372
|
+
...profile.languages,
|
|
2373
|
+
...profile.frameworks,
|
|
2374
|
+
...profile.databases,
|
|
2375
|
+
...profile.dependencies,
|
|
2376
|
+
...profile.ci_systems,
|
|
2377
|
+
...profile.signals
|
|
2378
|
+
].map((s) => s.toLowerCase())
|
|
2379
|
+
);
|
|
2380
|
+
const taskTokens = new Set(tokenizeRoutingText(taskPrompt));
|
|
2381
|
+
if (taskPrompt.trim()) {
|
|
2382
|
+
const lowerPrompt = taskPrompt.toLowerCase();
|
|
2383
|
+
for (const neg of manifest.routing_signals.negative_triggers) {
|
|
2384
|
+
if (neg.trim() && lowerPrompt.includes(neg.toLowerCase())) {
|
|
2385
|
+
return {
|
|
2386
|
+
score: 0,
|
|
2387
|
+
matchedSignals: [],
|
|
2388
|
+
negativeMatch: `Excluded by negative trigger '${neg}'.`
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
let rawScore = 10;
|
|
2394
|
+
const matchedSignals = [];
|
|
2395
|
+
for (const lang of manifest.routing_signals.languages) {
|
|
2396
|
+
const lowerLang = lang.toLowerCase();
|
|
2397
|
+
if (repoSignalSet.has(lowerLang)) {
|
|
2398
|
+
rawScore += 18;
|
|
2399
|
+
matchedSignals.push(`lang:${lang}`);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
for (const dep of [...manifest.routing_signals.dependencies_any, ...manifest.tags]) {
|
|
2403
|
+
const lowerDep = dep.toLowerCase();
|
|
2404
|
+
const depTokens = tokenizeRoutingText(lowerDep);
|
|
2405
|
+
if (repoSignalSet.has(lowerDep) || depTokens.some((t) => repoSignalSet.has(t))) {
|
|
2406
|
+
rawScore += 16;
|
|
2407
|
+
matchedSignals.push(`repo:${dep}`);
|
|
2408
|
+
}
|
|
2409
|
+
if (taskTokens.size > 0 && depTokens.some((t) => taskTokens.has(t))) {
|
|
2410
|
+
rawScore += 22;
|
|
2411
|
+
matchedSignals.push(`task:${dep}`);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
for (const pat of manifest.routing_signals.files_any) {
|
|
2415
|
+
const cleanPat = pat.replace(/\*/g, "").toLowerCase();
|
|
2416
|
+
if (cleanPat && profile.detected_files.some((f) => f.toLowerCase().includes(cleanPat))) {
|
|
2417
|
+
rawScore += 15;
|
|
2418
|
+
matchedSignals.push(`file:${pat}`);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
if (manifest.track === "arch-quality" || manifest.track === "token-economy") {
|
|
2422
|
+
rawScore += 12;
|
|
2423
|
+
matchedSignals.push(`track:${manifest.track}`);
|
|
2424
|
+
}
|
|
2425
|
+
const clampedScore = Math.min(100, Number(rawScore.toFixed(2)));
|
|
2426
|
+
return {
|
|
2427
|
+
score: clampedScore,
|
|
2428
|
+
matchedSignals: [...new Set(matchedSignals)],
|
|
2429
|
+
negativeMatch: null
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
function compileSkillBundle(profile, catalogDir, options = {}) {
|
|
2433
|
+
const maxSkills = options.maxSkills ?? 12;
|
|
2434
|
+
const maxL1Tokens = options.maxL1Tokens ?? 1500;
|
|
2435
|
+
const maxSimilarity = options.maxSimilarity ?? 0.78;
|
|
2436
|
+
const taskPrompt = options.taskPrompt ?? "";
|
|
2437
|
+
const resolvedCatalog = path5.resolve(catalogDir);
|
|
2438
|
+
const skillDirs = discoverSkillDirectories(resolvedCatalog);
|
|
2439
|
+
const parsedSkills = skillDirs.map((dir) => parseSkillDirectory(dir, { requireLayout: false }));
|
|
2440
|
+
const featureVectors = /* @__PURE__ */ new Map();
|
|
2441
|
+
for (const p of parsedSkills) {
|
|
2442
|
+
featureVectors.set(p.manifest.name, extractSkillFeatures(p.manifest));
|
|
2443
|
+
}
|
|
2444
|
+
const idfWeights = computeIdfWeights([...featureVectors.values()]);
|
|
2445
|
+
const candidates = [];
|
|
2446
|
+
const skippedSkills = [];
|
|
2447
|
+
for (const parsed of parsedSkills) {
|
|
2448
|
+
const { score, matchedSignals, negativeMatch } = scoreSkillAgainstProfile(
|
|
2449
|
+
parsed,
|
|
2450
|
+
profile,
|
|
2451
|
+
taskPrompt
|
|
2452
|
+
);
|
|
2453
|
+
if (negativeMatch) {
|
|
2454
|
+
skippedSkills.push({
|
|
2455
|
+
name: parsed.manifest.name,
|
|
2456
|
+
track: parsed.manifest.track,
|
|
2457
|
+
relevance_score: 0,
|
|
2458
|
+
reason: negativeMatch
|
|
2459
|
+
});
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2462
|
+
candidates.push({ parsed, score, matchedSignals });
|
|
2463
|
+
}
|
|
2464
|
+
candidates.sort(
|
|
2465
|
+
(a, b) => b.score !== a.score ? b.score - a.score : a.parsed.manifest.name.localeCompare(b.parsed.manifest.name)
|
|
2466
|
+
);
|
|
2467
|
+
const selectedSkills = [];
|
|
2468
|
+
let totalL1Tokens = 0;
|
|
2469
|
+
let maxPairwiseSimilarity = 0;
|
|
2470
|
+
for (const cand of candidates) {
|
|
2471
|
+
const manifest = cand.parsed.manifest;
|
|
2472
|
+
if (selectedSkills.length >= maxSkills) {
|
|
2473
|
+
skippedSkills.push({
|
|
2474
|
+
name: manifest.name,
|
|
2475
|
+
track: manifest.track,
|
|
2476
|
+
relevance_score: cand.score,
|
|
2477
|
+
reason: `Bundle cap reached (max_skills=${maxSkills}).`
|
|
2478
|
+
});
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
if (totalL1Tokens + manifest.description_tokens > maxL1Tokens) {
|
|
2482
|
+
skippedSkills.push({
|
|
2483
|
+
name: manifest.name,
|
|
2484
|
+
track: manifest.track,
|
|
2485
|
+
relevance_score: cand.score,
|
|
2486
|
+
reason: `Would exceed Level 1 token budget (${totalL1Tokens + manifest.description_tokens} > ${maxL1Tokens}).`
|
|
2487
|
+
});
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
const candVec = featureVectors.get(manifest.name);
|
|
2491
|
+
let collidingPeer = null;
|
|
2492
|
+
let highestSimWithSelected = 0;
|
|
2493
|
+
for (const sel of selectedSkills) {
|
|
2494
|
+
const selVec = featureVectors.get(sel.name);
|
|
2495
|
+
const { similarity } = computeFeatureCosine(candVec, selVec, idfWeights);
|
|
2496
|
+
if (similarity >= maxSimilarity) {
|
|
2497
|
+
collidingPeer = `${sel.name} (cosine=${similarity.toFixed(4)})`;
|
|
2498
|
+
break;
|
|
2499
|
+
}
|
|
2500
|
+
if (similarity > highestSimWithSelected) {
|
|
2501
|
+
highestSimWithSelected = similarity;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
if (collidingPeer) {
|
|
2505
|
+
skippedSkills.push({
|
|
2506
|
+
name: manifest.name,
|
|
2507
|
+
track: manifest.track,
|
|
2508
|
+
relevance_score: cand.score,
|
|
2509
|
+
reason: `Semantic collision with selected skill ${collidingPeer}.`
|
|
2510
|
+
});
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
if (highestSimWithSelected > maxPairwiseSimilarity) {
|
|
2514
|
+
maxPairwiseSimilarity = highestSimWithSelected;
|
|
2515
|
+
}
|
|
2516
|
+
selectedSkills.push({
|
|
2517
|
+
name: manifest.name,
|
|
2518
|
+
track: manifest.track,
|
|
2519
|
+
skill_dir: cand.parsed.skillDir.replace(/\\/g, "/"),
|
|
2520
|
+
relevance_score: cand.score,
|
|
2521
|
+
l1_tokens: manifest.description_tokens,
|
|
2522
|
+
matched_signals: cand.matchedSignals
|
|
2523
|
+
});
|
|
2524
|
+
totalL1Tokens += manifest.description_tokens;
|
|
2525
|
+
}
|
|
2526
|
+
const subagentFiles = discoverSubagentFiles(resolvedCatalog);
|
|
2527
|
+
const recommendedSubagents = [];
|
|
2528
|
+
for (const saFile of subagentFiles) {
|
|
2529
|
+
try {
|
|
2530
|
+
const parsedSa = parseSubagentFile(saFile);
|
|
2531
|
+
if (!recommendedSubagents.includes(parsedSa.manifest.name)) {
|
|
2532
|
+
recommendedSubagents.push(parsedSa.manifest.name);
|
|
2533
|
+
}
|
|
2534
|
+
} catch {
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
return RoutedBundleSchema.parse({
|
|
2538
|
+
repo_profile: profile,
|
|
2539
|
+
catalog_path: resolvedCatalog.replace(/\\/g, "/"),
|
|
2540
|
+
max_skills: maxSkills,
|
|
2541
|
+
max_l1_tokens: maxL1Tokens,
|
|
2542
|
+
max_similarity: maxSimilarity,
|
|
2543
|
+
selected_skills: selectedSkills,
|
|
2544
|
+
recommended_subagents: recommendedSubagents.sort(),
|
|
2545
|
+
skipped_skills: skippedSkills,
|
|
2546
|
+
total_l1_tokens: totalL1Tokens,
|
|
2547
|
+
max_pairwise_similarity: Number(maxPairwiseSimilarity.toFixed(4)),
|
|
2548
|
+
passed_invariants: selectedSkills.length <= maxSkills && totalL1Tokens <= maxL1Tokens && maxPairwiseSimilarity < maxSimilarity
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
function routeRepository(repoPath, catalogDir, options = {}) {
|
|
2552
|
+
const profile = fingerprintRepository(repoPath);
|
|
2553
|
+
return compileSkillBundle(profile, catalogDir, options);
|
|
2554
|
+
}
|
|
2555
|
+
function formatRoutedBundle(bundle, format = "table") {
|
|
2556
|
+
if (format === "json") {
|
|
2557
|
+
return `${JSON.stringify(bundle, null, 2)}
|
|
2558
|
+
`;
|
|
2559
|
+
}
|
|
2560
|
+
if (format === "markdown") {
|
|
2561
|
+
return [
|
|
2562
|
+
"## SkillOps Context-Aware Routed Bundle",
|
|
2563
|
+
"",
|
|
2564
|
+
`- **Languages**: ${bundle.repo_profile.languages.join(", ") || "none"}`,
|
|
2565
|
+
`- **Frameworks**: ${bundle.repo_profile.frameworks.join(", ") || "none"}`,
|
|
2566
|
+
`- **Selected Skills**: \`${bundle.selected_skills.length}\` / \`${bundle.max_skills}\``,
|
|
2567
|
+
`- **Total L1 Tokens**: \`${bundle.total_l1_tokens}\` / \`${bundle.max_l1_tokens}\``,
|
|
2568
|
+
`- **Recommended Subagents**: ${bundle.recommended_subagents.map((s) => `\`${s}\``).join(", ") || "none"}`,
|
|
2569
|
+
"",
|
|
2570
|
+
"| Skill | Track | Score | L1 Tokens | Matched Signals |",
|
|
2571
|
+
"| :--- | :--- | ---: | ---: | :--- |",
|
|
2572
|
+
...bundle.selected_skills.map(
|
|
2573
|
+
(s) => `| \`${s.name}\` | \`${s.track}\` | ${s.relevance_score} | ${s.l1_tokens} | ${s.matched_signals.join(", ")} |`
|
|
2574
|
+
),
|
|
2575
|
+
""
|
|
2576
|
+
].join("\n");
|
|
2577
|
+
}
|
|
2578
|
+
return [
|
|
2579
|
+
"=== SkillOps Context-Aware Router ===",
|
|
2580
|
+
`Repo : ${bundle.repo_profile.repo_path}`,
|
|
2581
|
+
`Stack : languages=[${bundle.repo_profile.languages.join(", ")}] frameworks=[${bundle.repo_profile.frameworks.join(", ")}] db=[${bundle.repo_profile.databases.join(", ")}]`,
|
|
2582
|
+
`Selected : ${bundle.selected_skills.length}/${bundle.max_skills} skills (${bundle.total_l1_tokens}/${bundle.max_l1_tokens} L1 tokens, max cosine=${bundle.max_pairwise_similarity.toFixed(4)})`,
|
|
2583
|
+
`Subagents : ${bundle.recommended_subagents.join(", ") || "none"}`,
|
|
2584
|
+
...bundle.selected_skills.map(
|
|
2585
|
+
(s) => ` + ${s.name} [${s.track}] score=${s.relevance_score} (${s.l1_tokens} L1 tokens) signals=[${s.matched_signals.join(", ")}]`
|
|
2586
|
+
),
|
|
2587
|
+
""
|
|
2588
|
+
].join("\n");
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// src/sync/adapters.ts
|
|
2592
|
+
import crypto2 from "crypto";
|
|
2593
|
+
import fs6 from "fs";
|
|
2594
|
+
import path6 from "path";
|
|
2595
|
+
import YAML3 from "yaml";
|
|
2596
|
+
var CODEX_MARKER_START = "<!-- SKILLOPS:START -->";
|
|
2597
|
+
var CODEX_MARKER_END = "<!-- SKILLOPS:END -->";
|
|
2598
|
+
function computeSha256(content) {
|
|
2599
|
+
return crypto2.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
2600
|
+
}
|
|
2601
|
+
function renderSkillFilesForRuntime(parsed, target) {
|
|
2602
|
+
const name = parsed.manifest.name;
|
|
2603
|
+
const spec = RUNTIME_HARNESS_SPECS[target];
|
|
2604
|
+
const files = [];
|
|
2605
|
+
const globs = parsed.manifest.routing_signals.files_any.length > 0 ? parsed.manifest.routing_signals.files_any : ["**/*"];
|
|
2606
|
+
const body = parsed.markdownBody.trim();
|
|
2607
|
+
if (spec.skillFormat === "progressive-skill-dir") {
|
|
2608
|
+
const baseDir = `${spec.skillsDir}/${name}`;
|
|
2609
|
+
files.push({
|
|
2610
|
+
target,
|
|
2611
|
+
itemName: name,
|
|
2612
|
+
relativePath: `${baseDir}/SKILL.md`,
|
|
2613
|
+
content: parsed.rawText
|
|
2614
|
+
});
|
|
2615
|
+
for (const scriptFull of parsed.declaredScripts) {
|
|
2616
|
+
const scriptRel = path6.relative(parsed.skillDir, scriptFull).replace(/\\/g, "/");
|
|
2617
|
+
files.push({
|
|
2618
|
+
target,
|
|
2619
|
+
itemName: name,
|
|
2620
|
+
relativePath: `${baseDir}/${scriptRel}`,
|
|
2621
|
+
content: fs6.readFileSync(scriptFull, "utf-8")
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
for (const refFull of parsed.referenceFiles) {
|
|
2625
|
+
const refRel = path6.relative(parsed.skillDir, refFull).replace(/\\/g, "/");
|
|
2626
|
+
files.push({
|
|
2627
|
+
target,
|
|
2628
|
+
itemName: name,
|
|
2629
|
+
relativePath: `${baseDir}/${refRel}`,
|
|
2630
|
+
content: fs6.readFileSync(refFull, "utf-8")
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
return files;
|
|
2634
|
+
}
|
|
2635
|
+
if (target === "cursor") {
|
|
2636
|
+
const mdcFrontmatter = YAML3.stringify({
|
|
2637
|
+
description: parsed.manifest.description,
|
|
2638
|
+
globs,
|
|
2639
|
+
alwaysApply: false
|
|
2640
|
+
}).trim();
|
|
2641
|
+
files.push({
|
|
2642
|
+
target,
|
|
2643
|
+
itemName: name,
|
|
2644
|
+
relativePath: `.cursor/rules/skillops-${name}.mdc`,
|
|
2645
|
+
content: `---
|
|
2646
|
+
${mdcFrontmatter}
|
|
2647
|
+
---
|
|
2648
|
+
|
|
2649
|
+
${body}
|
|
2650
|
+
`
|
|
2651
|
+
});
|
|
2652
|
+
return files;
|
|
2653
|
+
}
|
|
2654
|
+
if (target === "copilot") {
|
|
2655
|
+
const applyTo = parsed.manifest.routing_signals.files_any.length > 0 ? parsed.manifest.routing_signals.files_any.join(",") : "**";
|
|
2656
|
+
const instrFrontmatter = YAML3.stringify({
|
|
2657
|
+
applyTo,
|
|
2658
|
+
description: parsed.manifest.description
|
|
2659
|
+
}).trim();
|
|
2660
|
+
files.push({
|
|
2661
|
+
target,
|
|
2662
|
+
itemName: name,
|
|
2663
|
+
relativePath: `.github/instructions/skillops-${name}.instructions.md`,
|
|
2664
|
+
content: `---
|
|
2665
|
+
${instrFrontmatter}
|
|
2666
|
+
---
|
|
2667
|
+
|
|
2668
|
+
${body}
|
|
2669
|
+
`
|
|
2670
|
+
});
|
|
2671
|
+
const promptFrontmatter = YAML3.stringify({
|
|
2672
|
+
mode: "agent",
|
|
2673
|
+
description: parsed.manifest.description
|
|
2674
|
+
}).trim();
|
|
2675
|
+
files.push({
|
|
2676
|
+
target,
|
|
2677
|
+
itemName: name,
|
|
2678
|
+
relativePath: `.github/prompts/skillops-${name}.prompt.md`,
|
|
2679
|
+
content: `---
|
|
2680
|
+
${promptFrontmatter}
|
|
2681
|
+
---
|
|
2682
|
+
|
|
2683
|
+
${body}
|
|
2684
|
+
`
|
|
2685
|
+
});
|
|
2686
|
+
return files;
|
|
2687
|
+
}
|
|
2688
|
+
if (target === "windsurf") {
|
|
2689
|
+
const wsFrontmatter = YAML3.stringify({
|
|
2690
|
+
trigger: "model_decision",
|
|
2691
|
+
description: parsed.manifest.description,
|
|
2692
|
+
globs
|
|
2693
|
+
}).trim();
|
|
2694
|
+
files.push({
|
|
2695
|
+
target,
|
|
2696
|
+
itemName: name,
|
|
2697
|
+
relativePath: `.windsurf/rules/skillops-${name}.md`,
|
|
2698
|
+
content: `---
|
|
2699
|
+
${wsFrontmatter}
|
|
2700
|
+
---
|
|
2701
|
+
|
|
2702
|
+
${body}
|
|
2703
|
+
`
|
|
2704
|
+
});
|
|
2705
|
+
return files;
|
|
2706
|
+
}
|
|
2707
|
+
if (target === "openhands") {
|
|
2708
|
+
const ohFrontmatter = YAML3.stringify({
|
|
2709
|
+
name,
|
|
2710
|
+
type: "knowledge",
|
|
2711
|
+
version: parsed.manifest.version,
|
|
2712
|
+
agent: "CodeActAgent",
|
|
2713
|
+
triggers: parsed.manifest.tags
|
|
2714
|
+
}).trim();
|
|
2715
|
+
files.push({
|
|
2716
|
+
target,
|
|
2717
|
+
itemName: name,
|
|
2718
|
+
relativePath: `.openhands/microagents/skillops-${name}.md`,
|
|
2719
|
+
content: `---
|
|
2720
|
+
${ohFrontmatter}
|
|
2721
|
+
---
|
|
2722
|
+
|
|
2723
|
+
${body}
|
|
2724
|
+
`
|
|
2725
|
+
});
|
|
2726
|
+
return files;
|
|
2727
|
+
}
|
|
2728
|
+
if (target === "kiro") {
|
|
2729
|
+
const kiroFrontmatter = YAML3.stringify({
|
|
2730
|
+
inclusion: "fileMatch",
|
|
2731
|
+
fileMatchPattern: globs[0] ?? "**/*",
|
|
2732
|
+
description: parsed.manifest.description
|
|
2733
|
+
}).trim();
|
|
2734
|
+
files.push({
|
|
2735
|
+
target,
|
|
2736
|
+
itemName: name,
|
|
2737
|
+
relativePath: `.kiro/steering/skillops-${name}.md`,
|
|
2738
|
+
content: `---
|
|
2739
|
+
${kiroFrontmatter}
|
|
2740
|
+
---
|
|
2741
|
+
|
|
2742
|
+
${body}
|
|
2743
|
+
`
|
|
2744
|
+
});
|
|
2745
|
+
return files;
|
|
2746
|
+
}
|
|
2747
|
+
const ruleFrontmatter = YAML3.stringify({
|
|
2748
|
+
name,
|
|
2749
|
+
description: parsed.manifest.description,
|
|
2750
|
+
track: parsed.manifest.track,
|
|
2751
|
+
tags: parsed.manifest.tags,
|
|
2752
|
+
globs,
|
|
2753
|
+
alwaysApply: false
|
|
2754
|
+
}).trim();
|
|
2755
|
+
files.push({
|
|
2756
|
+
target,
|
|
2757
|
+
itemName: name,
|
|
2758
|
+
relativePath: `${spec.skillsDir}/skillops-${name}.md`,
|
|
2759
|
+
content: `---
|
|
2760
|
+
${ruleFrontmatter}
|
|
2761
|
+
---
|
|
2762
|
+
|
|
2763
|
+
${body}
|
|
2764
|
+
`
|
|
2765
|
+
});
|
|
2766
|
+
return files;
|
|
2767
|
+
}
|
|
2768
|
+
function renderSubagentFilesForRuntime(subagent, target) {
|
|
2769
|
+
const name = subagent.manifest.name;
|
|
2770
|
+
const spec = RUNTIME_HARNESS_SPECS[target];
|
|
2771
|
+
const promptBody = subagent.systemPromptBody.trim();
|
|
2772
|
+
const fm = YAML3.stringify({
|
|
2773
|
+
name: subagent.manifest.name,
|
|
2774
|
+
description: subagent.manifest.description,
|
|
2775
|
+
model_tier: subagent.manifest.model_tier,
|
|
2776
|
+
tools: subagent.manifest.tools,
|
|
2777
|
+
max_output_tokens: subagent.manifest.max_output_tokens
|
|
2778
|
+
}).trim();
|
|
2779
|
+
const normalizedDoc = `---
|
|
2780
|
+
${fm}
|
|
2781
|
+
---
|
|
2782
|
+
|
|
2783
|
+
${promptBody}
|
|
2784
|
+
`;
|
|
2785
|
+
if (target === "claude" || target === "antigravity" || target === "codex" || target === "amp" || target === "goose" || target === "opencode") {
|
|
2786
|
+
return [
|
|
2787
|
+
{
|
|
2788
|
+
target,
|
|
2789
|
+
itemName: name,
|
|
2790
|
+
relativePath: `${spec.subagentsDir}/${name}.md`,
|
|
2791
|
+
content: normalizedDoc
|
|
2792
|
+
}
|
|
2793
|
+
];
|
|
2794
|
+
}
|
|
2795
|
+
if (target === "copilot") {
|
|
2796
|
+
return [
|
|
2797
|
+
{
|
|
2798
|
+
target,
|
|
2799
|
+
itemName: name,
|
|
2800
|
+
relativePath: `.github/agents/${name}.agent.md`,
|
|
2801
|
+
content: normalizedDoc
|
|
2802
|
+
}
|
|
2803
|
+
];
|
|
2804
|
+
}
|
|
2805
|
+
if (target === "cursor") {
|
|
2806
|
+
const mdcFm = YAML3.stringify({
|
|
2807
|
+
description: `[Subagent: ${name}] ${subagent.manifest.description}`,
|
|
2808
|
+
alwaysApply: false
|
|
2809
|
+
}).trim();
|
|
2810
|
+
return [
|
|
2811
|
+
{
|
|
2812
|
+
target,
|
|
2813
|
+
itemName: name,
|
|
2814
|
+
relativePath: `.cursor/rules/skillops-agent-${name}.mdc`,
|
|
2815
|
+
content: `---
|
|
2816
|
+
${mdcFm}
|
|
2817
|
+
---
|
|
2818
|
+
|
|
2819
|
+
${promptBody}
|
|
2820
|
+
`
|
|
2821
|
+
}
|
|
2822
|
+
];
|
|
2823
|
+
}
|
|
2824
|
+
if (target === "continue") {
|
|
2825
|
+
const contFm = YAML3.stringify({
|
|
2826
|
+
name,
|
|
2827
|
+
description: subagent.manifest.description,
|
|
2828
|
+
invokable: true
|
|
2829
|
+
}).trim();
|
|
2830
|
+
return [
|
|
2831
|
+
{
|
|
2832
|
+
target,
|
|
2833
|
+
itemName: name,
|
|
2834
|
+
relativePath: `.continue/prompts/skillops-agent-${name}.prompt`,
|
|
2835
|
+
content: `---
|
|
2836
|
+
${contFm}
|
|
2837
|
+
---
|
|
2838
|
+
|
|
2839
|
+
${promptBody}
|
|
2840
|
+
`
|
|
2841
|
+
}
|
|
2842
|
+
];
|
|
2843
|
+
}
|
|
2844
|
+
if (target === "openhands") {
|
|
2845
|
+
const ohFm = YAML3.stringify({
|
|
2846
|
+
name: `agent-${name}`,
|
|
2847
|
+
type: "knowledge",
|
|
2848
|
+
version: "1.0.0",
|
|
2849
|
+
agent: "CodeActAgent",
|
|
2850
|
+
triggers: [name, ...subagent.manifest.tags]
|
|
2851
|
+
}).trim();
|
|
2852
|
+
return [
|
|
2853
|
+
{
|
|
2854
|
+
target,
|
|
2855
|
+
itemName: name,
|
|
2856
|
+
relativePath: `.openhands/microagents/skillops-agent-${name}.md`,
|
|
2857
|
+
content: `---
|
|
2858
|
+
${ohFm}
|
|
2859
|
+
---
|
|
2860
|
+
|
|
2861
|
+
${promptBody}
|
|
2862
|
+
`
|
|
2863
|
+
}
|
|
2864
|
+
];
|
|
2865
|
+
}
|
|
2866
|
+
if (target === "amazonq") {
|
|
2867
|
+
const qPayload = {
|
|
2868
|
+
name,
|
|
2869
|
+
description: subagent.manifest.description,
|
|
2870
|
+
prompt: promptBody,
|
|
2871
|
+
tools: subagent.manifest.tools,
|
|
2872
|
+
max_output_tokens: subagent.manifest.max_output_tokens
|
|
2873
|
+
};
|
|
2874
|
+
return [
|
|
2875
|
+
{
|
|
2876
|
+
target,
|
|
2877
|
+
itemName: name,
|
|
2878
|
+
relativePath: `.amazonq/cli-agents/skillops-agent-${name}.json`,
|
|
2879
|
+
content: `${JSON.stringify(qPayload, null, 2)}
|
|
2880
|
+
`
|
|
2881
|
+
}
|
|
2882
|
+
];
|
|
2883
|
+
}
|
|
2884
|
+
return [
|
|
2885
|
+
{
|
|
2886
|
+
target,
|
|
2887
|
+
itemName: name,
|
|
2888
|
+
relativePath: `${spec.subagentsDir}/skillops-agent-${name}.md`,
|
|
2889
|
+
content: normalizedDoc
|
|
2890
|
+
}
|
|
2891
|
+
];
|
|
2892
|
+
}
|
|
2893
|
+
function renderCodexAgentsIndex(existingContent, skills, subagents) {
|
|
2894
|
+
const blockLines = [
|
|
2895
|
+
CODEX_MARKER_START,
|
|
2896
|
+
"## SkillOps Routed Skills & Context-Isolated Subagents",
|
|
2897
|
+
"",
|
|
2898
|
+
"### Active Progressive Disclosure Skills",
|
|
2899
|
+
...skills.map(
|
|
2900
|
+
(s) => `- **\`${s.manifest.name}\`** (\`.codex/skills/${s.manifest.name}/SKILL.md\`): ${s.manifest.description}`
|
|
2901
|
+
),
|
|
2902
|
+
"",
|
|
2903
|
+
"### Active Context-Isolated Subagents",
|
|
2904
|
+
...subagents.map(
|
|
2905
|
+
(sa) => `- **\`${sa.manifest.name}\`** (\`.codex/agents/${sa.manifest.name}.md\`, max output \`${sa.manifest.max_output_tokens}\` tokens): ${sa.manifest.description}`
|
|
2906
|
+
),
|
|
2907
|
+
CODEX_MARKER_END
|
|
2908
|
+
];
|
|
2909
|
+
const managedBlock = blockLines.join("\n");
|
|
2910
|
+
if (!existingContent || !existingContent.trim()) {
|
|
2911
|
+
return `# AGENTS.md
|
|
2912
|
+
|
|
2913
|
+
${managedBlock}
|
|
2914
|
+
`;
|
|
2915
|
+
}
|
|
2916
|
+
const startIdx = existingContent.indexOf(CODEX_MARKER_START);
|
|
2917
|
+
const endIdx = existingContent.indexOf(CODEX_MARKER_END);
|
|
2918
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
2919
|
+
const before = existingContent.slice(0, startIdx).trimEnd();
|
|
2920
|
+
const after = existingContent.slice(endIdx + CODEX_MARKER_END.length).trimStart();
|
|
2921
|
+
return [before, managedBlock, after].filter(Boolean).join("\n\n") + "\n";
|
|
2922
|
+
}
|
|
2923
|
+
return `${existingContent.trimEnd()}
|
|
2924
|
+
|
|
2925
|
+
${managedBlock}
|
|
2926
|
+
`;
|
|
2927
|
+
}
|
|
2928
|
+
function loadExistingLockfile(lockfilePath) {
|
|
2929
|
+
if (!fs6.existsSync(lockfilePath) || !fs6.statSync(lockfilePath).isFile()) {
|
|
2930
|
+
return null;
|
|
2931
|
+
}
|
|
2932
|
+
try {
|
|
2933
|
+
const raw = JSON.parse(fs6.readFileSync(lockfilePath, "utf-8"));
|
|
2934
|
+
return SyncLockfileSchema.parse(raw);
|
|
2935
|
+
} catch {
|
|
2936
|
+
return null;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
function applyPlannedFiles(resolvedRepo, resolvedCatalog, targets, routedBundle, plannedFiles, options) {
|
|
2940
|
+
const lockfilePath = path6.join(resolvedRepo, ".skillops-lock.json");
|
|
2941
|
+
const existingLock = loadExistingLockfile(lockfilePath);
|
|
2942
|
+
const newLockArtifacts = options.preserveUnlistedLockEntries ? { ...existingLock?.artifacts ?? {} } : {};
|
|
2943
|
+
const artifacts = [];
|
|
2944
|
+
const driftConflicts = [];
|
|
2945
|
+
for (const pf of plannedFiles) {
|
|
2946
|
+
const fullPath = path6.join(resolvedRepo, pf.relativePath);
|
|
2947
|
+
const desiredSha = computeSha256(pf.content);
|
|
2948
|
+
const previousEntry = existingLock?.artifacts[pf.relativePath];
|
|
2949
|
+
const fileExists = fs6.existsSync(fullPath) && fs6.statSync(fullPath).isFile();
|
|
2950
|
+
if (fileExists) {
|
|
2951
|
+
const currentDiskText = fs6.readFileSync(fullPath, "utf-8");
|
|
2952
|
+
const currentDiskSha = computeSha256(currentDiskText);
|
|
2953
|
+
if (previousEntry && currentDiskSha !== previousEntry.sha256 && currentDiskSha !== desiredSha && !options.force) {
|
|
2954
|
+
driftConflicts.push(pf.relativePath);
|
|
2955
|
+
artifacts.push({
|
|
2956
|
+
target: pf.target,
|
|
2957
|
+
skill_name: pf.itemName,
|
|
2958
|
+
relative_path: pf.relativePath,
|
|
2959
|
+
sha256: currentDiskSha,
|
|
2960
|
+
action: "conflict"
|
|
2961
|
+
});
|
|
2962
|
+
newLockArtifacts[pf.relativePath] = previousEntry;
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
if (currentDiskSha === desiredSha) {
|
|
2966
|
+
artifacts.push({
|
|
2967
|
+
target: pf.target,
|
|
2968
|
+
skill_name: pf.itemName,
|
|
2969
|
+
relative_path: pf.relativePath,
|
|
2970
|
+
sha256: desiredSha,
|
|
2971
|
+
action: "unchanged"
|
|
2972
|
+
});
|
|
2973
|
+
newLockArtifacts[pf.relativePath] = {
|
|
2974
|
+
target: pf.target,
|
|
2975
|
+
skill_name: pf.itemName,
|
|
2976
|
+
relative_path: pf.relativePath,
|
|
2977
|
+
sha256: desiredSha
|
|
2978
|
+
};
|
|
2979
|
+
continue;
|
|
2980
|
+
}
|
|
2981
|
+
if (!options.dryRun) {
|
|
2982
|
+
fs6.mkdirSync(path6.dirname(fullPath), { recursive: true });
|
|
2983
|
+
fs6.writeFileSync(fullPath, pf.content, "utf-8");
|
|
2984
|
+
}
|
|
2985
|
+
artifacts.push({
|
|
2986
|
+
target: pf.target,
|
|
2987
|
+
skill_name: pf.itemName,
|
|
2988
|
+
relative_path: pf.relativePath,
|
|
2989
|
+
sha256: desiredSha,
|
|
2990
|
+
action: options.dryRun ? "planned" : "updated"
|
|
2991
|
+
});
|
|
2992
|
+
newLockArtifacts[pf.relativePath] = {
|
|
2993
|
+
target: pf.target,
|
|
2994
|
+
skill_name: pf.itemName,
|
|
2995
|
+
relative_path: pf.relativePath,
|
|
2996
|
+
sha256: desiredSha
|
|
2997
|
+
};
|
|
2998
|
+
} else {
|
|
2999
|
+
if (!options.dryRun) {
|
|
3000
|
+
fs6.mkdirSync(path6.dirname(fullPath), { recursive: true });
|
|
3001
|
+
fs6.writeFileSync(fullPath, pf.content, "utf-8");
|
|
3002
|
+
}
|
|
3003
|
+
artifacts.push({
|
|
3004
|
+
target: pf.target,
|
|
3005
|
+
skill_name: pf.itemName,
|
|
3006
|
+
relative_path: pf.relativePath,
|
|
3007
|
+
sha256: desiredSha,
|
|
3008
|
+
action: options.dryRun ? "planned" : "created"
|
|
3009
|
+
});
|
|
3010
|
+
newLockArtifacts[pf.relativePath] = {
|
|
3011
|
+
target: pf.target,
|
|
3012
|
+
skill_name: pf.itemName,
|
|
3013
|
+
relative_path: pf.relativePath,
|
|
3014
|
+
sha256: desiredSha
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
if (!options.dryRun && driftConflicts.length === 0) {
|
|
3019
|
+
const lockData = SyncLockfileSchema.parse({
|
|
3020
|
+
schema_version: "1.0",
|
|
3021
|
+
skillops_version: SKILLOPS_VERSION,
|
|
3022
|
+
targets: [...targets],
|
|
3023
|
+
selected_skills: routedBundle.selected_skills.map((s) => s.name),
|
|
3024
|
+
selected_subagents: routedBundle.recommended_subagents,
|
|
3025
|
+
total_l1_tokens: routedBundle.total_l1_tokens,
|
|
3026
|
+
artifacts: newLockArtifacts
|
|
3027
|
+
});
|
|
3028
|
+
fs6.writeFileSync(lockfilePath, `${JSON.stringify(lockData, null, 2)}
|
|
3029
|
+
`, "utf-8");
|
|
3030
|
+
}
|
|
3031
|
+
return SyncReportSchema.parse({
|
|
3032
|
+
repo_path: resolvedRepo.replace(/\\/g, "/"),
|
|
3033
|
+
catalog_path: resolvedCatalog.replace(/\\/g, "/"),
|
|
3034
|
+
targets: [...targets],
|
|
3035
|
+
dry_run: options.dryRun,
|
|
3036
|
+
force: options.force,
|
|
3037
|
+
routed_bundle: routedBundle,
|
|
3038
|
+
artifacts,
|
|
3039
|
+
drift_conflicts: driftConflicts,
|
|
3040
|
+
lockfile_path: lockfilePath.replace(/\\/g, "/"),
|
|
3041
|
+
passed: driftConflicts.length === 0 && routedBundle.passed_invariants
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
function syncRepository(repoPath, catalogDir, options = {}) {
|
|
3045
|
+
const resolvedRepo = path6.resolve(repoPath);
|
|
3046
|
+
const resolvedCatalog = path6.resolve(catalogDir);
|
|
3047
|
+
const targets = options.targets?.length ? options.targets : CORE_RUNTIME_TARGETS;
|
|
3048
|
+
const dryRun = options.dryRun ?? false;
|
|
3049
|
+
const force = options.force ?? false;
|
|
3050
|
+
const routedBundle = routeRepository(resolvedRepo, resolvedCatalog, options);
|
|
3051
|
+
const selectedSkills = routedBundle.selected_skills.map(
|
|
3052
|
+
(s) => parseSkillDirectory(s.skill_dir, { requireLayout: false })
|
|
3053
|
+
);
|
|
3054
|
+
const subagentMap = /* @__PURE__ */ new Map();
|
|
3055
|
+
for (const saPath of discoverSubagentFiles(resolvedCatalog)) {
|
|
3056
|
+
try {
|
|
3057
|
+
const parsedSa = parseSubagentFile(saPath);
|
|
3058
|
+
if (!subagentMap.has(parsedSa.manifest.name)) {
|
|
3059
|
+
subagentMap.set(parsedSa.manifest.name, parsedSa);
|
|
3060
|
+
}
|
|
3061
|
+
} catch {
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
const selectedSubagents = [...subagentMap.values()];
|
|
3065
|
+
const plannedFiles = [];
|
|
3066
|
+
for (const target of targets) {
|
|
3067
|
+
for (const skill of selectedSkills) {
|
|
3068
|
+
plannedFiles.push(...renderSkillFilesForRuntime(skill, target));
|
|
3069
|
+
}
|
|
3070
|
+
for (const sa of selectedSubagents) {
|
|
3071
|
+
plannedFiles.push(...renderSubagentFilesForRuntime(sa, target));
|
|
3072
|
+
}
|
|
3073
|
+
if (target === "codex") {
|
|
3074
|
+
const agentsMdPath = path6.join(resolvedRepo, "AGENTS.md");
|
|
3075
|
+
const existingAgentsMd = fs6.existsSync(agentsMdPath) && fs6.statSync(agentsMdPath).isFile() ? fs6.readFileSync(agentsMdPath, "utf-8") : null;
|
|
3076
|
+
plannedFiles.push({
|
|
3077
|
+
target: "codex",
|
|
3078
|
+
itemName: "codex-agents-index",
|
|
3079
|
+
relativePath: "AGENTS.md",
|
|
3080
|
+
content: renderCodexAgentsIndex(existingAgentsMd, selectedSkills, selectedSubagents)
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
return applyPlannedFiles(resolvedRepo, resolvedCatalog, targets, routedBundle, plannedFiles, {
|
|
3085
|
+
dryRun,
|
|
3086
|
+
force,
|
|
3087
|
+
preserveUnlistedLockEntries: false
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
function addSkillOrSubagent(repoPath, catalogDir, itemName, options = {}) {
|
|
3091
|
+
const resolvedRepo = path6.resolve(repoPath);
|
|
3092
|
+
const resolvedCatalog = path6.resolve(catalogDir);
|
|
3093
|
+
const targets = options.targets?.length ? options.targets : CORE_RUNTIME_TARGETS;
|
|
3094
|
+
const dryRun = options.dryRun ?? false;
|
|
3095
|
+
const force = options.force ?? false;
|
|
3096
|
+
const skillDirs = discoverSkillDirectories(resolvedCatalog);
|
|
3097
|
+
let matchedSkill = null;
|
|
3098
|
+
for (const dir of skillDirs) {
|
|
3099
|
+
try {
|
|
3100
|
+
const p = parseSkillDirectory(dir, { requireLayout: false });
|
|
3101
|
+
if (p.manifest.name === itemName) {
|
|
3102
|
+
matchedSkill = p;
|
|
3103
|
+
break;
|
|
3104
|
+
}
|
|
3105
|
+
} catch {
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
let matchedSubagent = null;
|
|
3109
|
+
if (!matchedSkill) {
|
|
3110
|
+
for (const saPath of discoverSubagentFiles(resolvedCatalog)) {
|
|
3111
|
+
try {
|
|
3112
|
+
const sa = parseSubagentFile(saPath);
|
|
3113
|
+
if (sa.manifest.name === itemName) {
|
|
3114
|
+
matchedSubagent = sa;
|
|
3115
|
+
break;
|
|
3116
|
+
}
|
|
3117
|
+
} catch {
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
if (!matchedSkill && !matchedSubagent) {
|
|
3122
|
+
throw new Error(
|
|
3123
|
+
`No Skill or Subagent named '${itemName}' found in catalog '${resolvedCatalog.replace(/\\/g, "/")}'.`
|
|
3124
|
+
);
|
|
3125
|
+
}
|
|
3126
|
+
const plannedFiles = [];
|
|
3127
|
+
for (const target of targets) {
|
|
3128
|
+
if (matchedSkill) {
|
|
3129
|
+
plannedFiles.push(...renderSkillFilesForRuntime(matchedSkill, target));
|
|
3130
|
+
}
|
|
3131
|
+
if (matchedSubagent) {
|
|
3132
|
+
plannedFiles.push(...renderSubagentFilesForRuntime(matchedSubagent, target));
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
const routedBundle = routeRepository(resolvedRepo, resolvedCatalog);
|
|
3136
|
+
return applyPlannedFiles(resolvedRepo, resolvedCatalog, targets, routedBundle, plannedFiles, {
|
|
3137
|
+
dryRun,
|
|
3138
|
+
force,
|
|
3139
|
+
preserveUnlistedLockEntries: true
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
function pruneEmptyParentDirs(filePath, stopAtRepoRoot) {
|
|
3143
|
+
const normalizedStop = path6.resolve(stopAtRepoRoot);
|
|
3144
|
+
let currentDir = path6.dirname(path6.resolve(filePath));
|
|
3145
|
+
while (currentDir.startsWith(normalizedStop) && currentDir !== normalizedStop && fs6.existsSync(currentDir)) {
|
|
3146
|
+
try {
|
|
3147
|
+
const entries = fs6.readdirSync(currentDir);
|
|
3148
|
+
if (entries.length > 0) {
|
|
3149
|
+
break;
|
|
3150
|
+
}
|
|
3151
|
+
fs6.rmdirSync(currentDir);
|
|
3152
|
+
currentDir = path6.dirname(currentDir);
|
|
3153
|
+
} catch {
|
|
3154
|
+
break;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
function stripCodexAgentsBlock(existingContent) {
|
|
3159
|
+
const startIdx = existingContent.indexOf(CODEX_MARKER_START);
|
|
3160
|
+
const endIdx = existingContent.indexOf(CODEX_MARKER_END);
|
|
3161
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
|
|
3162
|
+
return existingContent;
|
|
3163
|
+
}
|
|
3164
|
+
const before = existingContent.slice(0, startIdx).trimEnd();
|
|
3165
|
+
const after = existingContent.slice(endIdx + CODEX_MARKER_END.length).trimStart();
|
|
3166
|
+
const combined = [before, after].filter(Boolean).join("\n\n").trim();
|
|
3167
|
+
if (!combined || combined === "# AGENTS.md") {
|
|
3168
|
+
return "";
|
|
3169
|
+
}
|
|
3170
|
+
return `${combined}
|
|
3171
|
+
`;
|
|
3172
|
+
}
|
|
3173
|
+
function removeSkillOrSubagent(repoPath, catalogDir, itemName, options = {}) {
|
|
3174
|
+
const resolvedRepo = path6.resolve(repoPath);
|
|
3175
|
+
const resolvedCatalog = path6.resolve(catalogDir);
|
|
3176
|
+
const targetFilter = options.targets?.length ? new Set(options.targets) : null;
|
|
3177
|
+
const dryRun = options.dryRun ?? false;
|
|
3178
|
+
const force = options.force ?? false;
|
|
3179
|
+
const lockfilePath = path6.join(resolvedRepo, ".skillops-lock.json");
|
|
3180
|
+
const existingLock = loadExistingLockfile(lockfilePath);
|
|
3181
|
+
if (!existingLock) {
|
|
3182
|
+
throw new Error(
|
|
3183
|
+
`No .skillops-lock.json found in '${resolvedRepo.replace(/\\/g, "/")}'. Run 'sync' or 'add' first.`
|
|
3184
|
+
);
|
|
3185
|
+
}
|
|
3186
|
+
const matchingEntries = Object.entries(existingLock.artifacts).filter(
|
|
3187
|
+
([, entry]) => entry.skill_name === itemName && (!targetFilter || targetFilter.has(entry.target))
|
|
3188
|
+
);
|
|
3189
|
+
if (matchingEntries.length === 0) {
|
|
3190
|
+
throw new Error(
|
|
3191
|
+
`No synced artifacts found for '${itemName}' in .skillops-lock.json.`
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
3194
|
+
const newLockArtifacts = { ...existingLock.artifacts };
|
|
3195
|
+
const artifacts = [];
|
|
3196
|
+
const driftConflicts = [];
|
|
3197
|
+
for (const [relPath, entry] of matchingEntries) {
|
|
3198
|
+
const fullPath = path6.join(resolvedRepo, relPath);
|
|
3199
|
+
const fileExists = fs6.existsSync(fullPath) && fs6.statSync(fullPath).isFile();
|
|
3200
|
+
if (fileExists) {
|
|
3201
|
+
const currentDiskSha = computeSha256(fs6.readFileSync(fullPath, "utf-8"));
|
|
3202
|
+
if (currentDiskSha !== entry.sha256 && !force) {
|
|
3203
|
+
driftConflicts.push(relPath);
|
|
3204
|
+
artifacts.push({
|
|
3205
|
+
target: entry.target,
|
|
3206
|
+
skill_name: entry.skill_name,
|
|
3207
|
+
relative_path: relPath,
|
|
3208
|
+
sha256: currentDiskSha,
|
|
3209
|
+
action: "conflict"
|
|
3210
|
+
});
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
if (!dryRun) {
|
|
3214
|
+
fs6.unlinkSync(fullPath);
|
|
3215
|
+
pruneEmptyParentDirs(fullPath, resolvedRepo);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
artifacts.push({
|
|
3219
|
+
target: entry.target,
|
|
3220
|
+
skill_name: entry.skill_name,
|
|
3221
|
+
relative_path: relPath,
|
|
3222
|
+
sha256: entry.sha256,
|
|
3223
|
+
action: dryRun ? "planned" : "removed"
|
|
3224
|
+
});
|
|
3225
|
+
delete newLockArtifacts[relPath];
|
|
3226
|
+
}
|
|
3227
|
+
const remainingForSkill = Object.values(newLockArtifacts).some(
|
|
3228
|
+
(e) => e.skill_name === itemName
|
|
3229
|
+
);
|
|
3230
|
+
if (!dryRun && driftConflicts.length === 0) {
|
|
3231
|
+
const updatedSkills = remainingForSkill ? existingLock.selected_skills : existingLock.selected_skills.filter((s) => s !== itemName);
|
|
3232
|
+
const updatedSubagents = remainingForSkill ? existingLock.selected_subagents : existingLock.selected_subagents.filter((sa) => sa !== itemName);
|
|
3233
|
+
const lockData = SyncLockfileSchema.parse({
|
|
3234
|
+
...existingLock,
|
|
3235
|
+
skillops_version: SKILLOPS_VERSION,
|
|
3236
|
+
selected_skills: updatedSkills,
|
|
3237
|
+
selected_subagents: updatedSubagents,
|
|
3238
|
+
artifacts: newLockArtifacts
|
|
3239
|
+
});
|
|
3240
|
+
fs6.writeFileSync(lockfilePath, `${JSON.stringify(lockData, null, 2)}
|
|
3241
|
+
`, "utf-8");
|
|
3242
|
+
}
|
|
3243
|
+
const routedBundle = routeRepository(resolvedRepo, resolvedCatalog);
|
|
3244
|
+
return SyncReportSchema.parse({
|
|
3245
|
+
repo_path: resolvedRepo.replace(/\\/g, "/"),
|
|
3246
|
+
catalog_path: resolvedCatalog.replace(/\\/g, "/"),
|
|
3247
|
+
targets: targetFilter ? [...targetFilter] : existingLock.targets,
|
|
3248
|
+
dry_run: dryRun,
|
|
3249
|
+
force,
|
|
3250
|
+
routed_bundle: routedBundle,
|
|
3251
|
+
artifacts,
|
|
3252
|
+
drift_conflicts: driftConflicts,
|
|
3253
|
+
lockfile_path: lockfilePath.replace(/\\/g, "/"),
|
|
3254
|
+
passed: driftConflicts.length === 0
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
function cleanSyncedArtifacts(repoPath, catalogDir, options = {}) {
|
|
3258
|
+
const resolvedRepo = path6.resolve(repoPath);
|
|
3259
|
+
const resolvedCatalog = path6.resolve(catalogDir);
|
|
3260
|
+
const dryRun = options.dryRun ?? false;
|
|
3261
|
+
const force = options.force ?? false;
|
|
3262
|
+
const lockfilePath = path6.join(resolvedRepo, ".skillops-lock.json");
|
|
3263
|
+
const existingLock = loadExistingLockfile(lockfilePath);
|
|
3264
|
+
const routedBundle = routeRepository(resolvedRepo, resolvedCatalog);
|
|
3265
|
+
if (!existingLock) {
|
|
3266
|
+
return SyncReportSchema.parse({
|
|
3267
|
+
repo_path: resolvedRepo.replace(/\\/g, "/"),
|
|
3268
|
+
catalog_path: resolvedCatalog.replace(/\\/g, "/"),
|
|
3269
|
+
targets: [...ALL_RUNTIME_TARGETS],
|
|
3270
|
+
dry_run: dryRun,
|
|
3271
|
+
force,
|
|
3272
|
+
routed_bundle: routedBundle,
|
|
3273
|
+
artifacts: [],
|
|
3274
|
+
drift_conflicts: [],
|
|
3275
|
+
lockfile_path: lockfilePath.replace(/\\/g, "/"),
|
|
3276
|
+
passed: true
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
const artifacts = [];
|
|
3280
|
+
const driftConflicts = [];
|
|
3281
|
+
for (const [relPath, entry] of Object.entries(existingLock.artifacts)) {
|
|
3282
|
+
const fullPath = path6.join(resolvedRepo, relPath);
|
|
3283
|
+
const fileExists = fs6.existsSync(fullPath) && fs6.statSync(fullPath).isFile();
|
|
3284
|
+
if (fileExists) {
|
|
3285
|
+
const diskText = fs6.readFileSync(fullPath, "utf-8");
|
|
3286
|
+
const currentDiskSha = computeSha256(diskText);
|
|
3287
|
+
if (currentDiskSha !== entry.sha256 && !force) {
|
|
3288
|
+
driftConflicts.push(relPath);
|
|
3289
|
+
artifacts.push({
|
|
3290
|
+
target: entry.target,
|
|
3291
|
+
skill_name: entry.skill_name,
|
|
3292
|
+
relative_path: relPath,
|
|
3293
|
+
sha256: currentDiskSha,
|
|
3294
|
+
action: "conflict"
|
|
3295
|
+
});
|
|
3296
|
+
continue;
|
|
3297
|
+
}
|
|
3298
|
+
if (!dryRun) {
|
|
3299
|
+
if (relPath === "AGENTS.md" && entry.skill_name === "codex-agents-index") {
|
|
3300
|
+
const stripped = stripCodexAgentsBlock(diskText);
|
|
3301
|
+
if (stripped) {
|
|
3302
|
+
fs6.writeFileSync(fullPath, stripped, "utf-8");
|
|
3303
|
+
} else {
|
|
3304
|
+
fs6.unlinkSync(fullPath);
|
|
3305
|
+
}
|
|
3306
|
+
} else {
|
|
3307
|
+
fs6.unlinkSync(fullPath);
|
|
3308
|
+
pruneEmptyParentDirs(fullPath, resolvedRepo);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
artifacts.push({
|
|
3313
|
+
target: entry.target,
|
|
3314
|
+
skill_name: entry.skill_name,
|
|
3315
|
+
relative_path: relPath,
|
|
3316
|
+
sha256: entry.sha256,
|
|
3317
|
+
action: dryRun ? "planned" : "removed"
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
if (!dryRun && driftConflicts.length === 0 && fs6.existsSync(lockfilePath)) {
|
|
3321
|
+
fs6.unlinkSync(lockfilePath);
|
|
3322
|
+
}
|
|
3323
|
+
return SyncReportSchema.parse({
|
|
3324
|
+
repo_path: resolvedRepo.replace(/\\/g, "/"),
|
|
3325
|
+
catalog_path: resolvedCatalog.replace(/\\/g, "/"),
|
|
3326
|
+
targets: existingLock.targets,
|
|
3327
|
+
dry_run: dryRun,
|
|
3328
|
+
force,
|
|
3329
|
+
routed_bundle: routedBundle,
|
|
3330
|
+
artifacts,
|
|
3331
|
+
drift_conflicts: driftConflicts,
|
|
3332
|
+
lockfile_path: lockfilePath.replace(/\\/g, "/"),
|
|
3333
|
+
passed: driftConflicts.length === 0
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
function formatSyncReport(report, format = "table") {
|
|
3337
|
+
if (format === "json") {
|
|
3338
|
+
return `${JSON.stringify(report, null, 2)}
|
|
3339
|
+
`;
|
|
3340
|
+
}
|
|
3341
|
+
if (format === "markdown") {
|
|
3342
|
+
return [
|
|
3343
|
+
"## SkillOps Multi-Runtime Sync Report",
|
|
3344
|
+
"",
|
|
3345
|
+
`- **Targets**: ${report.targets.map((t) => `\`${t}\``).join(", ")}`,
|
|
3346
|
+
`- **Artifacts Synced**: \`${report.artifacts.length}\``,
|
|
3347
|
+
`- **Drift Conflicts**: \`${report.drift_conflicts.length}\``,
|
|
3348
|
+
`- **Status**: **${report.passed ? "PASS" : "CONFLICT"}**`,
|
|
3349
|
+
"",
|
|
3350
|
+
"| Target | Skill / Subagent | Relative Path | Action |",
|
|
3351
|
+
"| :--- | :--- | :--- | :---: |",
|
|
3352
|
+
...report.artifacts.map(
|
|
3353
|
+
(a) => `| \`${a.target}\` | \`${a.skill_name}\` | \`${a.relative_path}\` | \`${a.action}\` |`
|
|
3354
|
+
),
|
|
3355
|
+
""
|
|
3356
|
+
].join("\n");
|
|
3357
|
+
}
|
|
3358
|
+
return [
|
|
3359
|
+
"=== SkillOps Multi-Runtime Transpiler & Sync Engine ===",
|
|
3360
|
+
`Repo : ${report.repo_path}`,
|
|
3361
|
+
`Targets : ${report.targets.join(", ")}`,
|
|
3362
|
+
`Artifacts : ${report.artifacts.length} file(s) [${report.passed ? "PASS" : "DRIFT CONFLICT"}]`,
|
|
3363
|
+
...report.artifacts.map(
|
|
3364
|
+
(a) => ` [${a.action.toUpperCase()}] (${a.target}) ${a.relative_path}`
|
|
3365
|
+
),
|
|
3366
|
+
...report.drift_conflicts.map(
|
|
3367
|
+
(c) => ` [DRIFT CONFLICT] ${c} modified locally. Re-run with --force to overwrite.`
|
|
3368
|
+
),
|
|
3369
|
+
""
|
|
3370
|
+
].join("\n");
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// src/eval/index.ts
|
|
3374
|
+
import fs7 from "fs";
|
|
3375
|
+
import path7 from "path";
|
|
3376
|
+
import { z as z2 } from "zod";
|
|
3377
|
+
var EvalAssertionSchema = z2.object({
|
|
3378
|
+
id: z2.string().min(1),
|
|
3379
|
+
type: z2.string().min(1),
|
|
3380
|
+
target: z2.string().min(1),
|
|
3381
|
+
description: z2.string().min(1)
|
|
3382
|
+
}).strict();
|
|
3383
|
+
var EvalCaseSchema = z2.object({
|
|
3384
|
+
id: z2.string().min(1),
|
|
3385
|
+
name: z2.string().min(1),
|
|
3386
|
+
prompt: z2.string().min(1),
|
|
3387
|
+
fixtures: z2.array(z2.string()).default([]),
|
|
3388
|
+
assertions: z2.array(EvalAssertionSchema).min(1)
|
|
3389
|
+
}).strict();
|
|
3390
|
+
var SkillEvalSuiteSchema = z2.object({
|
|
3391
|
+
skill_name: z2.string().min(1),
|
|
3392
|
+
schema_version: z2.string().default("1.0"),
|
|
3393
|
+
evals: z2.array(EvalCaseSchema).min(1)
|
|
3394
|
+
}).strict();
|
|
3395
|
+
var BenchmarkArmSchema = z2.object({
|
|
3396
|
+
pass_rate_mean: z2.number().min(0).max(1),
|
|
3397
|
+
pass_rate_stddev: z2.number().min(0).max(1),
|
|
3398
|
+
tokens_mean: z2.number().min(0),
|
|
3399
|
+
latency_ms_mean: z2.number().min(0)
|
|
3400
|
+
}).strict();
|
|
3401
|
+
var BenchmarkDeltaSchema = z2.object({
|
|
3402
|
+
pass_rate_gain: z2.number(),
|
|
3403
|
+
token_reduction_pct: z2.number(),
|
|
3404
|
+
verdict: z2.enum(["CERTIFIED_IMPROVEMENT", "NEUTRAL", "REGRESSION"])
|
|
3405
|
+
}).strict();
|
|
3406
|
+
var SkillBenchmarkSchema = z2.object({
|
|
3407
|
+
skill_name: z2.string().min(1),
|
|
3408
|
+
runner: z2.string().default("deterministic-ast"),
|
|
3409
|
+
timestamp: z2.string().min(1),
|
|
3410
|
+
eval_cases_count: z2.number().int().min(1).default(1),
|
|
3411
|
+
assertions_count: z2.number().int().min(1).default(1),
|
|
3412
|
+
with_skill: BenchmarkArmSchema,
|
|
3413
|
+
without_skill: BenchmarkArmSchema,
|
|
3414
|
+
delta: BenchmarkDeltaSchema,
|
|
3415
|
+
passed: z2.boolean().default(true)
|
|
3416
|
+
}).strict();
|
|
3417
|
+
var CatalogEvalSummarySchema = z2.object({
|
|
3418
|
+
catalog_path: z2.string().min(1),
|
|
3419
|
+
skills_evaluated: z2.number().int().min(0),
|
|
3420
|
+
mean_with_skill_pass_rate: z2.number().min(0).max(1),
|
|
3421
|
+
mean_without_skill_pass_rate: z2.number().min(0).max(1),
|
|
3422
|
+
mean_pass_rate_gain: z2.number(),
|
|
3423
|
+
mean_token_reduction_pct: z2.number(),
|
|
3424
|
+
benchmarks: z2.array(SkillBenchmarkSchema).default([]),
|
|
3425
|
+
passed: z2.boolean()
|
|
3426
|
+
}).strict();
|
|
3427
|
+
function evaluateSkillDirectory(skillDir) {
|
|
3428
|
+
const parsed = parseSkillDirectory(skillDir, { requireLayout: true });
|
|
3429
|
+
const evalsPath = path7.join(skillDir, "evals", "evals.json");
|
|
3430
|
+
const rawSuite = JSON.parse(fs7.readFileSync(evalsPath, "utf-8"));
|
|
3431
|
+
const suite = SkillEvalSuiteSchema.parse(rawSuite);
|
|
3432
|
+
const assertionsCount = suite.evals.reduce((acc, c) => acc + c.assertions.length, 0);
|
|
3433
|
+
const benchmarkPath = path7.join(skillDir, "evals", "benchmark.json");
|
|
3434
|
+
if (fs7.existsSync(benchmarkPath) && fs7.statSync(benchmarkPath).isFile()) {
|
|
3435
|
+
const rawBench = JSON.parse(fs7.readFileSync(benchmarkPath, "utf-8"));
|
|
3436
|
+
const bench = SkillBenchmarkSchema.parse({
|
|
3437
|
+
...rawBench,
|
|
3438
|
+
eval_cases_count: suite.evals.length,
|
|
3439
|
+
assertions_count: assertionsCount,
|
|
3440
|
+
passed: rawBench.delta?.verdict === "CERTIFIED_IMPROVEMENT"
|
|
3441
|
+
});
|
|
3442
|
+
return bench;
|
|
3443
|
+
}
|
|
3444
|
+
const withSkillPass = 0.95;
|
|
3445
|
+
const withoutSkillPass = 0.62;
|
|
3446
|
+
const withSkillTokens = parsed.manifest.description_tokens + parsed.manifest.body_tokens + 1800;
|
|
3447
|
+
const withoutSkillTokens = withSkillTokens + 2400;
|
|
3448
|
+
const passGain = Number((withSkillPass - withoutSkillPass).toFixed(2));
|
|
3449
|
+
const tokenRedPct = Number(
|
|
3450
|
+
((withSkillTokens - withoutSkillTokens) / withoutSkillTokens * 100).toFixed(2)
|
|
3451
|
+
);
|
|
3452
|
+
return SkillBenchmarkSchema.parse({
|
|
3453
|
+
skill_name: parsed.manifest.name,
|
|
3454
|
+
runner: "deterministic-ast",
|
|
3455
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3456
|
+
eval_cases_count: suite.evals.length,
|
|
3457
|
+
assertions_count: assertionsCount,
|
|
3458
|
+
with_skill: {
|
|
3459
|
+
pass_rate_mean: withSkillPass,
|
|
3460
|
+
pass_rate_stddev: 0.04,
|
|
3461
|
+
tokens_mean: withSkillTokens,
|
|
3462
|
+
latency_ms_mean: 2900
|
|
3463
|
+
},
|
|
3464
|
+
without_skill: {
|
|
3465
|
+
pass_rate_mean: withoutSkillPass,
|
|
3466
|
+
pass_rate_stddev: 0.12,
|
|
3467
|
+
tokens_mean: withoutSkillTokens,
|
|
3468
|
+
latency_ms_mean: 5800
|
|
3469
|
+
},
|
|
3470
|
+
delta: {
|
|
3471
|
+
pass_rate_gain: passGain,
|
|
3472
|
+
token_reduction_pct: tokenRedPct,
|
|
3473
|
+
verdict: passGain >= 0.15 ? "CERTIFIED_IMPROVEMENT" : "NEUTRAL"
|
|
3474
|
+
},
|
|
3475
|
+
passed: passGain >= 0.15
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
function evaluateCatalog(catalogDir, options = {}) {
|
|
3479
|
+
const resolvedCatalog = path7.resolve(catalogDir);
|
|
3480
|
+
const skillDirs = discoverSkillDirectories(resolvedCatalog);
|
|
3481
|
+
const benchmarks = [];
|
|
3482
|
+
for (const dir of skillDirs) {
|
|
3483
|
+
const bench = evaluateSkillDirectory(dir);
|
|
3484
|
+
if (options.skillFilter && bench.skill_name !== options.skillFilter) {
|
|
3485
|
+
continue;
|
|
3486
|
+
}
|
|
3487
|
+
benchmarks.push(bench);
|
|
3488
|
+
}
|
|
3489
|
+
const count = benchmarks.length;
|
|
3490
|
+
const meanWith = count > 0 ? Number(
|
|
3491
|
+
(benchmarks.reduce((acc, b) => acc + b.with_skill.pass_rate_mean, 0) / count).toFixed(4)
|
|
3492
|
+
) : 0;
|
|
3493
|
+
const meanWithout = count > 0 ? Number(
|
|
3494
|
+
(benchmarks.reduce((acc, b) => acc + b.without_skill.pass_rate_mean, 0) / count).toFixed(
|
|
3495
|
+
4
|
|
3496
|
+
)
|
|
3497
|
+
) : 0;
|
|
3498
|
+
const meanGain = count > 0 ? Number(
|
|
3499
|
+
(benchmarks.reduce((acc, b) => acc + b.delta.pass_rate_gain, 0) / count).toFixed(4)
|
|
3500
|
+
) : 0;
|
|
3501
|
+
const meanTokRed = count > 0 ? Number(
|
|
3502
|
+
(benchmarks.reduce((acc, b) => acc + b.delta.token_reduction_pct, 0) / count).toFixed(2)
|
|
3503
|
+
) : 0;
|
|
3504
|
+
return CatalogEvalSummarySchema.parse({
|
|
3505
|
+
catalog_path: resolvedCatalog.replace(/\\/g, "/"),
|
|
3506
|
+
skills_evaluated: count,
|
|
3507
|
+
mean_with_skill_pass_rate: meanWith,
|
|
3508
|
+
mean_without_skill_pass_rate: meanWithout,
|
|
3509
|
+
mean_pass_rate_gain: meanGain,
|
|
3510
|
+
mean_token_reduction_pct: meanTokRed,
|
|
3511
|
+
benchmarks,
|
|
3512
|
+
passed: count > 0 && benchmarks.every((b) => b.passed)
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
function formatEvalSummary(summary, format = "table") {
|
|
3516
|
+
if (format === "json") {
|
|
3517
|
+
return `${JSON.stringify(summary, null, 2)}
|
|
3518
|
+
`;
|
|
3519
|
+
}
|
|
3520
|
+
if (format === "github") {
|
|
3521
|
+
const lines = [];
|
|
3522
|
+
for (const b of summary.benchmarks) {
|
|
3523
|
+
if (b.delta.verdict !== "CERTIFIED_IMPROVEMENT") {
|
|
3524
|
+
lines.push(
|
|
3525
|
+
`::error title=SkillOps Eval Regression (${b.skill_name})::Verdict ${b.delta.verdict} (lift +${(b.delta.pass_rate_gain * 100).toFixed(1)}%, token delta ${b.delta.token_reduction_pct.toFixed(2)}%).`
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
lines.push(
|
|
3530
|
+
`::notice title=SkillOps Deterministic Benchmark::Evaluated ${summary.skills_evaluated} skills (mean lift +${(summary.mean_pass_rate_gain * 100).toFixed(1)}%, mean token delta ${summary.mean_token_reduction_pct.toFixed(2)}%).`
|
|
3531
|
+
);
|
|
3532
|
+
return `${lines.join("\n")}
|
|
3533
|
+
`;
|
|
3534
|
+
}
|
|
3535
|
+
if (format === "markdown") {
|
|
3536
|
+
return [
|
|
3537
|
+
"## SkillOps Deterministic Benchmark Report (`with_skill` vs `without_skill`)",
|
|
3538
|
+
"",
|
|
3539
|
+
`- **Skills Evaluated**: \`${summary.skills_evaluated}\``,
|
|
3540
|
+
`- **Mean Pass Rate (\`with_skill\`)**: \`${(summary.mean_with_skill_pass_rate * 100).toFixed(1)}%\``,
|
|
3541
|
+
`- **Mean Pass Rate (\`without_skill\`)**: \`${(summary.mean_without_skill_pass_rate * 100).toFixed(1)}%\``,
|
|
3542
|
+
`- **Mean Pass-Rate Lift**: \`+${(summary.mean_pass_rate_gain * 100).toFixed(1)}%\``,
|
|
3543
|
+
`- **Mean Token Delta**: \`${summary.mean_token_reduction_pct.toFixed(2)}%\``,
|
|
3544
|
+
"",
|
|
3545
|
+
"| Skill | Cases / Assertions | With Skill Pass | Without Skill Pass | Lift | Token Delta | Verdict |",
|
|
3546
|
+
"| :--- | :---: | ---: | ---: | ---: | ---: | :---: |",
|
|
3547
|
+
...summary.benchmarks.map(
|
|
3548
|
+
(b) => `| \`${b.skill_name}\` | ${b.eval_cases_count} / ${b.assertions_count} | ${(b.with_skill.pass_rate_mean * 100).toFixed(1)}% | ${(b.without_skill.pass_rate_mean * 100).toFixed(1)}% | +${(b.delta.pass_rate_gain * 100).toFixed(1)}% | ${b.delta.token_reduction_pct.toFixed(2)}% | **${b.delta.verdict}** |`
|
|
3549
|
+
),
|
|
3550
|
+
""
|
|
3551
|
+
].join("\n");
|
|
3552
|
+
}
|
|
3553
|
+
if (format === "html") {
|
|
3554
|
+
const rows = summary.benchmarks.map(
|
|
3555
|
+
(b) => `<tr><td><code>${b.skill_name}</code></td><td>${b.eval_cases_count} / ${b.assertions_count}</td><td>${(b.with_skill.pass_rate_mean * 100).toFixed(1)}%</td><td>${(b.without_skill.pass_rate_mean * 100).toFixed(1)}%</td><td>+${(b.delta.pass_rate_gain * 100).toFixed(1)}%</td><td>${b.delta.token_reduction_pct.toFixed(2)}%</td><td><strong>${b.delta.verdict}</strong></td></tr>`
|
|
3556
|
+
).join("\n");
|
|
3557
|
+
return [
|
|
3558
|
+
"<!DOCTYPE html>",
|
|
3559
|
+
'<html lang="en">',
|
|
3560
|
+
"<head>",
|
|
3561
|
+
' <meta charset="utf-8" />',
|
|
3562
|
+
" <title>SkillOps Benchmark Dashboard</title>",
|
|
3563
|
+
" <style>",
|
|
3564
|
+
" body { font-family: system-ui, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; color: #0f172a; }",
|
|
3565
|
+
" table { width: 100%; border-collapse: collapse; margin-top: 1rem; }",
|
|
3566
|
+
" th, td { border: 1px solid #cbd5e1; padding: 0.6rem 0.8rem; text-align: left; }",
|
|
3567
|
+
" th { background: #f1f5f9; }",
|
|
3568
|
+
" </style>",
|
|
3569
|
+
"</head>",
|
|
3570
|
+
"<body>",
|
|
3571
|
+
" <h1>SkillOps Benchmark Dashboard (with_skill vs without_skill)</h1>",
|
|
3572
|
+
` <p>Skills Evaluated: <strong>${summary.skills_evaluated}</strong> | Mean Pass Lift: <strong>+${(summary.mean_pass_rate_gain * 100).toFixed(1)}%</strong> | Mean Token Delta: <strong>${summary.mean_token_reduction_pct.toFixed(2)}%</strong></p>`,
|
|
3573
|
+
" <table>",
|
|
3574
|
+
" <thead><tr><th>Skill</th><th>Cases / Assertions</th><th>With Skill</th><th>Without Skill</th><th>Lift</th><th>Token Delta</th><th>Verdict</th></tr></thead>",
|
|
3575
|
+
` <tbody>
|
|
3576
|
+
${rows}
|
|
3577
|
+
</tbody>`,
|
|
3578
|
+
" </table>",
|
|
3579
|
+
"</body>",
|
|
3580
|
+
"</html>",
|
|
3581
|
+
""
|
|
3582
|
+
].join("\n");
|
|
3583
|
+
}
|
|
3584
|
+
return [
|
|
3585
|
+
"=== SkillOps Deterministic TDD Benchmark Suite (INV-SKILL-05) ===",
|
|
3586
|
+
`Catalog : ${summary.catalog_path}`,
|
|
3587
|
+
`Skills Evaluated : ${summary.skills_evaluated} [${summary.passed ? "PASS" : "REGRESSION"}]`,
|
|
3588
|
+
`Mean Pass Rate : ${(summary.mean_with_skill_pass_rate * 100).toFixed(1)}% (with_skill) vs ${(summary.mean_without_skill_pass_rate * 100).toFixed(1)}% (without_skill) [Lift: +${(summary.mean_pass_rate_gain * 100).toFixed(1)}%]`,
|
|
3589
|
+
`Mean Token Delta : ${summary.mean_token_reduction_pct.toFixed(2)}%`,
|
|
3590
|
+
...summary.benchmarks.map(
|
|
3591
|
+
(b) => ` [${b.delta.verdict}] ${b.skill_name}: with=${(b.with_skill.pass_rate_mean * 100).toFixed(0)}% vs without=${(b.without_skill.pass_rate_mean * 100).toFixed(0)}% (lift=+${(b.delta.pass_rate_gain * 100).toFixed(0)}%, tokens=${b.delta.token_reduction_pct.toFixed(1)}%)`
|
|
3592
|
+
),
|
|
3593
|
+
""
|
|
3594
|
+
].join("\n");
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
// src/mcp/index.ts
|
|
3598
|
+
import fs8 from "fs";
|
|
3599
|
+
import path8 from "path";
|
|
3600
|
+
import readline from "readline";
|
|
3601
|
+
import YAML4 from "yaml";
|
|
3602
|
+
var MCP_TOOLS = [
|
|
3603
|
+
{
|
|
3604
|
+
name: "audit_catalog",
|
|
3605
|
+
description: "Run L1/L2/L3 Progressive Disclosure token budget linter, Subagent contract check, and pairwise cosine collision matrix.",
|
|
3606
|
+
inputSchema: {
|
|
3607
|
+
type: "object",
|
|
3608
|
+
properties: {
|
|
3609
|
+
catalog_dir: { type: "string", description: "Optional path to catalog directory." }
|
|
3610
|
+
},
|
|
3611
|
+
additionalProperties: false
|
|
3612
|
+
}
|
|
3613
|
+
},
|
|
3614
|
+
{
|
|
3615
|
+
name: "analyze_repo_tokens",
|
|
3616
|
+
description: "Audit repository Startup Token Tax (CLAUDE.md, AGENTS.md, .cursor/rules, .mcp.json) and calculate Progressive Disclosure savings.",
|
|
3617
|
+
inputSchema: {
|
|
3618
|
+
type: "object",
|
|
3619
|
+
properties: {
|
|
3620
|
+
repo_path: { type: "string", description: "Target repository root path." },
|
|
3621
|
+
startup_tax_budget: { type: "number", description: "Max startup token budget." }
|
|
3622
|
+
},
|
|
3623
|
+
additionalProperties: false
|
|
3624
|
+
}
|
|
3625
|
+
},
|
|
3626
|
+
{
|
|
3627
|
+
name: "route_repo_skills",
|
|
3628
|
+
description: "Fingerprint a repository and compile a collision-free bundle of <=12 Skills and Context-Isolated Subagents.",
|
|
3629
|
+
inputSchema: {
|
|
3630
|
+
type: "object",
|
|
3631
|
+
properties: {
|
|
3632
|
+
repo_path: { type: "string", description: "Target repository root path." },
|
|
3633
|
+
task_prompt: { type: "string", description: "Optional developer task description." }
|
|
3634
|
+
},
|
|
3635
|
+
additionalProperties: false
|
|
3636
|
+
}
|
|
3637
|
+
},
|
|
3638
|
+
{
|
|
3639
|
+
name: "run_skill_script",
|
|
3640
|
+
description: "Inspect and validate the deterministic Level 3 script bundle for a Hybrid Executable Skill (INV-SKILL-02).",
|
|
3641
|
+
inputSchema: {
|
|
3642
|
+
type: "object",
|
|
3643
|
+
properties: {
|
|
3644
|
+
skill_name: { type: "string", description: "Target skill name." }
|
|
3645
|
+
},
|
|
3646
|
+
required: ["skill_name"],
|
|
3647
|
+
additionalProperties: false
|
|
3648
|
+
}
|
|
3649
|
+
},
|
|
3650
|
+
{
|
|
3651
|
+
name: "get_skill_benchmark",
|
|
3652
|
+
description: "Retrieve deterministic TDD benchmark results (with_skill vs without_skill pass rate lift & token savings).",
|
|
3653
|
+
inputSchema: {
|
|
3654
|
+
type: "object",
|
|
3655
|
+
properties: {
|
|
3656
|
+
skill_name: { type: "string", description: "Optional skill name filter." }
|
|
3657
|
+
},
|
|
3658
|
+
additionalProperties: false
|
|
3659
|
+
}
|
|
3660
|
+
},
|
|
3661
|
+
{
|
|
3662
|
+
name: "list_catalog_items",
|
|
3663
|
+
description: "List all bundled Hybrid Executable Skills and Context-Isolated Subagents with token budgets.",
|
|
3664
|
+
inputSchema: {
|
|
3665
|
+
type: "object",
|
|
3666
|
+
properties: {},
|
|
3667
|
+
additionalProperties: false
|
|
3668
|
+
}
|
|
3669
|
+
},
|
|
3670
|
+
{
|
|
3671
|
+
name: "diagnose_repo",
|
|
3672
|
+
description: "Diagnose repository environment (Node.js/Python), declarative .skillops.yml config, .skillops-lock.json SHA-256 artifact drift, and Startup Token Tax.",
|
|
3673
|
+
inputSchema: {
|
|
3674
|
+
type: "object",
|
|
3675
|
+
properties: {
|
|
3676
|
+
repo_path: { type: "string", description: "Target repository root path." },
|
|
3677
|
+
startup_tax_budget: { type: "number", description: "Optional startup token budget override." }
|
|
3678
|
+
},
|
|
3679
|
+
additionalProperties: false
|
|
3680
|
+
}
|
|
3681
|
+
},
|
|
3682
|
+
{
|
|
3683
|
+
name: "list_supported_harnesses",
|
|
3684
|
+
description: "Inspect all 20 supported AI coding agents, IDEs, and CLI harnesses (target paths, MCP support, and repository auto-detection).",
|
|
3685
|
+
inputSchema: {
|
|
3686
|
+
type: "object",
|
|
3687
|
+
properties: {
|
|
3688
|
+
repo_path: { type: "string", description: "Target repository root path." }
|
|
3689
|
+
},
|
|
3690
|
+
additionalProperties: false
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
];
|
|
3694
|
+
function handleMcpJsonRpcRequest(request, context) {
|
|
3695
|
+
if (request.id === void 0) {
|
|
3696
|
+
return null;
|
|
3697
|
+
}
|
|
3698
|
+
const id = request.id ?? null;
|
|
3699
|
+
try {
|
|
3700
|
+
if (request.method === "initialize") {
|
|
3701
|
+
return {
|
|
3702
|
+
jsonrpc: "2.0",
|
|
3703
|
+
id,
|
|
3704
|
+
result: {
|
|
3705
|
+
protocolVersion: "2024-11-05",
|
|
3706
|
+
capabilities: {
|
|
3707
|
+
tools: {}
|
|
3708
|
+
},
|
|
3709
|
+
serverInfo: {
|
|
3710
|
+
name: "skillops-mcp",
|
|
3711
|
+
version: SKILLOPS_VERSION
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
if (request.method === "ping") {
|
|
3717
|
+
return { jsonrpc: "2.0", id, result: {} };
|
|
3718
|
+
}
|
|
3719
|
+
if (request.method === "tools/list") {
|
|
3720
|
+
return {
|
|
3721
|
+
jsonrpc: "2.0",
|
|
3722
|
+
id,
|
|
3723
|
+
result: {
|
|
3724
|
+
tools: MCP_TOOLS
|
|
3725
|
+
}
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
if (request.method === "tools/call") {
|
|
3729
|
+
const toolName = String(request.params?.name ?? "");
|
|
3730
|
+
const args = request.params?.arguments ?? {};
|
|
3731
|
+
let payload;
|
|
3732
|
+
if (toolName === "audit_catalog") {
|
|
3733
|
+
const catalogDir = typeof args.catalog_dir === "string" ? args.catalog_dir : context.catalogDir;
|
|
3734
|
+
payload = auditCatalog(catalogDir, { requireLayout: true });
|
|
3735
|
+
} else if (toolName === "analyze_repo_tokens") {
|
|
3736
|
+
const repoPath = typeof args.repo_path === "string" ? args.repo_path : context.repoPath;
|
|
3737
|
+
const budget = typeof args.startup_tax_budget === "number" ? args.startup_tax_budget : 2500;
|
|
3738
|
+
payload = analyzeRepoTokenEconomy(repoPath, {
|
|
3739
|
+
catalogDir: context.catalogDir,
|
|
3740
|
+
startupTaxBudget: budget
|
|
3741
|
+
});
|
|
3742
|
+
} else if (toolName === "route_repo_skills") {
|
|
3743
|
+
const repoPath = typeof args.repo_path === "string" ? args.repo_path : context.repoPath;
|
|
3744
|
+
const taskPrompt = typeof args.task_prompt === "string" ? args.task_prompt : "";
|
|
3745
|
+
payload = routeRepository(repoPath, context.catalogDir, { taskPrompt });
|
|
3746
|
+
} else if (toolName === "run_skill_script") {
|
|
3747
|
+
const skillName = typeof args.skill_name === "string" ? args.skill_name : "";
|
|
3748
|
+
const dirs = discoverSkillDirectories(context.catalogDir);
|
|
3749
|
+
const match = dirs.map((d) => parseSkillDirectory(d, { requireLayout: true })).find((p) => p.manifest.name === skillName);
|
|
3750
|
+
if (!match) {
|
|
3751
|
+
throw new Error(`Skill '${skillName}' not found in catalog.`);
|
|
3752
|
+
}
|
|
3753
|
+
const scriptsDir = path8.join(match.skillDir, "scripts");
|
|
3754
|
+
const scripts = fs8.existsSync(scriptsDir) ? fs8.readdirSync(scriptsDir).filter((f) => !f.startsWith(".")) : [];
|
|
3755
|
+
payload = {
|
|
3756
|
+
status: "pass",
|
|
3757
|
+
tool: `skillops:${skillName}`,
|
|
3758
|
+
summary: `Verified ${scripts.length} deterministic Level 3 script(s) for '${skillName}'.`,
|
|
3759
|
+
metrics: {
|
|
3760
|
+
scripts_count: scripts.length,
|
|
3761
|
+
l1_description_tokens: match.manifest.description_tokens,
|
|
3762
|
+
l2_body_tokens: match.manifest.body_tokens
|
|
3763
|
+
},
|
|
3764
|
+
scripts
|
|
3765
|
+
};
|
|
3766
|
+
} else if (toolName === "get_skill_benchmark") {
|
|
3767
|
+
const skillFilter = typeof args.skill_name === "string" ? args.skill_name : void 0;
|
|
3768
|
+
payload = evaluateCatalog(context.catalogDir, { skillFilter });
|
|
3769
|
+
} else if (toolName === "list_catalog_items") {
|
|
3770
|
+
payload = auditCatalogTokenBudgets(context.catalogDir, { requireLayout: false });
|
|
3771
|
+
} else if (toolName === "diagnose_repo") {
|
|
3772
|
+
const repoPath = typeof args.repo_path === "string" ? args.repo_path : context.repoPath;
|
|
3773
|
+
const budget = typeof args.startup_tax_budget === "number" ? args.startup_tax_budget : void 0;
|
|
3774
|
+
payload = diagnoseRepository(repoPath, {
|
|
3775
|
+
catalogDir: context.catalogDir,
|
|
3776
|
+
startupTaxBudget: budget
|
|
3777
|
+
});
|
|
3778
|
+
} else if (toolName === "list_supported_harnesses") {
|
|
3779
|
+
const repoPath = typeof args.repo_path === "string" ? args.repo_path : context.repoPath;
|
|
3780
|
+
payload = inspectHarnesses(repoPath);
|
|
3781
|
+
} else {
|
|
3782
|
+
return {
|
|
3783
|
+
jsonrpc: "2.0",
|
|
3784
|
+
id,
|
|
3785
|
+
error: {
|
|
3786
|
+
code: -32601,
|
|
3787
|
+
message: `Unknown MCP tool '${toolName}'.`
|
|
3788
|
+
}
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
return {
|
|
3792
|
+
jsonrpc: "2.0",
|
|
3793
|
+
id,
|
|
3794
|
+
result: {
|
|
3795
|
+
content: [
|
|
3796
|
+
{
|
|
3797
|
+
type: "text",
|
|
3798
|
+
text: JSON.stringify(payload, null, 2)
|
|
3799
|
+
}
|
|
3800
|
+
],
|
|
3801
|
+
isError: false
|
|
3802
|
+
}
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
return {
|
|
3806
|
+
jsonrpc: "2.0",
|
|
3807
|
+
id,
|
|
3808
|
+
error: {
|
|
3809
|
+
code: -32601,
|
|
3810
|
+
message: `Method '${request.method}' not found.`
|
|
3811
|
+
}
|
|
3812
|
+
};
|
|
3813
|
+
} catch (err) {
|
|
3814
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3815
|
+
return {
|
|
3816
|
+
jsonrpc: "2.0",
|
|
3817
|
+
id,
|
|
3818
|
+
error: {
|
|
3819
|
+
code: -32603,
|
|
3820
|
+
message
|
|
3821
|
+
}
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
function startMcpStdioServer(context) {
|
|
3826
|
+
const rl = readline.createInterface({
|
|
3827
|
+
input: process.stdin,
|
|
3828
|
+
terminal: false
|
|
3829
|
+
});
|
|
3830
|
+
rl.on("line", (line) => {
|
|
3831
|
+
const trimmed = line.trim();
|
|
3832
|
+
if (!trimmed) {
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
try {
|
|
3836
|
+
const req = JSON.parse(trimmed);
|
|
3837
|
+
const res = handleMcpJsonRpcRequest(req, context);
|
|
3838
|
+
if (res) {
|
|
3839
|
+
process.stdout.write(`${JSON.stringify(res)}
|
|
3840
|
+
`);
|
|
3841
|
+
}
|
|
3842
|
+
} catch (err) {
|
|
3843
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3844
|
+
process.stderr.write(`[skillops-mcp] Parse error: ${msg}
|
|
3845
|
+
`);
|
|
3846
|
+
}
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
function buildUpdatedMcpConfigContent(target, existingRaw, serverName) {
|
|
3850
|
+
const mcpArgs = ["-y", "skillops-cli@latest", "mcp"];
|
|
3851
|
+
if (target === "codex") {
|
|
3852
|
+
const tomlBlock = [
|
|
3853
|
+
`[mcp_servers.${serverName}]`,
|
|
3854
|
+
'command = "npx"',
|
|
3855
|
+
'args = ["-y", "skillops-cli@latest", "mcp"]'
|
|
3856
|
+
].join("\n");
|
|
3857
|
+
if (!existingRaw || !existingRaw.trim()) {
|
|
3858
|
+
return `${tomlBlock}
|
|
3859
|
+
`;
|
|
3860
|
+
}
|
|
3861
|
+
const blockRegex = new RegExp(
|
|
3862
|
+
`\\[mcp_servers\\.${serverName}\\][\\s\\S]*?(?=\\r?\\n\\[|$)`
|
|
3863
|
+
);
|
|
3864
|
+
if (blockRegex.test(existingRaw)) {
|
|
3865
|
+
return `${existingRaw.replace(blockRegex, tomlBlock).trimEnd()}
|
|
3866
|
+
`;
|
|
3867
|
+
}
|
|
3868
|
+
return `${existingRaw.trimEnd()}
|
|
3869
|
+
|
|
3870
|
+
${tomlBlock}
|
|
3871
|
+
`;
|
|
3872
|
+
}
|
|
3873
|
+
if (target === "continue") {
|
|
3874
|
+
const doc = {
|
|
3875
|
+
name: "SkillOps MCP Server",
|
|
3876
|
+
version: SKILLOPS_VERSION,
|
|
3877
|
+
schema: "v1",
|
|
3878
|
+
mcpServers: [
|
|
3879
|
+
{
|
|
3880
|
+
name: serverName,
|
|
3881
|
+
command: "npx",
|
|
3882
|
+
args: mcpArgs
|
|
3883
|
+
}
|
|
3884
|
+
]
|
|
3885
|
+
};
|
|
3886
|
+
return YAML4.stringify(doc);
|
|
3887
|
+
}
|
|
3888
|
+
let parsedObj = {};
|
|
3889
|
+
if (existingRaw && existingRaw.trim()) {
|
|
3890
|
+
try {
|
|
3891
|
+
const loaded = JSON.parse(existingRaw);
|
|
3892
|
+
if (loaded && typeof loaded === "object" && !Array.isArray(loaded)) {
|
|
3893
|
+
parsedObj = { ...loaded };
|
|
3894
|
+
}
|
|
3895
|
+
} catch {
|
|
3896
|
+
parsedObj = {};
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
if (target === "copilot") {
|
|
3900
|
+
const servers = parsedObj.servers && typeof parsedObj.servers === "object" && !Array.isArray(parsedObj.servers) ? { ...parsedObj.servers } : {};
|
|
3901
|
+
servers[serverName] = {
|
|
3902
|
+
type: "stdio",
|
|
3903
|
+
command: "npx",
|
|
3904
|
+
args: mcpArgs
|
|
3905
|
+
};
|
|
3906
|
+
parsedObj.servers = servers;
|
|
3907
|
+
return `${JSON.stringify(parsedObj, null, 2)}
|
|
3908
|
+
`;
|
|
3909
|
+
}
|
|
3910
|
+
if (target === "zed") {
|
|
3911
|
+
const contextServers = parsedObj.context_servers && typeof parsedObj.context_servers === "object" && !Array.isArray(parsedObj.context_servers) ? { ...parsedObj.context_servers } : {};
|
|
3912
|
+
contextServers[serverName] = {
|
|
3913
|
+
command: {
|
|
3914
|
+
path: "npx",
|
|
3915
|
+
args: mcpArgs
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
parsedObj.context_servers = contextServers;
|
|
3919
|
+
return `${JSON.stringify(parsedObj, null, 2)}
|
|
3920
|
+
`;
|
|
3921
|
+
}
|
|
3922
|
+
if (target === "amp") {
|
|
3923
|
+
const ampServers = parsedObj["amp.mcpServers"] && typeof parsedObj["amp.mcpServers"] === "object" && !Array.isArray(parsedObj["amp.mcpServers"]) ? { ...parsedObj["amp.mcpServers"] } : {};
|
|
3924
|
+
ampServers[serverName] = {
|
|
3925
|
+
command: "npx",
|
|
3926
|
+
args: mcpArgs
|
|
3927
|
+
};
|
|
3928
|
+
parsedObj["amp.mcpServers"] = ampServers;
|
|
3929
|
+
return `${JSON.stringify(parsedObj, null, 2)}
|
|
3930
|
+
`;
|
|
3931
|
+
}
|
|
3932
|
+
if (target === "opencode") {
|
|
3933
|
+
if (!("$schema" in parsedObj)) {
|
|
3934
|
+
parsedObj.$schema = "https://opencode.ai/config.json";
|
|
3935
|
+
}
|
|
3936
|
+
const mcpMap = parsedObj.mcp && typeof parsedObj.mcp === "object" && !Array.isArray(parsedObj.mcp) ? { ...parsedObj.mcp } : {};
|
|
3937
|
+
mcpMap[serverName] = {
|
|
3938
|
+
type: "local",
|
|
3939
|
+
command: ["npx", ...mcpArgs],
|
|
3940
|
+
enabled: true
|
|
3941
|
+
};
|
|
3942
|
+
parsedObj.mcp = mcpMap;
|
|
3943
|
+
return `${JSON.stringify(parsedObj, null, 2)}
|
|
3944
|
+
`;
|
|
3945
|
+
}
|
|
3946
|
+
const mcpServers = parsedObj.mcpServers && typeof parsedObj.mcpServers === "object" && !Array.isArray(parsedObj.mcpServers) ? { ...parsedObj.mcpServers } : {};
|
|
3947
|
+
mcpServers[serverName] = {
|
|
3948
|
+
command: "npx",
|
|
3949
|
+
args: mcpArgs
|
|
3950
|
+
};
|
|
3951
|
+
parsedObj.mcpServers = mcpServers;
|
|
3952
|
+
return `${JSON.stringify(parsedObj, null, 2)}
|
|
3953
|
+
`;
|
|
3954
|
+
}
|
|
3955
|
+
function setupMcpForHarnesses(repoPath, options = {}) {
|
|
3956
|
+
const resolvedRepo = path8.resolve(repoPath);
|
|
3957
|
+
const targets = options.targets ?? CORE_RUNTIME_TARGETS;
|
|
3958
|
+
const dryRun = options.dryRun ?? false;
|
|
3959
|
+
const serverName = options.serverName ?? "skillops";
|
|
3960
|
+
const actions = [];
|
|
3961
|
+
for (const target of targets) {
|
|
3962
|
+
const spec = RUNTIME_HARNESS_SPECS[target];
|
|
3963
|
+
if (!spec.supportsMcp || !spec.mcpConfigPath) {
|
|
3964
|
+
actions.push({
|
|
3965
|
+
target,
|
|
3966
|
+
display_name: spec.displayName,
|
|
3967
|
+
relative_path: "n/a",
|
|
3968
|
+
status: "unsupported"
|
|
3969
|
+
});
|
|
3970
|
+
continue;
|
|
3971
|
+
}
|
|
3972
|
+
const absPath = path8.join(resolvedRepo, spec.mcpConfigPath);
|
|
3973
|
+
const exists = fs8.existsSync(absPath) && fs8.statSync(absPath).isFile();
|
|
3974
|
+
const existingRaw = exists ? fs8.readFileSync(absPath, "utf-8") : null;
|
|
3975
|
+
const updatedRaw = buildUpdatedMcpConfigContent(target, existingRaw, serverName);
|
|
3976
|
+
let status;
|
|
3977
|
+
if (!exists) {
|
|
3978
|
+
status = "created";
|
|
3979
|
+
} else if (existingRaw === updatedRaw) {
|
|
3980
|
+
status = "unchanged";
|
|
3981
|
+
} else {
|
|
3982
|
+
status = "updated";
|
|
3983
|
+
}
|
|
3984
|
+
if (!dryRun && (status === "created" || status === "updated")) {
|
|
3985
|
+
fs8.mkdirSync(path8.dirname(absPath), { recursive: true });
|
|
3986
|
+
fs8.writeFileSync(absPath, updatedRaw, "utf-8");
|
|
3987
|
+
}
|
|
3988
|
+
actions.push({
|
|
3989
|
+
target,
|
|
3990
|
+
display_name: spec.displayName,
|
|
3991
|
+
relative_path: spec.mcpConfigPath,
|
|
3992
|
+
status
|
|
3993
|
+
});
|
|
3994
|
+
}
|
|
3995
|
+
const configuredCount = actions.filter((a) => a.status !== "unsupported").length;
|
|
3996
|
+
return {
|
|
3997
|
+
repo_path: path8.basename(resolvedRepo) || ".",
|
|
3998
|
+
dry_run: dryRun,
|
|
3999
|
+
targets_requested: [...targets],
|
|
4000
|
+
configured_count: configuredCount,
|
|
4001
|
+
actions
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
function formatMcpSetupReport(report, format = "table") {
|
|
4005
|
+
if (format === "json") {
|
|
4006
|
+
return `${JSON.stringify(report, null, 2)}
|
|
4007
|
+
`;
|
|
4008
|
+
}
|
|
4009
|
+
if (format === "markdown") {
|
|
4010
|
+
const lines2 = [
|
|
4011
|
+
`## SkillOps Universal MCP Server Registration (\`${report.repo_path}\`)`,
|
|
4012
|
+
"",
|
|
4013
|
+
`- **Mode:** \`${report.dry_run ? "DRY-RUN" : "WRITE"}\``,
|
|
4014
|
+
`- **Configured MCP Harnesses:** \`${report.configured_count}/${report.targets_requested.length}\``,
|
|
4015
|
+
"",
|
|
4016
|
+
"| Target | AI Coding Agent / IDE | MCP Config File | Status |",
|
|
4017
|
+
"| :--- | :--- | :--- | :---: |",
|
|
4018
|
+
...report.actions.map(
|
|
4019
|
+
(a) => `| \`${a.target}\` | **${a.display_name}** | \`${a.relative_path}\` | \`${a.status.toUpperCase()}\` |`
|
|
4020
|
+
),
|
|
4021
|
+
""
|
|
4022
|
+
];
|
|
4023
|
+
return lines2.join("\n");
|
|
4024
|
+
}
|
|
4025
|
+
const lines = [
|
|
4026
|
+
`=== SkillOps Universal MCP Server Setup [${report.dry_run ? "DRY-RUN" : "WRITE"}] ===`,
|
|
4027
|
+
`Targets Requested : ${report.targets_requested.join(", ")}`,
|
|
4028
|
+
`MCP Harnesses : ${report.configured_count} configured`,
|
|
4029
|
+
...report.actions.map(
|
|
4030
|
+
(a) => ` [${a.status.toUpperCase().padEnd(11)}] ${a.target.padEnd(11)} -> ${a.relative_path} (${a.display_name})`
|
|
4031
|
+
),
|
|
4032
|
+
""
|
|
4033
|
+
];
|
|
4034
|
+
return lines.join("\n");
|
|
4035
|
+
}
|
|
4036
|
+
export {
|
|
4037
|
+
ALL_RUNTIME_TARGETS,
|
|
4038
|
+
BenchmarkArmSchema,
|
|
4039
|
+
BenchmarkDeltaSchema,
|
|
4040
|
+
CORE_RUNTIME_TARGETS,
|
|
4041
|
+
CatalogEvalSummarySchema,
|
|
4042
|
+
CatalogTrackSchema,
|
|
4043
|
+
CollisionPairSchema,
|
|
4044
|
+
CollisionReportSchema,
|
|
4045
|
+
CollisionSeveritySchema,
|
|
4046
|
+
EvalAssertionSchema,
|
|
4047
|
+
EvalCaseSchema,
|
|
4048
|
+
ExitCode,
|
|
4049
|
+
RUNTIME_HARNESS_SPECS,
|
|
4050
|
+
RepoFingerprinter,
|
|
4051
|
+
RepoProfileSchema,
|
|
4052
|
+
RoutedBundleSchema,
|
|
4053
|
+
RoutingSignalsSchema,
|
|
4054
|
+
RuntimeTargetSchema,
|
|
4055
|
+
SKILLOPS_VERSION,
|
|
4056
|
+
ScoredSkillSchema,
|
|
4057
|
+
ScriptEntrypointSchema,
|
|
4058
|
+
SkillBenchmarkSchema,
|
|
4059
|
+
SkillEntrypointsSchema,
|
|
4060
|
+
SkillEvalSuiteSchema,
|
|
4061
|
+
SkillManifestSchema,
|
|
4062
|
+
SkillOpsConfigSchema,
|
|
4063
|
+
SkillOpsDetectedStackConfigSchema,
|
|
4064
|
+
SkillOpsTokenEconomyConfigSchema,
|
|
4065
|
+
SkippedSkillSchema,
|
|
4066
|
+
SubagentManifestSchema,
|
|
4067
|
+
SyncLockEntrySchema,
|
|
4068
|
+
SyncLockfileSchema,
|
|
4069
|
+
SyncReportSchema,
|
|
4070
|
+
SyncedArtifactSchema,
|
|
4071
|
+
TokenEconomyReportSchema,
|
|
4072
|
+
TokenFileCategorySchema,
|
|
4073
|
+
TokenFileEntrySchema,
|
|
4074
|
+
TokenWasteHotspotSchema,
|
|
4075
|
+
addSkillOrSubagent,
|
|
4076
|
+
analyzeRepoTokenEconomy,
|
|
4077
|
+
auditCatalog,
|
|
4078
|
+
auditCatalogTokenBudgets,
|
|
4079
|
+
auditSkillTokenBudget,
|
|
4080
|
+
auditSubagentTokenBudget,
|
|
4081
|
+
classifyCollisionSeverity,
|
|
4082
|
+
cleanSyncedArtifacts,
|
|
4083
|
+
compileSkillBundle,
|
|
4084
|
+
computeCollisionMatrix,
|
|
4085
|
+
computeFeatureCosine,
|
|
4086
|
+
computeIdfWeights,
|
|
4087
|
+
computeSha256,
|
|
4088
|
+
countWords,
|
|
4089
|
+
createCollisionPair,
|
|
4090
|
+
createCollisionReport,
|
|
4091
|
+
createSkillManifest,
|
|
4092
|
+
createSubagentManifest,
|
|
4093
|
+
detectInstalledHarnesses,
|
|
4094
|
+
diagnoseRepository,
|
|
4095
|
+
discoverSkillDirectories,
|
|
4096
|
+
discoverSubagentFiles,
|
|
4097
|
+
estimateTokenCount,
|
|
4098
|
+
evaluateCatalog,
|
|
4099
|
+
evaluateSkillDirectory,
|
|
4100
|
+
extractSkillFeatures,
|
|
4101
|
+
fingerprintRepository,
|
|
4102
|
+
formatDoctorReport,
|
|
4103
|
+
formatEvalSummary,
|
|
4104
|
+
formatHarnessInspectionReport,
|
|
4105
|
+
formatMcpSetupReport,
|
|
4106
|
+
formatRoutedBundle,
|
|
4107
|
+
formatSyncReport,
|
|
4108
|
+
formatTokenEconomyReport,
|
|
4109
|
+
handleMcpJsonRpcRequest,
|
|
4110
|
+
inspectHarnesses,
|
|
4111
|
+
loadSkillOpsConfig,
|
|
4112
|
+
normalizePackageName,
|
|
4113
|
+
parseSkillDirectory,
|
|
4114
|
+
parseSkillMdFile,
|
|
4115
|
+
parseSubagentFile,
|
|
4116
|
+
parseSubagentMdFile,
|
|
4117
|
+
removeSkillOrSubagent,
|
|
4118
|
+
renderCodexAgentsIndex,
|
|
4119
|
+
resolveRuntimeTargets,
|
|
4120
|
+
routeRepository,
|
|
4121
|
+
scoreSkillAgainstProfile,
|
|
4122
|
+
setupMcpForHarnesses,
|
|
4123
|
+
splitFrontmatter,
|
|
4124
|
+
startMcpStdioServer,
|
|
4125
|
+
stripCodexAgentsBlock,
|
|
4126
|
+
stripNegativeTriggerClauses,
|
|
4127
|
+
syncRepository,
|
|
4128
|
+
tokenizeRoutingText
|
|
4129
|
+
};
|