feat(3536): Configuration Module via shared manifests + generator (Phase 2 of #3524)

Phase 2 of the CJS↔SDK hard-seam migration (parent #3524).
Eliminates the structural drift surface that produced bug class

After this phase, neither bin/lib/ nor sdk/src/ defines
CONFIG_DEFAULTS, VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, or the
four legacy-key normalizations inline. All come from one canonical
source: the Configuration Module (sdk/src/configuration/index.ts)
+ two JSON manifests (sdk/shared/config-{defaults,schema}.manifest.json).
The CJS mirror is generator-emitted (get-shit-done/bin/lib/configuration.generated.cjs)
with a CI freshness check (sdk/scripts/check-configuration-fresh.mjs).

- sdk/shared/config-defaults.manifest.json — canonical nested defaults,
  union of CJS + SDK keys (includes security_*, post_planning_gaps,
  agent_skills, mode, every git/workflow/hooks sub-section).
- sdk/shared/config-schema.manifest.json — VALID_CONFIG_KEYS array,
  RUNTIME_STATE_KEYS array, DYNAMIC_KEY_PATTERNS array with source
  strings (regex reconstructed at runtime).
- sdk/src/configuration/index.ts — source of truth. Exports
  loadConfig (pure read), normalizeLegacyKeys (pure, idempotent,
  returns Normalization[]), mergeDefaults (deep-merge), migrateOnDisk
  (explicit opt-in disk writeback), plus CONFIG_DEFAULTS,
  VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS.
- sdk/src/configuration/index.test.ts — 29 vitest pinning tests.
- sdk/scripts/gen-configuration.mjs — generator (Function.prototype.toString()
  inspection of compiled SDK dist, plus brace-balanced text scan for
  internal helpers, matching the Phase 1 pattern).
- sdk/scripts/check-configuration-fresh.mjs — CI freshness gate.
- tests/configuration-generator.test.cjs — 27 parity assertions
  (CJS-generated == SDK source).
- tests/configuration-migrate-config.test.cjs — 3 cases for the new
  gsd-tools migrate-config subcommand.

- bin/lib/core.cjs: CONFIG_DEFAULTS literal now sources values from
  CANONICAL_CONFIG_DEFAULTS (the manifest), with a thin flat
  projection at the load boundary to preserve the existing
  flat-shape return contract for the ~21 CJS test files and 100+
  consumers. All four legacy-key migration blocks (branching_strategy,
  sub_repos, multiRepo, depth — historically lines 351-358, 388-397,
  401-408, 416-423) collapse to a single normalizeLegacyKeys call
  in each code path. The inline platformWriteSync writeback stays
  for now to preserve sync loadConfig semantics; the new async
  migrateOnDisk is reachable via gsd-tools migrate-config.
- bin/lib/config-schema.cjs: 135 → 31 lines. Re-exports from the
  generated Module.
- bin/lib/config.cjs: adds cmdMigrateConfig handler (calls
  migrateOnDisk on the explicit user-driven path).
- bin/gsd-tools.cjs: wires migrate-config into command dispatch.

- sdk/src/config.ts: re-exports CONFIG_DEFAULTS and mergeDefaults
  from the Module. loadConfig now calls normalizeLegacyKeys before
  mergeDefaults (replaces the inline branching_strategy graft).
- sdk/src/query/config-schema.ts: 160 → 36 lines. Re-exports from
  the Module.

- tests/config-schema-sdk-parity.test.cjs: refactored from
  "CJS Set equals SDK Set" (trivially true post-migration) to
  "both sides source from the manifest" — structural plus runtime
  invariant.
- Four other tests that text-grepped source files for valid keys
  (plan-review-convergence, bug-3212, bug-2492, feat-3210) are
  updated to use runtime VALID_CONFIG_KEYS.has() or manifest JSON
  lookups.

- CONTEXT.md: new Configuration Module entry with full Interface
  contract.
- Root package.json: check:configuration-fresh proxy script.
- sdk/package.json: gen:configuration + check:configuration-fresh.
- .githooks/pre-commit: configuration drift block.
- .github/workflows/test.yml: configuration drift step after the
  alias drift check.

- 9201 CJS tests pass (baseline pre-cycle: 9195; +6 net new tests
  across migrate-config + parity refactor)
- 1872 SDK vitest tests pass
- 29 Configuration Module vitest fixtures
- 27 CJS/SDK parity fixtures
- Net diff: +388 / −519 = 131-line reduction across the seven cycles,
  despite adding the new Module, manifests, generator, freshness
  check, and two new test files.

1. SDK CONFIG_DEFAULTS now includes manifest-canonical keys
   (resolve_model_ids: false, context_window: 200000, phase_naming,
   claude_md_path, git.create_tag, workflow.security_*,
   workflow.code_review_*, planning.*, hooks.workflow_guard, ship.*).
   Consumers accessing via [key: string]: unknown index get
   the manifest default instead of undefined.
2. SDK mergeDefaults is now proper recursive deep-merge instead of
   spread-per-section. Overlay { workflow: { research: false } }
   now preserves sibling workflow keys; previously it replaced
   the entire workflow section with only research + the section's
   defaults. Semantically identical for the common case;
   strictly better for partial nested overrides.
3. New gsd-tools migrate-config CLI subcommand for the explicit,
   opt-in on-disk migration path.

Closes #3536.
This commit is contained in:
Tom Boucher
2026-05-14 23:50:15 -04:00
parent a7f0af2ce9
commit 2de2d185fa
28 changed files with 2166 additions and 522 deletions

View File

@@ -0,0 +1,72 @@
{
"_comment": "Canonical CONFIG_DEFAULTS for the Configuration Module. Nested shape is canonical. CJS flat projection (branching_strategy, sub_repos, etc.) is handled by consumers at the boundary. Security keys (security_enforcement, security_asvs_level, security_block_on) and post_planning_gaps live in workflow.* as their canonical location. resolve_model_ids, context_window, phase_naming are CJS-originated top-level keys included here. brave_search, firecrawl, exa_search default to false in the manifest; at runtime buildNewProjectConfig detects API keys. The plan_checker (CJS flat) → workflow.plan_check (canonical nested) divergence is resolved: canonical name is workflow.plan_check. _auto_chain_active is a runtime-state field included for completeness.",
"model_profile": "balanced",
"commit_docs": true,
"parallelization": true,
"search_gitignored": false,
"brave_search": false,
"firecrawl": false,
"exa_search": false,
"resolve_model_ids": false,
"context_window": 200000,
"phase_naming": "sequential",
"project_code": null,
"mode": "interactive",
"claude_md_path": "./CLAUDE.md",
"git": {
"branching_strategy": "none",
"create_tag": true,
"base_branch": null,
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}",
"quick_branch_template": null
},
"workflow": {
"research": true,
"plan_check": true,
"verifier": true,
"nyquist_validation": true,
"ai_integration_phase": true,
"tdd_mode": false,
"human_verify_mode": "end-of-phase",
"auto_advance": false,
"_auto_chain_active": false,
"node_repair": true,
"node_repair_budget": 2,
"ui_phase": true,
"ui_safety_gate": true,
"text_mode": false,
"research_before_questions": false,
"discuss_mode": "discuss",
"skip_discuss": false,
"max_discuss_passes": 3,
"subagent_timeout": 300000,
"context_coverage_gate": true,
"code_review": true,
"code_review_depth": "standard",
"code_review_command": null,
"pattern_mapper": true,
"plan_bounce": false,
"plan_bounce_script": null,
"plan_bounce_passes": 2,
"auto_prune_state": false,
"post_planning_gaps": true,
"security_enforcement": true,
"security_asvs_level": 1,
"security_block_on": "high"
},
"planning": {
"commit_docs": true,
"search_gitignored": false,
"sub_repos": [],
"granularity": "standard"
},
"hooks": {
"context_warnings": true,
"workflow_guard": false
},
"ship": {
"pr_body_sections": []
},
"agent_skills": {}
}

View File

@@ -0,0 +1,143 @@
{
"_comment": "Canonical schema manifest for valid config key paths. validKeys is the union of CJS config-schema.cjs and SDK query/config-schema.ts (they are enforced set-equal by tests/config-schema-sdk-parity.test.cjs). runtimeStateKeys mirrors RUNTIME_STATE_KEYS. dynamicKeyPatterns mirrors DYNAMIC_KEY_PATTERNS from SDK (which includes the canonical 'source' regex strings). The .test function is reconstructed at runtime from new RegExp(source).",
"validKeys": [
"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.human_verify_mode",
"workflow.text_mode",
"workflow.research_before_questions",
"workflow.discuss_mode",
"workflow.skip_discuss",
"workflow.auto_prune_state",
"workflow.use_worktrees",
"workflow.worktree_skip_hooks",
"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.plan_chunked",
"workflow.plan_review_convergence",
"workflow.post_planning_gaps",
"workflow.security_enforcement",
"workflow.security_asvs_level",
"workflow.security_block_on",
"workflow.drift_threshold",
"workflow.drift_action",
"code_quality.fallow.enabled",
"code_quality.fallow.scope",
"code_quality.fallow.profile",
"code_quality.fallow.mcp",
"ship.pr_body_sections",
"git.branching_strategy",
"git.base_branch",
"git.create_tag",
"git.phase_branch_template",
"git.milestone_branch_template",
"git.quick_branch_template",
"planning.commit_docs",
"planning.search_gitignored",
"planning.sub_repos",
"review.ollama_host",
"review.lm_studio_host",
"review.llama_cpp_host",
"review.default_reviewers",
"workflow.cross_ai_execution",
"workflow.cross_ai_command",
"workflow.cross_ai_timeout",
"workflow.subagent_timeout",
"executor.stall_detect_interval_minutes",
"executor.stall_threshold_minutes",
"workflow.inline_plan_threshold",
"hooks.context_warnings",
"hooks.workflow_guard",
"workflow.context_coverage_gate",
"statusline.show_last_command",
"statusline.context_position",
"workflow.ui_review",
"workflow.max_discuss_passes",
"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",
"context_window",
"intel.enabled",
"graphify.enabled",
"graphify.build_timeout",
"claude_md_path",
"claude_md_assembly.mode",
"runtime",
"resolve_model_ids"
],
"runtimeStateKeys": [
"workflow._auto_chain_active"
],
"dynamicKeyPatterns": [
{
"topLevel": "agent_skills",
"source": "^agent_skills\\.[a-zA-Z0-9_-]+$",
"description": "agent_skills.<agent-type>"
},
{
"topLevel": "review",
"source": "^review\\.models\\.[a-zA-Z0-9_-]+$",
"description": "review.models.<cli-name>"
},
{
"topLevel": "features",
"source": "^features\\.[a-zA-Z0-9_]+$",
"description": "features.<feature_name>"
},
{
"topLevel": "claude_md_assembly",
"source": "^claude_md_assembly\\.blocks\\.[a-zA-Z0-9_]+$",
"description": "claude_md_assembly.blocks.<section>"
},
{
"topLevel": "model_profile_overrides",
"source": "^model_profile_overrides\\.[a-zA-Z0-9_-]+\\.(opus|sonnet|haiku)$",
"description": "model_profile_overrides.<runtime>.<opus|sonnet|haiku>"
},
{
"topLevel": "models",
"source": "^models\\.(planning|discuss|research|execution|verification|completion)$",
"description": "models.<planning|discuss|research|execution|verification|completion>"
},
{
"topLevel": "dynamic_routing",
"source": "^dynamic_routing\\.(enabled|escalate_on_failure|max_escalations|tier_models\\.(light|standard|heavy))$",
"description": "dynamic_routing.<enabled|escalate_on_failure|max_escalations|tier_models.<light|standard|heavy>>"
},
{
"topLevel": "model_overrides",
"source": "^model_overrides\\.[a-zA-Z0-9_-]+$",
"description": "model_overrides.<agent-id>"
}
]
}