* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "<phase-number> [--all] [--auto]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
|
||||
@@ -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: "<phase> [--all] [--auto] [--chain] [--batch] [--analyze] [--text] [--power]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <file>] [--resolve auto|interactive]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
|
||||
@@ -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: "<phase> [--codex] [--gemini] [--claude] [--opencode] [--ollama] [--lm-studio] [--llama-cpp] [--text] [--ws <name>] [--all] [--max-cycles N]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "<phase> [--auto] [--text]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
83
scripts/lint-descriptions.cjs
Normal file
83
scripts/lint-descriptions.cjs
Normal file
@@ -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);
|
||||
192
tests/enh-2789-description-budget.test.cjs
Normal file
192
tests/enh-2789-description-budget.test.cjs
Normal file
@@ -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'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user