Merge pull request #1181 from diegomarino/feat/materialize-new-project-config

feat: materialize full config on new-project initialization
This commit is contained in:
Tom Boucher
2026-03-20 11:07:09 -04:00
committed by GitHub
7 changed files with 1255 additions and 199 deletions

View File

@@ -188,7 +188,7 @@ async function main() {
const command = args[0];
if (!command) {
error('Usage: gsd-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
error('Usage: gsd-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-new-project, init');
}
// Multi-repo guard: resolve project root for commands that read/write .planning/.
@@ -449,6 +449,11 @@ async function main() {
break;
}
case 'config-new-project': {
config.cmdConfigNewProject(cwd, args[1], raw);
break;
}
case 'history-digest': {
commands.cmdHistoryDigest(cwd, raw);
break;

View File

@@ -16,10 +16,12 @@ const VALID_CONFIG_KEYS = new Set([
'search_gitignored', 'brave_search', 'firecrawl', 'exa_search',
'workflow.research', 'workflow.plan_check', 'workflow.verifier',
'workflow.nyquist_validation', 'workflow.ui_phase', 'workflow.ui_safety_gate',
'workflow.auto_advance', 'workflow.node_repair', 'workflow.node_repair_budget',
'workflow.text_mode',
'workflow._auto_chain_active',
'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
'planning.commit_docs', 'planning.search_gitignored',
'hooks.context_warnings',
]);
const CONFIG_KEY_SUGGESTIONS = {
@@ -35,6 +37,154 @@ function validateKnownConfigKeyPath(keyPath) {
}
}
/**
* Build a fully-materialized config object for a new project.
*
* Merges (increasing priority):
* 1. Hardcoded defaults — every key that loadConfig() resolves, plus mode/granularity
* 2. User-level defaults from ~/.gsd/defaults.json (if present)
* 3. userChoices — the settings the user explicitly selected during /gsd:new-project
*
* Uses the canonical `git` namespace for branching keys (consistent with VALID_CONFIG_KEYS
* and the settings workflow). loadConfig() handles both flat and nested formats, so this
* is backward-compatible with existing projects that have flat keys.
*
* Returns a plain object — does NOT write any files.
*/
function buildNewProjectConfig(userChoices) {
const choices = userChoices || {};
const homedir = require('os').homedir();
// Detect API key availability
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
const firecrawlKeyFile = path.join(homedir, '.gsd', 'firecrawl_api_key');
const hasFirecrawl = !!(process.env.FIRECRAWL_API_KEY || fs.existsSync(firecrawlKeyFile));
const exaKeyFile = path.join(homedir, '.gsd', 'exa_api_key');
const hasExaSearch = !!(process.env.EXA_API_KEY || fs.existsSync(exaKeyFile));
// Load user-level defaults from ~/.gsd/defaults.json if available
const globalDefaultsPath = path.join(homedir, '.gsd', 'defaults.json');
let userDefaults = {};
try {
if (fs.existsSync(globalDefaultsPath)) {
userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8'));
// Migrate deprecated "depth" key to "granularity"
if ('depth' in userDefaults && !('granularity' in userDefaults)) {
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
userDefaults.granularity = depthToGranularity[userDefaults.depth] || userDefaults.depth;
delete userDefaults.depth;
try {
fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8');
} catch { /* intentionally empty */ }
}
}
} catch {
// Ignore malformed global defaults
}
const hardcoded = {
model_profile: 'balanced',
commit_docs: true,
parallelization: true,
search_gitignored: false,
brave_search: hasBraveSearch,
firecrawl: hasFirecrawl,
exa_search: hasExaSearch,
git: {
branching_strategy: 'none',
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,
auto_advance: false,
node_repair: true,
node_repair_budget: 2,
ui_phase: true,
ui_safety_gate: true,
text_mode: false,
},
hooks: {
context_warnings: true,
},
};
// Three-level deep merge: hardcoded <- userDefaults <- choices
return {
...hardcoded,
...userDefaults,
...choices,
git: {
...hardcoded.git,
...(userDefaults.git || {}),
...(choices.git || {}),
},
workflow: {
...hardcoded.workflow,
...(userDefaults.workflow || {}),
...(choices.workflow || {}),
},
hooks: {
...hardcoded.hooks,
...(userDefaults.hooks || {}),
...(choices.hooks || {}),
},
};
}
/**
* Command: create a fully-materialized .planning/config.json for a new project.
*
* Accepts user-chosen settings as a JSON string (the keys the user explicitly
* configured during /gsd:new-project). All remaining keys are filled from
* hardcoded defaults and optional ~/.gsd/defaults.json.
*
* Idempotent: if config.json already exists, returns { created: false }.
*/
function cmdConfigNewProject(cwd, choicesJson, raw) {
const configPath = path.join(cwd, '.planning', 'config.json');
const planningDir = path.join(cwd, '.planning');
// Idempotent: don't overwrite existing config
if (fs.existsSync(configPath)) {
output({ created: false, reason: 'already_exists' }, raw, 'exists');
return;
}
// Parse user choices
let userChoices = {};
if (choicesJson && choicesJson.trim() !== '') {
try {
userChoices = JSON.parse(choicesJson);
} catch (err) {
error('Invalid JSON for config-new-project: ' + err.message);
}
}
// Ensure .planning directory exists
try {
if (!fs.existsSync(planningDir)) {
fs.mkdirSync(planningDir, { recursive: true });
}
} catch (err) {
error('Failed to create .planning directory: ' + err.message);
}
const config = buildNewProjectConfig(userChoices);
try {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
output({ created: true, path: '.planning/config.json' }, raw, 'created');
} catch (err) {
error('Failed to write config.json: ' + err.message);
}
}
/**
* Ensures the config file exists (creates it if needed).
*
@@ -59,67 +209,10 @@ function ensureConfigFile(cwd) {
return { created: false, reason: 'already_exists' };
}
// Detect Brave Search API key availability
const homedir = require('os').homedir();
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
// Detect Firecrawl API key availability
const firecrawlKeyFile = path.join(homedir, '.gsd', 'firecrawl_api_key');
const hasFirecrawl = !!(process.env.FIRECRAWL_API_KEY || fs.existsSync(firecrawlKeyFile));
// Detect Exa API key availability
const exaKeyFile = path.join(homedir, '.gsd', 'exa_api_key');
const hasExaSearch = !!(process.env.EXA_API_KEY || fs.existsSync(exaKeyFile));
// Load user-level defaults from ~/.gsd/defaults.json if available
const globalDefaultsPath = path.join(homedir, '.gsd', 'defaults.json');
let userDefaults = {};
try {
if (fs.existsSync(globalDefaultsPath)) {
userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8'));
// Migrate deprecated "depth" key to "granularity"
if ('depth' in userDefaults && !('granularity' in userDefaults)) {
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
userDefaults.granularity = depthToGranularity[userDefaults.depth] || userDefaults.depth;
delete userDefaults.depth;
try {
fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8');
} catch { /* intentionally empty */ }
}
}
} catch (err) {
// Ignore malformed global defaults, fall back to hardcoded
}
// Create default config (user-level defaults override hardcoded defaults)
const hardcoded = {
model_profile: 'balanced',
commit_docs: true,
search_gitignored: false,
branching_strategy: 'none',
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,
},
parallelization: true,
brave_search: hasBraveSearch,
firecrawl: hasFirecrawl,
exa_search: hasExaSearch,
};
const defaults = {
...hardcoded,
...userDefaults,
workflow: { ...hardcoded.workflow, ...(userDefaults.workflow || {}) },
};
const config = buildNewProjectConfig({});
try {
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
return { created: true, path: '.planning/config.json' };
} catch (err) {
error('Failed to create config.json: ' + err.message);
@@ -316,4 +409,5 @@ module.exports = {
cmdConfigSet,
cmdConfigGet,
cmdConfigSetModelProfile,
cmdConfigNewProject,
};

View File

@@ -7,11 +7,13 @@ Read all files referenced by the invoking prompt's execution_context before star
</required_reading>
<auto_mode>
## Auto Mode Detection
Check if `--auto` flag is present in $ARGUMENTS.
**If auto mode:**
- Skip brownfield mapping offer (assume greenfield)
- Skip deep questioning (extract context from provided document)
- Config: YOLO mode is implicit (skip that question), but ask granularity/git/agents FIRST (Step 2a)
@@ -23,6 +25,7 @@ Check if `--auto` flag is present in $ARGUMENTS.
**Document requirement:**
Auto mode requires an idea document — either:
- File reference: `/gsd:new-project --auto @prd.md`
- Pasted/written text in the prompt
@@ -37,6 +40,7 @@ Usage:
The document should describe what you want to build.
```
</auto_mode>
<process>
@@ -55,6 +59,7 @@ Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `co
**If `project_exists` is true:** Error — project already initialized. Use `/gsd:progress`.
**If `has_git` is false:** Initialize git:
```bash
git init
```
@@ -66,6 +71,7 @@ git init
**If `needs_codebase_map` is true** (from init — existing code detected but no codebase map):
Use AskUserQuestion:
- header: "Codebase"
- question: "I detected existing code in this directory. Would you like to map the codebase first?"
- options:
@@ -73,9 +79,11 @@ Use AskUserQuestion:
- "Skip mapping" — Proceed with project initialization
**If "Map codebase first":**
```
Run `/gsd:map-codebase` first, then return to `/gsd:new-project`
```
Exit command.
**If "Skip mapping" OR `needs_codebase_map` is false:** Continue to Step 3.
@@ -166,23 +174,11 @@ AskUserQuestion([
])
```
Create `.planning/config.json` with mode set to "yolo":
Create `.planning/config.json` with all settings (CLI fills in remaining defaults automatically):
```json
{
"mode": "yolo",
"granularity": "[selected]",
"parallelization": true|false,
"commit_docs": true|false,
"model_profile": "quality|balanced|budget|inherit",
"workflow": {
"research": true|false,
"plan_check": true|false,
"verifier": true|false,
"nyquist_validation": depth !== "quick",
"auto_advance": true
}
}
```bash
mkdir -p .planning
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-new-project '{"mode":"yolo","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":true|false,"auto_advance":true}}'
```
**If commit_docs = No:** Add `.planning/` to `.gitignore`.
@@ -223,6 +219,7 @@ Ask inline (freeform, NOT AskUserQuestion):
Wait for their response. This gives you the context needed to ask intelligent follow-up questions.
**Research-before-questions mode:** Check if `research_questions` is enabled in `.planning/config.json` (or the config from init context). When enabled, before asking follow-up questions about a topic area:
1. Do a brief web search for best practices related to what the user described
2. Mention key findings naturally as you ask questions (e.g., "Most projects like this use X — is that what you're thinking, or something different?")
3. This makes questions more informed without changing the conversational flow
@@ -234,6 +231,7 @@ When disabled (default), ask questions directly as before.
Based on what they said, ask follow-up questions that dig into their response. Use AskUserQuestion with options that probe what they mentioned — interpretations, clarifications, concrete examples.
Keep following threads. Each answer opens new threads to explore. Ask about:
- What excited them
- What problem sparked this
- What they mean by vague terms
@@ -241,6 +239,7 @@ Keep following threads. Each answer opens new threads to explore. Ask about:
- What's already decided
Consult `questioning.md` for techniques:
- Challenge vagueness
- Make abstract concrete
- Surface assumptions
@@ -495,29 +494,22 @@ questions: [
]
```
Create `.planning/config.json` with all settings:
Create `.planning/config.json` with all settings (CLI fills in remaining defaults automatically):
```json
{
"mode": "yolo|interactive",
"granularity": "coarse|standard|fine",
"parallelization": true|false,
"commit_docs": true|false,
"model_profile": "quality|balanced|budget|inherit",
"workflow": {
"research": true|false,
"plan_check": true|false,
"verifier": true|false,
"nyquist_validation": depth !== "quick"
}
}
```bash
mkdir -p .planning
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-new-project '{"mode":"[yolo|interactive]","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":[false if granularity=coarse, true otherwise]}}'
```
**Note:** Run `/gsd:settings` anytime to update model profile, workflow agents, branching strategy, and other preferences.
**If commit_docs = No:**
- Set `commit_docs: false` in config.json
- Add `.planning/` to `.gitignore` (create if needed)
**If commit_docs = Yes:**
- No additional gitignore entries needed
**Commit config.json:**
@@ -526,8 +518,6 @@ Create `.planning/config.json` with all settings:
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: add project config" --files .planning/config.json
```
**Note:** Run `/gsd:settings` anytime to update these preferences.
## 5.1. Sub-Repo Detection
**Detect multi-repo workspace:**
@@ -543,6 +533,7 @@ find . -maxdepth 1 -type d -not -name ".*" -not -name "node_modules" -exec test
Strip the `./` prefix to get directory names (e.g., `./backend` → `backend`).
Use AskUserQuestion:
- header: "Multi-Repo Workspace"
- question: "I detected separate git repos in this workspace. Which directories contain code that GSD should commit to?"
- multiSelect: true
@@ -550,6 +541,7 @@ Use AskUserQuestion:
- "[directory name]" — Separate git repo
**If user selects one or more directories:**
- Set `planning.sub_repos` in config.json to the selected directory names array (e.g., `["backend", "frontend"]`)
- Auto-set `planning.commit_docs` to `false` (planning docs stay local in multi-repo workspaces)
- Add `.planning/` to `.gitignore` if not already present
@@ -567,6 +559,7 @@ Use models from init: `researcher_model`, `synthesizer_model`, `roadmapper_model
**If auto mode:** Default to "Research first" without asking.
Use AskUserQuestion:
- header: "Research"
- question: "Research the domain ecosystem before defining requirements?"
- options:
@@ -576,6 +569,7 @@ Use AskUserQuestion:
**If "Research first":**
Display stage banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► RESEARCHING
@@ -585,6 +579,7 @@ Researching [domain] ecosystem...
```
Create research directory:
```bash
mkdir -p .planning/research
```
@@ -592,10 +587,12 @@ mkdir -p .planning/research
**Determine milestone context:**
Check if this is greenfield or subsequent milestone:
- If no "Validated" requirements in PROJECT.md → Greenfield (building from scratch)
- If "Validated" requirements exist → Subsequent milestone (adding to existing app)
Display spawning indicator:
```
◆ Spawning 4 researchers in parallel...
→ Stack research
@@ -784,6 +781,7 @@ Commit after writing.
```
Display research complete banner and key findings:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► RESEARCH COMPLETE ✓
@@ -803,6 +801,7 @@ Files: `.planning/research/`
## 7. Define Requirements
Display stage banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► DEFINING REQUIREMENTS
@@ -812,6 +811,7 @@ Display stage banner:
**Load context:**
Read PROJECT.md and extract:
- Core value (the ONE thing that must work)
- Stated constraints (budget, timeline, tech limitations)
- Any explicit scope boundaries
@@ -819,6 +819,7 @@ Read PROJECT.md and extract:
**If research exists:** Read research/FEATURES.md and extract feature categories.
**If auto mode:**
- Auto-include all table stakes features (users expect these)
- Include features explicitly mentioned in provided document
- Auto-defer differentiators not mentioned in document
@@ -857,6 +858,7 @@ Here are the features for [domain]:
Ask: "What are the main things users need to be able to do?"
For each capability mentioned:
- Ask clarifying questions to make it specific
- Probe for related capabilities
- Group into categories
@@ -875,6 +877,7 @@ For each category, use AskUserQuestion:
- "None for v1" — Defer entire category
Track responses:
- Selected features → v1 requirements
- Unselected table stakes → v2 (users expect these)
- Unselected differentiators → out of scope
@@ -882,6 +885,7 @@ Track responses:
**Identify gaps:**
Use AskUserQuestion:
- header: "Additions"
- question: "Any requirements research missed? (Features specific to your vision)"
- options:
@@ -895,6 +899,7 @@ Cross-check requirements against Core Value from PROJECT.md. If gaps detected, s
**Generate REQUIREMENTS.md:**
Create `.planning/REQUIREMENTS.md` with:
- v1 Requirements grouped by category (checkboxes, REQ-IDs)
- v2 Requirements (deferred)
- Out of Scope (explicit exclusions with reasoning)
@@ -905,12 +910,14 @@ Create `.planning/REQUIREMENTS.md` with:
**Requirement quality criteria:**
Good requirements are:
- **Specific and testable:** "User can reset password via email link" (not "Handle password reset")
- **User-centric:** "User can X" (not "System does Y")
- **Atomic:** One capability per requirement (not "User can login and manage profile")
- **Independent:** Minimal dependencies on other requirements
Reject vague requirements. Push for specificity:
- "Handle authentication" → "User can log in with email/password and stay logged in across sessions"
- "Support sharing" → "User can share post via link that opens in recipient's browser"
@@ -948,6 +955,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define v1 req
## 8. Create Roadmap
Display stage banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► CREATING ROADMAP
@@ -988,6 +996,7 @@ Write files first, then return. This ensures artifacts persist even if context i
**Handle roadmapper return:**
**If `## ROADMAP BLOCKED`:**
- Present blocker information
- Work with user to resolve
- Re-spawn when resolved
@@ -1037,6 +1046,7 @@ Success criteria:
**CRITICAL: Ask for approval before committing (interactive mode only):**
Use AskUserQuestion:
- header: "Roadmap"
- question: "Does this roadmap structure work for you?"
- options:
@@ -1047,8 +1057,10 @@ Use AskUserQuestion:
**If "Approve":** Continue to commit.
**If "Adjust phases":**
- Get user's adjustment notes
- Re-spawn roadmapper with revision context:
```
Task(prompt="
<revision>
@@ -1064,6 +1076,7 @@ Use AskUserQuestion:
</revision>
", subagent_type="gsd-roadmapper", model="{roadmapper_model}", description="Revise roadmap")
```
- Present revised roadmap
- Loop until user approves