agent-skills-standard 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  **Sync 242 AI coding standards to any project in one command.** Works with Cursor, Claude Code, GitHub Copilot, Gemini, Windsurf, Trae, Kiro, and Roo.
7
7
 
8
- **Current release:** `v2.4.7` — workflow-end telemetry guidance, MCP session-cost helpers, and clearer runtime integration for SDLC handoff reporting.
8
+ **Current release:** `v2.5.1` — automated SkillSpector security scanning, pull request evaluation gates, and verified release tags.
9
9
 
10
10
  ```bash
11
11
  npx agent-skills-standard@latest init # detect your stack
@@ -126,6 +126,7 @@ The CLI **never** modifies user-home files (`~/.cursor/mcp.json`, `~/.gemini/set
126
126
 
127
127
  | Version | Date | Skills | Avg Tokens | Savings (%) | Report |
128
128
  | --- | --- | --- | --- | --- | --- |
129
+ | v2.6.0 | 2026-07-10 | 264 | 528 | 46% | [Report](benchmarks/archive/v2.6.0.md) |
129
130
  | v2.4.7 | 2026-06-15 | 251 | 551 | 85% | [Report](benchmarks/archive/v2.4.7.md) |
130
131
  | v2.4.6 | 2026-06-10 | 251 | 548 | 85% | [Report](benchmarks/archive/v2.4.6.md) |
131
132
  | v2.4.1 | 2026-05-18 | 247 | 540 | 85% | [Report](benchmarks/archive/v2.4.1.md) |
