@undeemed/get-shit-done-codex 1.20.8 → 1.20.10
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.md +29 -29
- package/README.md +102 -30
- package/agents/gsd-debugger.md +53 -8
- package/agents/gsd-planner.md +86 -5
- package/agents/gsd-verifier.md +15 -0
- package/bin/install.js +524 -37
- package/commands/gsd/add-tests.md +41 -0
- package/commands/gsd/debug.md +3 -0
- package/commands/gsd/join-discord.md +1 -1
- package/commands/gsd/plan-phase.md +2 -1
- package/get-shit-done/bin/gsd-tools.cjs +39 -4
- package/get-shit-done/bin/lib/commands.cjs +5 -8
- package/get-shit-done/bin/lib/core.cjs +22 -9
- package/get-shit-done/bin/lib/init.cjs +17 -1
- package/get-shit-done/bin/lib/milestone.cjs +2 -1
- package/get-shit-done/bin/lib/phase.cjs +18 -20
- package/get-shit-done/bin/lib/roadmap.cjs +7 -7
- package/get-shit-done/bin/lib/state.cjs +216 -27
- package/get-shit-done/bin/lib/verify.cjs +9 -8
- package/get-shit-done/templates/DEBUG.md +7 -2
- package/get-shit-done/templates/VALIDATION.md +18 -46
- package/get-shit-done/templates/retrospective.md +54 -0
- package/get-shit-done/workflows/add-tests.md +350 -0
- package/get-shit-done/workflows/complete-milestone.md +63 -0
- package/get-shit-done/workflows/discuss-phase.md +2 -0
- package/get-shit-done/workflows/help.md +3 -0
- package/package.json +2 -1
package/commands/gsd/debug.md
CHANGED
|
@@ -110,6 +110,9 @@ Task(
|
|
|
110
110
|
**If `## CHECKPOINT REACHED`:**
|
|
111
111
|
- Present checkpoint details to user
|
|
112
112
|
- Get user response
|
|
113
|
+
- If checkpoint type is `human-verify`:
|
|
114
|
+
- If user confirms fixed: continue so agent can finalize/resolve/archive
|
|
115
|
+
- If user reports issues: continue so agent returns to investigation/fixing
|
|
113
116
|
- Spawn continuation agent (see step 5)
|
|
114
117
|
|
|
115
118
|
**If `## INVESTIGATION INCONCLUSIVE`:**
|
|
@@ -12,7 +12,7 @@ Display the Discord invite link for the GSD community server.
|
|
|
12
12
|
|
|
13
13
|
Connect with other GSD users, get help, share what you're building, and stay updated.
|
|
14
14
|
|
|
15
|
-
**Invite link:** https://discord.gg/
|
|
15
|
+
**Invite link:** https://discord.gg/gsd
|
|
16
16
|
|
|
17
17
|
Click the link or paste it into your browser to join.
|
|
18
18
|
</output>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gsd:plan-phase
|
|
3
3
|
description: Create detailed phase plan (PLAN.md) with verification loop
|
|
4
|
-
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--gaps] [--skip-verify]"
|
|
4
|
+
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--gaps] [--skip-verify] [--prd <file>]"
|
|
5
5
|
agent: gsd-planner
|
|
6
6
|
allowed-tools:
|
|
7
7
|
- Read
|
|
@@ -34,6 +34,7 @@ Phase number: $ARGUMENTS (optional — auto-detects next unplanned phase if omit
|
|
|
34
34
|
- `--skip-research` — Skip research, go straight to planning
|
|
35
35
|
- `--gaps` — Gap closure mode (reads VERIFICATION.md, skips research)
|
|
36
36
|
- `--skip-verify` — Skip verification loop
|
|
37
|
+
- `--prd <file>` — Use a PRD/acceptance criteria file instead of discuss-phase. Parses requirements into CONTEXT.md automatically. Skips discuss-phase entirely.
|
|
37
38
|
|
|
38
39
|
Normalize phase input in step 2 before any directory lookups.
|
|
39
40
|
</context>
|
|
@@ -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);
|
|
@@ -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, output, error, findPhaseInternal } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
|
|
10
10
|
function cmdGenerateSlug(text, raw) {
|
|
@@ -304,6 +304,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
|
304
304
|
tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
|
|
305
305
|
patterns: fm['patterns-established'] || [],
|
|
306
306
|
decisions: parseDecisions(fm['key-decisions']),
|
|
307
|
+
requirements_completed: fm['requirements-completed'] || [],
|
|
307
308
|
};
|
|
308
309
|
|
|
309
310
|
// If fields specified, filter to only those fields
|
|
@@ -394,14 +395,10 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
394
395
|
|
|
395
396
|
try {
|
|
396
397
|
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
|
-
});
|
|
398
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
402
399
|
|
|
403
400
|
for (const dir of dirs) {
|
|
404
|
-
const dm = dir.match(/^(\d+(?:\.\d+)
|
|
401
|
+
const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
|
405
402
|
const phaseNum = dm ? dm[1] : dir;
|
|
406
403
|
const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
407
404
|
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
@@ -421,7 +418,7 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
421
418
|
}
|
|
422
419
|
} catch {}
|
|
423
420
|
|
|
424
|
-
const percent = totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0;
|
|
421
|
+
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
|
425
422
|
|
|
426
423
|
if (format === 'table') {
|
|
427
424
|
// Render markdown table
|
|
@@ -147,8 +147,12 @@ function execGit(cwd, args) {
|
|
|
147
147
|
|
|
148
148
|
// ─── Phase utilities ──────────────────────────────────────────────────────────
|
|
149
149
|
|
|
150
|
+
function escapeRegex(value) {
|
|
151
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
152
|
+
}
|
|
153
|
+
|
|
150
154
|
function normalizePhaseName(phase) {
|
|
151
|
-
const match = phase.match(/^(\d+)([A-Z])?(
|
|
155
|
+
const match = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
152
156
|
if (!match) return phase;
|
|
153
157
|
const padded = match[1].padStart(2, '0');
|
|
154
158
|
const letter = match[2] ? match[2].toUpperCase() : '';
|
|
@@ -157,8 +161,8 @@ function normalizePhaseName(phase) {
|
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
function comparePhaseNum(a, b) {
|
|
160
|
-
const pa = String(a).match(/^(\d+)([A-Z])?(
|
|
161
|
-
const pb = String(b).match(/^(\d+)([A-Z])?(
|
|
164
|
+
const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
165
|
+
const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
162
166
|
if (!pa || !pb) return String(a).localeCompare(String(b));
|
|
163
167
|
const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
|
|
164
168
|
if (intDiff !== 0) return intDiff;
|
|
@@ -170,10 +174,18 @@ function comparePhaseNum(a, b) {
|
|
|
170
174
|
if (!lb) return 1;
|
|
171
175
|
return la < lb ? -1 : 1;
|
|
172
176
|
}
|
|
173
|
-
//
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
+
// Segment-by-segment decimal comparison: 12A < 12A.1 < 12A.1.2 < 12A.2
|
|
178
|
+
const aDecParts = pa[3] ? pa[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
179
|
+
const bDecParts = pb[3] ? pb[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
180
|
+
const maxLen = Math.max(aDecParts.length, bDecParts.length);
|
|
181
|
+
if (aDecParts.length === 0 && bDecParts.length > 0) return -1;
|
|
182
|
+
if (bDecParts.length === 0 && aDecParts.length > 0) return 1;
|
|
183
|
+
for (let i = 0; i < maxLen; i++) {
|
|
184
|
+
const av = Number.isFinite(aDecParts[i]) ? aDecParts[i] : 0;
|
|
185
|
+
const bv = Number.isFinite(bDecParts[i]) ? bDecParts[i] : 0;
|
|
186
|
+
if (av !== bv) return av - bv;
|
|
187
|
+
}
|
|
188
|
+
return 0;
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
@@ -183,7 +195,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
183
195
|
const match = dirs.find(d => d.startsWith(normalized));
|
|
184
196
|
if (!match) return null;
|
|
185
197
|
|
|
186
|
-
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)
|
|
198
|
+
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
187
199
|
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
188
200
|
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
189
201
|
const phaseDir = path.join(baseDir, match);
|
|
@@ -302,7 +314,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
302
314
|
|
|
303
315
|
try {
|
|
304
316
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
305
|
-
const escapedPhase = phaseNum.toString()
|
|
317
|
+
const escapedPhase = escapeRegex(phaseNum.toString());
|
|
306
318
|
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
|
|
307
319
|
const headerMatch = content.match(phasePattern);
|
|
308
320
|
if (!headerMatch) return null;
|
|
@@ -385,6 +397,7 @@ module.exports = {
|
|
|
385
397
|
loadConfig,
|
|
386
398
|
isGitIgnored,
|
|
387
399
|
execGit,
|
|
400
|
+
escapeRegex,
|
|
388
401
|
normalizePhaseName,
|
|
389
402
|
comparePhaseNum,
|
|
390
403
|
searchPhaseInDir,
|
|
@@ -16,6 +16,13 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
16
16
|
const phaseInfo = findPhaseInternal(cwd, phase);
|
|
17
17
|
const milestone = getMilestoneInfo(cwd);
|
|
18
18
|
|
|
19
|
+
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
20
|
+
const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
|
|
21
|
+
const reqExtracted = reqMatch
|
|
22
|
+
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
|
|
23
|
+
: null;
|
|
24
|
+
const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
|
|
25
|
+
|
|
19
26
|
const result = {
|
|
20
27
|
// Models
|
|
21
28
|
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
|
|
@@ -35,6 +42,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
35
42
|
phase_number: phaseInfo?.phase_number || null,
|
|
36
43
|
phase_name: phaseInfo?.phase_name || null,
|
|
37
44
|
phase_slug: phaseInfo?.phase_slug || null,
|
|
45
|
+
phase_req_ids,
|
|
38
46
|
|
|
39
47
|
// Plan inventory
|
|
40
48
|
plans: phaseInfo?.plans || [],
|
|
@@ -80,6 +88,13 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
80
88
|
const config = loadConfig(cwd);
|
|
81
89
|
const phaseInfo = findPhaseInternal(cwd, phase);
|
|
82
90
|
|
|
91
|
+
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
92
|
+
const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
|
|
93
|
+
const reqExtracted = reqMatch
|
|
94
|
+
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
|
|
95
|
+
: null;
|
|
96
|
+
const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
|
|
97
|
+
|
|
83
98
|
const result = {
|
|
84
99
|
// Models
|
|
85
100
|
researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
|
|
@@ -99,6 +114,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
99
114
|
phase_name: phaseInfo?.phase_name || null,
|
|
100
115
|
phase_slug: phaseInfo?.phase_slug || null,
|
|
101
116
|
padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
|
|
117
|
+
phase_req_ids,
|
|
102
118
|
|
|
103
119
|
// Existing artifacts
|
|
104
120
|
has_research: phaseInfo?.has_research || false,
|
|
@@ -595,7 +611,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
595
611
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
596
612
|
|
|
597
613
|
for (const dir of dirs) {
|
|
598
|
-
const match = dir.match(/^(\d+(?:\.\d+)
|
|
614
|
+
const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
|
599
615
|
const phaseNumber = match ? match[1] : dir;
|
|
600
616
|
const phaseName = match && match[2] ? match[2] : null;
|
|
601
617
|
|
|
@@ -6,6 +6,7 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { output, error } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
+
const { writeStateMd } = require('./state.cjs');
|
|
9
10
|
|
|
10
11
|
function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
11
12
|
if (!reqIdsRaw || reqIdsRaw.length === 0) {
|
|
@@ -169,7 +170,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
169
170
|
/(\*\*Last Activity Description:\*\*\s*).*/,
|
|
170
171
|
`$1${version} milestone completed and archived`
|
|
171
172
|
);
|
|
172
|
-
|
|
173
|
+
writeStateMd(statePath, stateContent, cwd);
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
// Archive phase directories if requested
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, output, error } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, output, error } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
+
const { writeStateMd } = require('./state.cjs');
|
|
9
10
|
|
|
10
11
|
function cmdPhasesList(cwd, options, raw) {
|
|
11
12
|
const phasesDir = path.join(cwd, '.planning', 'phases');
|
|
@@ -70,7 +71,7 @@ function cmdPhasesList(cwd, options, raw) {
|
|
|
70
71
|
const result = {
|
|
71
72
|
files,
|
|
72
73
|
count: files.length,
|
|
73
|
-
phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)
|
|
74
|
+
phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)*-?/, '') : null,
|
|
74
75
|
};
|
|
75
76
|
output(result, raw, files.join('\n'));
|
|
76
77
|
return;
|
|
@@ -121,11 +122,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
// Sort numerically
|
|
124
|
-
existingDecimals.sort((a, b) =>
|
|
125
|
-
const aNum = parseFloat(a);
|
|
126
|
-
const bNum = parseFloat(b);
|
|
127
|
-
return aNum - bNum;
|
|
128
|
-
});
|
|
125
|
+
existingDecimals.sort((a, b) => comparePhaseNum(a, b));
|
|
129
126
|
|
|
130
127
|
// Calculate next decimal
|
|
131
128
|
let nextDecimal;
|
|
@@ -172,7 +169,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
172
169
|
return;
|
|
173
170
|
}
|
|
174
171
|
|
|
175
|
-
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)
|
|
172
|
+
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
176
173
|
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
177
174
|
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
178
175
|
|
|
@@ -318,7 +315,7 @@ function cmdPhaseAdd(cwd, description, raw) {
|
|
|
318
315
|
const slug = generateSlugInternal(description);
|
|
319
316
|
|
|
320
317
|
// Find highest integer phase number
|
|
321
|
-
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)
|
|
318
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
|
322
319
|
let maxPhase = 0;
|
|
323
320
|
let m;
|
|
324
321
|
while ((m = phasePattern.exec(content)) !== null) {
|
|
@@ -336,7 +333,7 @@ function cmdPhaseAdd(cwd, description, raw) {
|
|
|
336
333
|
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
337
334
|
|
|
338
335
|
// Build phase entry
|
|
339
|
-
const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseNum} to break down)\n`;
|
|
336
|
+
const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseNum} to break down)\n`;
|
|
340
337
|
|
|
341
338
|
// Find insertion point: before last "---" or at end
|
|
342
339
|
let updatedContent;
|
|
@@ -407,7 +404,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
407
404
|
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
408
405
|
|
|
409
406
|
// Build phase entry
|
|
410
|
-
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`;
|
|
407
|
+
const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${decimalPhase} to break down)\n`;
|
|
411
408
|
|
|
412
409
|
// Insert after the target phase section
|
|
413
410
|
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
|
@@ -599,7 +596,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
599
596
|
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
600
597
|
|
|
601
598
|
// Remove the target phase section
|
|
602
|
-
const targetEscaped = targetPhase
|
|
599
|
+
const targetEscaped = escapeRegex(targetPhase);
|
|
603
600
|
const sectionPattern = new RegExp(
|
|
604
601
|
`\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
|
|
605
602
|
'i'
|
|
@@ -679,7 +676,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
679
676
|
const oldTotal = parseInt(ofMatch[2], 10);
|
|
680
677
|
stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
|
|
681
678
|
}
|
|
682
|
-
|
|
679
|
+
writeStateMd(statePath, stateContent, cwd);
|
|
683
680
|
}
|
|
684
681
|
|
|
685
682
|
const result = {
|
|
@@ -720,13 +717,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
720
717
|
|
|
721
718
|
// Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
|
|
722
719
|
const checkboxPattern = new RegExp(
|
|
723
|
-
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseNum
|
|
720
|
+
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
|
|
724
721
|
'i'
|
|
725
722
|
);
|
|
726
723
|
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
|
727
724
|
|
|
728
725
|
// Progress table: update Status to Complete, add date
|
|
729
|
-
const phaseEscaped = phaseNum
|
|
726
|
+
const phaseEscaped = escapeRegex(phaseNum);
|
|
730
727
|
const tablePattern = new RegExp(
|
|
731
728
|
`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
|
|
732
729
|
'i'
|
|
@@ -753,7 +750,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
753
750
|
if (fs.existsSync(reqPath)) {
|
|
754
751
|
// Extract Requirements line from roadmap for this phase
|
|
755
752
|
const reqMatch = roadmapContent.match(
|
|
756
|
-
new RegExp(`Phase\\s+${phaseNum
|
|
753
|
+
new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
|
|
757
754
|
);
|
|
758
755
|
|
|
759
756
|
if (reqMatch) {
|
|
@@ -761,14 +758,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
761
758
|
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
762
759
|
|
|
763
760
|
for (const reqId of reqIds) {
|
|
761
|
+
const reqEscaped = escapeRegex(reqId);
|
|
764
762
|
// Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
|
|
765
763
|
reqContent = reqContent.replace(
|
|
766
|
-
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${
|
|
764
|
+
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
|
|
767
765
|
'$1x$2'
|
|
768
766
|
);
|
|
769
767
|
// Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
|
|
770
768
|
reqContent = reqContent.replace(
|
|
771
|
-
new RegExp(`(\\|\\s*${
|
|
769
|
+
new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
|
|
772
770
|
'$1 Complete $2'
|
|
773
771
|
);
|
|
774
772
|
}
|
|
@@ -789,7 +787,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
789
787
|
|
|
790
788
|
// Find the next phase directory after current
|
|
791
789
|
for (const dir of dirs) {
|
|
792
|
-
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)
|
|
790
|
+
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
793
791
|
if (dm) {
|
|
794
792
|
if (comparePhaseNum(dm[1], phaseNum) > 0) {
|
|
795
793
|
nextPhaseNum = dm[1];
|
|
@@ -843,7 +841,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
843
841
|
`$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
|
|
844
842
|
);
|
|
845
843
|
|
|
846
|
-
|
|
844
|
+
writeStateMd(statePath, stateContent, cwd);
|
|
847
845
|
}
|
|
848
846
|
|
|
849
847
|
const result = {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
|
|
8
8
|
|
|
9
9
|
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
10
10
|
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
|
|
@@ -18,7 +18,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
18
18
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
19
19
|
|
|
20
20
|
// Escape special regex chars in phase number, handle decimal
|
|
21
|
-
const escapedPhase = phaseNum
|
|
21
|
+
const escapedPhase = escapeRegex(phaseNum);
|
|
22
22
|
|
|
23
23
|
// Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
|
|
24
24
|
const phasePattern = new RegExp(
|
|
@@ -102,7 +102,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
102
102
|
const phasesDir = path.join(cwd, '.planning', 'phases');
|
|
103
103
|
|
|
104
104
|
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
|
105
|
-
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)
|
|
105
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
106
106
|
const phases = [];
|
|
107
107
|
let match;
|
|
108
108
|
|
|
@@ -153,7 +153,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
153
153
|
} catch {}
|
|
154
154
|
|
|
155
155
|
// Check ROADMAP checkbox status
|
|
156
|
-
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum
|
|
156
|
+
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}`, 'i');
|
|
157
157
|
const checkboxMatch = content.match(checkboxPattern);
|
|
158
158
|
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
159
159
|
|
|
@@ -192,7 +192,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
192
192
|
const completedPhases = phases.filter(p => p.disk_status === 'complete').length;
|
|
193
193
|
|
|
194
194
|
// Detect phases in summary list without detail sections (malformed ROADMAP)
|
|
195
|
-
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)
|
|
195
|
+
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi;
|
|
196
196
|
const checklistPhases = new Set();
|
|
197
197
|
let checklistMatch;
|
|
198
198
|
while ((checklistMatch = checklistPattern.exec(content)) !== null) {
|
|
@@ -208,7 +208,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
208
208
|
completed_phases: completedPhases,
|
|
209
209
|
total_plans: totalPlans,
|
|
210
210
|
total_summaries: totalSummaries,
|
|
211
|
-
progress_percent: totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0,
|
|
211
|
+
progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0,
|
|
212
212
|
current_phase: currentPhase ? currentPhase.number : null,
|
|
213
213
|
next_phase: nextPhase ? nextPhase.number : null,
|
|
214
214
|
missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
|
|
@@ -247,7 +247,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
250
|
-
const phaseEscaped = phaseNum
|
|
250
|
+
const phaseEscaped = escapeRegex(phaseNum);
|
|
251
251
|
|
|
252
252
|
// Progress table row: update Plans column (summaries/plans) and Status column
|
|
253
253
|
const tablePattern = new RegExp(
|