feat(#2789): trim skill description anti-patterns; enforce 100-char budget (#2823)

* 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:
Tom Boucher
2026-04-29 08:14:11 -04:00
committed by GitHub
parent 4815b3c972
commit e81592878e
19 changed files with 326 additions and 24 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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`.

View File

@@ -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

View File

@@ -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"

View 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);

View 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'));
});
});