@@ -135,4 +136,3 @@ The CLI **never** modifies user-home files (`~/.cursor/mcp.json`, `~/.gemini/set
135
136
  | v2.2.0 | 2026-04-22 | 242 | 538 | 85% | [Report](benchmarks/archive/v2.2.0.md) |
136
137
  | v2.1.2 | 2026-04-11 | 237 | 516 | 86% | [Report](benchmarks/archive/v2.1.2.md) |
137
138
  | v2.1.1 | 2026-04-11 | 237 | 516 | 86% | [Report](benchmarks/archive/v2.1.1.md) |
138
- | v2.1.0 | 2026-04-04 | 237 | 526 | 86% | [Report](benchmarks/archive/v2.1.0.md) |
package/dist/index.js CHANGED
@@ -26,14 +26,14 @@ let dotenv = require("dotenv");
26
26
  dotenv = __toESM(dotenv);
27
27
  let picocolors = require("picocolors");
28
28
  picocolors = __toESM(picocolors);
29
- let inquirer = require("inquirer");
30
- inquirer = __toESM(inquirer);
31
29
  let fs_extra = require("fs-extra");
32
30
  fs_extra = __toESM(fs_extra);
33
- let js_yaml = require("js-yaml");
34
- js_yaml = __toESM(js_yaml);
35
31
  let path = require("path");
36
32
  path = __toESM(path);
33
+ let inquirer = require("inquirer");
34
+ inquirer = __toESM(inquirer);
35
+ let js_yaml = require("js-yaml");
36
+ js_yaml = __toESM(js_yaml);
37
37
  let zod = require("zod");
38
38
  let cross_fetch = require("cross-fetch");
39
39
  cross_fetch = __toESM(cross_fetch);
@@ -43,7 +43,7 @@ let child_process = require("child_process");
43
43
  let fs = require("fs");
44
44
  var package_default = {
45
45
  name: "agent-skills-standard",
46
- version: "2.5.0",
46
+ version: "2.6.0",
47
47
  description: "A CLI to manage and sync AI agent skills standard for Cursor, Claude, Copilot, Windsurf, and more.",
48
48
  main: "dist/index.js",
49
49
  bin: {
@@ -94,6 +94,229 @@ var package_default = {
94
94
  }
95
95
  };
96
96
  //#endregion
97
+ //#region src/services/EvalsVerifier.ts
98
+ function checkAssertion(assertion, transcript) {
99
+ const haystack = transcript.toLowerCase();
100
+ if (assertion.type === "contains_any") return (Array.isArray(assertion.value) ? assertion.value : [assertion.value]).some((value) => haystack.includes(value.toLowerCase()));
101
+ if (assertion.type === "not_contains") return !haystack.includes(String(assertion.value).toLowerCase());
102
+ if (assertion.type === "regex") try {
103
+ return new RegExp(String(assertion.value), "i").test(transcript);
104
+ } catch {
105
+ return false;
106
+ }
107
+ if (assertion.type === "file_reference") {
108
+ const value = String(assertion.value).toLowerCase();
109
+ return haystack.includes(value) || haystack.includes(path.basename(value));
110
+ }
111
+ return haystack.includes(String(assertion.value).toLowerCase());
112
+ }
113
+ function answerPath(runDir, manifest, skill, caseId, arm) {
114
+ const categoryParts = manifest.category === "all" ? [skill.category] : [];
115
+ const filename = arm ? `${caseId}.${arm}.md` : `${caseId}.md`;
116
+ return path.join(runDir, "answers", ...categoryParts, skill.skillName, filename);
117
+ }
118
+ function readAnswer(runDir, manifest, skill, currentCase, arm) {
119
+ const filePath = answerPath(runDir, manifest, skill, currentCase.id, arm);
120
+ return fs_extra.default.existsSync(filePath) ? fs_extra.default.readFileSync(filePath, "utf8") : null;
121
+ }
122
+ function readInputs(runDir) {
123
+ const inputsPath = path.join(runDir, "inputs.json");
124
+ return fs_extra.default.existsSync(inputsPath) ? fs_extra.default.readJSONSync(inputsPath) : null;
125
+ }
126
+ function evalDataFor(projectRoot, runDir, skill) {
127
+ const inputs = readInputs(runDir);
128
+ if (inputs) {
129
+ const source = inputs.sources[`${skill.category}/${skill.skillName}`];
130
+ if (!source) throw new Error(`inputs.json is missing ${skill.category}/${skill.skillName}`);
131
+ return source.evals;
132
+ }
133
+ const evalsPath = path.join(projectRoot, "skills", skill.category, skill.skillName, "evals", "evals.json");
134
+ return fs_extra.default.existsSync(evalsPath) ? fs_extra.default.readJSONSync(evalsPath) : {};
135
+ }
136
+ function passRate(results) {
137
+ return results.length > 0 ? results.filter(Boolean).length / results.length : 0;
138
+ }
139
+ function assertionRate(results) {
140
+ const total = results.reduce((sum, result) => sum + result.total, 0);
141
+ return total > 0 ? results.reduce((sum, result) => sum + result.passed, 0) / total : 0;
142
+ }
143
+ function triggerDecision(transcript) {
144
+ return transcript.match(/^\s*TRIGGER:\s*(yes|no)\s*$/im)?.[1].toLowerCase();
145
+ }
146
+ function summarizeSkill(projectRoot, runDir, manifest, skill) {
147
+ const evalsData = evalDataFor(projectRoot, runDir, skill);
148
+ const evalById = new Map((evalsData.evals ?? []).map((evaluation) => [String(evaluation.id), evaluation]));
149
+ const pressureByIndex = evalsData.pressure_scenarios ?? [];
150
+ const baseline = [];
151
+ const withSkill = [];
152
+ const baselineAssertions = [];
153
+ const withSkillAssertions = [];
154
+ const positive = [];
155
+ const negative = [];
156
+ for (const currentCase of skill.cases) {
157
+ if (currentCase.kind === "trigger") {
158
+ const transcript = readAnswer(runDir, manifest, skill, currentCase);
159
+ if (transcript === null) throw new Error(`missing answer: ${skill.skillName}/${currentCase.id}`);
160
+ const actual = triggerDecision(transcript);
161
+ if ((currentCase.expectedTrigger ?? "no") === "yes") positive.push(actual === "yes");
162
+ else negative.push(actual === "no");
163
+ continue;
164
+ }
165
+ const assertions = currentCase.kind === "eval" ? evalById.get(currentCase.id.replace("eval-", ""))?.assertions ?? [] : (pressureByIndex[Number(currentCase.id.replace("pressure-", "")) - 1]?.behavior_assertions ?? []).map((value) => ({
166
+ type: "contains",
167
+ value
168
+ }));
169
+ for (const arm of ["baseline", "with-skill"]) {
170
+ if (!(arm in currentCase.arms)) continue;
171
+ const transcript = readAnswer(runDir, manifest, skill, currentCase, arm);
172
+ if (transcript === null) throw new Error(`missing answer: ${skill.skillName}/${currentCase.id}.${arm}`);
173
+ const checks = assertions.map((assertion) => checkAssertion(assertion, transcript));
174
+ const passed = checks.every(Boolean);
175
+ (arm === "baseline" ? baseline : withSkill).push(passed);
176
+ (arm === "baseline" ? baselineAssertions : withSkillAssertions).push({
177
+ passed: checks.filter(Boolean).length,
178
+ total: checks.length
179
+ });
180
+ }
181
+ }
182
+ const baselineCompromised = manifest.compromisedSkills?.some((record) => record.category === skill.category && record.skillName === skill.skillName && record.arm === "baseline") ?? false;
183
+ const baselinePassRate = passRate(baseline);
184
+ const withSkillPassRate = passRate(withSkill);
185
+ const triggerRecall = positive.length > 0 ? positive.filter(Boolean).length / positive.length : null;
186
+ const triggerSpecificity = negative.length > 0 ? negative.filter(Boolean).length / negative.length : null;
187
+ const balancedTriggerAccuracy = triggerRecall === null || triggerSpecificity === null ? null : (triggerRecall + triggerSpecificity) / 2;
188
+ return {
189
+ baselinePassRate: baselineCompromised ? "n/a" : baselinePassRate,
190
+ withSkillPassRate,
191
+ delta: baselineCompromised ? "n/a" : withSkillPassRate - baselinePassRate,
192
+ casePassRate: {
193
+ baseline: baselineCompromised ? "n/a" : baselinePassRate,
194
+ withSkill: withSkillPassRate
195
+ },
196
+ assertionPassRate: {
197
+ baseline: baselineCompromised ? "n/a" : assertionRate(baselineAssertions),
198
+ withSkill: assertionRate(withSkillAssertions)
199
+ },
200
+ triggerRecall,
201
+ triggerSpecificity,
202
+ balancedTriggerAccuracy
203
+ };
204
+ }
205
+ function compareMetric(diffs, name, committed, recomputed) {
206
+ if (JSON.stringify(committed) !== JSON.stringify(recomputed)) diffs.push(`${name} committed=${JSON.stringify(committed)} recomputed=${JSON.stringify(recomputed)}`);
207
+ }
208
+ function verifyEvalRun(projectRoot, runId) {
209
+ const runDir = path.join(projectRoot, "benchmarks", "evals", "runs", runId);
210
+ const resultsPath = path.join(runDir, "results.json");
211
+ const manifestPath = path.join(runDir, "manifest.json");
212
+ if (!fs_extra.default.existsSync(runDir)) return {
213
+ runId,
214
+ ok: false,
215
+ reason: `run directory not found: ${runDir}`
216
+ };
217
+ if (!fs_extra.default.existsSync(resultsPath) || !fs_extra.default.existsSync(manifestPath)) return {
218
+ runId,
219
+ ok: false,
220
+ reason: "run is missing manifest.json or results.json"
221
+ };
222
+ const manifest = fs_extra.default.readJSONSync(manifestPath);
223
+ if (manifest.schemaVersion === 2 && readInputs(runDir) === null) return {
224
+ runId,
225
+ ok: false,
226
+ reason: "v2 run is missing immutable inputs.json"
227
+ };
228
+ const committed = fs_extra.default.readJSONSync(resultsPath);
229
+ const diffs = [];
230
+ for (const skill of manifest.skills) {
231
+ let recomputed;
232
+ try {
233
+ recomputed = summarizeSkill(projectRoot, runDir, manifest, skill);
234
+ } catch (error) {
235
+ diffs.push(`${skill.skillName}: ${error instanceof Error ? error.message : String(error)}`);
236
+ continue;
237
+ }
238
+ const committedSkill = committed.skills.find((entry) => entry.skillName === skill.skillName && (!entry.category || entry.category === skill.category));
239
+ if (!committedSkill) {
240
+ diffs.push(`${skill.skillName}: missing from committed results.json`);
241
+ continue;
242
+ }
243
+ if (manifest.schemaVersion === 2 || committed.schemaVersion === 2) {
244
+ compareMetric(diffs, `${skill.skillName}.baselinePassRate`, committedSkill.baselinePassRate, recomputed.baselinePassRate);
245
+ compareMetric(diffs, `${skill.skillName}.withSkillPassRate`, committedSkill.withSkillPassRate, recomputed.withSkillPassRate);
246
+ compareMetric(diffs, `${skill.skillName}.delta`, committedSkill.delta, recomputed.delta);
247
+ compareMetric(diffs, `${skill.skillName}.casePassRate`, committedSkill.casePassRate, recomputed.casePassRate);
248
+ compareMetric(diffs, `${skill.skillName}.assertionPassRate`, committedSkill.assertionPassRate, recomputed.assertionPassRate);
249
+ compareMetric(diffs, `${skill.skillName}.triggerRecall`, committedSkill.triggerRecall, recomputed.triggerRecall);
250
+ compareMetric(diffs, `${skill.skillName}.triggerSpecificity`, committedSkill.triggerSpecificity, recomputed.triggerSpecificity);
251
+ compareMetric(diffs, `${skill.skillName}.balancedTriggerAccuracy`, committedSkill.balancedTriggerAccuracy, recomputed.balancedTriggerAccuracy);
252
+ } else {
253
+ compareMetric(diffs, `${skill.skillName}.baselinePassRate`, committedSkill.baselinePassRate, recomputed.baselinePassRate);
254
+ compareMetric(diffs, `${skill.skillName}.withSkillPassRate`, committedSkill.withSkillPassRate, recomputed.withSkillPassRate);
255
+ }
256
+ }
257
+ return diffs.length > 0 ? {
258
+ runId,
259
+ ok: false,
260
+ reason: "recomputed scores differ from committed results.json",
261
+ diffs
262
+ } : {
263
+ runId,
264
+ ok: true
265
+ };
266
+ }
267
+ function listEvalRuns(projectRoot) {
268
+ const runsDir = path.join(projectRoot, "benchmarks", "evals", "runs");
269
+ if (!fs_extra.default.existsSync(runsDir)) return [];
270
+ return fs_extra.default.readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
271
+ }
272
+ function readEvalsReport(projectRoot) {
273
+ const reportPath = path.join(projectRoot, "evals-report.md");
274
+ return fs_extra.default.existsSync(reportPath) ? fs_extra.default.readFileSync(reportPath, "utf8") : null;
275
+ }
276
+ //#endregion
277
+ //#region src/commands/evals.ts
278
+ /**
279
+ * Command for verifying and displaying live skill-eval runs
280
+ * (see docs/EVALS.md and the `evals-run` workflow for how runs are produced).
281
+ */
282
+ var EvalsCommand = class {
283
+ async run(action, options = {}, projectRoot = process.cwd()) {
284
+ switch (action) {
285
+ case "verify": return this.verify(options.run, projectRoot);
286
+ case "report": return this.report(projectRoot);
287
+ default:
288
+ console.error(picocolors.default.red(`Unknown evals action "${action}". Use: verify | report`));
289
+ process.exit(1);
290
+ }
291
+ }
292
+ async verify(runId, projectRoot) {
293
+ const runIds = runId ? [runId] : listEvalRuns(projectRoot);
294
+ if (runIds.length === 0) {
295
+ console.log(picocolors.default.yellow("No eval runs found under benchmarks/evals/runs/. Run the `evals-run` workflow first (see docs/EVALS.md)."));
296
+ return;
297
+ }
298
+ let anyFailed = false;
299
+ for (const id of runIds) {
300
+ const outcome = verifyEvalRun(projectRoot, id);
301
+ if (outcome.ok) console.log(picocolors.default.green(`✅ ${id}: verified`));
302
+ else {
303
+ anyFailed = true;
304
+ console.log(picocolors.default.red(`❌ ${id}: ${outcome.reason}`));
305
+ for (const d of outcome.diffs || []) console.log(picocolors.default.red(` - ${d}`));
306
+ }
307
+ }
308
+ if (anyFailed) process.exit(1);
309
+ }
310
+ async report(projectRoot) {
311
+ const report = readEvalsReport(projectRoot);
312
+ if (!report) {
313
+ console.log(picocolors.default.yellow("No evals-report.md found at the project root yet. Run the `evals-run` workflow (see docs/EVALS.md), then `pnpm evals:report`."));
314
+ return;
315
+ }
316
+ console.log(report);
317
+ }
318
+ };
319
+ //#endregion
97
320
  //#region src/services/FeedbackService.ts
98
321
  /**
99
322
  * Service to handle automated feedback reporting via Proxy Backend.
@@ -342,6 +565,7 @@ var Framework = /* @__PURE__ */ function(Framework) {
342
565
  Framework["Android"] = "android";
343
566
  Framework["iOS"] = "ios";
344
567
  Framework["Laravel"] = "laravel";
568
+ Framework["Python"] = "python";
345
569
  return Framework;
346
570
  }({});
347
571
  //#endregion
@@ -672,7 +896,8 @@ var BACKEND_FRAMEWORKS = [
672
896
  Framework.NestJS,
673
897
  Framework.Golang,
674
898
  Framework.SpringBoot,
675
- Framework.Laravel
899
+ Framework.Laravel,
900
+ Framework.Python
676
901
  ];
677
902
  var FRONTEND_FRAMEWORKS = [
678
903
  Framework.React,
@@ -744,8 +969,18 @@ var DEFAULT_WORKFLOWS = [
744
969
  "implement-feature",
745
970
  "verify-work",
746
971
  "verify-bug",
747
- "security-test"
972
+ "security-test",
973
+ "uat-signoff",
974
+ "incident-hotfix"
748
975
  ];
976
+ /**
977
+ * Workflows that must never be synced to a consumer project, regardless of
978
+ * `.skillsrc` (even `workflows: true`). These depend on agent-skills-standard's
979
+ * own repo-root tooling (e.g. `scripts/evals/*` and its `pnpm evals:*` scripts,
980
+ * `benchmarks/evals/`) which is never distributed to consumers — the workflow
981
+ * would be broken/non-functional outside this monorepo.
982
+ */
983
+ var INTERNAL_ONLY_WORKFLOWS = ["evals-run"];
749
984
  var getAgentDefinition = (id) => {
750
985
  switch (id) {
751
986
  case Agent.Cursor: return {
@@ -872,8 +1107,8 @@ var getAgentDefinition = (id) => {
872
1107
  ruleExtension: ".md",
873
1108
  frontmatterStyle: "cursor",
874
1109
  detectionFiles: [".windsurf"],
875
- workflowFormat: "none",
876
- workflowPath: "",
1110
+ workflowFormat: "native",
1111
+ workflowPath: ".windsurf/workflows",
877
1112
  hookScriptPath: ".windsurf/hooks/preedit-skill-loader.js",
878
1113
  hookConfigPath: ".windsurf/hooks.json"
879
1114
  };
@@ -1007,6 +1242,20 @@ var getFrameworkDefinition = (id) => {
1007
1242
  javascript: ["resources/js", "vite.config.js"]
1008
1243
  }
1009
1244
  };
1245
+ case Framework.Python: return {
1246
+ id,
1247
+ name: "Python",
1248
+ languages: ["python"],
1249
+ detectionFiles: [
1250
+ "pyproject.toml",
1251
+ "requirements.txt",
1252
+ "setup.py",
1253
+ "setup.cfg",
1254
+ "Pipfile",
1255
+ "poetry.lock",
1256
+ "uv.lock"
1257
+ ]
1258
+ };
1010
1259
  }
1011
1260
  };
1012
1261
  var SUPPORTED_AGENTS = Object.values(Agent).map(getAgentDefinition);
@@ -3819,7 +4068,7 @@ var SkillSyncService = class {
3819
4068
  };
3820
4069
  //#endregion
3821
4070
  //#region src/services/utils/WorkflowTransformer.ts
3822
- var WORKFLOW_ARGUMENTS = "Optional args: slug=<feature>, ticket=<id/url>, mode=interactive|autonomous|channel, channel=<id>, auto_continue=true|false.";
4071
+ var WORKFLOW_ARGUMENTS = "Optional args: slug=<feature>, ticket=<id/url>, mode=interactive|autonomous|channel, channel=<id>, auto_continue=true|false, profile=business|hybrid|technical.";
3823
4072
  /**
3824
4073
  * Transforms workflow markdown into each agent's native user-invoked command format.
3825
4074
  *
@@ -3846,11 +4095,11 @@ var WorkflowTransformer = class {
3846
4095
  rawContent: source.content
3847
4096
  };
3848
4097
  }
3849
- static transform(source, format, workflowSourcePath = ".agents/workflows") {
4098
+ static transform(source, format) {
3850
4099
  const parsed = this.parse(source);
3851
- return this.transformParsed(parsed, format, workflowSourcePath);
4100
+ return this.transformParsed(parsed, format);
3852
4101
  }
3853
- static transformParsed(parsed, format, workflowSourcePath = ".agents/workflows") {
4102
+ static transformParsed(parsed, format) {
3854
4103
  if (format === "none") return null;
3855
4104
  const { description, body } = parsed;
3856
4105
  const baseName = parsed.key;
@@ -3865,7 +4114,7 @@ var WorkflowTransformer = class {
3865
4114
  };
3866
4115
  case "toml": return {
3867
4116
  name: `${baseName}.toml`,
3868
- content: this.toGeminiCommand(baseName, description, workflowSourcePath)
4117
+ content: this.toGeminiCommand(description, body)
3869
4118
  };
3870
4119
  case "prompt": return {
3871
4120
  name: `${baseName}.prompt.md`,
@@ -3913,17 +4162,18 @@ ${body}`;
3913
4162
  /**
3914
4163
  * Gemini CLI TOML command format.
3915
4164
  * Lives at .gemini/commands/<name>.toml
3916
- * References the workflow source file via prompt.
4165
+ * Inlines the workflow body so the command is self-contained even when
4166
+ * .agents/workflows/ is not also synced to this runtime.
3917
4167
  * User invokes via /<name> [arguments]
3918
4168
  */
3919
- static toGeminiCommand(name, description, workflowSourcePath) {
4169
+ static toGeminiCommand(description, body) {
3920
4170
  return `description = "${description.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"
3921
4171
  prompt = """
3922
- Please execute the workflow defined in \`${workflowSourcePath}/${name}.md\` for: {{args}}
4172
+ Execute this workflow for: {{args}}
3923
4173
 
3924
4174
  ${WORKFLOW_ARGUMENTS}
3925
4175
 
3926
- Follow the exact steps in the workflow file.
4176
+ ${body.replace(/"""/g, "\\\"\\\"\\\"")}
3927
4177
  """
3928
4178
  `;
3929
4179
  }
@@ -3989,7 +4239,7 @@ var WorkflowSyncService = class {
3989
4239
  const ref = (await this.githubService.getRepoInfo(owner, repo))?.default_branch || "main";
3990
4240
  const treeData = await this.githubService.getRepoTree(owner, repo, ref);
3991
4241
  if (!treeData) return false;
3992
- const availableWorkflows = treeData.tree.filter((f) => this.isWorkflowMarkdownPath(f.path)).map((f) => this.workflowNameFromPath(f.path));
4242
+ const availableWorkflows = treeData.tree.filter((f) => this.isWorkflowMarkdownPath(f.path)).map((f) => this.workflowNameFromPath(f.path)).filter((wf) => !INTERNAL_ONLY_WORKFLOWS.includes(wf));
3993
4243
  if (availableWorkflows.length === 0) return false;
3994
4244
  let changed = false;
3995
4245
  if (Array.isArray(config.workflows)) {
@@ -4028,6 +4278,7 @@ var WorkflowSyncService = class {
4028
4278
  }
4029
4279
  const workflowFiles = treeData.tree.filter((f) => {
4030
4280
  if (!this.isWorkflowMarkdownPath(f.path)) return false;
4281
+ if (INTERNAL_ONLY_WORKFLOWS.includes(this.workflowNameFromPath(f.path))) return false;
4031
4282
  if (typeof config.workflows === "boolean") return config.workflows;
4032
4283
  if (Array.isArray(config.workflows)) return config.workflows.includes(this.workflowNameFromPath(f.path));
4033
4284
  return false;
@@ -4070,8 +4321,6 @@ var WorkflowSyncService = class {
4070
4321
  if (!agentDef || agentDef.workflowFormat === "none") continue;
4071
4322
  const workflowDir = path.default.join(process.cwd(), agentDef.workflowPath);
4072
4323
  await fs_extra.default.ensureDir(workflowDir);
4073
- const sourceWorkflowDir = path.default.join(process.cwd(), ".agents/workflows");
4074
- const workflowSourceRelative = path.default.relative(workflowDir, sourceWorkflowDir);
4075
4324
  let written = 0;
4076
4325
  for (const wf of workflows) {
4077
4326
  if (wf.skill !== "workflows") continue;
@@ -4080,7 +4329,7 @@ var WorkflowSyncService = class {
4080
4329
  name: fileItem.name,
4081
4330
  content: fileItem.content
4082
4331
  });
4083
- const transformed = WorkflowTransformer.transformParsed(parsed, agentDef.workflowFormat, workflowSourceRelative);
4332
+ const transformed = WorkflowTransformer.transformParsed(parsed, agentDef.workflowFormat);
4084
4333
  if (!transformed) continue;
4085
4334
  let targetFilePath;
4086
4335
  if (agentDef.workflowFormat === "skill") {
@@ -4361,25 +4610,35 @@ var MarkdownUtils = class {
4361
4610
  * @param rootDir Project root directory
4362
4611
  * @param targets Array of target file names
4363
4612
  * @param indexContent The markdown content to inject
4613
+ * @returns Array of target files that were successfully updated/created
4364
4614
  */
4365
4615
  static async injectIndex(rootDir, targets, indexContent) {
4616
+ const updated = [];
4366
4617
  for (const target of targets) {
4367
4618
  const targetPath = path.default.join(rootDir, target);
4368
4619
  let content = "";
4620
+ const markerStart = "<!-- SKILLS_INDEX_START -->";
4621
+ const markerEnd = "<!-- SKILLS_INDEX_END -->";
4369
4622
  if (await fs_extra.default.pathExists(targetPath)) {
4370
4623
  content = await fs_extra.default.readFile(targetPath, "utf8");
4371
- const markerStart = "<!-- SKILLS_INDEX_START -->";
4372
- const markerEnd = "<!-- SKILLS_INDEX_END -->";
4373
4624
  const startIndex = content.indexOf(markerStart);
4374
4625
  const endIndex = content.indexOf(markerEnd);
4375
4626
  if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) content = `${content.substring(0, startIndex + 27)}\n${indexContent}\n${content.substring(endIndex)}`;
4376
- else if (startIndex !== -1 || endIndex !== -1) {
4377
- content = content.replace(markerStart, "").replace(markerEnd, "");
4378
- content = content.trimEnd() + `\n\n${markerStart}\n${indexContent}\n${markerEnd}\n`;
4379
- } else content = content.trimEnd() + `\n\n${markerStart}\n${indexContent}\n${markerEnd}\n`;
4380
- } else content = `<!-- SKILLS_INDEX_START -->\n${indexContent}\n<!-- SKILLS_INDEX_END -->\n`;
4627
+ else continue;
4628
+ } else content = [
4629
+ "# Project Context for AI Agents",
4630
+ "",
4631
+ "This file provides context and instructions for AI agents working in this repository.",
4632
+ "",
4633
+ markerStart,
4634
+ indexContent,
4635
+ markerEnd,
4636
+ ""
4637
+ ].join("\n");
4381
4638
  await fs_extra.default.outputFile(targetPath, content);
4639
+ updated.push(target);
4382
4640
  }
4641
+ return updated;
4383
4642
  }
4384
4643
  };
4385
4644
  //#endregion
@@ -4475,13 +4734,11 @@ var SyncService = class {
4475
4734
  if (Object.keys(categoryIndices).length > 0) console.log(picocolors.default.green(` ✅ Generated _INDEX.md for ${Object.keys(categoryIndices).length} categories.`));
4476
4735
  const mcpEnabled = config.mcp?.enabled === true;
4477
4736
  const routerIndex = await generator.assembleRouterIndex(baseDir, allowedCategories, mcpEnabled);
4478
- await MarkdownUtils.injectIndex(process.cwd(), ["AGENTS.md"], routerIndex);
4479
- console.log(picocolors.default.green(" ✅ AGENTS.md router index updated."));
4737
+ if ((await MarkdownUtils.injectIndex(process.cwd(), ["AGENTS.md"], routerIndex)).length > 0) console.log(picocolors.default.green(" ✅ AGENTS.md router index updated."));
4738
+ else console.log(picocolors.default.yellow(" ⚠️ Skipped AGENTS.md update: index markers <!-- SKILLS_INDEX_START --> … <!-- SKILLS_INDEX_END --> are missing or out of order. Opt-in by adding these markers to your file."));
4480
4739
  const serverDir = path.default.join(process.cwd(), "server");
4481
- if (await fs_extra.default.pathExists(serverDir)) {
4482
- await MarkdownUtils.injectIndex(serverDir, ["AGENTS.md"], routerIndex);
4483
- console.log(picocolors.default.green(" ✅ server/AGENTS.md router index updated."));
4484
- }
4740
+ if (await fs_extra.default.pathExists(serverDir)) if ((await MarkdownUtils.injectIndex(serverDir, ["AGENTS.md"], routerIndex)).length > 0) console.log(picocolors.default.green(" ✅ server/AGENTS.md router index updated."));
4741
+ else console.log(picocolors.default.yellow(" ⚠️ Skipped server/AGENTS.md update: index markers <!-- SKILLS_INDEX_START --> … <!-- SKILLS_INDEX_END --> are missing or out of order in server/AGENTS.md. Opt-in via markers."));
4485
4742
  await new AgentBridgeService().bridge(process.cwd(), agents);
4486
4743
  } catch (error) {
4487
4744
  console.log(picocolors.default.yellow(` ⚠️ Failed to update index: ${error}`));
@@ -5048,7 +5305,7 @@ var FrontmatterRule = class {
5048
5305
  if (descMatch && descMatch[1].length > 1024) {
5049
5306
  result.errors.push(`Description too long (${descMatch[1].length} chars > 1024 limit)`);
5050
5307
  result.passed = false;
5051
- }
5308
+ } else if (descMatch && descMatch[1].length > 300) result.warnings.push(`Description is ${descMatch[1].length} chars (> 300); trim for _INDEX.md readability`);
5052
5309
  }
5053
5310
  return result;
5054
5311
  }
@@ -5088,16 +5345,82 @@ var InstructionsStyleRule = class {
5088
5345
  }
5089
5346
  };
5090
5347
  /**
5091
- * Ensures the mandatory priority section is present.
5348
+ * Canonical priority label per tier, enforced by PriorityRule.
5349
+ */
5350
+ var CANONICAL_PRIORITY_LABELS = {
5351
+ P0: "CRITICAL",
5352
+ P1: "HIGH",
5353
+ P2: "MEDIUM",
5354
+ P3: "LOW"
5355
+ };
5356
+ /**
5357
+ * Ensures the mandatory priority section is present and, once `labelSeverity`
5358
+ * is 'error', that its label matches the canonical vocabulary.
5092
5359
  */
5093
5360
  var PriorityRule = class {
5094
5361
  name = "Priority Section";
5362
+ constructor(labelSeverity = "warning") {
5363
+ this.labelSeverity = labelSeverity;
5364
+ }
5095
5365
  async validate(content) {
5096
- if (!content.includes("## **Priority:")) return {
5366
+ const result = {
5367
+ passed: true,
5368
+ errors: [],
5369
+ warnings: []
5370
+ };
5371
+ const match = content.match(/^## \*\*Priority:\s*(P\d)(?:\s*\(([^)]*)\))?([^\n]*)\*\*/m);
5372
+ if (!match) return {
5097
5373
  passed: false,
5098
5374
  errors: ["Missing priority section"],
5099
5375
  warnings: []
5100
5376
  };
5377
+ const [, tier, label, trailing] = match;
5378
+ const canonicalLabel = CANONICAL_PRIORITY_LABELS[tier];
5379
+ if (!canonicalLabel) {
5380
+ result.errors.push(`Unknown priority tier "${tier}"`);
5381
+ result.passed = false;
5382
+ return result;
5383
+ }
5384
+ if (!(label === canonicalLabel && !trailing?.trim())) {
5385
+ const message = `Priority label must be "${tier} (${canonicalLabel})"`;
5386
+ if (this.labelSeverity === "error") {
5387
+ result.errors.push(message);
5388
+ result.passed = false;
5389
+ } else result.warnings.push(message);
5390
+ }
5391
+ return result;
5392
+ }
5393
+ };
5394
+ /**
5395
+ * Ensures every skill is routable: at least one file glob or keyword trigger.
5396
+ */
5397
+ var TriggersRule = class {
5398
+ name = "Triggers";
5399
+ async validate(content) {
5400
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
5401
+ if (!frontmatterMatch) return {
5402
+ passed: true,
5403
+ errors: [],
5404
+ warnings: []
5405
+ };
5406
+ let parsed;
5407
+ try {
5408
+ parsed = js_yaml.default.load(frontmatterMatch[1]);
5409
+ } catch {
5410
+ return {
5411
+ passed: false,
5412
+ errors: ["Invalid YAML frontmatter"],
5413
+ warnings: []
5414
+ };
5415
+ }
5416
+ const triggers = parsed?.metadata?.triggers;
5417
+ const files = Array.isArray(triggers?.files) ? triggers.files : [];
5418
+ const keywords = Array.isArray(triggers?.keywords) ? triggers.keywords : [];
5419
+ if (files.length === 0 && keywords.length === 0) return {
5420
+ passed: false,
5421
+ errors: ["Skill is unroutable: metadata.triggers must define at least one files glob or keywords entry"],
5422
+ warnings: []
5423
+ };
5101
5424
  return {
5102
5425
  passed: true,
5103
5426
  errors: [],
@@ -5149,7 +5472,8 @@ var SkillValidator = class {
5149
5472
  new SizeRule(),
5150
5473
  new FrontmatterRule(),
5151
5474
  new InstructionsStyleRule(),
5152
- new PriorityRule(),
5475
+ new PriorityRule("error"),
5476
+ new TriggersRule(),
5153
5477
  new DirectoryStructureRule()
5154
5478
  ];
5155
5479
  /**
@@ -5305,6 +5629,9 @@ program.command("mcp <action> [scope]").description("Manage the optional MCP ser
5305
5629
  program.command("hooks <action>").description("Manage the PreToolUse skill-loader hook. Actions: status | install | uninstall").action(async (action) => {
5306
5630
  await new HooksCommand().run(action);
5307
5631
  });
5632
+ program.command("evals <action>").description("Verify or display live skill-eval runs. Actions: verify | report").option("--run <runId>", "For verify: check a single run id instead of all committed runs").action(async (action, opts) => {
5633
+ await new EvalsCommand().run(action, opts);
5634
+ });
5308
5635
  program.parse();
5309
5636
  //#endregion
5310
5637