diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c35106..5dfd484 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: node-version: 22 - name: Structural validation run: node scripts/validate.mjs + - name: SKILL.md routing tables match references/index.json + run: node scripts/sync-router.mjs --check - name: skills CLI discovers exactly a6 and a7 run: | out=$(npx -y skills add . --list 2>&1 | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g') diff --git a/AGENTS.md b/AGENTS.md index e0b30dd..2acea06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ Each product is exactly **one** skill. The skills CLI discovers `skills//S - `SKILL.md` is a router. The agent loads only its `description` at startup, so the description must list every plugin and workflow name the skill covers (max 1024 chars). Detailed guidance lives in `references/`, loaded on demand. - Every reference file keeps a small frontmatter (`title`, `description`, `metadata.category`, `metadata._commands`, …). Tooling reads it; agents ignore it. -- Adding a reference: create `references//.md`, add a row to the routing table in `SKILL.md`, add an entry to `references/index.json`, run `node scripts/validate.mjs`. +- Adding a reference: create `references//.md`, add an entry to `references/index.json`, then run `node scripts/sync-router.mjs` — it regenerates the frontmatter description and the routing tables in `SKILL.md` from index.json (never edit the tables by hand; CI runs `--check`). Finish with `node scripts/validate.mjs`. - `scripts/split-from-cli.mjs` was the one-time migration from the flat `skills/--/SKILL.md` layout in the a6/a7 repositories. After the migration this repository is the source of truth; do not re-run it against the CLI repos unless you intend to overwrite local edits. - Shell examples must only use commands and flags that exist in the current `a6` / `a7` CLI. The CLI repositories' `test/skills` Go test validates this; it is run against a checkout of this repository from their CI. @@ -43,5 +43,6 @@ Each product is exactly **one** skill. The skills CLI discovers `skills//S ```bash node scripts/validate.mjs +node scripts/sync-router.mjs --check npx -y skills add . --list # must report exactly 2 skills: a6, a7 ``` diff --git a/scripts/lib/router.mjs b/scripts/lib/router.mjs new file mode 100644 index 0000000..83b122a --- /dev/null +++ b/scripts/lib/router.mjs @@ -0,0 +1,82 @@ +// Shared rendering for the SKILL.md router: the frontmatter description and the +// three routing tables are derived from references/index.json so humans only +// maintain index.json and the reference files. Used by split-from-cli.mjs +// (one-time migration) and sync-router.mjs (ongoing). + +export const PRODUCTS = { + a6: { name: 'Apache APISIX', short: 'APISIX', audience: 'the open-source Apache APISIX gateway', versionKey: 'apisix_version' }, + a7: { name: 'API7 Enterprise Edition', short: 'API7 EE', audience: 'API7 Enterprise Edition (API7 Gateway)', versionKey: 'apisix_version' }, +}; + +export const TABLE_START = ''; +export const TABLE_END = ''; + +export function renderDescription(cli, entries) { + const p = PRODUCTS[cli]; + const plugins = entries.filter((e) => e.category === 'plugin').map((e) => e.id).join(', '); + const recipes = entries.filter((e) => e.category === 'recipe').map((e) => e.id).join(', '); + const d = + `Configure and operate ${p.audience} through the ${cli} CLI. Use whenever the user wants to ` + + `create, inspect, change, or delete ${p.short} routes, services, upstreams, consumers, credentials, SSL certificates, ` + + `global rules, or plugins (${plugins}), or run a workflow such as ${recipes}. ` + + `Includes developer and platform-operator personas and the ${cli} command conventions.`; + if (d.length > 1024) throw new Error(`${cli}: description is ${d.length} chars (> 1024)`); + return d; +} + +export const foldDescription = (d) => d.match(/.{1,96}(\s|$)/g).map((l) => l.trim()).join('\n '); + +const short = (d, n = 110) => (d.length > n ? d.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : d); + +function stripPrefix(cli, d) { + const p = PRODUCTS[cli]; + const prod = p.name.replace(/[()]/g, '\\$&'); + d = d + .replace(new RegExp(`^(Skill|Recipe skill|Persona skill|Core skill) for (configuring|implementing|working with|setting up|managing)?\\s*(the )?(${prod}|Apache APISIX|APISIX|API7 Enterprise Edition \\(API7 EE\\)|API7 EE)?\\s*`, 'i'), '') + .replace(new RegExp(`\\s*(via|using|with) the ${cli} CLI[^.]*\\.\\s*`, 'i'), '. ') + .replace(/^\s*(plugin|recipe|persona)?\s*/i, '') + .replace(/^\w/, (c) => c.toUpperCase()); + d = d.replace(new RegExp(`\\s*(using|on|with|for) (the )?(${prod}( \\(API7 EE\\))?|Apache APISIX|APISIX|API7 EE)( and the ${cli} CLI)?`, 'g'), ''); + d = d.replace(/^[^.]{0,60}?\b(plugin|recipe|persona|releases?|workflows?|strategies|patterns)\.\s*/i, ''); + return short(d.replace(/^\w/, (c) => c.toUpperCase())); +} + +const row = (cli, e) => `| \`${e.category === 'plugin' ? (e.plugin_name || e.id) : e.id}\` | [${e.path.replace('references/', '')}](${e.path}) | ${stripPrefix(cli, e.description)} |`; + +export function renderTables(cli, entries) { + const plugins = entries.filter((e) => e.category === 'plugin'); + const recipes = entries.filter((e) => e.category === 'recipe'); + const personas = entries.filter((e) => e.category === 'persona'); + return `${TABLE_START} + +### Plugins (${plugins.length}) + +| Plugin | Reference | Covers | +|---|---|---| +${plugins.map((e) => row(cli, e)).join('\n')} + +### Recipes — multi-step workflows (${recipes.length}) + +| Workflow | Reference | Covers | +|---|---|---| +${recipes.map((e) => row(cli, e)).join('\n')} + +### Personas — role-based guidance (${personas.length}) + +| Role | Reference | Covers | +|---|---|---| +${personas.map((e) => row(cli, e)).join('\n')} + +${TABLE_END}`; +} + +/** Apply description + tables to an existing SKILL.md; returns the new text. */ +export function applyRouter(cli, skillMd, entries) { + const desc = foldDescription(renderDescription(cli, entries)); + let out = skillMd.replace(/^description: >-\n(?: .*\n)+/m, `description: >-\n ${desc}\n`); + const start = out.indexOf(TABLE_START); + const end = out.indexOf(TABLE_END); + if (start === -1 || end === -1) throw new Error(`${cli}: SKILL.md is missing the routing-tables markers`); + out = out.slice(0, start) + renderTables(cli, entries) + out.slice(end + TABLE_END.length); + return out; +} diff --git a/scripts/sync-router.mjs b/scripts/sync-router.mjs new file mode 100644 index 0000000..7842ac0 --- /dev/null +++ b/scripts/sync-router.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/** + * Regenerate the generated parts of each skills//SKILL.md from + * references/index.json: the frontmatter `description` (must list every plugin + * and workflow) and the three routing tables between the markers. + * + * node scripts/sync-router.mjs # rewrite SKILL.md files in place + * node scripts/sync-router.mjs --check # exit 1 if any SKILL.md is stale (CI) + * + * Everything outside the markers (intro, operating rules, example) is hand-written. + */ +import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { applyRouter, PRODUCTS } from './lib/router.mjs'; + +const ROOT = new URL('..', import.meta.url).pathname; +const check = process.argv.includes('--check'); +let stale = 0; +for (const cli of readdirSync(join(ROOT, 'skills'))) { + if (!PRODUCTS[cli]) continue; + const dir = join(ROOT, 'skills', cli); + const index = JSON.parse(readFileSync(join(dir, 'references', 'index.json'), 'utf8')); + const file = join(dir, 'SKILL.md'); + const current = readFileSync(file, 'utf8'); + const next = applyRouter(cli, current, index.entries); + if (next === current) { console.log(`${cli}: up to date`); continue; } + stale++; + if (check) console.error(`${cli}: SKILL.md is out of date with references/index.json — run node scripts/sync-router.mjs`); + else { writeFileSync(file, next); console.log(`${cli}: SKILL.md updated`); } +} +process.exit(check && stale ? 1 : 0); diff --git a/skills/a6/SKILL.md b/skills/a6/SKILL.md index 1c3303c..8dc01ce 100644 --- a/skills/a6/SKILL.md +++ b/skills/a6/SKILL.md @@ -33,7 +33,9 @@ Read [references/shared.md](references/shared.md) once per session before runnin ## 2. Pick the reference for the task -Match the user's request against the tables below and read the linked file. Load one plugin or recipe file at a time; add a second only when the task clearly spans both (for example key-auth + limit-count). +Match the user's request against the tables below and read the linked file. The tables are generated from `references/index.json`; edit that file, not the tables. Load one plugin or recipe file at a time; add a second only when the task clearly spans both (for example key-auth + limit-count). + + ### Plugins (29) @@ -89,6 +91,8 @@ Match the user's request against the tables below and read the linked file. Load | `developer` | [personas/developer.md](references/personas/developer.md) | API developers building and testing APIs. Provides decision frameworks for API design, route configuration,… | | `operator` | [personas/operator.md](references/personas/operator.md) | Platform operators and DevOps engineers managing APISIX instances. Provides decision frameworks for… | + + If nothing matches, the request is probably plain resource CRUD (routes, services, upstreams, consumers, SSL, global rules): [references/shared.md](references/shared.md) is sufficient. Machine-readable metadata for every reference is in [references/index.json](references/index.json). ## 3. Operating rules diff --git a/skills/a7/SKILL.md b/skills/a7/SKILL.md index 13e039e..a109dff 100644 --- a/skills/a7/SKILL.md +++ b/skills/a7/SKILL.md @@ -33,7 +33,9 @@ Read [references/shared.md](references/shared.md) once per session before runnin ## 2. Pick the reference for the task -Match the user's request against the tables below and read the linked file. Load one plugin or recipe file at a time; add a second only when the task clearly spans both (for example key-auth + limit-count). +Match the user's request against the tables below and read the linked file. The tables are generated from `references/index.json`; edit that file, not the tables. Load one plugin or recipe file at a time; add a second only when the task clearly spans both (for example key-auth + limit-count). + + ### Plugins (29) @@ -89,6 +91,8 @@ Match the user's request against the tables below and read the linked file. Load | `developer` | [personas/developer.md](references/personas/developer.md) | API developers building and testing APIs. Provides decision frameworks for service-backed API design, route… | | `operator` | [personas/operator.md](references/personas/operator.md) | Platform operators and DevOps engineers managing API7 Enterprise Edition (API7 EE) instances. Provides… | + + If nothing matches, the request is probably plain resource CRUD (routes, services, upstreams, consumers, SSL, global rules): [references/shared.md](references/shared.md) is sufficient. Machine-readable metadata for every reference is in [references/index.json](references/index.json). ## 3. Operating rules