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