diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 418b921d2..186d00c68 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -113,6 +113,12 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
| `response_language` | string | language code | (none) | Language for agent responses (e.g., `"pt"`, `"ko"`, `"ja"`). Propagates to all spawned agents for cross-phase language consistency. Added in v1.32 |
| `context_profile` | string | `dev`, `research`, `review` | (none) | Execution context preset that applies a pre-configured bundle of mode, model, and workflow settings for the current type of work. Added in v1.34 |
| `claude_md_path` | string | any file path | `./CLAUDE.md` | Custom output path for the generated CLAUDE.md file. Useful for monorepos or projects that need CLAUDE.md in a non-root location. Defaults to `./CLAUDE.md` at the project root. Added in v1.36 |
+| `context` | string | any text | (none) | Custom context string injected into every agent prompt for the project. Use to provide persistent project-specific guidance (e.g., coding conventions, team practices) that every agent should be aware of |
+| `phase_naming` | string | any string | (none) | Custom prefix for phase directory names. When set, overrides the auto-generated phase slug (e.g., `"feature"` produces `feature-01-setup/` instead of the roadmap-derived slug) |
+| `brave_search` | boolean | `true`/`false` | auto-detected | Override auto-detection of Brave Search API availability. When unset, GSD checks for `BRAVE_API_KEY` env var or `~/.gsd/brave_api_key` file |
+| `firecrawl` | boolean | `true`/`false` | auto-detected | Override auto-detection of Firecrawl API availability. When unset, GSD checks for `FIRECRAWL_API_KEY` env var or `~/.gsd/firecrawl_api_key` file |
+| `exa_search` | boolean | `true`/`false` | auto-detected | Override auto-detection of Exa Search API availability. When unset, GSD checks for `EXA_API_KEY` env var or `~/.gsd/exa_api_key` file |
+| `search_gitignored` | boolean | `true`/`false` | `false` | Legacy top-level alias for `planning.search_gitignored`. Prefer the namespaced form; this alias is accepted for backward compatibility |
> **Note:** `granularity` was renamed from `depth` in v1.22.3. Existing configs are auto-migrated.
@@ -148,6 +154,11 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
| `workflow.cross_ai_execution` | boolean | `false` | Delegate phase execution to an external AI CLI instead of spawning local executor agents. Useful for leveraging a different model's strengths for specific phases. Added in v1.36 |
| `workflow.cross_ai_command` | string | (none) | Shell command template for cross-AI execution. Receives the phase prompt via stdin. Must produce SUMMARY.md-compatible output. Required when `cross_ai_execution` is `true`. Added in v1.36 |
| `workflow.cross_ai_timeout` | number | `300` | Timeout in seconds for cross-AI execution commands. Prevents runaway external processes. Added in v1.36 |
+| `workflow.ai_integration_phase` | boolean | `true` | Enable the `/gsd-ai-integration-phase` command. When `false`, the command exits with a configuration gate message |
+| `workflow.auto_prune_state` | boolean | `false` | When `true`, automatically prune stale entries from STATE.md at phase boundaries instead of prompting |
+| `workflow.pattern_mapper` | boolean | `true` | Run the `gsd-pattern-mapper` agent between research and planning to map new files to existing codebase analogs |
+| `workflow.subagent_timeout` | number | `600` | Timeout in seconds for individual subagent invocations. Increase for long-running research or execution phases |
+| `workflow.inline_plan_threshold` | number | `3` | Maximum number of tasks in a phase before the planner generates a separate PLAN.md file instead of inlining tasks in the prompt |
### Recommended Presets
@@ -266,6 +277,7 @@ Toggle optional capabilities via the `features.*` config namespace. Feature flag
|---------|------|---------|-------------|
| `features.thinking_partner` | boolean | `false` | Enable thinking partner analysis at workflow decision points |
| `features.global_learnings` | boolean | `false` | Enable cross-project learnings pipeline (auto-copy at phase completion, planner injection) |
+| `learnings.max_inject` | number | `10` | Maximum number of cross-project learnings injected into each planner prompt. Lower values reduce prompt size; higher values provide broader historical context |
| `intel.enabled` | boolean | `false` | Enable queryable codebase intelligence system. When `true`, `/gsd-intel` commands build and query a JSON index in `.planning/intel/`. Added in v1.34 |
@@ -294,6 +306,7 @@ The `features.*` namespace is a dynamic key pattern — new feature flags can be
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
+| `parallelization` | boolean | `true` | Shorthand for `parallelization.enabled`. Setting `parallelization false` disables parallel execution without changing other sub-keys |
| `parallelization.enabled` | boolean | `true` | Run independent plans simultaneously |
| `parallelization.plan_level` | boolean | `true` | Parallelize at plan level |
| `parallelization.task_level` | boolean | `false` | Parallelize tasks within a plan |
@@ -310,6 +323,7 @@ The `features.*` namespace is a dynamic key pattern — new feature flags can be
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `git.branching_strategy` | enum | `none` | `none`, `phase`, or `milestone` |
+| `git.base_branch` | string | `main` | The integration branch that phase/milestone branches are created from and merged back into. Override when your repo uses `master` or a release branch |
| `git.phase_branch_template` | string | `gsd/phase-{phase}-{slug}` | Branch name template for phase strategy |
| `git.milestone_branch_template` | string | `gsd/{milestone}-{slug}` | Branch name template for milestone strategy |
| `git.quick_branch_template` | string or null | `null` | Optional branch name template for `/gsd-quick` tasks |
diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json
new file mode 100644
index 000000000..b4666f178
--- /dev/null
+++ b/docs/INVENTORY-MANIFEST.json
@@ -0,0 +1,296 @@
+{
+ "generated": "2026-04-20",
+ "families": {
+ "agents": [
+ "gsd-advisor-researcher",
+ "gsd-ai-researcher",
+ "gsd-assumptions-analyzer",
+ "gsd-code-fixer",
+ "gsd-code-reviewer",
+ "gsd-codebase-mapper",
+ "gsd-debug-session-manager",
+ "gsd-debugger",
+ "gsd-doc-classifier",
+ "gsd-doc-synthesizer",
+ "gsd-doc-verifier",
+ "gsd-doc-writer",
+ "gsd-domain-researcher",
+ "gsd-eval-auditor",
+ "gsd-eval-planner",
+ "gsd-executor",
+ "gsd-framework-selector",
+ "gsd-integration-checker",
+ "gsd-intel-updater",
+ "gsd-nyquist-auditor",
+ "gsd-pattern-mapper",
+ "gsd-phase-researcher",
+ "gsd-plan-checker",
+ "gsd-planner",
+ "gsd-project-researcher",
+ "gsd-research-synthesizer",
+ "gsd-roadmapper",
+ "gsd-security-auditor",
+ "gsd-ui-auditor",
+ "gsd-ui-checker",
+ "gsd-ui-researcher",
+ "gsd-user-profiler",
+ "gsd-verifier"
+ ],
+ "commands": [
+ "/gsd-add-backlog",
+ "/gsd-add-phase",
+ "/gsd-add-tests",
+ "/gsd-add-todo",
+ "/gsd-ai-integration-phase",
+ "/gsd-analyze-dependencies",
+ "/gsd-audit-fix",
+ "/gsd-audit-milestone",
+ "/gsd-audit-uat",
+ "/gsd-autonomous",
+ "/gsd-check-todos",
+ "/gsd-cleanup",
+ "/gsd-code-review",
+ "/gsd-code-review-fix",
+ "/gsd-complete-milestone",
+ "/gsd-debug",
+ "/gsd-discuss-phase",
+ "/gsd-do",
+ "/gsd-docs-update",
+ "/gsd-eval-review",
+ "/gsd-execute-phase",
+ "/gsd-explore",
+ "/gsd-extract_learnings",
+ "/gsd-fast",
+ "/gsd-forensics",
+ "/gsd-from-gsd2",
+ "/gsd-graphify",
+ "/gsd-health",
+ "/gsd-help",
+ "/gsd-import",
+ "/gsd-inbox",
+ "/gsd-ingest-docs",
+ "/gsd-insert-phase",
+ "/gsd-intel",
+ "/gsd-join-discord",
+ "/gsd-list-phase-assumptions",
+ "/gsd-list-workspaces",
+ "/gsd-manager",
+ "/gsd-map-codebase",
+ "/gsd-milestone-summary",
+ "/gsd-new-milestone",
+ "/gsd-new-project",
+ "/gsd-new-workspace",
+ "/gsd-next",
+ "/gsd-note",
+ "/gsd-pause-work",
+ "/gsd-plan-milestone-gaps",
+ "/gsd-plan-phase",
+ "/gsd-plan-review-convergence",
+ "/gsd-plant-seed",
+ "/gsd-pr-branch",
+ "/gsd-profile-user",
+ "/gsd-progress",
+ "/gsd-quick",
+ "/gsd-reapply-patches",
+ "/gsd-remove-phase",
+ "/gsd-remove-workspace",
+ "/gsd-research-phase",
+ "/gsd-resume-work",
+ "/gsd-review",
+ "/gsd-review-backlog",
+ "/gsd-scan",
+ "/gsd-secure-phase",
+ "/gsd-session-report",
+ "/gsd-set-profile",
+ "/gsd-settings",
+ "/gsd-ship",
+ "/gsd-sketch",
+ "/gsd-sketch-wrap-up",
+ "/gsd-spec-phase",
+ "/gsd-spike",
+ "/gsd-spike-wrap-up",
+ "/gsd-stats",
+ "/gsd-thread",
+ "/gsd-ui-phase",
+ "/gsd-ui-review",
+ "/gsd-ultraplan-phase",
+ "/gsd-undo",
+ "/gsd-update",
+ "/gsd-validate-phase",
+ "/gsd-verify-work",
+ "/gsd-workstreams"
+ ],
+ "workflows": [
+ "add-phase.md",
+ "add-tests.md",
+ "add-todo.md",
+ "ai-integration-phase.md",
+ "analyze-dependencies.md",
+ "audit-fix.md",
+ "audit-milestone.md",
+ "audit-uat.md",
+ "autonomous.md",
+ "check-todos.md",
+ "cleanup.md",
+ "code-review-fix.md",
+ "code-review.md",
+ "complete-milestone.md",
+ "diagnose-issues.md",
+ "discovery-phase.md",
+ "discuss-phase-assumptions.md",
+ "discuss-phase-power.md",
+ "discuss-phase.md",
+ "do.md",
+ "docs-update.md",
+ "eval-review.md",
+ "execute-phase.md",
+ "execute-plan.md",
+ "explore.md",
+ "extract_learnings.md",
+ "fast.md",
+ "forensics.md",
+ "health.md",
+ "help.md",
+ "import.md",
+ "inbox.md",
+ "ingest-docs.md",
+ "insert-phase.md",
+ "list-phase-assumptions.md",
+ "list-workspaces.md",
+ "manager.md",
+ "map-codebase.md",
+ "milestone-summary.md",
+ "new-milestone.md",
+ "new-project.md",
+ "new-workspace.md",
+ "next.md",
+ "node-repair.md",
+ "note.md",
+ "pause-work.md",
+ "plan-milestone-gaps.md",
+ "plan-phase.md",
+ "plan-review-convergence.md",
+ "plant-seed.md",
+ "pr-branch.md",
+ "profile-user.md",
+ "progress.md",
+ "quick.md",
+ "remove-phase.md",
+ "remove-workspace.md",
+ "research-phase.md",
+ "resume-project.md",
+ "review.md",
+ "scan.md",
+ "secure-phase.md",
+ "session-report.md",
+ "settings.md",
+ "ship.md",
+ "sketch-wrap-up.md",
+ "sketch.md",
+ "spec-phase.md",
+ "spike-wrap-up.md",
+ "spike.md",
+ "stats.md",
+ "transition.md",
+ "ui-phase.md",
+ "ui-review.md",
+ "ultraplan-phase.md",
+ "undo.md",
+ "update.md",
+ "validate-phase.md",
+ "verify-phase.md",
+ "verify-work.md"
+ ],
+ "references": [
+ "agent-contracts.md",
+ "ai-evals.md",
+ "ai-frameworks.md",
+ "artifact-types.md",
+ "autonomous-smart-discuss.md",
+ "checkpoints.md",
+ "common-bug-patterns.md",
+ "context-budget.md",
+ "continuation-format.md",
+ "debugger-philosophy.md",
+ "decimal-phase-calculation.md",
+ "doc-conflict-engine.md",
+ "domain-probes.md",
+ "executor-examples.md",
+ "gate-prompts.md",
+ "gates.md",
+ "git-integration.md",
+ "git-planning-commit.md",
+ "ios-scaffold.md",
+ "mandatory-initial-read.md",
+ "model-profile-resolution.md",
+ "model-profiles.md",
+ "phase-argument-parsing.md",
+ "planner-antipatterns.md",
+ "planner-gap-closure.md",
+ "planner-reviews.md",
+ "planner-revision.md",
+ "planner-source-audit.md",
+ "planning-config.md",
+ "project-skills-discovery.md",
+ "questioning.md",
+ "revision-loop.md",
+ "sketch-interactivity.md",
+ "sketch-theme-system.md",
+ "sketch-tooling.md",
+ "sketch-variant-patterns.md",
+ "tdd.md",
+ "thinking-models-debug.md",
+ "thinking-models-execution.md",
+ "thinking-models-planning.md",
+ "thinking-models-research.md",
+ "thinking-models-verification.md",
+ "thinking-partner.md",
+ "ui-brand.md",
+ "universal-anti-patterns.md",
+ "user-profiling.md",
+ "verification-overrides.md",
+ "verification-patterns.md",
+ "workstream-flag.md"
+ ],
+ "cli_modules": [
+ "audit.cjs",
+ "commands.cjs",
+ "config-schema.cjs",
+ "config.cjs",
+ "core.cjs",
+ "docs.cjs",
+ "frontmatter.cjs",
+ "graphify.cjs",
+ "gsd2-import.cjs",
+ "init.cjs",
+ "intel.cjs",
+ "learnings.cjs",
+ "milestone.cjs",
+ "model-profiles.cjs",
+ "phase.cjs",
+ "profile-output.cjs",
+ "profile-pipeline.cjs",
+ "roadmap.cjs",
+ "schema-detect.cjs",
+ "security.cjs",
+ "state.cjs",
+ "template.cjs",
+ "uat.cjs",
+ "verify.cjs",
+ "workstream.cjs"
+ ],
+ "hooks": [
+ "gsd-check-update-worker.js",
+ "gsd-check-update.js",
+ "gsd-context-monitor.js",
+ "gsd-phase-boundary.sh",
+ "gsd-prompt-guard.js",
+ "gsd-read-guard.js",
+ "gsd-read-injection-scanner.js",
+ "gsd-session-state.sh",
+ "gsd-statusline.js",
+ "gsd-validate-commit.sh",
+ "gsd-workflow-guard.js"
+ ]
+ }
+}
diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md
index 5ae187f72..e7021fd93 100644
--- a/docs/INVENTORY.md
+++ b/docs/INVENTORY.md
@@ -350,7 +350,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
---
-## CLI Modules (24 shipped)
+## CLI Modules (25 shipped)
Full listing: `get-shit-done/bin/lib/*.cjs`.
@@ -358,7 +358,8 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
|--------|----------------|
| `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers |
| `commands.cjs` | Misc CLI commands (slug, timestamp, todos, scaffolding, stats) |
-| `config.cjs` | `config.json` read/write, section initialization, `VALID_CONFIG_KEYS` |
+| `config-schema.cjs` | Single source of truth for `VALID_CONFIG_KEYS` and dynamic key patterns; imported by both the validator and the config-schema-docs parity test |
+| `config.cjs` | `config.json` read/write, section initialization; imports validator from `config-schema.cjs` |
| `core.cjs` | Error handling, output formatting, shared utilities, runtime fallbacks |
| `docs.cjs` | Docs-update workflow init, Markdown scanning, monorepo detection |
| `frontmatter.cjs` | YAML frontmatter CRUD operations |
diff --git a/get-shit-done/bin/lib/config-schema.cjs b/get-shit-done/bin/lib/config-schema.cjs
new file mode 100644
index 000000000..bea2658f6
--- /dev/null
+++ b/get-shit-done/bin/lib/config-schema.cjs
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * Single source of truth for valid config key paths.
+ *
+ * Imported by:
+ * - config.cjs (isValidConfigKey validator)
+ * - tests/config-schema-docs-parity.test.cjs (CI drift guard)
+ *
+ * Adding a key here without documenting it in docs/CONFIGURATION.md will
+ * fail the parity test. Adding a key to docs/CONFIGURATION.md without
+ * adding it here will cause config-set to reject it at runtime.
+ */
+
+/** Exact-match config key paths accepted by config-set. */
+const VALID_CONFIG_KEYS = new Set([
+ 'mode', 'granularity', 'parallelization', 'commit_docs', 'model_profile',
+ 'search_gitignored', 'brave_search', 'firecrawl', 'exa_search',
+ 'workflow.research', 'workflow.plan_check', 'workflow.verifier',
+ 'workflow.nyquist_validation', 'workflow.ai_integration_phase', 'workflow.ui_phase', 'workflow.ui_safety_gate',
+ 'workflow.auto_advance', 'workflow.node_repair', 'workflow.node_repair_budget',
+ 'workflow.tdd_mode',
+ 'workflow.text_mode',
+ 'workflow.research_before_questions',
+ 'workflow.discuss_mode',
+ 'workflow.skip_discuss',
+ 'workflow.auto_prune_state',
+ 'workflow._auto_chain_active',
+ 'workflow.use_worktrees',
+ 'workflow.code_review',
+ 'workflow.code_review_depth',
+ 'workflow.code_review_command',
+ 'workflow.pattern_mapper',
+ 'workflow.plan_bounce',
+ 'workflow.plan_bounce_script',
+ 'workflow.plan_bounce_passes',
+ 'workflow.security_enforcement',
+ 'workflow.security_asvs_level',
+ 'workflow.security_block_on',
+ 'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
+ 'planning.commit_docs', 'planning.search_gitignored', 'planning.sub_repos',
+ 'workflow.cross_ai_execution', 'workflow.cross_ai_command', 'workflow.cross_ai_timeout',
+ 'workflow.subagent_timeout',
+ 'workflow.inline_plan_threshold',
+ 'hooks.context_warnings',
+ 'features.thinking_partner',
+ 'context',
+ 'features.global_learnings',
+ 'learnings.max_inject',
+ 'project_code', 'phase_naming',
+ 'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute',
+ 'response_language',
+ 'intel.enabled',
+ 'graphify.enabled',
+ 'graphify.build_timeout',
+ 'claude_md_path',
+]);
+
+/**
+ * Dynamic-pattern validators — keys matching these regexes are also accepted.
+ * Each entry has a `test` function and a human-readable `description`.
+ */
+const DYNAMIC_KEY_PATTERNS = [
+ { test: (k) => /^agent_skills\.[a-zA-Z0-9_-]+$/.test(k), description: 'agent_skills.' },
+ { test: (k) => /^review\.models\.[a-zA-Z0-9_-]+$/.test(k), description: 'review.models.' },
+ { test: (k) => /^features\.[a-zA-Z0-9_]+$/.test(k), description: 'features.' },
+];
+
+/**
+ * Returns true if keyPath is a valid config key (exact or dynamic pattern).
+ */
+function isValidConfigKey(keyPath) {
+ if (VALID_CONFIG_KEYS.has(keyPath)) return true;
+ return DYNAMIC_KEY_PATTERNS.some((p) => p.test(keyPath));
+}
+
+module.exports = { VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, isValidConfigKey };
diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs
index 6b9d5e3a4..4f5079755 100644
--- a/get-shit-done/bin/lib/config.cjs
+++ b/get-shit-done/bin/lib/config.cjs
@@ -10,70 +10,7 @@ const {
getAgentToModelMapForProfile,
formatAgentToModelMapAsTable,
} = require('./model-profiles.cjs');
-
-const VALID_CONFIG_KEYS = new Set([
- 'mode', 'granularity', 'parallelization', 'commit_docs', 'model_profile',
- 'search_gitignored', 'brave_search', 'firecrawl', 'exa_search',
- 'workflow.research', 'workflow.plan_check', 'workflow.verifier',
- 'workflow.nyquist_validation', 'workflow.ai_integration_phase', 'workflow.ui_phase', 'workflow.ui_safety_gate',
- 'workflow.auto_advance', 'workflow.node_repair', 'workflow.node_repair_budget',
- 'workflow.tdd_mode',
- 'workflow.text_mode',
- 'workflow.security_asvs_level',
- 'workflow.security_block_on',
- 'workflow.security_enforcement',
- 'workflow.research_before_questions',
- 'workflow.discuss_mode',
- 'workflow.skip_discuss',
- 'workflow.auto_prune_state',
- 'workflow._auto_chain_active',
- 'workflow.use_worktrees',
- 'workflow.code_review',
- 'workflow.code_review_depth',
- 'workflow.code_review_command',
- 'workflow.pattern_mapper',
- 'workflow.plan_bounce',
- 'workflow.plan_bounce_script',
- 'workflow.plan_bounce_passes',
- 'workflow.security_enforcement',
- 'workflow.security_asvs_level',
- 'workflow.security_block_on',
- 'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
- 'planning.commit_docs', 'planning.search_gitignored',
- 'workflow.cross_ai_execution', 'workflow.cross_ai_command', 'workflow.cross_ai_timeout',
- 'workflow.subagent_timeout',
- 'workflow.inline_plan_threshold',
- 'hooks.context_warnings',
- 'features.thinking_partner',
- 'context',
- 'features.global_learnings',
- 'learnings.max_inject',
- 'project_code', 'phase_naming',
- 'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute',
- 'response_language',
- 'intel.enabled',
- 'graphify.enabled',
- 'graphify.build_timeout',
- 'claude_md_path',
-]);
-
-/**
- * Check whether a config key path is valid.
- * Supports exact matches from VALID_CONFIG_KEYS plus dynamic patterns
- * like `agent_skills.` where the sub-key is freeform.
- */
-function isValidConfigKey(keyPath) {
- if (VALID_CONFIG_KEYS.has(keyPath)) return true;
- // Allow agent_skills. with any agent type string
- if (/^agent_skills\.[a-zA-Z0-9_-]+$/.test(keyPath)) return true;
- // Allow review.models. for per-CLI model selection in /gsd-review
- if (/^review\.models\.[a-zA-Z0-9_-]+$/.test(keyPath)) return true;
- // Allow features. — dynamic namespace for feature flags.
- // Intentionally open-ended so new flags (e.g., features.global_learnings) work
- // without updating VALID_CONFIG_KEYS each time.
- if (/^features\.[a-zA-Z0-9_]+$/.test(keyPath)) return true;
- return false;
-}
+const { VALID_CONFIG_KEYS, isValidConfigKey } = require('./config-schema.cjs');
const CONFIG_KEY_SUGGESTIONS = {
'workflow.nyquist_validation_enabled': 'workflow.nyquist_validation',
diff --git a/scripts/gen-inventory-manifest.cjs b/scripts/gen-inventory-manifest.cjs
new file mode 100644
index 000000000..fc3390591
--- /dev/null
+++ b/scripts/gen-inventory-manifest.cjs
@@ -0,0 +1,109 @@
+#!/usr/bin/env node
+'use strict';
+
+/**
+ * Generates docs/INVENTORY-MANIFEST.json — a structural skeleton of every
+ * shipped surface derived entirely from the filesystem. Commit this file;
+ * CI re-runs the script and diffs. A non-empty diff means a surface shipped
+ * without an INVENTORY.md row.
+ *
+ * Usage:
+ * node scripts/gen-inventory-manifest.cjs # print to stdout
+ * node scripts/gen-inventory-manifest.cjs --write # write docs/INVENTORY-MANIFEST.json
+ * node scripts/gen-inventory-manifest.cjs --check # exit 1 if committed manifest is stale
+ */
+
+const fs = require('node:fs');
+const path = require('node:path');
+
+const ROOT = path.resolve(__dirname, '..');
+const MANIFEST_PATH = path.join(ROOT, 'docs', 'INVENTORY-MANIFEST.json');
+
+const FAMILIES = [
+ {
+ name: 'agents',
+ dir: path.join(ROOT, 'agents'),
+ filter: (f) => /^gsd-.*\.md$/.test(f),
+ toName: (f) => f.replace(/\.md$/, ''),
+ },
+ {
+ name: 'commands',
+ dir: path.join(ROOT, 'commands', 'gsd'),
+ filter: (f) => f.endsWith('.md'),
+ toName: (f) => '/gsd-' + f.replace(/\.md$/, ''),
+ },
+ {
+ name: 'workflows',
+ dir: path.join(ROOT, 'get-shit-done', 'workflows'),
+ filter: (f) => f.endsWith('.md'),
+ toName: (f) => f,
+ },
+ {
+ name: 'references',
+ dir: path.join(ROOT, 'get-shit-done', 'references'),
+ filter: (f) => f.endsWith('.md'),
+ toName: (f) => f,
+ },
+ {
+ name: 'cli_modules',
+ dir: path.join(ROOT, 'get-shit-done', 'bin', 'lib'),
+ filter: (f) => f.endsWith('.cjs'),
+ toName: (f) => f,
+ },
+ {
+ name: 'hooks',
+ dir: path.join(ROOT, 'hooks'),
+ filter: (f) => /\.(js|sh)$/.test(f),
+ toName: (f) => f,
+ },
+];
+
+function buildManifest() {
+ const manifest = { generated: new Date().toISOString().slice(0, 10), families: {} };
+ for (const { name, dir, filter, toName } of FAMILIES) {
+ manifest.families[name] = fs
+ .readdirSync(dir)
+ .filter((f) => fs.statSync(path.join(dir, f)).isFile() && filter(f))
+ .map(toName)
+ .sort();
+ }
+ return manifest;
+}
+
+const [, , flag] = process.argv;
+
+if (flag === '--check') {
+ const committed = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
+ const live = buildManifest();
+ // Strip the generated date for comparison
+ delete committed.generated;
+ delete live.generated;
+ const committedStr = JSON.stringify(committed, null, 2);
+ const liveStr = JSON.stringify(live, null, 2);
+ if (committedStr !== liveStr) {
+ process.stderr.write(
+ 'docs/INVENTORY-MANIFEST.json is stale. Run:\n' +
+ ' node scripts/gen-inventory-manifest.cjs --write\n' +
+ 'then add a matching row in docs/INVENTORY.md for each new entry.\n\n',
+ );
+ // Show diff-friendly output
+ for (const family of Object.keys(live.families)) {
+ const liveSet = new Set(live.families[family]);
+ const committedSet = new Set((committed.families || {})[family] || []);
+ for (const name of liveSet) {
+ if (!committedSet.has(name)) process.stderr.write(' + ' + family + '/' + name + '\n');
+ }
+ for (const name of committedSet) {
+ if (!liveSet.has(name)) process.stderr.write(' - ' + family + '/' + name + '\n');
+ }
+ }
+ process.exit(1);
+ }
+ process.stdout.write('docs/INVENTORY-MANIFEST.json is up to date.\n');
+} else if (flag === '--write') {
+ const manifest = buildManifest();
+ fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
+ process.stdout.write('Wrote ' + MANIFEST_PATH + '\n');
+} else {
+ process.stdout.write(JSON.stringify(buildManifest(), null, 2) + '\n');
+}
diff --git a/tests/config-schema-docs-parity.test.cjs b/tests/config-schema-docs-parity.test.cjs
new file mode 100644
index 000000000..a2e91a5c2
--- /dev/null
+++ b/tests/config-schema-docs-parity.test.cjs
@@ -0,0 +1,40 @@
+'use strict';
+
+/**
+ * Asserts every exact-match key in config-schema.cjs appears at least once
+ * in docs/CONFIGURATION.md. A key present in the validator but absent from
+ * the docs means users can set it but have no guidance. A key in the docs but
+ * absent from the validator means config-set silently rejects it.
+ *
+ * Dynamic patterns (agent_skills.*, review.models.*, features.*) are excluded
+ * from this check — they are documented by namespace in CONFIGURATION.md.
+ */
+
+const { 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 { VALID_CONFIG_KEYS } = require('../get-shit-done/bin/lib/config-schema.cjs');
+const CONFIGURATION_MD = fs.readFileSync(path.join(ROOT, 'docs', 'CONFIGURATION.md'), 'utf8');
+
+// Keys starting with _ are internal runtime state, not user-facing config.
+const INTERNAL_KEYS = new Set(['workflow._auto_chain_active']);
+
+test('every key in VALID_CONFIG_KEYS is documented in docs/CONFIGURATION.md', () => {
+ const undocumented = [];
+ for (const key of VALID_CONFIG_KEYS) {
+ if (INTERNAL_KEYS.has(key)) continue;
+ if (!CONFIGURATION_MD.includes('`' + key + '`')) {
+ undocumented.push(key);
+ }
+ }
+ assert.deepStrictEqual(
+ undocumented,
+ [],
+ 'Keys in VALID_CONFIG_KEYS with no mention in docs/CONFIGURATION.md:\n' +
+ undocumented.map((k) => ' ' + k).join('\n') +
+ '\nAdd a row in the appropriate section of docs/CONFIGURATION.md.',
+ );
+});
diff --git a/tests/inventory-manifest-sync.test.cjs b/tests/inventory-manifest-sync.test.cjs
new file mode 100644
index 000000000..f6048a73c
--- /dev/null
+++ b/tests/inventory-manifest-sync.test.cjs
@@ -0,0 +1,54 @@
+'use strict';
+
+/**
+ * Asserts docs/INVENTORY-MANIFEST.json is in sync with the filesystem.
+ * A stale manifest means a surface shipped without updating INVENTORY.md.
+ * Fix by running: node scripts/gen-inventory-manifest.cjs --write
+ * then adding the corresponding row(s) in docs/INVENTORY.md.
+ */
+
+const { 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 MANIFEST_PATH = path.join(ROOT, 'docs', 'INVENTORY-MANIFEST.json');
+
+const FAMILIES = [
+ { name: 'agents', dir: path.join(ROOT, 'agents'), filter: (f) => /^gsd-.*\.md$/.test(f), toName: (f) => f.replace(/\.md$/, '') },
+ { name: 'commands', dir: path.join(ROOT, 'commands', 'gsd'), filter: (f) => f.endsWith('.md'), toName: (f) => '/gsd-' + f.replace(/\.md$/, '') },
+ { name: 'workflows', dir: path.join(ROOT, 'get-shit-done', 'workflows'), filter: (f) => f.endsWith('.md'), toName: (f) => f },
+ { name: 'references', dir: path.join(ROOT, 'get-shit-done', 'references'), filter: (f) => f.endsWith('.md'), toName: (f) => f },
+ { name: 'cli_modules', dir: path.join(ROOT, 'get-shit-done', 'bin', 'lib'), filter: (f) => f.endsWith('.cjs'), toName: (f) => f },
+ { name: 'hooks', dir: path.join(ROOT, 'hooks'), filter: (f) => /\.(js|sh)$/.test(f), toName: (f) => f },
+];
+
+test('docs/INVENTORY-MANIFEST.json matches the filesystem', () => {
+ const committed = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
+ const additions = [];
+ const removals = [];
+
+ for (const { name, dir, filter, toName } of FAMILIES) {
+ const live = new Set(
+ fs.readdirSync(dir)
+ .filter((f) => fs.statSync(path.join(dir, f)).isFile() && filter(f))
+ .map(toName),
+ );
+ const recorded = new Set((committed.families || {})[name] || []);
+
+ for (const entry of live) {
+ if (!recorded.has(entry)) additions.push(name + '/' + entry);
+ }
+ for (const entry of recorded) {
+ if (!live.has(entry)) removals.push(name + '/' + entry);
+ }
+ }
+
+ const msg = [
+ additions.length ? 'New surfaces not in manifest (run node scripts/gen-inventory-manifest.cjs --write):\n' + additions.map((e) => ' + ' + e).join('\n') : '',
+ removals.length ? 'Manifest entries with no matching file:\n' + removals.map((e) => ' - ' + e).join('\n') : '',
+ ].filter(Boolean).join('\n');
+
+ assert.ok(additions.length === 0 && removals.length === 0, msg);
+});
diff --git a/tests/inventory-source-parity.test.cjs b/tests/inventory-source-parity.test.cjs
new file mode 100644
index 000000000..785331abb
--- /dev/null
+++ b/tests/inventory-source-parity.test.cjs
@@ -0,0 +1,132 @@
+'use strict';
+
+/**
+ * Reverse-direction parity: every row declared in docs/INVENTORY.md must
+ * resolve to a real file on the filesystem. Complements the forward tests
+ * (actual ⊆ INVENTORY) with the reverse direction (INVENTORY ⊆ actual),
+ * catching ghost entries left behind when artifacts are deleted or renamed.
+ */
+
+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 = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
+
+/** Extract the text of a named top-level section (## Header ... next ##). */
+function section(header) {
+ const start = INVENTORY.indexOf('## ' + header);
+ if (start === -1) return '';
+ const next = INVENTORY.indexOf('\n## ', start + 1);
+ return next === -1 ? INVENTORY.slice(start) : INVENTORY.slice(start, next);
+}
+
+/** Extract backtick-quoted filenames from column-1 table cells. */
+function backtickNames(text, ext) {
+ const re = new RegExp('\\|\\s*`([^`]+\\.' + ext + ')`\\s*\\|', 'gm');
+ const names = [];
+ let m;
+ while ((m = re.exec(text)) !== null) names.push(m[1]);
+ return names;
+}
+
+/** Extract agent names from `| gsd-xxx | ...` rows (no backticks). */
+function agentNames(text) {
+ const re = /^\|\s*(gsd-[a-z0-9-]+)\s*\|/gm;
+ const names = [];
+ let m;
+ while ((m = re.exec(text)) !== null) names.push(m[1]);
+ return names;
+}
+
+/** Extract relative source paths from markdown links in Commands section. */
+function commandSourcePaths(text) {
+ const re = /\[commands\/gsd\/[^\]]+\]\(\.\.\/(commands\/gsd\/[^)]+)\)/g;
+ const paths = [];
+ let m;
+ while ((m = re.exec(text)) !== null) paths.push(m[1]);
+ return paths;
+}
+
+describe('INVENTORY.md declared artifacts exist on the filesystem (ghost-entry guard)', () => {
+ describe('Agents', () => {
+ const names = agentNames(section('Agents'));
+ for (const name of names) {
+ test(name, () => {
+ const p = path.join(ROOT, 'agents', name + '.md');
+ assert.ok(
+ fs.existsSync(p),
+ 'INVENTORY.md declares agent "' + name + '" but agents/' + name + '.md does not exist — remove the ghost row or restore the file',
+ );
+ });
+ }
+ });
+
+ describe('Commands', () => {
+ const paths = commandSourcePaths(section('Commands'));
+ for (const rel of paths) {
+ test(rel, () => {
+ const p = path.join(ROOT, rel);
+ assert.ok(
+ fs.existsSync(p),
+ 'INVENTORY.md declares source "' + rel + '" but the file does not exist — remove the ghost row or restore the file',
+ );
+ });
+ }
+ });
+
+ describe('Workflows', () => {
+ const names = backtickNames(section('Workflows'), 'md');
+ for (const name of names) {
+ test(name, () => {
+ const p = path.join(ROOT, 'get-shit-done', 'workflows', name);
+ assert.ok(
+ fs.existsSync(p),
+ 'INVENTORY.md declares workflow "' + name + '" but get-shit-done/workflows/' + name + ' does not exist — remove the ghost row or restore the file',
+ );
+ });
+ }
+ });
+
+ describe('References', () => {
+ const names = backtickNames(section('References'), 'md');
+ for (const name of names) {
+ test(name, () => {
+ const p = path.join(ROOT, 'get-shit-done', 'references', name);
+ assert.ok(
+ fs.existsSync(p),
+ 'INVENTORY.md declares reference "' + name + '" but get-shit-done/references/' + name + ' does not exist — remove the ghost row or restore the file',
+ );
+ });
+ }
+ });
+
+ describe('CLI Modules', () => {
+ const names = backtickNames(section('CLI Modules'), 'cjs');
+ for (const name of names) {
+ test(name, () => {
+ const p = path.join(ROOT, 'get-shit-done', 'bin', 'lib', name);
+ assert.ok(
+ fs.existsSync(p),
+ 'INVENTORY.md declares CLI module "' + name + '" but get-shit-done/bin/lib/' + name + ' does not exist — remove the ghost row or restore the file',
+ );
+ });
+ }
+ });
+
+ describe('Hooks', () => {
+ const jsNames = backtickNames(section('Hooks'), 'js');
+ const shNames = backtickNames(section('Hooks'), 'sh');
+ for (const name of [...jsNames, ...shNames]) {
+ test(name, () => {
+ const p = path.join(ROOT, 'hooks', name);
+ assert.ok(
+ fs.existsSync(p),
+ 'INVENTORY.md declares hook "' + name + '" but hooks/' + name + ' does not exist — remove the ghost row or restore the file',
+ );
+ });
+ }
+ });
+});