feat(config): add user-level default settings via ~/.gsd/defaults.json (closes #492)
New projects seed config from ~/.gsd/defaults.json when available, skipping the 8 setup questions. /gsd:settings offers to save current settings as global defaults after configuration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -852,9 +852,9 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
||||
// Escape special regex chars in phase number, handle decimal
|
||||
const escapedPhase = phaseNum.replace(/\./g, '\\.');
|
||||
|
||||
// Match "## Phase X:" or "### Phase X:" with optional name
|
||||
// Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
|
||||
const phasePattern = new RegExp(
|
||||
`#{2,3}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
|
||||
`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
|
||||
'i'
|
||||
);
|
||||
const headerMatch = content.match(phasePattern);
|
||||
@@ -888,7 +888,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
||||
|
||||
// Find the end of this section (next ## or ### phase header, or end of file)
|
||||
const restOfContent = content.slice(headerIndex);
|
||||
const nextHeaderMatch = restOfContent.match(/\n#{2,3}\s+Phase\s+\d/i);
|
||||
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
||||
const sectionEnd = nextHeaderMatch
|
||||
? headerIndex + nextHeaderMatch.index
|
||||
: content.length;
|
||||
@@ -2475,7 +2475,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
||||
const phasesDir = path.join(cwd, '.planning', 'phases');
|
||||
|
||||
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
||||
const phasePattern = /#{2,3}\s*Phase\s+(\d+(?:\.\d+)?)\s*:\s*([^\n]+)/gi;
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+(?:\.\d+)?)\s*:\s*([^\n]+)/gi;
|
||||
const phases = [];
|
||||
let match;
|
||||
|
||||
@@ -2486,7 +2486,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
||||
// Extract goal from the section
|
||||
const sectionStart = match.index;
|
||||
const restOfContent = content.slice(sectionStart);
|
||||
const nextHeader = restOfContent.match(/\n#{2,3}\s+Phase\s+\d/i);
|
||||
const nextHeader = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
||||
const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
|
||||
const section = content.slice(sectionStart, sectionEnd);
|
||||
|
||||
@@ -2606,7 +2606,7 @@ function cmdPhaseAdd(cwd, description, raw) {
|
||||
const slug = generateSlugInternal(description);
|
||||
|
||||
// Find highest integer phase number
|
||||
const phasePattern = /#{2,3}\s*Phase\s+(\d+)(?:\.\d+)?:/gi;
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+)(?:\.\d+)?:/gi;
|
||||
let maxPhase = 0;
|
||||
let m;
|
||||
while ((m = phasePattern.exec(content)) !== null) {
|
||||
@@ -2666,7 +2666,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
||||
const normalizedAfter = normalizePhaseName(afterPhase);
|
||||
const unpadded = normalizedAfter.replace(/^0+/, '');
|
||||
const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
|
||||
const targetPattern = new RegExp(`#{2,3}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
|
||||
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
|
||||
if (!targetPattern.test(content)) {
|
||||
error(`Phase ${afterPhase} not found in ROADMAP.md`);
|
||||
}
|
||||
@@ -2698,7 +2698,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
||||
const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${decimalPhase} to break down)\n`;
|
||||
|
||||
// Insert after the target phase section
|
||||
const headerPattern = new RegExp(`(#{2,3}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
||||
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
||||
const headerMatch = content.match(headerPattern);
|
||||
if (!headerMatch) {
|
||||
error(`Could not find Phase ${afterPhase} header`);
|
||||
@@ -2706,7 +2706,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
||||
|
||||
const headerIdx = content.indexOf(headerMatch[0]);
|
||||
const afterHeader = content.slice(headerIdx + headerMatch[0].length);
|
||||
const nextPhaseMatch = afterHeader.match(/\n#{2,3}\s+Phase\s+\d/i);
|
||||
const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
|
||||
|
||||
let insertIdx;
|
||||
if (nextPhaseMatch) {
|
||||
@@ -2889,7 +2889,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
||||
// Remove the target phase section
|
||||
const targetEscaped = targetPhase.replace(/\./g, '\\.');
|
||||
const sectionPattern = new RegExp(
|
||||
`\\n?#{2,3}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,3}\\s+Phase\\s+\\d|$)`,
|
||||
`\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
|
||||
'i'
|
||||
);
|
||||
roadmapContent = roadmapContent.replace(sectionPattern, '');
|
||||
@@ -2917,7 +2917,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
||||
|
||||
// Phase headings: ## Phase 18: or ### Phase 18: → ## Phase 17: or ### Phase 17:
|
||||
roadmapContent = roadmapContent.replace(
|
||||
new RegExp(`(#{2,3}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'),
|
||||
new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'),
|
||||
`$1${newStr}$2`
|
||||
);
|
||||
|
||||
@@ -3029,7 +3029,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
||||
|
||||
// Update plan count in phase detail section
|
||||
const planCountPattern = new RegExp(
|
||||
`(#{2,3}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
||||
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
||||
'i'
|
||||
);
|
||||
const planCountText = isComplete
|
||||
@@ -3104,7 +3104,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
|
||||
// Update plan count in phase section
|
||||
const planCountPattern = new RegExp(
|
||||
`(#{2,3}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
||||
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
||||
'i'
|
||||
);
|
||||
roadmapContent = roadmapContent.replace(
|
||||
@@ -3368,7 +3368,7 @@ function cmdValidateConsistency(cwd, raw) {
|
||||
|
||||
// Extract phases from ROADMAP
|
||||
const roadmapPhases = new Set();
|
||||
const phasePattern = /#{2,3}\s*Phase\s+(\d+(?:\.\d+)?)\s*:/gi;
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+(?:\.\d+)?)\s*:/gi;
|
||||
let m;
|
||||
while ((m = phasePattern.exec(roadmapContent)) !== null) {
|
||||
roadmapPhases.add(m[1]);
|
||||
@@ -3712,14 +3712,14 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
||||
try {
|
||||
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const escapedPhase = phaseNum.toString().replace(/\./g, '\\.');
|
||||
const phasePattern = new RegExp(`#{2,3}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
|
||||
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
|
||||
const headerMatch = content.match(phasePattern);
|
||||
if (!headerMatch) return null;
|
||||
|
||||
const phaseName = headerMatch[1].trim();
|
||||
const headerIndex = headerMatch.index;
|
||||
const restOfContent = content.slice(headerIndex);
|
||||
const nextHeaderMatch = restOfContent.match(/\n#{2,3}\s+Phase\s+\d/i);
|
||||
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
||||
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
|
||||
const section = content.slice(headerIndex, sectionEnd).trim();
|
||||
|
||||
|
||||
@@ -217,6 +217,26 @@ node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit "docs: initialize project"
|
||||
|
||||
## 5. Workflow Preferences
|
||||
|
||||
**Check for global defaults** at `~/.gsd/defaults.json`. If the file exists, offer to use saved defaults:
|
||||
|
||||
```
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Use your saved default settings? (from ~/.gsd/defaults.json)",
|
||||
header: "Defaults",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Yes (Recommended)", description: "Use saved defaults, skip settings questions" },
|
||||
{ label: "No", description: "Configure settings manually" }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
If "Yes": read `~/.gsd/defaults.json`, use those values for config.json, and skip directly to **Commit config.json** below.
|
||||
|
||||
If "No" or `~/.gsd/defaults.json` doesn't exist: proceed with the questions below.
|
||||
|
||||
**Round 1 — Core workflow settings (4 questions):**
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<purpose>
|
||||
Interactive configuration of GSD workflow agents (research, plan_check, verifier) and model profile selection via multi-question prompt. Updates .planning/config.json with user preferences.
|
||||
Interactive configuration of GSD workflow agents (research, plan_check, verifier) and model profile selection via multi-question prompt. Updates .planning/config.json with user preferences. Optionally saves settings as global defaults (~/.gsd/defaults.json) for future projects.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
@@ -119,6 +119,48 @@ Merge new settings into existing config.json:
|
||||
Write updated config to `.planning/config.json`.
|
||||
</step>
|
||||
|
||||
<step name="save_as_defaults">
|
||||
Ask whether to save these settings as global defaults for future projects:
|
||||
|
||||
```
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Save these as default settings for all new projects?",
|
||||
header: "Defaults",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Yes", description: "New projects start with these settings (saved to ~/.gsd/defaults.json)" },
|
||||
{ label: "No", description: "Only apply to this project" }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
If "Yes": write the same config object (minus project-specific fields like `brave_search`) to `~/.gsd/defaults.json`:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.gsd
|
||||
```
|
||||
|
||||
Write `~/.gsd/defaults.json` with:
|
||||
```json
|
||||
{
|
||||
"mode": <current>,
|
||||
"depth": <current>,
|
||||
"model_profile": <current>,
|
||||
"commit_docs": <current>,
|
||||
"parallelization": <current>,
|
||||
"branching_strategy": <current>,
|
||||
"workflow": {
|
||||
"research": <current>,
|
||||
"plan_check": <current>,
|
||||
"verifier": <current>,
|
||||
"auto_advance": <current>
|
||||
}
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="confirm">
|
||||
Display:
|
||||
|
||||
@@ -135,6 +177,7 @@ Display:
|
||||
| Execution Verifier | {On/Off} |
|
||||
| Auto-Advance | {On/Off} |
|
||||
| Git Branching | {None/Per Phase/Per Milestone} |
|
||||
| Saved as Defaults | {Yes/No} |
|
||||
|
||||
These settings apply to future /gsd:plan-phase and /gsd:execute-phase runs.
|
||||
|
||||
@@ -152,5 +195,6 @@ Quick commands:
|
||||
- [ ] Current config read
|
||||
- [ ] User presented with 6 settings (profile + 4 workflow toggles + git branching)
|
||||
- [ ] Config updated with model_profile, workflow, and git sections
|
||||
- [ ] User offered to save as global defaults (~/.gsd/defaults.json)
|
||||
- [ ] Changes confirmed to user
|
||||
</success_criteria>
|
||||
|
||||
Reference in New Issue
Block a user