gsd-opencode 1.20.4 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/gsd-codebase-mapper.md +9 -1
- package/agents/gsd-debugger.md +66 -10
- package/agents/gsd-executor.md +36 -16
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-nyquist-auditor.md +178 -0
- package/agents/gsd-phase-researcher.md +28 -34
- package/agents/gsd-plan-checker.md +42 -78
- package/agents/gsd-planner.md +139 -24
- package/agents/gsd-project-researcher.md +11 -1
- package/agents/gsd-research-synthesizer.md +13 -3
- package/agents/gsd-roadmapper.md +25 -15
- package/agents/gsd-verifier.md +29 -6
- package/bin/dm/lib/constants.js +6 -1
- package/bin/dm/src/services/file-ops.js +14 -1
- package/commands/gsd/gsd-add-phase.md +6 -6
- package/commands/gsd/gsd-add-tests.md +41 -0
- package/commands/gsd/gsd-add-todo.md +7 -7
- package/commands/gsd/gsd-audit-milestone.md +9 -9
- package/commands/gsd/gsd-check-profile.md +3 -3
- package/commands/gsd/gsd-check-todos.md +7 -7
- package/commands/gsd/gsd-cleanup.md +2 -2
- package/commands/gsd/gsd-complete-milestone.md +6 -6
- package/commands/gsd/gsd-debug.md +11 -7
- package/commands/gsd/gsd-discuss-phase.md +26 -19
- package/commands/gsd/gsd-execute-phase.md +13 -13
- package/commands/gsd/gsd-health.md +7 -7
- package/commands/gsd/gsd-help.md +2 -2
- package/commands/gsd/gsd-insert-phase.md +6 -6
- package/commands/gsd/gsd-join-discord.md +1 -1
- package/commands/gsd/gsd-list-phase-assumptions.md +6 -6
- package/commands/gsd/gsd-map-codebase.md +8 -8
- package/commands/gsd/gsd-new-milestone.md +12 -12
- package/commands/gsd/gsd-new-project.md +12 -12
- package/commands/gsd/gsd-pause-work.md +6 -6
- package/commands/gsd/gsd-plan-milestone-gaps.md +9 -9
- package/commands/gsd/gsd-plan-phase.md +14 -13
- package/commands/gsd/gsd-progress.md +8 -8
- package/commands/gsd/gsd-quick.md +17 -13
- package/commands/gsd/gsd-reapply-patches.md +19 -11
- package/commands/gsd/gsd-remove-phase.md +7 -7
- package/commands/gsd/gsd-research-phase.md +12 -11
- package/commands/gsd/gsd-resume-work.md +8 -8
- package/commands/gsd/gsd-set-profile.md +6 -6
- package/commands/gsd/gsd-settings.md +7 -7
- package/commands/gsd/gsd-update.md +5 -5
- package/commands/gsd/gsd-validate-phase.md +35 -0
- package/commands/gsd/gsd-verify-work.md +11 -11
- package/get-shit-done/bin/gsd-tools.cjs +45 -6
- package/get-shit-done/bin/lib/commands.cjs +11 -19
- package/get-shit-done/bin/lib/config.cjs +8 -1
- package/get-shit-done/bin/lib/core.cjs +131 -16
- package/get-shit-done/bin/lib/init.cjs +28 -12
- package/get-shit-done/bin/lib/milestone.cjs +34 -8
- package/get-shit-done/bin/lib/phase.cjs +74 -50
- package/get-shit-done/bin/lib/roadmap.cjs +7 -7
- package/get-shit-done/bin/lib/state.cjs +294 -63
- package/get-shit-done/bin/lib/template.cjs +3 -3
- package/get-shit-done/bin/lib/verify.cjs +56 -8
- package/get-shit-done/references/checkpoints.md +1 -1
- package/get-shit-done/references/decimal-phase-calculation.md +6 -6
- package/get-shit-done/references/git-integration.md +3 -3
- package/get-shit-done/references/git-planning-commit.md +2 -2
- package/get-shit-done/references/model-profile-resolution.md +1 -1
- package/get-shit-done/references/model-profiles.md +1 -0
- package/get-shit-done/references/phase-argument-parsing.md +4 -4
- package/get-shit-done/references/planning-config.md +10 -6
- package/get-shit-done/references/questioning.md +17 -0
- package/get-shit-done/references/verification-patterns.md +1 -1
- package/get-shit-done/templates/DEBUG.md +7 -2
- package/get-shit-done/templates/VALIDATION.md +18 -46
- package/get-shit-done/templates/codebase/structure.md +3 -3
- package/get-shit-done/templates/config.json +2 -2
- package/get-shit-done/templates/context.md +14 -0
- package/get-shit-done/templates/phase-prompt.md +10 -10
- package/get-shit-done/templates/retrospective.md +54 -0
- package/get-shit-done/templates/roadmap.md +1 -1
- package/get-shit-done/workflows/add-phase.md +3 -2
- package/get-shit-done/workflows/add-tests.md +351 -0
- package/get-shit-done/workflows/add-todo.md +4 -3
- package/get-shit-done/workflows/audit-milestone.md +40 -5
- package/get-shit-done/workflows/check-todos.md +3 -2
- package/get-shit-done/workflows/cleanup.md +1 -1
- package/get-shit-done/workflows/complete-milestone.md +69 -5
- package/get-shit-done/workflows/diagnose-issues.md +2 -2
- package/get-shit-done/workflows/discovery-phase.md +6 -6
- package/get-shit-done/workflows/discuss-phase.md +194 -58
- package/get-shit-done/workflows/execute-phase.md +29 -23
- package/get-shit-done/workflows/execute-plan.md +22 -18
- package/get-shit-done/workflows/health.md +5 -2
- package/get-shit-done/workflows/help.md +4 -1
- package/get-shit-done/workflows/insert-phase.md +3 -2
- package/get-shit-done/workflows/map-codebase.md +3 -2
- package/get-shit-done/workflows/new-milestone.md +12 -10
- package/get-shit-done/workflows/new-project.md +44 -49
- package/get-shit-done/workflows/pause-work.md +2 -2
- package/get-shit-done/workflows/plan-milestone-gaps.md +3 -3
- package/get-shit-done/workflows/plan-phase.md +155 -73
- package/get-shit-done/workflows/progress.md +8 -7
- package/get-shit-done/workflows/quick.md +158 -10
- package/get-shit-done/workflows/remove-phase.md +5 -4
- package/get-shit-done/workflows/research-phase.md +5 -4
- package/get-shit-done/workflows/resume-project.md +3 -2
- package/get-shit-done/workflows/set-profile.md +3 -2
- package/get-shit-done/workflows/settings.md +6 -6
- package/get-shit-done/workflows/transition.md +5 -5
- package/get-shit-done/workflows/update.md +45 -19
- package/get-shit-done/workflows/validate-phase.md +167 -0
- package/get-shit-done/workflows/verify-phase.md +10 -9
- package/get-shit-done/workflows/verify-work.md +18 -4
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gsd-settings
|
|
3
3
|
description: Configure GSD workflow toggles and model profile
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
permissions:
|
|
5
|
+
read: true
|
|
6
|
+
write: true
|
|
7
|
+
bash: true
|
|
8
|
+
question: true
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
<objective>
|
|
@@ -20,11 +20,11 @@ Routes to the settings workflow which handles:
|
|
|
20
20
|
</objective>
|
|
21
21
|
|
|
22
22
|
<execution_context>
|
|
23
|
-
|
|
23
|
+
@$HOME/.config/opencode/get-shit-done/workflows/settings.md
|
|
24
24
|
</execution_context>
|
|
25
25
|
|
|
26
26
|
<process>
|
|
27
|
-
**Follow the settings workflow** from
|
|
27
|
+
**Follow the settings workflow** from `@$HOME/.config/opencode/get-shit-done/workflows/settings.md`.
|
|
28
28
|
|
|
29
29
|
The workflow handles all logic including:
|
|
30
30
|
1. Config file creation with defaults if missing
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gsd-update
|
|
3
3
|
description: Update GSD to latest version with changelog display
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
permissions:
|
|
5
|
+
bash: true
|
|
6
|
+
question: true
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
<objective>
|
|
@@ -19,11 +19,11 @@ Routes to the update workflow which handles:
|
|
|
19
19
|
</objective>
|
|
20
20
|
|
|
21
21
|
<execution_context>
|
|
22
|
-
|
|
22
|
+
@$HOME/.config/opencode/get-shit-done/workflows/update.md
|
|
23
23
|
</execution_context>
|
|
24
24
|
|
|
25
25
|
<process>
|
|
26
|
-
**Follow the update workflow** from
|
|
26
|
+
**Follow the update workflow** from `@$HOME/.config/opencode/get-shit-done/workflows/update.md`.
|
|
27
27
|
|
|
28
28
|
The workflow handles all logic including:
|
|
29
29
|
1. Installed version detection (local/global)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gsd-validate-phase
|
|
3
|
+
description: Retroactively audit and fill Nyquist validation gaps for a completed phase
|
|
4
|
+
argument-hint: "[phase number]"
|
|
5
|
+
permissions:
|
|
6
|
+
read: true
|
|
7
|
+
write: true
|
|
8
|
+
edit: true
|
|
9
|
+
bash: true
|
|
10
|
+
glob: true
|
|
11
|
+
grep: true
|
|
12
|
+
task: true
|
|
13
|
+
question: true
|
|
14
|
+
---
|
|
15
|
+
<objective>
|
|
16
|
+
Audit Nyquist validation coverage for a completed phase. Three states:
|
|
17
|
+
- (A) VALIDATION.md exists — audit and fill gaps
|
|
18
|
+
- (B) No VALIDATION.md, SUMMARY.md exists — reconstruct from artifacts
|
|
19
|
+
- (C) Phase not executed — exit with guidance
|
|
20
|
+
|
|
21
|
+
Output: updated VALIDATION.md + generated test files.
|
|
22
|
+
</objective>
|
|
23
|
+
|
|
24
|
+
<execution_context>
|
|
25
|
+
@$HOME/.config/opencode/get-shit-done/workflows/validate-phase.md
|
|
26
|
+
</execution_context>
|
|
27
|
+
|
|
28
|
+
<context>
|
|
29
|
+
Phase: $ARGUMENTS — optional, defaults to last completed phase.
|
|
30
|
+
</context>
|
|
31
|
+
|
|
32
|
+
<process>
|
|
33
|
+
Execute @$HOME/.config/opencode/get-shit-done/workflows/validate-phase.md.
|
|
34
|
+
Preserve all workflow gates.
|
|
35
|
+
</process>
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
name: gsd-verify-work
|
|
3
3
|
description: Validate built features through conversational UAT
|
|
4
4
|
argument-hint: "[phase number, e.g., '4']"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
permissions:
|
|
6
|
+
read: true
|
|
7
|
+
bash: true
|
|
8
|
+
glob: true
|
|
9
|
+
grep: true
|
|
10
|
+
edit: true
|
|
11
|
+
write: true
|
|
12
|
+
task: true
|
|
13
13
|
---
|
|
14
14
|
<objective>
|
|
15
15
|
Validate built features through conversational testing with persistent state.
|
|
@@ -20,8 +20,8 @@ Output: {phase_num}-UAT.md tracking all test results. If issues found: diagnosed
|
|
|
20
20
|
</objective>
|
|
21
21
|
|
|
22
22
|
<execution_context>
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
@$HOME/.config/opencode/get-shit-done/workflows/verify-work.md
|
|
24
|
+
@$HOME/.config/opencode/get-shit-done/templates/UAT.md
|
|
25
25
|
</execution_context>
|
|
26
26
|
|
|
27
27
|
<context>
|
|
@@ -33,6 +33,6 @@ Context files are resolved inside the workflow (`init verify-work`) and delegate
|
|
|
33
33
|
</context>
|
|
34
34
|
|
|
35
35
|
<process>
|
|
36
|
-
Execute the verify-work workflow from
|
|
36
|
+
Execute the verify-work workflow from @$HOME/.config/opencode/get-shit-done/workflows/verify-work.md end-to-end.
|
|
37
37
|
Preserve all workflow gates (session management, test presentation, diagnosis, fix planning, routing).
|
|
38
38
|
</process>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Atomic Commands:
|
|
12
12
|
* state load Load project config + state
|
|
13
|
+
* state json Output STATE.md frontmatter as JSON
|
|
13
14
|
* state update <field> <value> Update a STATE.md field
|
|
14
15
|
* state get [section] Get STATE.md content or section
|
|
15
16
|
* state patch --field val ... Batch update STATE.md fields
|
|
@@ -102,7 +103,9 @@
|
|
|
102
103
|
* state update-progress Recalculate progress bar
|
|
103
104
|
* state add-decision --summary "..." Add decision to STATE.md
|
|
104
105
|
* [--phase N] [--rationale "..."]
|
|
106
|
+
* [--summary-file path] [--rationale-file path]
|
|
105
107
|
* state add-blocker --text "..." Add blocker
|
|
108
|
+
* [--text-file path]
|
|
106
109
|
* state resolve-blocker --text "..." Remove blocker
|
|
107
110
|
* state record-session Update session continuity
|
|
108
111
|
* --stopped-at "..."
|
|
@@ -123,6 +126,8 @@
|
|
|
123
126
|
* init progress All context for progress workflow
|
|
124
127
|
*/
|
|
125
128
|
|
|
129
|
+
const fs = require('fs');
|
|
130
|
+
const path = require('path');
|
|
126
131
|
const { error } = require('./lib/core.cjs');
|
|
127
132
|
const state = require('./lib/state.cjs');
|
|
128
133
|
const phase = require('./lib/phase.cjs');
|
|
@@ -139,21 +144,43 @@ const frontmatter = require('./lib/frontmatter.cjs');
|
|
|
139
144
|
|
|
140
145
|
async function main() {
|
|
141
146
|
const args = process.argv.slice(2);
|
|
147
|
+
|
|
148
|
+
// Optional cwd override for sandboxed subagents running outside project root.
|
|
149
|
+
let cwd = process.cwd();
|
|
150
|
+
const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
|
|
151
|
+
const cwdIdx = args.indexOf('--cwd');
|
|
152
|
+
if (cwdEqArg) {
|
|
153
|
+
const value = cwdEqArg.slice('--cwd='.length).trim();
|
|
154
|
+
if (!value) error('Missing value for --cwd');
|
|
155
|
+
args.splice(args.indexOf(cwdEqArg), 1);
|
|
156
|
+
cwd = path.resolve(value);
|
|
157
|
+
} else if (cwdIdx !== -1) {
|
|
158
|
+
const value = args[cwdIdx + 1];
|
|
159
|
+
if (!value || value.startsWith('--')) error('Missing value for --cwd');
|
|
160
|
+
args.splice(cwdIdx, 2);
|
|
161
|
+
cwd = path.resolve(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
|
165
|
+
error(`Invalid --cwd: ${cwd}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
142
168
|
const rawIndex = args.indexOf('--raw');
|
|
143
169
|
const raw = rawIndex !== -1;
|
|
144
170
|
if (rawIndex !== -1) args.splice(rawIndex, 1);
|
|
145
171
|
|
|
146
172
|
const command = args[0];
|
|
147
|
-
const cwd = process.cwd();
|
|
148
173
|
|
|
149
174
|
if (!command) {
|
|
150
|
-
error('Usage: gsd-tools <command> [args] [--raw]\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');
|
|
175
|
+
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');
|
|
151
176
|
}
|
|
152
177
|
|
|
153
178
|
switch (command) {
|
|
154
179
|
case 'state': {
|
|
155
180
|
const subcommand = args[1];
|
|
156
|
-
if (subcommand === '
|
|
181
|
+
if (subcommand === 'json') {
|
|
182
|
+
state.cmdStateJson(cwd, raw);
|
|
183
|
+
} else if (subcommand === 'update') {
|
|
157
184
|
state.cmdStateUpdate(cwd, args[2], args[3]);
|
|
158
185
|
} else if (subcommand === 'get') {
|
|
159
186
|
state.cmdStateGet(cwd, args[2], raw);
|
|
@@ -187,15 +214,23 @@ async function main() {
|
|
|
187
214
|
} else if (subcommand === 'add-decision') {
|
|
188
215
|
const phaseIdx = args.indexOf('--phase');
|
|
189
216
|
const summaryIdx = args.indexOf('--summary');
|
|
217
|
+
const summaryFileIdx = args.indexOf('--summary-file');
|
|
190
218
|
const rationaleIdx = args.indexOf('--rationale');
|
|
219
|
+
const rationaleFileIdx = args.indexOf('--rationale-file');
|
|
191
220
|
state.cmdStateAddDecision(cwd, {
|
|
192
221
|
phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
|
|
193
222
|
summary: summaryIdx !== -1 ? args[summaryIdx + 1] : null,
|
|
223
|
+
summary_file: summaryFileIdx !== -1 ? args[summaryFileIdx + 1] : null,
|
|
194
224
|
rationale: rationaleIdx !== -1 ? args[rationaleIdx + 1] : '',
|
|
225
|
+
rationale_file: rationaleFileIdx !== -1 ? args[rationaleFileIdx + 1] : null,
|
|
195
226
|
}, raw);
|
|
196
227
|
} else if (subcommand === 'add-blocker') {
|
|
197
228
|
const textIdx = args.indexOf('--text');
|
|
198
|
-
|
|
229
|
+
const textFileIdx = args.indexOf('--text-file');
|
|
230
|
+
state.cmdStateAddBlocker(cwd, {
|
|
231
|
+
text: textIdx !== -1 ? args[textIdx + 1] : null,
|
|
232
|
+
text_file: textFileIdx !== -1 ? args[textFileIdx + 1] : null,
|
|
233
|
+
}, raw);
|
|
199
234
|
} else if (subcommand === 'resolve-blocker') {
|
|
200
235
|
const textIdx = args.indexOf('--text');
|
|
201
236
|
state.cmdStateResolveBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
|
|
@@ -224,9 +259,13 @@ async function main() {
|
|
|
224
259
|
|
|
225
260
|
case 'commit': {
|
|
226
261
|
const amend = args.includes('--amend');
|
|
227
|
-
const message = args[1];
|
|
228
|
-
// Parse --files flag (collect args after --files, stopping at other flags)
|
|
229
262
|
const filesIndex = args.indexOf('--files');
|
|
263
|
+
// Collect all positional args between command name and first flag,
|
|
264
|
+
// then join them — handles both quoted ("multi word msg") and
|
|
265
|
+
// unquoted (multi word msg) invocations from different shells
|
|
266
|
+
const endIndex = filesIndex !== -1 ? filesIndex : args.length;
|
|
267
|
+
const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
|
|
268
|
+
const message = messageArgs.join(' ') || undefined;
|
|
230
269
|
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
|
231
270
|
commands.cmdCommit(cwd, message, files, raw, amend);
|
|
232
271
|
break;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
-
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, output, error, findPhaseInternal } = require('./core.cjs');
|
|
7
|
+
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
|
|
10
10
|
function cmdGenerateSlug(text, raw) {
|
|
@@ -68,7 +68,7 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
68
68
|
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
69
69
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
70
70
|
area: todoArea,
|
|
71
|
-
path: path.join('.planning', 'todos', 'pending', file),
|
|
71
|
+
path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
|
|
72
72
|
});
|
|
73
73
|
} catch {}
|
|
74
74
|
}
|
|
@@ -204,17 +204,12 @@ function cmdResolveModel(cwd, agentType, raw) {
|
|
|
204
204
|
|
|
205
205
|
const config = loadConfig(cwd);
|
|
206
206
|
const profile = config.model_profile || 'balanced';
|
|
207
|
+
const model = resolveModelInternal(cwd, agentType);
|
|
207
208
|
|
|
208
209
|
const agentModels = MODEL_PROFILES[agentType];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const resolved = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
|
216
|
-
const model = resolved === 'opus' ? 'inherit' : resolved;
|
|
217
|
-
const result = { model, profile };
|
|
210
|
+
const result = agentModels
|
|
211
|
+
? { model, profile }
|
|
212
|
+
: { model, profile, unknown_agent: true };
|
|
218
213
|
output(result, raw, model);
|
|
219
214
|
}
|
|
220
215
|
|
|
@@ -304,6 +299,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
|
304
299
|
tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
|
|
305
300
|
patterns: fm['patterns-established'] || [],
|
|
306
301
|
decisions: parseDecisions(fm['key-decisions']),
|
|
302
|
+
requirements_completed: fm['requirements-completed'] || [],
|
|
307
303
|
};
|
|
308
304
|
|
|
309
305
|
// If fields specified, filter to only those fields
|
|
@@ -394,14 +390,10 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
394
390
|
|
|
395
391
|
try {
|
|
396
392
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
397
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) =>
|
|
398
|
-
const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
399
|
-
const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
400
|
-
return aNum - bNum;
|
|
401
|
-
});
|
|
393
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
402
394
|
|
|
403
395
|
for (const dir of dirs) {
|
|
404
|
-
const dm = dir.match(/^(\d+(?:\.\d+)
|
|
396
|
+
const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
|
405
397
|
const phaseNum = dm ? dm[1] : dir;
|
|
406
398
|
const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
407
399
|
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
@@ -421,7 +413,7 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
421
413
|
}
|
|
422
414
|
} catch {}
|
|
423
415
|
|
|
424
|
-
const percent = totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0;
|
|
416
|
+
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
|
425
417
|
|
|
426
418
|
if (format === 'table') {
|
|
427
419
|
// Render markdown table
|
|
@@ -536,7 +528,7 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
536
528
|
}
|
|
537
529
|
|
|
538
530
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
539
|
-
const relPath = path.relative(cwd, filePath);
|
|
531
|
+
const relPath = toPosixPath(path.relative(cwd, filePath));
|
|
540
532
|
output({ created: true, path: relPath }, raw, relPath);
|
|
541
533
|
}
|
|
542
534
|
|
|
@@ -37,6 +37,13 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
37
37
|
try {
|
|
38
38
|
if (fs.existsSync(globalDefaultsPath)) {
|
|
39
39
|
userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8'));
|
|
40
|
+
// Migrate deprecated "depth" key to "granularity"
|
|
41
|
+
if ('depth' in userDefaults && !('granularity' in userDefaults)) {
|
|
42
|
+
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
|
43
|
+
userDefaults.granularity = depthToGranularity[userDefaults.depth] || userDefaults.depth;
|
|
44
|
+
delete userDefaults.depth;
|
|
45
|
+
try { fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8'); } catch {}
|
|
46
|
+
}
|
|
40
47
|
}
|
|
41
48
|
} catch (err) {
|
|
42
49
|
// Ignore malformed global defaults, fall back to hardcoded
|
|
@@ -54,7 +61,7 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
54
61
|
research: true,
|
|
55
62
|
plan_check: true,
|
|
56
63
|
verifier: true,
|
|
57
|
-
nyquist_validation:
|
|
64
|
+
nyquist_validation: true,
|
|
58
65
|
},
|
|
59
66
|
parallelization: true,
|
|
60
67
|
brave_search: hasBraveSearch,
|
|
@@ -6,6 +6,13 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
8
|
|
|
9
|
+
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Normalize a relative path to always use forward slashes (cross-platform). */
|
|
12
|
+
function toPosixPath(p) {
|
|
13
|
+
return p.split(path.sep).join('/');
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
// ─── Model Profile Table ─────────────────────────────────────────────────────
|
|
10
17
|
|
|
11
18
|
const MODEL_PROFILES = {
|
|
@@ -20,6 +27,7 @@ const MODEL_PROFILES = {
|
|
|
20
27
|
'gsd-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
21
28
|
'gsd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
22
29
|
'gsd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
30
|
+
'gsd-nyquist-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
// ─── Output helpers ───────────────────────────────────────────────────────────
|
|
@@ -69,6 +77,7 @@ function loadConfig(cwd) {
|
|
|
69
77
|
research: true,
|
|
70
78
|
plan_checker: true,
|
|
71
79
|
verifier: true,
|
|
80
|
+
nyquist_validation: true,
|
|
72
81
|
parallelization: true,
|
|
73
82
|
brave_search: false,
|
|
74
83
|
};
|
|
@@ -77,6 +86,14 @@ function loadConfig(cwd) {
|
|
|
77
86
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
78
87
|
const parsed = JSON.parse(raw);
|
|
79
88
|
|
|
89
|
+
// Migrate deprecated "depth" key to "granularity" with value mapping
|
|
90
|
+
if ('depth' in parsed && !('granularity' in parsed)) {
|
|
91
|
+
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
|
92
|
+
parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth;
|
|
93
|
+
delete parsed.depth;
|
|
94
|
+
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
const get = (key, nested) => {
|
|
81
98
|
if (parsed[key] !== undefined) return parsed[key];
|
|
82
99
|
if (nested && parsed[nested.section] && parsed[nested.section][nested.field] !== undefined) {
|
|
@@ -102,8 +119,10 @@ function loadConfig(cwd) {
|
|
|
102
119
|
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
|
|
103
120
|
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
|
104
121
|
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
|
122
|
+
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
|
|
105
123
|
parallelization,
|
|
106
124
|
brave_search: get('brave_search') ?? defaults.brave_search,
|
|
125
|
+
model_overrides: parsed.model_overrides || null,
|
|
107
126
|
};
|
|
108
127
|
} catch {
|
|
109
128
|
return defaults;
|
|
@@ -114,7 +133,11 @@ function loadConfig(cwd) {
|
|
|
114
133
|
|
|
115
134
|
function isGitIgnored(cwd, targetPath) {
|
|
116
135
|
try {
|
|
117
|
-
|
|
136
|
+
// --no-index checks .gitignore rules regardless of whether the file is tracked.
|
|
137
|
+
// Without it, git check-ignore returns "not ignored" for tracked files even when
|
|
138
|
+
// .gitignore explicitly lists them — a common source of confusion when .planning/
|
|
139
|
+
// was committed before being added to .gitignore.
|
|
140
|
+
execSync('git check-ignore -q --no-index -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
|
|
118
141
|
cwd,
|
|
119
142
|
stdio: 'pipe',
|
|
120
143
|
});
|
|
@@ -147,23 +170,55 @@ function execGit(cwd, args) {
|
|
|
147
170
|
|
|
148
171
|
// ─── Phase utilities ──────────────────────────────────────────────────────────
|
|
149
172
|
|
|
173
|
+
function escapeRegex(value) {
|
|
174
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
175
|
+
}
|
|
176
|
+
|
|
150
177
|
function normalizePhaseName(phase) {
|
|
151
|
-
const match = phase.match(/^(\d+(?:\.\d+)
|
|
178
|
+
const match = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
152
179
|
if (!match) return phase;
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
return
|
|
180
|
+
const padded = match[1].padStart(2, '0');
|
|
181
|
+
const letter = match[2] ? match[2].toUpperCase() : '';
|
|
182
|
+
const decimal = match[3] || '';
|
|
183
|
+
return padded + letter + decimal;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function comparePhaseNum(a, b) {
|
|
187
|
+
const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
188
|
+
const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
189
|
+
if (!pa || !pb) return String(a).localeCompare(String(b));
|
|
190
|
+
const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
|
|
191
|
+
if (intDiff !== 0) return intDiff;
|
|
192
|
+
// No letter sorts before letter: 12 < 12A < 12B
|
|
193
|
+
const la = (pa[2] || '').toUpperCase();
|
|
194
|
+
const lb = (pb[2] || '').toUpperCase();
|
|
195
|
+
if (la !== lb) {
|
|
196
|
+
if (!la) return -1;
|
|
197
|
+
if (!lb) return 1;
|
|
198
|
+
return la < lb ? -1 : 1;
|
|
199
|
+
}
|
|
200
|
+
// Segment-by-segment decimal comparison: 12A < 12A.1 < 12A.1.2 < 12A.2
|
|
201
|
+
const aDecParts = pa[3] ? pa[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
202
|
+
const bDecParts = pb[3] ? pb[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
203
|
+
const maxLen = Math.max(aDecParts.length, bDecParts.length);
|
|
204
|
+
if (aDecParts.length === 0 && bDecParts.length > 0) return -1;
|
|
205
|
+
if (bDecParts.length === 0 && aDecParts.length > 0) return 1;
|
|
206
|
+
for (let i = 0; i < maxLen; i++) {
|
|
207
|
+
const av = Number.isFinite(aDecParts[i]) ? aDecParts[i] : 0;
|
|
208
|
+
const bv = Number.isFinite(bDecParts[i]) ? bDecParts[i] : 0;
|
|
209
|
+
if (av !== bv) return av - bv;
|
|
210
|
+
}
|
|
211
|
+
return 0;
|
|
157
212
|
}
|
|
158
213
|
|
|
159
214
|
function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
160
215
|
try {
|
|
161
216
|
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
162
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
217
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
163
218
|
const match = dirs.find(d => d.startsWith(normalized));
|
|
164
219
|
if (!match) return null;
|
|
165
220
|
|
|
166
|
-
const dirMatch = match.match(/^(\d+(?:\.\d+)
|
|
221
|
+
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
167
222
|
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
168
223
|
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
169
224
|
const phaseDir = path.join(baseDir, match);
|
|
@@ -185,7 +240,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
185
240
|
|
|
186
241
|
return {
|
|
187
242
|
found: true,
|
|
188
|
-
directory: path.join(relBase, match),
|
|
243
|
+
directory: toPosixPath(path.join(relBase, match)),
|
|
189
244
|
phase_number: phaseNumber,
|
|
190
245
|
phase_name: phaseName,
|
|
191
246
|
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
@@ -208,7 +263,7 @@ function findPhaseInternal(cwd, phase) {
|
|
|
208
263
|
const normalized = normalizePhaseName(phase);
|
|
209
264
|
|
|
210
265
|
// Search current phases first
|
|
211
|
-
const current = searchPhaseInDir(phasesDir,
|
|
266
|
+
const current = searchPhaseInDir(phasesDir, '.planning/phases', normalized);
|
|
212
267
|
if (current) return current;
|
|
213
268
|
|
|
214
269
|
// Search archived milestone phases (newest first)
|
|
@@ -226,7 +281,7 @@ function findPhaseInternal(cwd, phase) {
|
|
|
226
281
|
for (const archiveName of archiveDirs) {
|
|
227
282
|
const version = archiveName.match(/^(v[\d.]+)-phases$/)[1];
|
|
228
283
|
const archivePath = path.join(milestonesDir, archiveName);
|
|
229
|
-
const relBase =
|
|
284
|
+
const relBase = '.planning/milestones/' + archiveName;
|
|
230
285
|
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
231
286
|
if (result) {
|
|
232
287
|
result.archived = version;
|
|
@@ -257,7 +312,7 @@ function getArchivedPhaseDirs(cwd) {
|
|
|
257
312
|
const version = archiveName.match(/^(v[\d.]+)-phases$/)[1];
|
|
258
313
|
const archivePath = path.join(milestonesDir, archiveName);
|
|
259
314
|
const entries = fs.readdirSync(archivePath, { withFileTypes: true });
|
|
260
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
315
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
261
316
|
|
|
262
317
|
for (const dir of dirs) {
|
|
263
318
|
results.push({
|
|
@@ -282,7 +337,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
282
337
|
|
|
283
338
|
try {
|
|
284
339
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
285
|
-
const escapedPhase = phaseNum.toString()
|
|
340
|
+
const escapedPhase = escapeRegex(phaseNum.toString());
|
|
286
341
|
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
|
|
287
342
|
const headerMatch = content.match(phasePattern);
|
|
288
343
|
if (!headerMatch) return null;
|
|
@@ -346,17 +401,73 @@ function generateSlugInternal(text) {
|
|
|
346
401
|
function getMilestoneInfo(cwd) {
|
|
347
402
|
try {
|
|
348
403
|
const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
349
|
-
|
|
350
|
-
|
|
404
|
+
|
|
405
|
+
// First: check for list-format roadmaps using 🚧 (in-progress) marker
|
|
406
|
+
// e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)"
|
|
407
|
+
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
|
|
408
|
+
if (inProgressMatch) {
|
|
409
|
+
return {
|
|
410
|
+
version: 'v' + inProgressMatch[1],
|
|
411
|
+
name: inProgressMatch[2].trim(),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Second: heading-format roadmaps — strip shipped milestones in <details> blocks
|
|
416
|
+
const cleaned = roadmap.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
|
417
|
+
// Extract version and name from the same ## heading for consistency
|
|
418
|
+
const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
|
|
419
|
+
if (headingMatch) {
|
|
420
|
+
return {
|
|
421
|
+
version: 'v' + headingMatch[1],
|
|
422
|
+
name: headingMatch[2].trim(),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
// Fallback: try bare version match
|
|
426
|
+
const versionMatch = cleaned.match(/v(\d+\.\d+)/);
|
|
351
427
|
return {
|
|
352
428
|
version: versionMatch ? versionMatch[0] : 'v1.0',
|
|
353
|
-
name:
|
|
429
|
+
name: 'milestone',
|
|
354
430
|
};
|
|
355
431
|
} catch {
|
|
356
432
|
return { version: 'v1.0', name: 'milestone' };
|
|
357
433
|
}
|
|
358
434
|
}
|
|
359
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Returns a filter function that checks whether a phase directory belongs
|
|
438
|
+
* to the current milestone based on ROADMAP.md phase headings.
|
|
439
|
+
* If no ROADMAP exists or no phases are listed, returns a pass-all filter.
|
|
440
|
+
*/
|
|
441
|
+
function getMilestonePhaseFilter(cwd) {
|
|
442
|
+
const milestonePhaseNums = new Set();
|
|
443
|
+
try {
|
|
444
|
+
const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
445
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
|
446
|
+
let m;
|
|
447
|
+
while ((m = phasePattern.exec(roadmap)) !== null) {
|
|
448
|
+
milestonePhaseNums.add(m[1]);
|
|
449
|
+
}
|
|
450
|
+
} catch {}
|
|
451
|
+
|
|
452
|
+
if (milestonePhaseNums.size === 0) {
|
|
453
|
+
const passAll = () => true;
|
|
454
|
+
passAll.phaseCount = 0;
|
|
455
|
+
return passAll;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const normalized = new Set(
|
|
459
|
+
[...milestonePhaseNums].map(n => (n.replace(/^0+/, '') || '0').toLowerCase())
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
function isDirInMilestone(dirName) {
|
|
463
|
+
const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
|
|
464
|
+
if (!m) return false;
|
|
465
|
+
return normalized.has(m[1].toLowerCase());
|
|
466
|
+
}
|
|
467
|
+
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
|
468
|
+
return isDirInMilestone;
|
|
469
|
+
}
|
|
470
|
+
|
|
360
471
|
module.exports = {
|
|
361
472
|
MODEL_PROFILES,
|
|
362
473
|
output,
|
|
@@ -365,7 +476,9 @@ module.exports = {
|
|
|
365
476
|
loadConfig,
|
|
366
477
|
isGitIgnored,
|
|
367
478
|
execGit,
|
|
479
|
+
escapeRegex,
|
|
368
480
|
normalizePhaseName,
|
|
481
|
+
comparePhaseNum,
|
|
369
482
|
searchPhaseInDir,
|
|
370
483
|
findPhaseInternal,
|
|
371
484
|
getArchivedPhaseDirs,
|
|
@@ -374,4 +487,6 @@ module.exports = {
|
|
|
374
487
|
pathExistsInternal,
|
|
375
488
|
generateSlugInternal,
|
|
376
489
|
getMilestoneInfo,
|
|
490
|
+
getMilestonePhaseFilter,
|
|
491
|
+
toPosixPath,
|
|
377
492
|
};
|