fix(skills): register cli-skill-collector in the agent-skills catalog (Integration 2/2 base-red) (#7310)

* fix(skills): register cli-skill-collector in the agent-skills catalog (#6294 shipped the dir only)

* chore(skills): regenerate cli-skill-collector SKILL.md via the generator, preserving the #6294 authored workflow in the custom block

* fix(skills): derive coverage totals from the id lists + align remaining count assertions (45 catalog / 21 cli)

* fix(skills): SkillCoverage totals are number, not stale literals
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-15 20:05:56 -03:00
committed by GitHub
parent 83cca4d20f
commit 13e312b311
12 changed files with 345 additions and 59 deletions

View File

@@ -0,0 +1 @@
- **Skills**: register `cli-skill-collector` in the agent-skills catalog (types union, curated entry, CLI id list) — #6294 shipped the `skills/cli-skill-collector/` directory without the catalog registration, so it was unreachable via the API and Integration CI failed on the catalog-integrity test; counts aligned (44 API+CLI, 45 with config)

View File

@@ -1,7 +1,275 @@
---
name: cli-skill-collector
description: "Agent workflow: detect installed CLI coding tools (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.), search GitHub for matching agent skills, and install them to the detected tools. Replaces the standalone Skill Collector Python app."
description: "Detect installed CLI coding tools (Claude Code, Codex, Cursor, Copilot, Cline and more), search GitHub for matching agent skills, and install them to the detected tools via OmniRoute's built-in APIs."
---
<!-- generated by src/lib/agentSkills/generator.ts; manual edits will be overwritten -->
## Overview
Detect installed CLI coding tools (Claude Code, Codex, Cursor, Copilot, Cline and more), search GitHub for matching agent skills, and install them to the detected tools via OmniRoute's built-in APIs.
## Quick install
```bash
npm install -g omniroute # or: npx omniroute
omniroute --version
```
## Subcommands
### `autostart`
**Example:**
```bash
omniroute autostart
```
### `autostart enable`
**Example:**
```bash
omniroute autostart enable
```
### `autostart disable`
**Example:**
```bash
omniroute autostart disable
```
### `autostart toggle`
**Example:**
```bash
omniroute autostart toggle
```
### `autostart status`
**Example:**
```bash
omniroute autostart status
```
### `config`
Show or update CLI tool configuration
**Example:**
```bash
omniroute config
```
### `config list`
List all CLI tools and config status
**Flags:**
- `--json`
**Example:**
```bash
omniroute config list
```
### `config get <tool>`
Show current config for a tool
**Flags:**
- `--json`
**Example:**
```bash
omniroute config get <tool>
```
### `config set <tool>`
Write config for a tool
**Flags:**
- `--model <model>`
- `--non-interactive`
- `--yes`
**Example:**
```bash
omniroute config set <tool>
```
### `config validate <tool>`
Validate config format without writing
**Flags:**
- `--model <model>`
- `--json`
**Example:**
```bash
omniroute config validate <tool>
```
### `config opencode`
Generate OpenCode config (alias for
**Flags:**
- `--model <model>`
- `--non-interactive`
- `--yes`
**Example:**
```bash
omniroute config opencode
```
### `config lang`
**Example:**
```bash
omniroute config lang
```
### `config get`
**Flags:**
- `--json`
**Example:**
```bash
omniroute config get
```
### `config set <code>`
**Flags:**
- `--force`
**Example:**
```bash
omniroute config set <code>
```
### `config list`
**Flags:**
- `--json`
**Example:**
```bash
omniroute config list
```
### `env`
Show and manage environment variables
**Example:**
```bash
omniroute env
```
### `env show`
Show current environment variables
**Flags:**
- `--json`
**Example:**
```bash
omniroute env show
```
### `env get <key>`
Get a single environment variable
**Example:**
```bash
omniroute env get <key>
```
### `env set <key> <value>`
Set an environment variable (current session only)
**Example:**
```bash
omniroute env set <key> <value>
```
### `setup`
**Flags:**
- `--password <value>`
- `--add-provider`
- `--provider <id>`
- `--provider-name <name>`
- `--api-key <value>`
- `--default-model <model>`
- `--provider-base-url <url>`
- `--test-provider`
- `--non-interactive`
- `--list`
**Example:**
```bash
omniroute setup
```
### `update`
**Flags:**
- `--check`
- `--apply`
- `--changelog`
- `--dry-run`
- `--no-backup`
- `--yes`
**Example:**
```bash
omniroute update
```
<!-- skill:custom-start -->
<!-- Preserved curated content from #6294 (skills/cli-skill-collector authored workflow) -->
# /cli-skill-collector — Agent Skill Collector
@@ -150,3 +418,4 @@ fi
- OmniRoute must be running locally on port 20128 (default) — see `docs/frameworks/SKILLS.md` for custom-port setups.
- The `/api/skills/collect/*` and `/api/github-skills` endpoints require **management-scoped authentication** the same way every other `/api/skills/*` route does: a dashboard session, the loopback CLI token, or an API key with the `manage` scope (`requireManagementAuth()`). Auth is only bypassed when the server has no login/API-key requirement configured at all.
- This replaces the standalone Skill Collector Python app — all logic is now inside OmniRoute.
<!-- skill:custom-end -->

View File

@@ -68,6 +68,7 @@ export const CLI_SKILL_IDS: readonly string[] = [
"cli-eval",
"cli-plugins-skills",
"cli-setup",
"cli-skill-collector",
] as const;
// ── Module-scope cache ──────────────────────────────────────────────────────
@@ -148,8 +149,10 @@ export function computeCoverage(): SkillCoverage {
const configHave = catalog.filter((s) => s.category === "config" && presentIds.has(s.id)).length;
return {
api: { have: apiHave, total: 23 },
cli: { have: cliHave, total: 20 },
// Totals derive from the id lists — hardcoded 23/20 went stale the first
// time the catalog grew (cli-skill-collector registration, 2026-07-15).
api: { have: apiHave, total: API_SKILL_IDS.length },
cli: { have: cliHave, total: CLI_SKILL_IDS.length },
config: { have: configHave, total: configTotal },
totalSkills: apiHave + cliHave + configHave,
generatedAt: new Date().toISOString(),

View File

@@ -48,7 +48,8 @@ export type SkillArea =
| "cli-batches"
| "cli-eval"
| "cli-plugins-skills"
| "cli-setup";
| "cli-setup"
| "cli-skill-collector";
export interface AgentSkill {
id: string; // canonical id (e.g. "omni-providers", "cli-serve")
@@ -66,8 +67,10 @@ export interface AgentSkill {
}
export interface SkillCoverage {
api: { have: number; total: 23 };
cli: { have: number; total: 20 };
// Totals are derived from the catalog id lists (literal types went stale the
// first time the catalog grew — cli-skill-collector, 2026-07-15).
api: { have: number; total: number };
cli: { have: number; total: number };
config: { have: number; total: number };
totalSkills: number; // sum
generatedAt: string; // ISO datetime

View File

@@ -428,6 +428,15 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [
area: "cli-setup",
icon: "build",
},
{
id: "cli-skill-collector",
name: "CLI: Agent Skill Collector",
description:
"Detect installed CLI coding tools (Claude Code, Codex, Cursor, Copilot, Cline and more), search GitHub for matching agent skills, and install them to the detected tools via OmniRoute's built-in APIs.",
category: "cli",
area: "cli-setup",
icon: "extension",
},
// ── Config Skills ────────────────────────────────────────────────────────────

View File

@@ -2,9 +2,9 @@
* Integration tests for Agent Skills content integrity.
*
* Verifies:
* 1. All 43 skill IDs from catalog have skills/{id}/ folder with SKILL.md.
* 1. All 44 skill IDs from catalog have skills/{id}/ folder with SKILL.md.
* 2. Zero omniroute-* folders remain (post-prune: old omniroute-* skill dirs were removed).
* 3. 10 specific IDs have <!-- skill:custom-start --> ... <!-- skill:custom-end --> blocks:
* 3. 12 specific IDs have <!-- skill:custom-start --> ... <!-- skill:custom-end --> blocks:
* omni-mcp, omni-compression, cli-providers, cli-eval, omni-agents-a2a,
* omni-combos-routing, omni-auth, omni-resilience, omni-inference, cli-serve.
*
@@ -22,6 +22,7 @@ const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as str
// IDs that must have a custom block
const CUSTOM_BLOCK_IDS = [
"cli-skill-collector",
"omni-mcp",
"omni-compression",
"cli-providers",
@@ -37,7 +38,7 @@ const CUSTOM_BLOCK_IDS = [
// ── §1: All 42 catalog IDs have skills/{id}/SKILL.md ─────────────────────────
test("all 43 catalog IDs have a skills/{id}/ directory", () => {
test("all 44 catalog IDs have a skills/{id}/ directory", () => {
const missing: string[] = [];
for (const id of ALL_IDS) {
const dirPath = path.join(SKILLS_DIR, id);
@@ -48,7 +49,7 @@ test("all 43 catalog IDs have a skills/{id}/ directory", () => {
assert.deepEqual(missing, [], `Missing skill directories: ${missing.join(", ")}`);
});
test("all 43 catalog IDs have a skills/{id}/SKILL.md file", () => {
test("all 44 catalog IDs have a skills/{id}/SKILL.md file", () => {
const missing: string[] = [];
for (const id of ALL_IDS) {
const skillPath = path.join(SKILLS_DIR, id, "SKILL.md");
@@ -113,7 +114,7 @@ for (const id of CUSTOM_BLOCK_IDS) {
// ── Additional integrity checks ───────────────────────────────────────────────
test("exactly 11 skills have custom blocks", () => {
test("exactly 12 skills have custom blocks", () => {
const withCustomBlocks: string[] = [];
for (const id of ALL_IDS) {
const skillPath = path.join(SKILLS_DIR, id, "SKILL.md");
@@ -128,7 +129,7 @@ test("exactly 11 skills have custom blocks", () => {
assert.deepEqual(
withCustomBlocks.sort(),
expectedIds,
`Expected exactly these 11 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`,
`Expected exactly these 12 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`,
);
});

View File

@@ -69,8 +69,8 @@ test("every CLI skill ID has skills/<id>/SKILL.md on disk", () => {
assert.deepEqual(missing, [], `Missing CLI SKILL.md files: ${missing.join(", ")}`);
});
test("total skill count is exactly 43 (23 API + 20 CLI)", () => {
assert.equal(API_SKILL_IDS.length + CLI_SKILL_IDS.length, 43);
test("total skill count is exactly 44 (23 API + 21 CLI)", () => {
assert.equal(API_SKILL_IDS.length + CLI_SKILL_IDS.length, 44);
});
// ── §2: Frontmatter validation ────────────────────────────────────────────────
@@ -120,11 +120,11 @@ test("each SKILL.md body is at least 100 chars", () => {
// ── §3: MCP tool omniroute_agent_skills_list ─────────────────────────────────
test("MCP omniroute_agent_skills_list handler returns count 44 (43 + config)", async () => {
test("MCP omniroute_agent_skills_list handler returns count 45 (44 + config)", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
assert.equal(result.count, 44, `Expected 44 but got ${result.count}`);
assert.equal(result.count, 45, `Expected 45 but got ${result.count}`);
assert.ok(Array.isArray(result.skills));
assert.equal(result.skills.length, 44);
assert.equal(result.skills.length, 45);
});
test("MCP omniroute_agent_skills_list result has all 42 IDs", async () => {
@@ -157,9 +157,9 @@ test("A2A list-capabilities artifact content contains 42 skill IDs as table rows
assert.ok(rows.length >= 42, `Expected at least 42 data rows but got ${rows.length}`);
});
test("A2A list-capabilities metadata.totalSkills === 44 (43 + config)", async () => {
test("A2A list-capabilities metadata.totalSkills === 45 (44 + config)", async () => {
const result = await executeListCapabilities(stubTask);
assert.equal(result.metadata.totalSkills, 44);
assert.equal(result.metadata.totalSkills, 45);
});
test("A2A list-capabilities artifact contains all 42 skill IDs", async () => {

View File

@@ -58,11 +58,11 @@ test("each agentSkillTool has name, description, inputSchema, and handler", () =
// ─── omniroute_agent_skills_list ────────────────────────────────────────────
test("omniroute_agent_skills_list with no filters returns all 44 skills", async () => {
test("omniroute_agent_skills_list with no filters returns all 45 skills", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
assert.equal(result.count, 44, `Expected 44 but got ${result.count}`);
assert.equal(result.count, 45, `Expected 45 but got ${result.count}`);
assert.ok(Array.isArray(result.skills));
assert.equal(result.skills.length, 44);
assert.equal(result.skills.length, 45);
});
test("omniroute_agent_skills_list({category:'api'}) returns exactly 23 entries", async () => {
@@ -71,9 +71,9 @@ test("omniroute_agent_skills_list({category:'api'}) returns exactly 23 entries",
assert.ok(result.skills.every((s: { category: string }) => s.category === "api"));
});
test("omniroute_agent_skills_list({category:'cli'}) returns exactly 20 entries", async () => {
test("omniroute_agent_skills_list({category:'cli'}) returns exactly 21 entries", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({ category: "cli" });
assert.equal(result.count, 20, `Expected 20 cli skills but got ${result.count}`);
assert.equal(result.count, 21, `Expected 21 cli skills but got ${result.count}`);
assert.ok(result.skills.every((s: { category: string }) => s.category === "cli"));
});
@@ -83,7 +83,7 @@ test("omniroute_agent_skills_list result includes coverage shape", async () => {
assert.ok(typeof result.coverage.api === "object");
assert.ok(typeof result.coverage.cli === "object");
assert.equal(result.coverage.api.total, 23);
assert.equal(result.coverage.cli.total, 20);
assert.equal(result.coverage.cli.total, 21);
assert.ok(typeof result.coverage.totalSkills === "number");
assert.ok(typeof result.coverage.generatedAt === "string");
});
@@ -168,11 +168,11 @@ test("omniroute_agent_skills_coverage({}) returns coverage shape", async () => {
assert.ok(typeof result.api === "object");
assert.ok(typeof result.cli === "object");
assert.equal(result.api.total, 23);
assert.equal(result.cli.total, 20);
assert.equal(result.cli.total, 21);
assert.ok(typeof result.api.have === "number");
assert.ok(typeof result.cli.have === "number");
assert.ok(result.api.have >= 0 && result.api.have <= 23);
assert.ok(result.cli.have >= 0 && result.cli.have <= 20);
assert.ok(result.cli.have >= 0 && result.cli.have <= 21);
assert.ok(typeof result.totalSkills === "number");
assert.equal(result.totalSkills, result.api.have + result.cli.have + (result.config?.have ?? 0));
assert.ok(typeof result.generatedAt === "string");

View File

@@ -15,10 +15,10 @@ const agentSkillsConstants = await import("../../src/shared/constants/agentSkill
// ─── Counts ───────────────────────────────────────────────────────────────────
test("getCatalog() returns exactly 44 entries", () => {
test("getCatalog() returns exactly 45 entries", () => {
refreshCatalog();
const catalog = getCatalog();
assert.equal(catalog.length, 44, `Expected 44 but got ${catalog.length}`);
assert.equal(catalog.length, 45, `Expected 45 but got ${catalog.length}`);
});
test("API_SKILL_IDS has exactly 23 entries", () => {
@@ -26,7 +26,7 @@ test("API_SKILL_IDS has exactly 23 entries", () => {
});
test("CLI_SKILL_IDS has exactly 20 entries", () => {
assert.equal(CLI_SKILL_IDS.length, 20);
assert.equal(CLI_SKILL_IDS.length, 21);
});
test("getCatalog() contains exactly 22 api skills", () => {
@@ -34,9 +34,9 @@ test("getCatalog() contains exactly 22 api skills", () => {
assert.equal(apiSkills.length, 23);
});
test("getCatalog() contains exactly 20 cli skills", () => {
test("getCatalog() contains exactly 21 cli skills", () => {
const cliSkills = getCatalog().filter((s) => s.category === "cli");
assert.equal(cliSkills.length, 20);
assert.equal(cliSkills.length, 21);
});
// ─── ID format ────────────────────────────────────────────────────────────────
@@ -160,9 +160,9 @@ test("filterCatalog({ category: 'api' }) returns 23 api skills", () => {
}
});
test("filterCatalog({ category: 'cli' }) returns 20 cli skills", () => {
test("filterCatalog({ category: 'cli' }) returns 21 cli skills", () => {
const skills = filterCatalog({ category: "cli" });
assert.equal(skills.length, 20);
assert.equal(skills.length, 21);
for (const s of skills) {
assert.equal(s.category, "cli");
}
@@ -185,9 +185,9 @@ test("filterCatalog({ area: 'nonexistent' }) returns empty array", () => {
assert.equal(skills.length, 0);
});
test("filterCatalog({}) returns full catalog (44 entries)", () => {
test("filterCatalog({}) returns full catalog (45 entries)", () => {
const skills = filterCatalog({});
assert.equal(skills.length, 44);
assert.equal(skills.length, 45);
});
// ─── refreshCatalog ───────────────────────────────────────────────────────────
@@ -214,9 +214,9 @@ test("computeCoverage() returns valid SkillCoverage shape", () => {
assert.ok(cov.api.have >= 0 && cov.api.have <= 23);
assert.ok(typeof cov.cli === "object");
assert.equal(cov.cli.total, 20);
assert.equal(cov.cli.total, 21);
assert.ok(typeof cov.cli.have === "number");
assert.ok(cov.cli.have >= 0 && cov.cli.have <= 20);
assert.ok(cov.cli.have >= 0 && cov.cli.have <= 21);
assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0));
@@ -255,6 +255,6 @@ test("CLI_SKILL_IDS first entry is cli-serve", () => {
assert.equal(CLI_SKILL_IDS[0], "cli-serve");
});
test("CLI_SKILL_IDS last entry is cli-setup", () => {
assert.equal(CLI_SKILL_IDS[CLI_SKILL_IDS.length - 1], "cli-setup");
test("CLI_SKILL_IDS last entry is cli-skill-collector", () => {
assert.equal(CLI_SKILL_IDS[CLI_SKILL_IDS.length - 1], "cli-skill-collector");
});

View File

@@ -61,11 +61,11 @@ test("dry-run (default) returns report without writing any files", async () => {
outputDir: tmpDir,
});
// All 44 skills should appear as generated (would-write) since dir is empty
// All 45 skills should appear as generated (would-write) since dir is empty
assert.equal(
report.generated.length + report.unchanged.length,
44,
`Expected 44 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`,
45,
`Expected 45 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`,
);
assert.equal(report.errors.length, 0, `Unexpected errors: ${JSON.stringify(report.errors)}`);
@@ -81,7 +81,7 @@ test("dry-run (default) returns report without writing any files", async () => {
}
});
test("dry-run generates report with 44 total (generated+unchanged)", async () => {
test("dry-run generates report with 45 total (generated+unchanged)", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
@@ -91,7 +91,7 @@ test("dry-run generates report with 44 total (generated+unchanged)", async () =>
outputDir: tmpDir,
});
const total = report.generated.length + report.unchanged.length;
assert.equal(total, 44);
assert.equal(total, 45);
} finally {
rmTmpDir(tmpDir);
}
@@ -134,7 +134,7 @@ test("apply mode writes SKILL.md with valid frontmatter for omni-providers", asy
}
});
test("apply mode writes all 44 SKILL.md files when no onlyIds filter", async () => {
test("apply mode writes all 45 SKILL.md files when no onlyIds filter", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
@@ -145,7 +145,7 @@ test("apply mode writes all 44 SKILL.md files when no onlyIds filter", async ()
});
assert.equal(report.errors.length, 0, `Errors: ${JSON.stringify(report.errors)}`);
assert.equal(report.generated.length, 44);
assert.equal(report.generated.length, 45);
// Verify all dirs exist
const catalog = getCatalog();

View File

@@ -101,15 +101,15 @@ test.after(() => {
// GET /api/agent-skills
// ═════════════════════════════════════════════════════════════════════════════
test("GET /api/agent-skills — returns 44 skills with count and coverage", async () => {
test("GET /api/agent-skills — returns 45 skills with count and coverage", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills");
const res = await listRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { skills: unknown[]; count: number; coverage: unknown };
assert.equal(body.count, 44, `Expected 44 skills but got ${body.count}`);
assert.equal(body.count, 45, `Expected 45 skills but got ${body.count}`);
assert.equal(Array.isArray(body.skills), true);
assert.equal(body.skills.length, 44);
assert.equal(body.skills.length, 45);
assert.ok(body.coverage !== undefined, "coverage should be present");
});
@@ -123,13 +123,13 @@ test("GET /api/agent-skills?category=api — returns 23 api skills", async () =>
assert.ok(body.skills.every((s) => s.category === "api"), "All skills should be api category");
});
test("GET /api/agent-skills?category=cli — returns 20 cli skills", async () => {
test("GET /api/agent-skills?category=cli — returns 21 cli skills", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills?category=cli");
const res = await listRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { skills: Array<{ category: string }>; count: number };
assert.equal(body.count, 20);
assert.equal(body.count, 21);
assert.ok(body.skills.every((s) => s.category === "cli"), "All skills should be cli category");
});
@@ -268,7 +268,7 @@ test("GET /api/agent-skills/coverage — returns valid SkillCoverage shape", asy
};
assert.equal(body.api.total, 23, "api.total must be 23");
assert.equal(body.cli.total, 20, "cli.total must be 20");
assert.equal(body.cli.total, 21, "cli.total must be 21");
assert.ok(typeof body.totalSkills === "number", "totalSkills must be a number");
assert.ok(typeof body.generatedAt === "string", "generatedAt must be a string");
// generatedAt must be a valid ISO datetime

View File

@@ -3,7 +3,7 @@
*
* Verifies:
* - Return shape matches §3.7 contract
* - Markdown table contains all 43 skill IDs
* - Markdown table contains all 44 skill IDs
* - Coverage bounds are within declared totals
* - metadata.source === "agent-skills-catalog"
* - metadata.generatedAt is an ISO datetime string
@@ -31,7 +31,7 @@ test("executeListCapabilities returns shape matching §3.7 contract", async () =
const { metadata } = result;
assert.ok(metadata, "metadata exists");
assert.equal(metadata.source, "agent-skills-catalog", "metadata.source matches");
assert.equal(metadata.totalSkills, 44, "metadata.totalSkills === 44 (43 + config)");
assert.equal(metadata.totalSkills, 45, "metadata.totalSkills === 45 (44 + config)");
assert.ok(metadata.coverage, "metadata.coverage exists");
assert.ok(metadata.coverage.api, "metadata.coverage.api exists");
assert.ok(metadata.coverage.cli, "metadata.coverage.cli exists");
@@ -39,12 +39,12 @@ test("executeListCapabilities returns shape matching §3.7 contract", async () =
assert.equal(metadata.coverage.cli.total, 20, "cli.total === 20");
});
test("executeListCapabilities markdown table contains all 43 API+CLI skill IDs", async () => {
test("executeListCapabilities markdown table contains all 44 API+CLI skill IDs", async () => {
const result = await executeListCapabilities(stubTask);
const content = result.artifacts[0].content;
const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[];
assert.equal(allIds.length, 43, "API+CLI catalog declares 43 skill IDs");
assert.equal(allIds.length, 44, "API+CLI catalog declares 44 skill IDs");
for (const id of allIds) {
assert.ok(content.includes(id), `Markdown table missing skill ID: ${id}`);
@@ -58,11 +58,11 @@ test("metadata.coverage.api.have is within [0, 23]", async () => {
assert.ok(api.have <= 23, "api.have <= 23");
});
test("metadata.coverage.cli.have is within [0, 20]", async () => {
test("metadata.coverage.cli.have is within [0, 21]", async () => {
const result = await executeListCapabilities(stubTask);
const { cli } = result.metadata.coverage;
assert.ok(cli.have >= 0, "cli.have >= 0");
assert.ok(cli.have <= 20, "cli.have <= 20");
assert.ok(cli.have <= 21, "cli.have <= 21");
});
test("metadata.generatedAt is a valid ISO datetime", async () => {