docs: authoritative shipped-surface inventory with filesystem-backed parity tests (#2390)

* docs: finish trust-bug fixes in user guide and commands

Correct load-bearing defects in the v1.36.0 docs corpus so readers stop
acting on wrong defaults and stale exhaustiveness claims.

- README.md: drop "Complete feature"/"Every command"/"All 18 agents"
  exhaustiveness claims; replace version-pinned "What's new in v1.32"
  bullet with a CHANGELOG pointer.
- CONFIGURATION.md: fix `claude_md_path` default (null/none -> `./CLAUDE.md`)
  in both Full Schema and core settings table; correct `workflow.tdd_mode`
  provenance from "Added in v1.37" to "Added in v1.36".
- USER-GUIDE.md: fix `workflow.discuss_mode` default (`standard` ->
  `discuss`) in the workflow-toggles table AND in the abbreviated Full
  Schema JSON block above it; align the Options cell with the shipped
  enum.
- COMMANDS.md: drop "Complete command syntax" subtitle overclaim to
  match the README posture.
- AGENTS.md: weaken "All 21 specialized agents" header to reflect that
  the `agents/` filesystem is authoritative (shipped roster is 31).

Part 1 of a stacked docs refresh series (PR 1/4).

* docs: refresh shipped surface coverage for v1.36

Close the v1.36.0 shipped-surface gaps in the docs corpus.

- COMMANDS.md: add /gsd-graphify section (build/query/status/diff) and
  its config gate; expand /gsd-quick with --validate flag and list/
  status/resume subcommands; expand /gsd-thread with list --open, list
  --resolved, close <slug>, status <slug>.
- CLI-TOOLS.md: replace the hardcoded "15 domain modules" count with a
  pointer to the Module Architecture table; add a graphify verb-family
  section (build/query/status/diff/snapshot); add Graphify and Learnings
  rows to the Module Architecture table.
- FEATURES.md: add TOC entries for #116 TDD Pipeline Mode and #117
  Knowledge Graph Integration; add the #117 body with REQ-GRAPH-01..05.
- CONFIGURATION.md: move security_enforcement / security_asvs_level /
  security_block_on from root into `workflow.*` in Full Schema to match
  templates/config.json and the gsd-sdk runtime reads; update Security
  Settings table to use the workflow.* prefix; add planning.sub_repos
  to Full Schema and description table; add a Graphify Settings section
  documenting graphify.enabled and graphify.build_timeout.

Note: VALID_CONFIG_KEYS in bin/lib/config.cjs does not yet include
workflow.security_* or planning.sub_repos, so config-set currently
rejects them. That is a pre-existing validator gap that this PR does
not attempt to fix; the docs now correctly describe where these keys
live per the shipped template and runtime reads.

Part 2 of a stacked docs refresh series (PR 2/5), based on PR 1.

* docs: make inventory authoritative and reconcile architecture

Upgrade docs/INVENTORY.md from "complete for agents, selective for others"
to authoritative across all six shipped-surface families, and reconcile
docs/ARCHITECTURE.md against the new inventory so the PR that introduces
INVENTORY does not also introduce an INVENTORY/ARCHITECTURE contradiction.

- docs/AGENTS.md: weaken "21 specialized agents" header to 21 primary +
  10 advanced (31 shipped); add new "Advanced and Specialized Agents"
  section with concise role cards for the 10 previously-omitted shipped
  agents (pattern-mapper, debug-session-manager, code-reviewer,
  code-fixer, ai-researcher, domain-researcher, eval-planner,
  eval-auditor, framework-selector, intel-updater); footnote the Agent
  Tool Permissions Summary as primary-agents-only so it no longer
  misleads.

- docs/INVENTORY.md (rewritten to be authoritative):
  * Full 31-agent roster with one-line role + spawner + primary-doc
    status per agent (unchanged from prior partial work).
  * Commands: full 75-row enumeration grouped by Core Workflow, Phase &
    Milestone Management, Session & Navigation, Codebase Intelligence,
    Review/Debug/Recovery, and Docs/Profile/Utilities — each row
    carries a one-line role derived from the command's frontmatter and
    a link to the source file.
  * Workflows: full 72-row enumeration covering every
    get-shit-done/workflows/*.md, with a one-line role per workflow and
    a column naming the user-facing command (or internal orchestrator)
    that invokes it.
  * References: full 41-row enumeration grouped by Core, Workflow,
    Thinking-Model clusters, and the Modular Planner decomposition,
    matching the groupings docs/ARCHITECTURE.md already uses; notes
    the few-shot-examples subdirectory separately.
  * CLI Modules and Hooks: unchanged — already full rosters.
  * Maintenance section rewritten to describe the drift-guard test
    suite that will land in PR4 (inventory-counts, commands-doc-parity,
    agents-doc-parity, cli-modules-doc-parity, hooks-doc-parity).

- docs/ARCHITECTURE.md reconciled against INVENTORY:
  * References block: drop the stale "(35 total)" count; point at
    INVENTORY.md#references-41-shipped for the authoritative count.
  * CLI Tools block: drop the stale "19 domain modules" count; point
    at INVENTORY.md#cli-modules-24-shipped for the authoritative roster.
  * Agent Spawn Categories: relabel as "Primary Agent Spawn Categories"
    and add a footer naming the 10 advanced agents and pointing at
    INVENTORY.md#agents-31-shipped for the full 31-agent roster.

- docs/CONFIGURATION.md: preserve the six model-profile rows added in
  the prior partial work, and tighten the fallback note so it names the
  13 shipped agents without an explicit profile row, documents
  model_overrides as the escape hatch, and points at INVENTORY.md for
  the authoritative 31-agent roster.

Part 3 of a stacked docs refresh series (PR 3/4). Remaining consistency
work (USER-GUIDE config-section delete-and-link, FEATURES.md TOC
reorder, ARCHITECTURE.md Hook-table expansion + installation-layout
collapse, CLI-TOOLS.md module-row additions, workflow-discuss-mode
invocation normalization, and the five doc-parity tests) lands in PR4.

* test(docs): add consistency guards and remove duplicate refs

Consolidates USER-GUIDE.md's command/config duplicates into pointers to
COMMANDS.md and CONFIGURATION.md (kills a ghost `resolve_model_ids` key
and a stale `discuss_mode: standard` default); reorders FEATURES.md TOC
chronologically so v1.32 precedes v1.34/1.35/1.36; expands
ARCHITECTURE.md's Hook table to the 11 shipped hooks
(gsd-read-injection-scanner, gsd-check-update-worker) and collapses
the installation-layout hook enumeration to the *.js/*.sh pattern form;
adds audit/gsd2-import/intel rows and state signal-*, audit-open,
from-gsd2 verbs to CLI-TOOLS.md; normalizes workflow-discuss-mode.md
invocations to `node gsd-tools.cjs config-set`.

Adds five drift guards anchored on docs/INVENTORY.md as the
authoritative roster: inventory-counts (all six families),
commands/agents/cli-modules/hooks parity checks that every shipped
surface has a row somewhere.

* fix(convergence): thread --ws to review agent; add stall and max-cycles behavioral tests

- Thread GSD_WS through to review agent spawn in plan-review-convergence
  workflow (step 5a) so --ws scoping is symmetric with planning step
- Add behavioral stall detection test: asserts workflow compares
  HIGH_COUNT >= prev_high_count and emits a stall warning
- Add behavioral --max-cycles 1 test: asserts workflow reaches escalation
  gate when cycle >= MAX_CYCLES with HIGH > 0 after a single cycle
- Include original PR files (commands, workflow, tests) as the branch
  predated the PR commits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(docs,config): PR #2390 review — security_* config keys and REQ-GRAPH-02 scope

Addresses trek-e's review items that don't require rebase:

- config.cjs: add workflow.security_enforcement, workflow.security_asvs_level,
  workflow.security_block_on to VALID_CONFIG_KEYS so gsd-sdk config-set accepts
  them (closed the gap where docs/CONFIGURATION.md listed keys the validator
  rejected).
- core.cjs: add matching CONFIG_DEFAULTS entries (true / 1 / 'high') so the
  canonical defaults table matches the documented values.
- config.cjs: wire the three keys into the new-project workflow defaults so
  fresh configs inherit them.
- planning-config.md: document the three keys in the Workflow Fields table,
  keeping the CONFIG_DEFAULTS ↔ doc parity test happy.
- config-field-docs.test.cjs: extend NAMESPACE_MAP so the flat keys in
  CONFIG_DEFAULTS resolve to their workflow.* doc rows.
- FEATURES.md REQ-GRAPH-02: split the slash-command surface (build|query|
  status|diff) from the CLI surface which additionally exposes `snapshot`
  (invoked automatically at the tail of `graphify build`). The prior text
  overstated the slash-command surface.

* docs(inventory): refresh rosters and counts for post-rebase drift

origin/main accumulated surfaces since this PR was authored:

- Agents: 31 → 33 (+ gsd-doc-classifier, gsd-doc-synthesizer)
- Commands: 76 → 82 (+ ingest-docs, ultraplan-phase, spike, spike-wrap-up,
  sketch, sketch-wrap-up)
- Workflows: 73 → 79 (same 6 names)
- References: 41 → 49 (+ debugger-philosophy, doc-conflict-engine,
  mandatory-initial-read, project-skills-discovery, sketch-interactivity,
  sketch-theme-system, sketch-tooling, sketch-variant-patterns)

Adds rows in the existing sub-groupings, introduces a Sketch References
subsection, and bumps all four headline counts. Roles are pulled from
source frontmatter / purpose blocks for each file. All 5 parity tests
(inventory-counts, agents-doc-parity, commands-doc-parity,
cli-modules-doc-parity, hooks-doc-parity) pass against this state —
156 assertions, 0 failures.

Also updates the 'Coverage note' advanced-agent count 10 → 12 and the
few-shot-examples footnote "41 top-level references" → "49" to keep the
file internally consistent.

* docs(agents): add advanced stubs for gsd-doc-classifier and gsd-doc-synthesizer

Both agents ship on main (spawned by /gsd-ingest-docs) but had no
coverage in docs/AGENTS.md. Adds the "advanced stub" entries (Role,
property table, Key behaviors) following the template used by the other
10 advanced/specialized agents in the same section.

Also updates the Agent Tool Permissions Summary scope note from
"10 advanced/specialized agents" to 12 to reflect the two new stubs.

* docs(commands): add entries for ingest-docs, ultraplan-phase, plan-review-convergence

These three commands ship on main (plan-review-convergence via trek-e's
4b452d29 commit on this branch) but had no user-facing section in
docs/COMMANDS.md — they lived only in INVENTORY.md. The commands-doc-parity
test already passes via INVENTORY, but the user-facing doc was missing
canonical explanations, argument tables, and examples.

- /gsd-plan-review-convergence → Core Workflow (after /gsd-plan-phase)
- /gsd-ultraplan-phase → Core Workflow (after plan-review-convergence)
- /gsd-ingest-docs → Brownfield (after /gsd-import, since both consume
  the references/doc-conflict-engine.md contract)

Content pulled from each command's frontmatter and workflow purpose block.

* test: remove redundant ARCHITECTURE.md count tests

tests/architecture-counts.test.cjs and tests/command-count-sync.test.cjs
were added when docs/ARCHITECTURE.md carried hardcoded counts for commands/
workflows/agents. With the PR #2390 cleanup, ARCHITECTURE.md no longer
owns those numbers — docs/INVENTORY.md does, enforced by
tests/inventory-counts.test.cjs (scans the same filesystem directories
with the same readdirSync filter).

Keeping these ARCHITECTURE-specific tests would re-introduce the hardcoded
counts they guard, defeating trek-e's review point. The single-source-of-
truth parity tests already catch the same drift scenarios.

Related: #2257 (the regression this replaced).

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Logan
2026-04-20 09:31:34 -04:00
committed by GitHub
parent 3d6c2bea4b
commit fbf30792f3
25 changed files with 1776 additions and 408 deletions

View File

@@ -0,0 +1,42 @@
'use strict';
/**
* For every `agents/gsd-*.md`, assert its agent name appears as a row
* in docs/INVENTORY.md's Agents table. AGENTS.md card presence is NOT
* enforced — that file is allowed to be a curated subset (primary
* cards + advanced stubs).
*
* Related: docs readiness refresh, lane-12 recommendation.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const AGENTS_DIR = path.join(ROOT, 'agents');
const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
const agentFiles = fs
.readdirSync(AGENTS_DIR)
.filter((f) => /^gsd-.*\.md$/.test(f));
function mentionedInInventoryAgents(name) {
// Row form in the Agents table: `| agent-name | role | ... |`
// The Agents table uses the raw name (no code fence) in column 1.
const rowRe = new RegExp(`^\\|\\s*${name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\s*\\|`, 'm');
return rowRe.test(INVENTORY_MD);
}
describe('every shipped agent has a row in INVENTORY.md', () => {
for (const file of agentFiles) {
const name = file.replace(/\.md$/, '');
test(name, () => {
assert.ok(
mentionedInInventoryAgents(name),
`agents/${file} has no row in docs/INVENTORY.md Agents table — add one`,
);
});
}
});

View File

@@ -1,59 +0,0 @@
'use strict';
/**
* Guards ARCHITECTURE.md component counts against drift.
*
* Both sides are computed at test runtime — no hardcoded numbers.
* Parsing ARCHITECTURE.md: regex extracts the documented count.
* Filesystem count: readdirSync filters to *.md files.
*
* To add a new component: append a row to COMPONENTS below and update
* docs/ARCHITECTURE.md with a matching "**Total <label>:** N" line.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const ARCH_MD = path.join(ROOT, 'docs', 'ARCHITECTURE.md');
const ARCH_CONTENT = fs.readFileSync(ARCH_MD, 'utf-8');
/** Components whose counts must stay in sync with ARCHITECTURE.md. */
const COMPONENTS = [
{ label: 'commands', dir: 'commands/gsd' },
{ label: 'workflows', dir: 'get-shit-done/workflows' },
{ label: 'agents', dir: 'agents' },
];
/**
* Parse "**Total <label>:** N" from ARCHITECTURE.md.
* Returns the integer N, or throws if the pattern is missing.
*/
function parseDocCount(label) {
const match = ARCH_CONTENT.match(new RegExp(`\\*\\*Total ${label}:\\*\\*\\s+(\\d+)`));
assert.ok(match, `ARCHITECTURE.md is missing "**Total ${label}:** N" — add it`);
return parseInt(match[1], 10);
}
/**
* Count *.md files in a directory (non-recursive).
*/
function countMdFiles(relDir) {
return fs.readdirSync(path.join(ROOT, relDir)).filter((f) => f.endsWith('.md')).length;
}
describe('ARCHITECTURE.md component counts', () => {
for (const { label, dir } of COMPONENTS) {
test(`Total ${label} matches ${dir}/*.md file count`, () => {
const documented = parseDocCount(label);
const actual = countMdFiles(dir);
assert.strictEqual(
documented,
actual,
`docs/ARCHITECTURE.md says "Total ${label}: ${documented}" but ${dir}/ has ${actual} .md files — update ARCHITECTURE.md`
);
});
}
});

View File

@@ -0,0 +1,40 @@
'use strict';
/**
* For every `get-shit-done/bin/lib/*.cjs`, assert the module name
* appears as a row in docs/INVENTORY.md's CLI Modules table.
* docs/CLI-TOOLS.md is allowed to describe a subset (narrative doc);
* INVENTORY.md is the authoritative module roster.
*
* Related: docs readiness refresh, lane-12 recommendation.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const LIB_DIR = path.join(ROOT, 'get-shit-done', 'bin', 'lib');
const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
const moduleFiles = fs
.readdirSync(LIB_DIR)
.filter((f) => f.endsWith('.cjs'));
function mentionedInInventoryCliModules(filename) {
// Row form: | `filename.cjs` | responsibility |
const rowRe = new RegExp(`\\|\\s*\\\`${filename.replace(/\./g, '\\.')}\\\`\\s*\\|`, 'm');
return rowRe.test(INVENTORY_MD);
}
describe('every CLI module has a row in INVENTORY.md', () => {
for (const file of moduleFiles) {
test(file, () => {
assert.ok(
mentionedInInventoryCliModules(file),
`get-shit-done/bin/lib/${file} has no row in docs/INVENTORY.md CLI Modules table — add one`,
);
});
}
});

View File

@@ -1,93 +0,0 @@
/**
* Regression test: command count in docs/ARCHITECTURE.md must match
* the actual number of .md files in commands/gsd/.
*
* Counts are extracted from the doc programmatically — never hardcoded
* in this test — so any future drift (adding a command without updating
* the doc, or vice-versa) is caught immediately.
*
* Related: issue #2257
*/
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
const ARCH_MD = path.join(ROOT, 'docs', 'ARCHITECTURE.md');
/**
* Count .md files that actually live in commands/gsd/.
* Does not recurse into subdirectories.
*/
function actualCommandCount() {
return fs
.readdirSync(COMMANDS_DIR)
.filter((f) => f.endsWith('.md'))
.length;
}
/**
* Extract the integer from the "**Total commands:** N" prose line in
* ARCHITECTURE.md. Returns null if the pattern is not found.
*/
function docProseCount(content) {
const m = content.match(/\*\*Total commands:\*\*\s+(\d+)/);
return m ? parseInt(m[1], 10) : null;
}
/**
* Extract the integer from the directory-tree comment line:
* ├── commands/gsd/*.md # N slash commands
* Returns null if the pattern is not found.
*/
function docTreeCount(content) {
const m = content.match(/commands\/gsd\/\*\.md[^\n]*#\s*(\d+)\s+slash commands/);
return m ? parseInt(m[1], 10) : null;
}
describe('ARCHITECTURE.md command count sync', () => {
const archContent = fs.readFileSync(ARCH_MD, 'utf8');
const actual = actualCommandCount();
test('docs/ARCHITECTURE.md contains a "Total commands:" prose count', () => {
const count = docProseCount(archContent);
assert.notEqual(count, null, 'Expected "**Total commands:** N" line not found in ARCHITECTURE.md');
});
test('docs/ARCHITECTURE.md contains a directory-tree slash-command count', () => {
const count = docTreeCount(archContent);
assert.notEqual(count, null, 'Expected "# N slash commands" tree comment not found in ARCHITECTURE.md');
});
test('"Total commands:" prose count matches actual commands/gsd/ file count', () => {
const prose = docProseCount(archContent);
assert.equal(
prose,
actual,
`ARCHITECTURE.md "Total commands:" says ${prose} but commands/gsd/ has ${actual} .md files — update the doc`,
);
});
test('directory-tree slash-command count matches actual commands/gsd/ file count', () => {
const tree = docTreeCount(archContent);
assert.equal(
tree,
actual,
`ARCHITECTURE.md directory tree says ${tree} slash commands but commands/gsd/ has ${actual} .md files — update the doc`,
);
});
test('"Total commands:" prose count and directory-tree count agree with each other', () => {
const prose = docProseCount(archContent);
const tree = docTreeCount(archContent);
assert.equal(
prose,
tree,
`ARCHITECTURE.md has two mismatched counts: "Total commands: ${prose}" vs tree "# ${tree} slash commands"`,
);
});
});

View File

@@ -0,0 +1,50 @@
'use strict';
/**
* For every `commands/gsd/*.md`, assert its `/gsd-<name>` slash command
* appears either (a) as a `### /gsd-...` heading in docs/COMMANDS.md or
* (b) as a row in docs/INVENTORY.md's Commands table. At least one of
* these must be true so every shipped command is reachable from docs.
*
* Related: docs readiness refresh, lane-12 recommendation.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
const COMMANDS_MD = fs.readFileSync(path.join(ROOT, 'docs', 'COMMANDS.md'), 'utf8');
const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
const commandFiles = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'));
function mentionedInCommandsDoc(slug) {
// Match a heading like: ### /gsd-<slug> or ## /gsd-<slug>
const headingRe = new RegExp(`^#{2,4}\\s+\\\`?/gsd-${slug}\\\`?(?:[\\s(]|$)`, 'm');
return headingRe.test(COMMANDS_MD);
}
function mentionedInInventory(slug) {
// Match a row like: | `/gsd-<slug>` | ... |
const rowRe = new RegExp(`\\|\\s*\\\`/gsd-${slug}\\\`\\s*\\|`, 'm');
return rowRe.test(INVENTORY_MD);
}
describe('every shipped command is documented somewhere', () => {
for (const file of commandFiles) {
// Command files may use `_` in their filename (e.g. extract_learnings.md)
// while the user-facing slash command uses `-` (/gsd-extract-learnings).
const slug = file.replace(/\.md$/, '').replace(/_/g, '-');
test(`/gsd-${slug}`, () => {
const inCommandsDoc = mentionedInCommandsDoc(slug);
const inInventory = mentionedInInventory(slug);
assert.ok(
inCommandsDoc || inInventory,
`commands/gsd/${file} is not mentioned in docs/COMMANDS.md (as a heading) or docs/INVENTORY.md (as a Commands row) — add a one-line entry to at least one`,
);
});
}
});

View File

@@ -80,6 +80,9 @@ describe('config-field-docs', () => {
phase_branch_template: 'git.phase_branch_template',
milestone_branch_template: 'git.milestone_branch_template',
quick_branch_template: 'git.quick_branch_template',
security_enforcement: 'workflow.security_enforcement',
security_asvs_level: 'workflow.security_asvs_level',
security_block_on: 'workflow.security_block_on',
};
const missing = keys.filter(k => {

View File

@@ -0,0 +1,39 @@
'use strict';
/**
* For every `hooks/*.(js|sh)`, assert the hook filename appears as a
* row in docs/INVENTORY.md's Hooks table. docs/ARCHITECTURE.md's hook
* table is allowed to lag — INVENTORY.md is authoritative.
*
* Related: docs readiness refresh, lane-12 recommendation.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const HOOKS_DIR = path.join(ROOT, 'hooks');
const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
const hookFiles = fs
.readdirSync(HOOKS_DIR)
.filter((f) => /\.(js|sh)$/.test(f));
function mentionedInInventoryHooks(filename) {
// Row form: | `filename.js` | event | purpose |
const rowRe = new RegExp(`\\|\\s*\\\`${filename.replace(/\./g, '\\.')}\\\`\\s*\\|`, 'm');
return rowRe.test(INVENTORY_MD);
}
describe('every shipped hook has a row in INVENTORY.md', () => {
for (const file of hookFiles) {
test(file, () => {
assert.ok(
mentionedInInventoryHooks(file),
`hooks/${file} has no row in docs/INVENTORY.md Hooks table — add one`,
);
});
}
});

View File

@@ -0,0 +1,59 @@
'use strict';
/**
* Locks docs/INVENTORY.md's "(N shipped)" headline counts against the
* filesystem for each of the six families. INVENTORY.md is the
* authoritative roster — if a surface ships, its row must exist here
* and the headline count must match ls.
*
* Both sides are computed at test runtime — no hardcoded numbers.
*
* Related: docs readiness refresh, lane-12 recommendation.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const INVENTORY_MD = path.join(ROOT, 'docs', 'INVENTORY.md');
const INVENTORY = fs.readFileSync(INVENTORY_MD, 'utf8');
const FAMILIES = [
{ label: 'Agents', dir: 'agents', filter: (f) => /^gsd-.*\.md$/.test(f) },
{ label: 'Commands', dir: 'commands/gsd', filter: (f) => f.endsWith('.md') },
{ label: 'Workflows', dir: 'get-shit-done/workflows', filter: (f) => f.endsWith('.md') },
{ label: 'References', dir: 'get-shit-done/references', filter: (f) => f.endsWith('.md') },
{ label: 'CLI Modules', dir: 'get-shit-done/bin/lib', filter: (f) => f.endsWith('.cjs') },
{ label: 'Hooks', dir: 'hooks', filter: (f) => /\.(js|sh)$/.test(f) },
];
function headlineCount(label) {
const re = new RegExp(`^##\\s+${label}\\s+\\((\\d+)\\s+shipped\\)`, 'm');
const m = INVENTORY.match(re);
assert.ok(m, `docs/INVENTORY.md is missing the "## ${label} (N shipped)" header`);
return parseInt(m[1], 10);
}
function fsCount(relDir, filter) {
return fs
.readdirSync(path.join(ROOT, relDir))
.filter((name) => fs.statSync(path.join(ROOT, relDir, name)).isFile())
.filter(filter)
.length;
}
describe('docs/INVENTORY.md headline counts match the filesystem', () => {
for (const { label, dir, filter } of FAMILIES) {
test(`"${label} (N shipped)" matches ${dir}/`, () => {
const documented = headlineCount(label);
const actual = fsCount(dir, filter);
assert.strictEqual(
documented,
actual,
`docs/INVENTORY.md "${label} (${documented} shipped)" disagrees with ${dir}/ file count (${actual}) — update the headline and the row list`,
);
});
}
});

View File

@@ -0,0 +1,315 @@
/**
* Tests for gsd:plan-review-convergence command (#2306)
*
* Validates that the command source and workflow contain the key structural
* elements required for correct cross-AI plan convergence loop behavior:
* initial planning gate, review agent spawning, HIGH count detection,
* stall detection, escalation gate, and STATE.md update on convergence.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const COMMAND_PATH = path.join(__dirname, '..', 'commands', 'gsd', 'plan-review-convergence.md');
const WORKFLOW_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'plan-review-convergence.md');
// ─── Command source ────────────────────────────────────────────────────────
describe('plan-review-convergence command source (#2306)', () => {
const command = fs.readFileSync(COMMAND_PATH, 'utf8');
test('command name uses gsd: prefix (installer converts to gsd- on install)', () => {
assert.ok(
command.includes('name: gsd:plan-review-convergence'),
'command name must use gsd: prefix so installer converts it to gsd-plan-review-convergence'
);
});
test('command declares all reviewer flags in context', () => {
assert.ok(command.includes('--codex'), 'must document --codex flag');
assert.ok(command.includes('--gemini'), 'must document --gemini flag');
assert.ok(command.includes('--claude'), 'must document --claude flag');
assert.ok(command.includes('--opencode'), 'must document --opencode flag');
assert.ok(command.includes('--all'), 'must document --all flag');
assert.ok(command.includes('--max-cycles'), 'must document --max-cycles flag');
});
test('command references the workflow file via execution_context', () => {
assert.ok(
command.includes('@$HOME/.claude/get-shit-done/workflows/plan-review-convergence.md'),
'execution_context must reference the workflow file'
);
});
test('command references supporting reference files', () => {
assert.ok(
command.includes('revision-loop.md'),
'must reference revision-loop.md for stall detection pattern'
);
assert.ok(
command.includes('gates.md'),
'must reference gates.md for gate taxonomy'
);
assert.ok(
command.includes('agent-contracts.md'),
'must reference agent-contracts.md for completion markers'
);
});
test('command declares Agent in allowed-tools (required for spawning sub-agents)', () => {
assert.ok(
command.includes('- Agent'),
'Agent must be in allowed-tools — command spawns isolated agents for planning and reviewing'
);
});
test('command has Copilot runtime_note for AskUserQuestion fallback', () => {
assert.ok(
command.includes('vscode_askquestions'),
'must document vscode_askquestions fallback for Copilot compatibility'
);
});
test('--codex is the default reviewer when no flag is specified', () => {
assert.ok(
command.includes('default if no reviewer specified') ||
command.includes('default: --codex') ||
command.includes('(default if no reviewer specified)'),
'--codex must be documented as the default reviewer'
);
});
});
// ─── Workflow: initialization ──────────────────────────────────────────────
describe('plan-review-convergence workflow: initialization (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow calls gsd-tools.cjs init plan-phase for initialization', () => {
assert.ok(
workflow.includes('gsd-tools.cjs') && workflow.includes('init') && workflow.includes('plan-phase'),
'workflow must initialize via gsd-tools.cjs init plan-phase'
);
});
test('workflow parses --max-cycles with default of 3', () => {
assert.ok(
workflow.includes('MAX_CYCLES') && workflow.includes('3'),
'workflow must parse --max-cycles with default of 3'
);
});
test('workflow displays a startup banner with phase number and reviewer flags', () => {
assert.ok(
workflow.includes('PLAN CONVERGENCE') || workflow.includes('Plan Convergence'),
'workflow must display a startup banner'
);
});
});
// ─── Workflow: initial planning gate ──────────────────────────────────────
describe('plan-review-convergence workflow: initial planning gate (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow skips initial planning when plans already exist', () => {
assert.ok(
workflow.includes('has_plans') || workflow.includes('plan_count'),
'workflow must check whether plans already exist before spawning planning agent'
);
});
test('workflow spawns isolated planning agent when no plans exist', () => {
assert.ok(
workflow.includes('gsd-plan-phase'),
'workflow must spawn Agent → gsd-plan-phase when no plans exist'
);
});
test('workflow errors if initial planning produces no PLAN.md files', () => {
assert.ok(
workflow.includes('PLAN_COUNT') || workflow.includes('plan_count'),
'workflow must verify PLAN.md files were created after initial planning'
);
});
});
// ─── Workflow: convergence loop ────────────────────────────────────────────
describe('plan-review-convergence workflow: convergence loop (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow spawns isolated review agent each cycle', () => {
assert.ok(
workflow.includes('gsd-review'),
'workflow must spawn Agent → gsd-review each cycle'
);
});
test('workflow detects HIGH concerns by grepping REVIEWS.md', () => {
assert.ok(
workflow.includes('HIGH_COUNT') || workflow.includes('grep'),
'workflow must grep REVIEWS.md for HIGH concerns to determine convergence'
);
});
test('workflow exits loop when HIGH_COUNT == 0 (converged)', () => {
assert.ok(
workflow.includes('HIGH_COUNT == 0') ||
workflow.includes('HIGH_COUNT === 0') ||
workflow.includes('converged'),
'workflow must exit the loop when no HIGH concerns remain'
);
});
test('workflow updates STATE.md on convergence', () => {
assert.ok(
workflow.includes('planned-phase') || workflow.includes('state'),
'workflow must update STATE.md via gsd-tools.cjs when converged'
);
});
test('workflow spawns replan agent with --reviews flag', () => {
assert.ok(
workflow.includes('--reviews'),
'replan agent must pass --reviews so gsd-plan-phase incorporates review feedback'
);
});
test('workflow passes --skip-research to replan agent (research already done)', () => {
assert.ok(
workflow.includes('--skip-research'),
'replan agent must skip research — only initial planning needs research'
);
});
});
// ─── Workflow: stall detection ─────────────────────────────────────────────
describe('plan-review-convergence workflow: stall detection (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow tracks previous HIGH count to detect stalls', () => {
assert.ok(
workflow.includes('prev_high_count') || workflow.includes('prev_HIGH'),
'workflow must track the previous cycle HIGH count for stall detection'
);
});
test('workflow warns when HIGH count is not decreasing', () => {
assert.ok(
workflow.includes('stall') || workflow.includes('Stall') || workflow.includes('not decreasing'),
'workflow must warn user when HIGH count is not decreasing between cycles'
);
});
});
// ─── Workflow: escalation gate ────────────────────────────────────────────
describe('plan-review-convergence workflow: escalation gate (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow escalates to user when max cycles reached with HIGHs remaining', () => {
assert.ok(
workflow.includes('MAX_CYCLES') &&
(workflow.includes('AskUserQuestion') || workflow.includes('vscode_askquestions')),
'workflow must escalate to user via AskUserQuestion when max cycles reached'
);
});
test('escalation offers "Proceed anyway" option', () => {
assert.ok(
workflow.includes('Proceed anyway'),
'escalation gate must offer "Proceed anyway" to accept plans with remaining HIGH concerns'
);
});
test('escalation offers "Manual review" option', () => {
assert.ok(
workflow.includes('Manual review') || workflow.includes('manual'),
'escalation gate must offer a manual review option'
);
});
test('workflow has text-mode fallback for escalation (plain numbered list)', () => {
assert.ok(
workflow.includes('TEXT_MODE') || workflow.includes('text_mode'),
'workflow must support TEXT_MODE for plain-text escalation prompt'
);
});
});
// ─── Workflow: stall detection — behavioral ───────────────────────────────
describe('plan-review-convergence workflow: stall detection behavioral (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow surfaces stall warning when prev_high_count equals current HIGH_COUNT', () => {
// Behavioral test: two consecutive cycles with the same HIGH count must trigger
// the stall warning. The workflow must compare HIGH_COUNT >= prev_high_count and
// emit a warning string that would appear in output.
assert.ok(
workflow.includes('prev_high_count') || workflow.includes('prev_HIGH'),
'workflow must track prev_high_count across cycles'
);
// The comparison that detects the stall
assert.ok(
workflow.includes('HIGH_COUNT >= prev_high_count') ||
workflow.includes('HIGH_COUNT >= prev_HIGH') ||
workflow.includes('not decreasing'),
'workflow must compare current HIGH count against previous to detect stall'
);
// The stall warning text that appears in output
assert.ok(
workflow.includes('stall') || workflow.includes('Stall') || workflow.includes('not decreasing'),
'workflow must emit a stall warning when HIGH count is not decreasing'
);
});
});
// ─── Workflow: --max-cycles 1 immediate escalation — behavioral ────────────
describe('plan-review-convergence workflow: --max-cycles 1 immediate escalation behavioral (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow escalates immediately after cycle 1 when --max-cycles 1 and HIGH > 0', () => {
// Behavioral test: when max_cycles=1, after the first review cycle, if HIGH_COUNT > 0
// the workflow must trigger the escalation gate (cycle >= MAX_CYCLES check fires on
// cycle 1 itself). Verify the workflow contains the logic for this edge case.
assert.ok(
workflow.includes('cycle >= MAX_CYCLES') ||
workflow.includes('cycle >= max_cycles') ||
(workflow.includes('MAX_CYCLES') && workflow.includes('AskUserQuestion')),
'workflow must check cycle >= MAX_CYCLES so --max-cycles 1 triggers escalation after first cycle'
);
// Escalation gate must fire when HIGH > 0 (not just at exactly max_cycles)
assert.ok(
workflow.includes('HIGH_COUNT > 0') ||
workflow.includes('HIGH concerns remain') ||
workflow.includes('Proceed anyway'),
'escalation gate must be reachable when HIGH_COUNT > 0 after a single cycle'
);
});
});
// ─── Workflow: REVIEWS.md verification ────────────────────────────────────
describe('plan-review-convergence workflow: artifact verification (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow verifies REVIEWS.md exists after each review cycle', () => {
assert.ok(
workflow.includes('REVIEWS.md') || workflow.includes('REVIEWS_FILE'),
'workflow must verify REVIEWS.md was produced by the review agent each cycle'
);
});
test('workflow errors if review agent does not produce REVIEWS.md', () => {
assert.ok(
workflow.includes('REVIEWS_FILE') || workflow.includes('review agent did not produce'),
'workflow must error if the review agent fails to produce REVIEWS.md'
);
});
});