pi-gsd 2.0.1 → 2.0.2
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/dist/pi-gsd-hooks.js +1532 -0
- package/package.json +3 -5
- package/.gsd/extensions/pi-gsd-hooks.ts +0 -973
- package/src/cli.ts +0 -644
- package/src/commands/base.ts +0 -67
- package/src/commands/commit.ts +0 -22
- package/src/commands/config.ts +0 -71
- package/src/commands/frontmatter.ts +0 -51
- package/src/commands/index.ts +0 -76
- package/src/commands/init.ts +0 -43
- package/src/commands/milestone.ts +0 -37
- package/src/commands/phase.ts +0 -92
- package/src/commands/progress.ts +0 -71
- package/src/commands/roadmap.ts +0 -40
- package/src/commands/scaffold.ts +0 -19
- package/src/commands/state.ts +0 -102
- package/src/commands/template.ts +0 -52
- package/src/commands/verify.ts +0 -70
- package/src/commands/workstream.ts +0 -98
- package/src/commands/wxp.ts +0 -65
- package/src/lib/commands.ts +0 -1040
- package/src/lib/config.ts +0 -385
- package/src/lib/core.ts +0 -1167
- package/src/lib/frontmatter.ts +0 -462
- package/src/lib/init.ts +0 -517
- package/src/lib/milestone.ts +0 -290
- package/src/lib/model-profiles.ts +0 -272
- package/src/lib/phase.ts +0 -1012
- package/src/lib/profile-output.ts +0 -237
- package/src/lib/profile-pipeline.ts +0 -556
- package/src/lib/roadmap.ts +0 -378
- package/src/lib/schemas.ts +0 -290
- package/src/lib/security.ts +0 -176
- package/src/lib/state.ts +0 -1175
- package/src/lib/template.ts +0 -246
- package/src/lib/uat.ts +0 -289
- package/src/lib/verify.ts +0 -879
- package/src/lib/workstream.ts +0 -524
- package/src/output.ts +0 -45
- package/src/schemas/pi-gsd-settings.schema.json +0 -80
- package/src/schemas/wxp.xsd +0 -619
- package/src/schemas/wxp.zod.ts +0 -318
- package/src/wxp/__tests__/arguments.test.ts +0 -86
- package/src/wxp/__tests__/conditions.test.ts +0 -106
- package/src/wxp/__tests__/executor.test.ts +0 -95
- package/src/wxp/__tests__/helpers.ts +0 -26
- package/src/wxp/__tests__/integration.test.ts +0 -166
- package/src/wxp/__tests__/new-features.test.ts +0 -222
- package/src/wxp/__tests__/parser.test.ts +0 -159
- package/src/wxp/__tests__/paste.test.ts +0 -66
- package/src/wxp/__tests__/schema.test.ts +0 -120
- package/src/wxp/__tests__/security.test.ts +0 -87
- package/src/wxp/__tests__/shell.test.ts +0 -85
- package/src/wxp/__tests__/string-ops.test.ts +0 -25
- package/src/wxp/__tests__/variables.test.ts +0 -65
- package/src/wxp/arguments.ts +0 -89
- package/src/wxp/conditions.ts +0 -78
- package/src/wxp/executor.ts +0 -191
- package/src/wxp/index.ts +0 -191
- package/src/wxp/parser.ts +0 -198
- package/src/wxp/paste.ts +0 -51
- package/src/wxp/security.ts +0 -102
- package/src/wxp/shell.ts +0 -81
- package/src/wxp/string-ops.ts +0 -44
- package/src/wxp/variables.ts +0 -109
package/src/lib/phase.ts
DELETED
|
@@ -1,1012 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* phase.ts - Phase CRUD, query, and lifecycle operations.
|
|
3
|
-
*
|
|
4
|
-
* Ported from lib/phase.cjs. All command signatures preserved.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import fs from "fs";
|
|
8
|
-
import path from "path";
|
|
9
|
-
import {
|
|
10
|
-
comparePhaseNum,
|
|
11
|
-
escapeRegex,
|
|
12
|
-
extractCurrentMilestone,
|
|
13
|
-
findPhaseInternal,
|
|
14
|
-
generateSlugInternal,
|
|
15
|
-
getArchivedPhaseDirs,
|
|
16
|
-
getMilestonePhaseFilter,
|
|
17
|
-
gsdError,
|
|
18
|
-
loadConfig,
|
|
19
|
-
normalizePhaseName,
|
|
20
|
-
output,
|
|
21
|
-
planningDir,
|
|
22
|
-
readSubdirectories,
|
|
23
|
-
replaceInCurrentMilestone,
|
|
24
|
-
toPosixPath,
|
|
25
|
-
} from "./core.js";
|
|
26
|
-
import { extractFrontmatter, asStr, asArr } from "./frontmatter.js";
|
|
27
|
-
|
|
28
|
-
// Shape of a single plan entry built by cmdPhasePlanIndex
|
|
29
|
-
interface PhasePlanEntry {
|
|
30
|
-
id: string;
|
|
31
|
-
wave: number;
|
|
32
|
-
autonomous: boolean;
|
|
33
|
-
objective: string | null;
|
|
34
|
-
files_modified: string[];
|
|
35
|
-
task_count: number;
|
|
36
|
-
has_summary: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
import {
|
|
40
|
-
stateExtractField,
|
|
41
|
-
stateReplaceField,
|
|
42
|
-
stateReplaceFieldWithFallback,
|
|
43
|
-
writeStateMd,
|
|
44
|
-
} from "./state.js";
|
|
45
|
-
|
|
46
|
-
// ─── cmdPhasesList ────────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
export function cmdPhasesList(
|
|
49
|
-
cwd: string,
|
|
50
|
-
options: {
|
|
51
|
-
type?: string | null;
|
|
52
|
-
phase?: string | null;
|
|
53
|
-
includeArchived?: boolean;
|
|
54
|
-
},
|
|
55
|
-
raw: boolean,
|
|
56
|
-
): void {
|
|
57
|
-
const phasesDir = path.join(planningDir(cwd), "phases");
|
|
58
|
-
const { type, phase, includeArchived } = options;
|
|
59
|
-
if (!fs.existsSync(phasesDir)) {
|
|
60
|
-
output(
|
|
61
|
-
type ? { files: [], count: 0 } : { directories: [], count: 0 },
|
|
62
|
-
raw,
|
|
63
|
-
"",
|
|
64
|
-
);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
let dirs = fs
|
|
69
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
70
|
-
.filter((e) => e.isDirectory())
|
|
71
|
-
.map((e) => e.name);
|
|
72
|
-
if (includeArchived) {
|
|
73
|
-
const archived = getArchivedPhaseDirs(cwd);
|
|
74
|
-
for (const a of archived) dirs.push(`${a.name} [${a.milestone}]`);
|
|
75
|
-
}
|
|
76
|
-
dirs.sort((a, b) => comparePhaseNum(a, b));
|
|
77
|
-
if (phase) {
|
|
78
|
-
const normalized = normalizePhaseName(phase);
|
|
79
|
-
const match = dirs.find((d) => d.startsWith(normalized));
|
|
80
|
-
if (!match) {
|
|
81
|
-
output(
|
|
82
|
-
{ files: [], count: 0, phase_dir: null, error: "Phase not found" },
|
|
83
|
-
raw,
|
|
84
|
-
"",
|
|
85
|
-
);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
dirs = [match];
|
|
89
|
-
}
|
|
90
|
-
if (type) {
|
|
91
|
-
const files: string[] = [];
|
|
92
|
-
for (const dir of dirs) {
|
|
93
|
-
const dirPath = path.join(phasesDir, dir);
|
|
94
|
-
const dirFiles = fs.readdirSync(dirPath);
|
|
95
|
-
let filtered: string[];
|
|
96
|
-
if (type === "plans")
|
|
97
|
-
filtered = dirFiles.filter(
|
|
98
|
-
(f) => f.endsWith("-PLAN.md") || f === "PLAN.md",
|
|
99
|
-
);
|
|
100
|
-
else if (type === "summaries")
|
|
101
|
-
filtered = dirFiles.filter(
|
|
102
|
-
(f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md",
|
|
103
|
-
);
|
|
104
|
-
else filtered = dirFiles;
|
|
105
|
-
files.push(...filtered.sort());
|
|
106
|
-
}
|
|
107
|
-
output(
|
|
108
|
-
{
|
|
109
|
-
files,
|
|
110
|
-
count: files.length,
|
|
111
|
-
phase_dir: phase ? dirs[0]?.replace(/^\d+(?:\.\d+)*-?/, "") : null,
|
|
112
|
-
},
|
|
113
|
-
raw,
|
|
114
|
-
files.join("\n"),
|
|
115
|
-
);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
output({ directories: dirs, count: dirs.length }, raw, dirs.join("\n"));
|
|
119
|
-
} catch (e) {
|
|
120
|
-
gsdError("Failed to list phases: " + (e as Error).message);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ─── cmdPhaseNextDecimal ──────────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
export function cmdPhaseNextDecimal(
|
|
127
|
-
cwd: string,
|
|
128
|
-
basePhase: string | undefined,
|
|
129
|
-
raw: boolean,
|
|
130
|
-
): void {
|
|
131
|
-
const phasesDir = path.join(planningDir(cwd), "phases");
|
|
132
|
-
const normalized = normalizePhaseName(basePhase ?? "");
|
|
133
|
-
if (!fs.existsSync(phasesDir)) {
|
|
134
|
-
output(
|
|
135
|
-
{
|
|
136
|
-
found: false,
|
|
137
|
-
base_phase: normalized,
|
|
138
|
-
next: `${normalized}.1`,
|
|
139
|
-
existing: [],
|
|
140
|
-
},
|
|
141
|
-
raw,
|
|
142
|
-
`${normalized}.1`,
|
|
143
|
-
);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
const dirs = fs
|
|
148
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
149
|
-
.filter((e) => e.isDirectory())
|
|
150
|
-
.map((e) => e.name);
|
|
151
|
-
const baseExists = dirs.some(
|
|
152
|
-
(d) => d.startsWith(normalized + "-") || d === normalized,
|
|
153
|
-
);
|
|
154
|
-
const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
|
|
155
|
-
const existingDecimals = dirs
|
|
156
|
-
.map((d) => {
|
|
157
|
-
const m = d.match(decimalPattern);
|
|
158
|
-
return m ? `${normalized}.${m[1]}` : null;
|
|
159
|
-
})
|
|
160
|
-
.filter(Boolean) as string[];
|
|
161
|
-
existingDecimals.sort((a, b) => comparePhaseNum(a, b));
|
|
162
|
-
const nextDecimal =
|
|
163
|
-
existingDecimals.length === 0
|
|
164
|
-
? `${normalized}.1`
|
|
165
|
-
: `${normalized}.${parseInt(existingDecimals[existingDecimals.length - 1].split(".")[1], 10) + 1}`;
|
|
166
|
-
output(
|
|
167
|
-
{
|
|
168
|
-
found: baseExists,
|
|
169
|
-
base_phase: normalized,
|
|
170
|
-
next: nextDecimal,
|
|
171
|
-
existing: existingDecimals,
|
|
172
|
-
},
|
|
173
|
-
raw,
|
|
174
|
-
nextDecimal,
|
|
175
|
-
);
|
|
176
|
-
} catch (e) {
|
|
177
|
-
gsdError("Failed to calculate next decimal phase: " + (e as Error).message);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ─── cmdFindPhase ─────────────────────────────────────────────────────────────
|
|
182
|
-
|
|
183
|
-
export function cmdFindPhase(
|
|
184
|
-
cwd: string,
|
|
185
|
-
phase: string | undefined,
|
|
186
|
-
raw: boolean,
|
|
187
|
-
): void {
|
|
188
|
-
if (!phase) gsdError("phase identifier required");
|
|
189
|
-
const phasesDir = path.join(planningDir(cwd), "phases");
|
|
190
|
-
const normalized = normalizePhaseName(phase!);
|
|
191
|
-
const notFound = {
|
|
192
|
-
found: false,
|
|
193
|
-
directory: null,
|
|
194
|
-
phase_number: null,
|
|
195
|
-
phase_name: null,
|
|
196
|
-
plans: [],
|
|
197
|
-
summaries: [],
|
|
198
|
-
};
|
|
199
|
-
try {
|
|
200
|
-
const dirs = fs
|
|
201
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
202
|
-
.filter((e) => e.isDirectory())
|
|
203
|
-
.map((e) => e.name)
|
|
204
|
-
.sort((a, b) => comparePhaseNum(a, b));
|
|
205
|
-
const match = dirs.find((d) => d.startsWith(normalized));
|
|
206
|
-
if (!match) {
|
|
207
|
-
output(notFound, raw, "");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
211
|
-
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
212
|
-
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
213
|
-
const phaseDir = path.join(phasesDir, match);
|
|
214
|
-
const phaseFiles = fs.readdirSync(phaseDir);
|
|
215
|
-
const plans = phaseFiles
|
|
216
|
-
.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md")
|
|
217
|
-
.sort();
|
|
218
|
-
const summaries = phaseFiles
|
|
219
|
-
.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md")
|
|
220
|
-
.sort();
|
|
221
|
-
output(
|
|
222
|
-
{
|
|
223
|
-
found: true,
|
|
224
|
-
directory: toPosixPath(
|
|
225
|
-
path.join(path.relative(cwd, planningDir(cwd)), "phases", match),
|
|
226
|
-
),
|
|
227
|
-
phase_number: phaseNumber,
|
|
228
|
-
phase_name: phaseName,
|
|
229
|
-
plans,
|
|
230
|
-
summaries,
|
|
231
|
-
},
|
|
232
|
-
raw,
|
|
233
|
-
toPosixPath(
|
|
234
|
-
path.join(path.relative(cwd, planningDir(cwd)), "phases", match),
|
|
235
|
-
),
|
|
236
|
-
);
|
|
237
|
-
} catch {
|
|
238
|
-
output(notFound, raw, "");
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ─── cmdPhasePlanIndex ────────────────────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
export function cmdPhasePlanIndex(
|
|
245
|
-
cwd: string,
|
|
246
|
-
phase: string | undefined,
|
|
247
|
-
raw: boolean,
|
|
248
|
-
): void {
|
|
249
|
-
if (!phase) gsdError("phase required for phase-plan-index");
|
|
250
|
-
const phasesDir = path.join(planningDir(cwd), "phases");
|
|
251
|
-
const normalized = normalizePhaseName(phase!);
|
|
252
|
-
let phaseDir: string | null = null;
|
|
253
|
-
try {
|
|
254
|
-
const dirs = fs
|
|
255
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
256
|
-
.filter((e) => e.isDirectory())
|
|
257
|
-
.map((e) => e.name)
|
|
258
|
-
.sort((a, b) => comparePhaseNum(a, b));
|
|
259
|
-
const match = dirs.find((d) => d.startsWith(normalized));
|
|
260
|
-
if (match) phaseDir = path.join(phasesDir, match);
|
|
261
|
-
} catch {
|
|
262
|
-
/* ok */
|
|
263
|
-
}
|
|
264
|
-
if (!phaseDir) {
|
|
265
|
-
output(
|
|
266
|
-
{
|
|
267
|
-
phase: normalized,
|
|
268
|
-
error: "Phase not found",
|
|
269
|
-
plans: [],
|
|
270
|
-
waves: {},
|
|
271
|
-
incomplete: [],
|
|
272
|
-
has_checkpoints: false,
|
|
273
|
-
},
|
|
274
|
-
raw,
|
|
275
|
-
);
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
const phaseFiles = fs.readdirSync(phaseDir);
|
|
279
|
-
const planFiles = phaseFiles
|
|
280
|
-
.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md")
|
|
281
|
-
.sort();
|
|
282
|
-
const summaryFiles = phaseFiles.filter(
|
|
283
|
-
(f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md",
|
|
284
|
-
);
|
|
285
|
-
const completedPlanIds = new Set(
|
|
286
|
-
summaryFiles.map((s) =>
|
|
287
|
-
s.replace("-SUMMARY.md", "").replace("SUMMARY.md", ""),
|
|
288
|
-
),
|
|
289
|
-
);
|
|
290
|
-
const plans: PhasePlanEntry[] = [],
|
|
291
|
-
waves: Record<string, string[]> = {},
|
|
292
|
-
incomplete: string[] = [];
|
|
293
|
-
let hasCheckpoints = false;
|
|
294
|
-
for (const planFile of planFiles) {
|
|
295
|
-
const planId = planFile.replace("-PLAN.md", "").replace("PLAN.md", "");
|
|
296
|
-
const content = fs.readFileSync(path.join(phaseDir, planFile), "utf-8");
|
|
297
|
-
const fm = extractFrontmatter(content);
|
|
298
|
-
const xmlTasks = content.match(/<task[\s>]/gi) || [],
|
|
299
|
-
mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
|
|
300
|
-
const taskCount = xmlTasks.length || mdTasks.length;
|
|
301
|
-
const wave = parseInt(String(fm.wave), 10) || 1;
|
|
302
|
-
let autonomous = true;
|
|
303
|
-
if (fm.autonomous !== undefined)
|
|
304
|
-
autonomous = fm.autonomous === "true" || fm.autonomous === true;
|
|
305
|
-
if (!autonomous) hasCheckpoints = true;
|
|
306
|
-
let filesModified: string[] = [];
|
|
307
|
-
const fmFiles = fm["files_modified"] ?? fm["files-modified"];
|
|
308
|
-
if (fmFiles) {
|
|
309
|
-
const arr = asArr(fmFiles);
|
|
310
|
-
filesModified = arr ? arr.map((f) => asStr(f) ?? String(f)) : [asStr(fmFiles) ?? String(fmFiles)];
|
|
311
|
-
}
|
|
312
|
-
const hasSummary = completedPlanIds.has(planId);
|
|
313
|
-
if (!hasSummary) incomplete.push(planId);
|
|
314
|
-
plans.push({
|
|
315
|
-
id: planId,
|
|
316
|
-
wave,
|
|
317
|
-
autonomous,
|
|
318
|
-
objective:
|
|
319
|
-
content.match(/<objective>\s*\n?\s*(.+)/)?.[1]?.trim() ??
|
|
320
|
-
asStr(fm.objective) ??
|
|
321
|
-
null,
|
|
322
|
-
files_modified: filesModified,
|
|
323
|
-
task_count: taskCount,
|
|
324
|
-
has_summary: hasSummary,
|
|
325
|
-
});
|
|
326
|
-
const waveKey = String(wave);
|
|
327
|
-
if (!waves[waveKey]) waves[waveKey] = [];
|
|
328
|
-
waves[waveKey].push(planId);
|
|
329
|
-
}
|
|
330
|
-
output(
|
|
331
|
-
{
|
|
332
|
-
phase: normalized,
|
|
333
|
-
plans,
|
|
334
|
-
waves,
|
|
335
|
-
incomplete,
|
|
336
|
-
has_checkpoints: hasCheckpoints,
|
|
337
|
-
},
|
|
338
|
-
raw,
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ─── cmdPhaseAdd ─────────────────────────────────────────────────────────────
|
|
343
|
-
|
|
344
|
-
export function cmdPhaseAdd(
|
|
345
|
-
cwd: string,
|
|
346
|
-
description: string | undefined,
|
|
347
|
-
raw: boolean,
|
|
348
|
-
customId?: string | null,
|
|
349
|
-
): void {
|
|
350
|
-
if (!description) gsdError("description required for phase add");
|
|
351
|
-
const config = loadConfig(cwd);
|
|
352
|
-
const roadmapPath = path.join(planningDir(cwd), "ROADMAP.md");
|
|
353
|
-
if (!fs.existsSync(roadmapPath)) gsdError("ROADMAP.md not found");
|
|
354
|
-
const rawContent = fs.readFileSync(roadmapPath, "utf-8");
|
|
355
|
-
const content = extractCurrentMilestone(rawContent, cwd);
|
|
356
|
-
const slug = generateSlugInternal(description!);
|
|
357
|
-
let newPhaseId: string | number, dirName: string;
|
|
358
|
-
if (customId || config.phase_naming === "custom") {
|
|
359
|
-
newPhaseId = customId || slug!.toUpperCase().replace(/-/g, "-");
|
|
360
|
-
if (!newPhaseId) gsdError('--id required when phase_naming is "custom"');
|
|
361
|
-
dirName = `${newPhaseId}-${slug}`;
|
|
362
|
-
} else {
|
|
363
|
-
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
|
364
|
-
let maxPhase = 0,
|
|
365
|
-
m: RegExpExecArray | null;
|
|
366
|
-
while ((m = phasePattern.exec(content)) !== null) {
|
|
367
|
-
const n = parseInt(m[1], 10);
|
|
368
|
-
if (n > maxPhase) maxPhase = n;
|
|
369
|
-
}
|
|
370
|
-
newPhaseId = maxPhase + 1;
|
|
371
|
-
const paddedNum = String(newPhaseId).padStart(2, "0");
|
|
372
|
-
dirName = `${paddedNum}-${slug}`;
|
|
373
|
-
}
|
|
374
|
-
const dirPath = path.join(planningDir(cwd), "phases", dirName);
|
|
375
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
376
|
-
fs.writeFileSync(path.join(dirPath, ".gitkeep"), "");
|
|
377
|
-
const dependsOn =
|
|
378
|
-
config.phase_naming === "custom"
|
|
379
|
-
? ""
|
|
380
|
-
: `\n**Depends on:** Phase ${typeof newPhaseId === "number" ? newPhaseId - 1 : "TBD"}`;
|
|
381
|
-
const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${newPhaseId} to break down)\n`;
|
|
382
|
-
const lastSeparator = rawContent.lastIndexOf("\n---");
|
|
383
|
-
const updatedContent =
|
|
384
|
-
lastSeparator > 0
|
|
385
|
-
? rawContent.slice(0, lastSeparator) +
|
|
386
|
-
phaseEntry +
|
|
387
|
-
rawContent.slice(lastSeparator)
|
|
388
|
-
: rawContent + phaseEntry;
|
|
389
|
-
fs.writeFileSync(roadmapPath, updatedContent, "utf-8");
|
|
390
|
-
output(
|
|
391
|
-
{
|
|
392
|
-
phase_number:
|
|
393
|
-
typeof newPhaseId === "number" ? newPhaseId : String(newPhaseId),
|
|
394
|
-
padded:
|
|
395
|
-
typeof newPhaseId === "number"
|
|
396
|
-
? String(newPhaseId).padStart(2, "0")
|
|
397
|
-
: String(newPhaseId),
|
|
398
|
-
name: description,
|
|
399
|
-
slug,
|
|
400
|
-
directory: toPosixPath(
|
|
401
|
-
path.join(path.relative(cwd, planningDir(cwd)), "phases", dirName),
|
|
402
|
-
),
|
|
403
|
-
naming_mode: config.phase_naming,
|
|
404
|
-
},
|
|
405
|
-
raw,
|
|
406
|
-
typeof newPhaseId === "number"
|
|
407
|
-
? String(newPhaseId).padStart(2, "0")
|
|
408
|
-
: String(newPhaseId),
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ─── cmdPhaseInsert ───────────────────────────────────────────────────────────
|
|
413
|
-
|
|
414
|
-
export function cmdPhaseInsert(
|
|
415
|
-
cwd: string,
|
|
416
|
-
afterPhase: string | undefined,
|
|
417
|
-
description: string | undefined,
|
|
418
|
-
raw: boolean,
|
|
419
|
-
): void {
|
|
420
|
-
if (!afterPhase || !description)
|
|
421
|
-
gsdError("after-phase and description required for phase insert");
|
|
422
|
-
const roadmapPath = path.join(planningDir(cwd), "ROADMAP.md");
|
|
423
|
-
if (!fs.existsSync(roadmapPath)) gsdError("ROADMAP.md not found");
|
|
424
|
-
const rawContent = fs.readFileSync(roadmapPath, "utf-8");
|
|
425
|
-
const content = extractCurrentMilestone(rawContent, cwd);
|
|
426
|
-
const slug = generateSlugInternal(description!);
|
|
427
|
-
const normalizedAfter = normalizePhaseName(afterPhase!);
|
|
428
|
-
const unpadded = normalizedAfter.replace(/^0+/, "");
|
|
429
|
-
const afterPhaseEscaped = unpadded.replace(/\./g, "\\.");
|
|
430
|
-
const targetPattern = new RegExp(
|
|
431
|
-
`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`,
|
|
432
|
-
"i",
|
|
433
|
-
);
|
|
434
|
-
if (!targetPattern.test(content))
|
|
435
|
-
gsdError(`Phase ${afterPhase} not found in ROADMAP.md`);
|
|
436
|
-
const phasesDir = path.join(planningDir(cwd), "phases");
|
|
437
|
-
const normalizedBase = normalizePhaseName(afterPhase!);
|
|
438
|
-
const existingDecimals: number[] = [];
|
|
439
|
-
try {
|
|
440
|
-
const dirs = fs
|
|
441
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
442
|
-
.filter((e) => e.isDirectory())
|
|
443
|
-
.map((e) => e.name);
|
|
444
|
-
const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
|
|
445
|
-
for (const dir of dirs) {
|
|
446
|
-
const dm = dir.match(decimalPattern);
|
|
447
|
-
if (dm) existingDecimals.push(parseInt(dm[1], 10));
|
|
448
|
-
}
|
|
449
|
-
} catch {
|
|
450
|
-
/* ok */
|
|
451
|
-
}
|
|
452
|
-
const nextDecimal =
|
|
453
|
-
existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
|
|
454
|
-
const decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
|
455
|
-
const dirName = `${decimalPhase}-${slug}`;
|
|
456
|
-
const dirPath = path.join(planningDir(cwd), "phases", dirName);
|
|
457
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
458
|
-
fs.writeFileSync(path.join(dirPath, ".gitkeep"), "");
|
|
459
|
-
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`;
|
|
460
|
-
const headerPattern = new RegExp(
|
|
461
|
-
`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`,
|
|
462
|
-
"i",
|
|
463
|
-
);
|
|
464
|
-
const headerMatch = rawContent.match(headerPattern);
|
|
465
|
-
if (!headerMatch) gsdError(`Could not find Phase ${afterPhase} header`);
|
|
466
|
-
const headerIdx = rawContent.indexOf(headerMatch![0]);
|
|
467
|
-
const afterHeader = rawContent.slice(headerIdx + headerMatch![0].length);
|
|
468
|
-
const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
469
|
-
const insertIdx = nextPhaseMatch
|
|
470
|
-
? headerIdx + headerMatch![0].length + nextPhaseMatch.index!
|
|
471
|
-
: rawContent.length;
|
|
472
|
-
fs.writeFileSync(
|
|
473
|
-
roadmapPath,
|
|
474
|
-
rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx),
|
|
475
|
-
"utf-8",
|
|
476
|
-
);
|
|
477
|
-
output(
|
|
478
|
-
{
|
|
479
|
-
phase_number: decimalPhase,
|
|
480
|
-
after_phase: afterPhase,
|
|
481
|
-
name: description,
|
|
482
|
-
slug,
|
|
483
|
-
directory: toPosixPath(
|
|
484
|
-
path.join(path.relative(cwd, planningDir(cwd)), "phases", dirName),
|
|
485
|
-
),
|
|
486
|
-
},
|
|
487
|
-
raw,
|
|
488
|
-
decimalPhase,
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// ─── cmdPhaseRemove ───────────────────────────────────────────────────────────
|
|
493
|
-
|
|
494
|
-
function renameDecimalPhases(
|
|
495
|
-
phasesDir: string,
|
|
496
|
-
baseInt: string,
|
|
497
|
-
removedDecimal: number,
|
|
498
|
-
): {
|
|
499
|
-
renamedDirs: Array<{ from: string; to: string }>;
|
|
500
|
-
renamedFiles: Array<{ from: string; to: string }>;
|
|
501
|
-
} {
|
|
502
|
-
const renamedDirs: Array<{ from: string; to: string }> = [],
|
|
503
|
-
renamedFiles: Array<{ from: string; to: string }> = [];
|
|
504
|
-
const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
|
|
505
|
-
const toRename = readSubdirectories(phasesDir, true)
|
|
506
|
-
.map((dir) => {
|
|
507
|
-
const m = dir.match(decPattern);
|
|
508
|
-
return m ? { dir, oldDecimal: parseInt(m[1], 10), slug: m[2] } : null;
|
|
509
|
-
})
|
|
510
|
-
.filter(
|
|
511
|
-
(x): x is NonNullable<typeof x> =>
|
|
512
|
-
x !== null && x.oldDecimal > removedDecimal,
|
|
513
|
-
)
|
|
514
|
-
.sort((a, b) => b.oldDecimal - a.oldDecimal);
|
|
515
|
-
for (const item of toRename) {
|
|
516
|
-
const newDecimal = item.oldDecimal - 1;
|
|
517
|
-
const oldPhaseId = `${baseInt}.${item.oldDecimal}`,
|
|
518
|
-
newPhaseId = `${baseInt}.${newDecimal}`;
|
|
519
|
-
const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
|
|
520
|
-
fs.renameSync(
|
|
521
|
-
path.join(phasesDir, item.dir),
|
|
522
|
-
path.join(phasesDir, newDirName),
|
|
523
|
-
);
|
|
524
|
-
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
525
|
-
for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
|
|
526
|
-
if (f.includes(oldPhaseId)) {
|
|
527
|
-
const newFileName = f.replace(oldPhaseId, newPhaseId);
|
|
528
|
-
fs.renameSync(
|
|
529
|
-
path.join(phasesDir, newDirName, f),
|
|
530
|
-
path.join(phasesDir, newDirName, newFileName),
|
|
531
|
-
);
|
|
532
|
-
renamedFiles.push({ from: f, to: newFileName });
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return { renamedDirs, renamedFiles };
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function renameIntegerPhases(
|
|
540
|
-
phasesDir: string,
|
|
541
|
-
removedInt: number,
|
|
542
|
-
): {
|
|
543
|
-
renamedDirs: Array<{ from: string; to: string }>;
|
|
544
|
-
renamedFiles: Array<{ from: string; to: string }>;
|
|
545
|
-
} {
|
|
546
|
-
const renamedDirs: Array<{ from: string; to: string }> = [],
|
|
547
|
-
renamedFiles: Array<{ from: string; to: string }> = [];
|
|
548
|
-
const toRename = readSubdirectories(phasesDir, true)
|
|
549
|
-
.map((dir) => {
|
|
550
|
-
const m = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
|
|
551
|
-
if (!m) return null;
|
|
552
|
-
const dirInt = parseInt(m[1], 10);
|
|
553
|
-
return dirInt > removedInt
|
|
554
|
-
? {
|
|
555
|
-
dir,
|
|
556
|
-
oldInt: dirInt,
|
|
557
|
-
letter: m[2] ? m[2].toUpperCase() : "",
|
|
558
|
-
decimal: m[3] ? parseInt(m[3], 10) : null,
|
|
559
|
-
slug: m[4],
|
|
560
|
-
}
|
|
561
|
-
: null;
|
|
562
|
-
})
|
|
563
|
-
.filter((x): x is NonNullable<typeof x> => x !== null)
|
|
564
|
-
.sort((a, b) =>
|
|
565
|
-
a.oldInt !== b.oldInt
|
|
566
|
-
? b.oldInt - a.oldInt
|
|
567
|
-
: (b.decimal || 0) - (a.decimal || 0),
|
|
568
|
-
);
|
|
569
|
-
for (const item of toRename) {
|
|
570
|
-
const newInt = item.oldInt - 1;
|
|
571
|
-
const newPadded = String(newInt).padStart(2, "0"),
|
|
572
|
-
oldPadded = String(item.oldInt).padStart(2, "0");
|
|
573
|
-
const letterSuffix = item.letter || "",
|
|
574
|
-
decimalSuffix = item.decimal !== null ? `.${item.decimal}` : "";
|
|
575
|
-
const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`,
|
|
576
|
-
newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
|
|
577
|
-
const newDirName = `${newPrefix}-${item.slug}`;
|
|
578
|
-
fs.renameSync(
|
|
579
|
-
path.join(phasesDir, item.dir),
|
|
580
|
-
path.join(phasesDir, newDirName),
|
|
581
|
-
);
|
|
582
|
-
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
583
|
-
for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
|
|
584
|
-
if (f.startsWith(oldPrefix)) {
|
|
585
|
-
const newFileName = newPrefix + f.slice(oldPrefix.length);
|
|
586
|
-
fs.renameSync(
|
|
587
|
-
path.join(phasesDir, newDirName, f),
|
|
588
|
-
path.join(phasesDir, newDirName, newFileName),
|
|
589
|
-
);
|
|
590
|
-
renamedFiles.push({ from: f, to: newFileName });
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return { renamedDirs, renamedFiles };
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function updateRoadmapAfterPhaseRemoval(
|
|
598
|
-
roadmapPath: string,
|
|
599
|
-
targetPhase: string,
|
|
600
|
-
isDecimal: boolean,
|
|
601
|
-
removedInt: number,
|
|
602
|
-
): void {
|
|
603
|
-
let content = fs.readFileSync(roadmapPath, "utf-8");
|
|
604
|
-
const escaped = escapeRegex(targetPhase);
|
|
605
|
-
content = content.replace(
|
|
606
|
-
new RegExp(
|
|
607
|
-
`\\n?#{2,4}\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
|
|
608
|
-
"i",
|
|
609
|
-
),
|
|
610
|
-
"",
|
|
611
|
-
);
|
|
612
|
-
content = content.replace(
|
|
613
|
-
new RegExp(
|
|
614
|
-
`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`,
|
|
615
|
-
"gi",
|
|
616
|
-
),
|
|
617
|
-
"",
|
|
618
|
-
);
|
|
619
|
-
content = content.replace(
|
|
620
|
-
new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, "gi"),
|
|
621
|
-
"",
|
|
622
|
-
);
|
|
623
|
-
if (!isDecimal) {
|
|
624
|
-
for (let oldNum = 99; oldNum > removedInt; oldNum--) {
|
|
625
|
-
const newNum = oldNum - 1,
|
|
626
|
-
oldStr = String(oldNum),
|
|
627
|
-
newStr = String(newNum);
|
|
628
|
-
const oldPad = oldStr.padStart(2, "0"),
|
|
629
|
-
newPad = newStr.padStart(2, "0");
|
|
630
|
-
content = content.replace(
|
|
631
|
-
new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, "gi"),
|
|
632
|
-
`$1${newStr}$2`,
|
|
633
|
-
);
|
|
634
|
-
content = content.replace(
|
|
635
|
-
new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, "g"),
|
|
636
|
-
`$1${newStr}$2`,
|
|
637
|
-
);
|
|
638
|
-
content = content.replace(
|
|
639
|
-
new RegExp(`${oldPad}-(\\d{2})`, "g"),
|
|
640
|
-
`${newPad}-$1`,
|
|
641
|
-
);
|
|
642
|
-
content = content.replace(
|
|
643
|
-
new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, "g"),
|
|
644
|
-
`$1${newStr}. `,
|
|
645
|
-
);
|
|
646
|
-
content = content.replace(
|
|
647
|
-
new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, "gi"),
|
|
648
|
-
`$1${newStr}`,
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
fs.writeFileSync(roadmapPath, content, "utf-8");
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
export function cmdPhaseRemove(
|
|
656
|
-
cwd: string,
|
|
657
|
-
targetPhase: string | undefined,
|
|
658
|
-
options: { force?: boolean },
|
|
659
|
-
raw: boolean,
|
|
660
|
-
): void {
|
|
661
|
-
if (!targetPhase) gsdError("phase number required for phase remove");
|
|
662
|
-
const roadmapPath = path.join(planningDir(cwd), "ROADMAP.md");
|
|
663
|
-
const phasesDir = path.join(planningDir(cwd), "phases");
|
|
664
|
-
if (!fs.existsSync(roadmapPath)) gsdError("ROADMAP.md not found");
|
|
665
|
-
const normalized = normalizePhaseName(targetPhase!);
|
|
666
|
-
const isDecimal = targetPhase!.includes(".");
|
|
667
|
-
const force = options.force || false;
|
|
668
|
-
const targetDir =
|
|
669
|
-
readSubdirectories(phasesDir, true).find(
|
|
670
|
-
(d) => d.startsWith(normalized + "-") || d === normalized,
|
|
671
|
-
) || null;
|
|
672
|
-
if (targetDir && !force) {
|
|
673
|
-
const summaries = fs
|
|
674
|
-
.readdirSync(path.join(phasesDir, targetDir))
|
|
675
|
-
.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md");
|
|
676
|
-
if (summaries.length > 0)
|
|
677
|
-
gsdError(
|
|
678
|
-
`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`,
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
if (targetDir)
|
|
682
|
-
fs.rmSync(path.join(phasesDir, targetDir), {
|
|
683
|
-
recursive: true,
|
|
684
|
-
force: true,
|
|
685
|
-
});
|
|
686
|
-
let renamedDirs: Array<{ from: string; to: string }> = [],
|
|
687
|
-
renamedFiles: Array<{ from: string; to: string }> = [];
|
|
688
|
-
try {
|
|
689
|
-
const renamed = isDecimal
|
|
690
|
-
? renameDecimalPhases(
|
|
691
|
-
phasesDir,
|
|
692
|
-
normalized.split(".")[0],
|
|
693
|
-
parseInt(normalized.split(".")[1], 10),
|
|
694
|
-
)
|
|
695
|
-
: renameIntegerPhases(phasesDir, parseInt(normalized, 10));
|
|
696
|
-
renamedDirs = renamed.renamedDirs;
|
|
697
|
-
renamedFiles = renamed.renamedFiles;
|
|
698
|
-
} catch {
|
|
699
|
-
/* ok */
|
|
700
|
-
}
|
|
701
|
-
updateRoadmapAfterPhaseRemoval(
|
|
702
|
-
roadmapPath,
|
|
703
|
-
targetPhase!,
|
|
704
|
-
isDecimal,
|
|
705
|
-
parseInt(normalized, 10),
|
|
706
|
-
);
|
|
707
|
-
const statePath = path.join(planningDir(cwd), "STATE.md");
|
|
708
|
-
if (fs.existsSync(statePath)) {
|
|
709
|
-
let stateContent = fs.readFileSync(statePath, "utf-8");
|
|
710
|
-
const totalRaw = stateExtractField(stateContent, "Total Phases");
|
|
711
|
-
if (totalRaw)
|
|
712
|
-
stateContent =
|
|
713
|
-
stateReplaceField(
|
|
714
|
-
stateContent,
|
|
715
|
-
"Total Phases",
|
|
716
|
-
String(parseInt(totalRaw, 10) - 1),
|
|
717
|
-
) ?? stateContent;
|
|
718
|
-
const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
|
|
719
|
-
if (ofMatch)
|
|
720
|
-
stateContent = stateContent.replace(
|
|
721
|
-
/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i,
|
|
722
|
-
`$1${parseInt(ofMatch[2], 10) - 1}$3`,
|
|
723
|
-
);
|
|
724
|
-
writeStateMd(statePath, stateContent, cwd);
|
|
725
|
-
}
|
|
726
|
-
output(
|
|
727
|
-
{
|
|
728
|
-
removed: targetPhase,
|
|
729
|
-
directory_deleted: targetDir,
|
|
730
|
-
renamed_directories: renamedDirs,
|
|
731
|
-
renamed_files: renamedFiles,
|
|
732
|
-
roadmap_updated: true,
|
|
733
|
-
state_updated: fs.existsSync(statePath),
|
|
734
|
-
},
|
|
735
|
-
raw,
|
|
736
|
-
);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// ─── cmdPhaseComplete ─────────────────────────────────────────────────────────
|
|
740
|
-
|
|
741
|
-
export function cmdPhaseComplete(
|
|
742
|
-
cwd: string,
|
|
743
|
-
phaseNum: string | undefined,
|
|
744
|
-
raw: boolean,
|
|
745
|
-
): void {
|
|
746
|
-
if (!phaseNum) gsdError("phase number required for phase complete");
|
|
747
|
-
const roadmapPath = path.join(planningDir(cwd), "ROADMAP.md");
|
|
748
|
-
const statePath = path.join(planningDir(cwd), "STATE.md");
|
|
749
|
-
const phasesDir = path.join(planningDir(cwd), "phases");
|
|
750
|
-
const normalized = normalizePhaseName(phaseNum!);
|
|
751
|
-
const today = new Date().toISOString().split("T")[0];
|
|
752
|
-
const phaseInfo = findPhaseInternal(cwd, phaseNum!);
|
|
753
|
-
if (!phaseInfo) gsdError(`Phase ${phaseNum} not found`);
|
|
754
|
-
const planCount = phaseInfo!.plans.length,
|
|
755
|
-
summaryCount = phaseInfo!.summaries.length;
|
|
756
|
-
let requirementsUpdated = false;
|
|
757
|
-
const warnings: string[] = [];
|
|
758
|
-
try {
|
|
759
|
-
const phaseFullDir = path.join(cwd, phaseInfo!.directory);
|
|
760
|
-
const phaseFiles = fs.readdirSync(phaseFullDir);
|
|
761
|
-
for (const file of phaseFiles.filter(
|
|
762
|
-
(f) => f.includes("-UAT") && f.endsWith(".md"),
|
|
763
|
-
)) {
|
|
764
|
-
const content = fs.readFileSync(path.join(phaseFullDir, file), "utf-8");
|
|
765
|
-
if (/result: pending/.test(content))
|
|
766
|
-
warnings.push(`${file}: has pending tests`);
|
|
767
|
-
if (/result: blocked/.test(content))
|
|
768
|
-
warnings.push(`${file}: has blocked tests`);
|
|
769
|
-
if (/status: partial/.test(content))
|
|
770
|
-
warnings.push(`${file}: testing incomplete (partial)`);
|
|
771
|
-
if (/status: diagnosed/.test(content))
|
|
772
|
-
warnings.push(`${file}: has diagnosed gaps`);
|
|
773
|
-
}
|
|
774
|
-
for (const file of phaseFiles.filter(
|
|
775
|
-
(f) => f.includes("-VERIFICATION") && f.endsWith(".md"),
|
|
776
|
-
)) {
|
|
777
|
-
const content = fs.readFileSync(path.join(phaseFullDir, file), "utf-8");
|
|
778
|
-
if (/status: human_needed/.test(content))
|
|
779
|
-
warnings.push(`${file}: needs human verification`);
|
|
780
|
-
if (/status: gaps_found/.test(content))
|
|
781
|
-
warnings.push(`${file}: has unresolved gaps`);
|
|
782
|
-
}
|
|
783
|
-
} catch {
|
|
784
|
-
/* ok */
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (fs.existsSync(roadmapPath)) {
|
|
788
|
-
let roadmapContent = fs.readFileSync(roadmapPath, "utf-8");
|
|
789
|
-
const checkboxPattern = new RegExp(
|
|
790
|
-
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum!)}[:\\s][^\\n]*)`,
|
|
791
|
-
"i",
|
|
792
|
-
);
|
|
793
|
-
roadmapContent = replaceInCurrentMilestone(
|
|
794
|
-
roadmapContent,
|
|
795
|
-
checkboxPattern,
|
|
796
|
-
`$1x$2 (completed ${today})`,
|
|
797
|
-
);
|
|
798
|
-
const phaseEscaped = escapeRegex(phaseNum!);
|
|
799
|
-
roadmapContent = roadmapContent.replace(
|
|
800
|
-
new RegExp(`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, "im"),
|
|
801
|
-
(fullRow) => {
|
|
802
|
-
const cells = fullRow.split("|").slice(1, -1);
|
|
803
|
-
if (cells.length === 5) {
|
|
804
|
-
cells[3] = " Complete ";
|
|
805
|
-
cells[4] = ` ${today} `;
|
|
806
|
-
} else if (cells.length === 4) {
|
|
807
|
-
cells[2] = " Complete ";
|
|
808
|
-
cells[3] = ` ${today} `;
|
|
809
|
-
}
|
|
810
|
-
return "|" + cells.join("|") + "|";
|
|
811
|
-
},
|
|
812
|
-
);
|
|
813
|
-
roadmapContent = replaceInCurrentMilestone(
|
|
814
|
-
roadmapContent,
|
|
815
|
-
new RegExp(
|
|
816
|
-
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
|
817
|
-
"i",
|
|
818
|
-
),
|
|
819
|
-
`$1${summaryCount}/${planCount} plans complete`,
|
|
820
|
-
);
|
|
821
|
-
fs.writeFileSync(roadmapPath, roadmapContent, "utf-8");
|
|
822
|
-
const reqPath = path.join(planningDir(cwd), "REQUIREMENTS.md");
|
|
823
|
-
if (fs.existsSync(reqPath)) {
|
|
824
|
-
const currentMilestoneRoadmap = extractCurrentMilestone(
|
|
825
|
-
roadmapContent,
|
|
826
|
-
cwd,
|
|
827
|
-
);
|
|
828
|
-
const phaseSectionMatch = currentMilestoneRoadmap.match(
|
|
829
|
-
new RegExp(
|
|
830
|
-
`(#{2,4}\\s*Phase\\s+${escapeRegex(phaseNum!)}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`,
|
|
831
|
-
"i",
|
|
832
|
-
),
|
|
833
|
-
);
|
|
834
|
-
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : "";
|
|
835
|
-
const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
|
|
836
|
-
if (reqMatch) {
|
|
837
|
-
const reqIds = reqMatch[1]
|
|
838
|
-
.replace(/[[\]]/g, "")
|
|
839
|
-
.split(/[,\s]+/)
|
|
840
|
-
.map((r) => r.trim())
|
|
841
|
-
.filter(Boolean);
|
|
842
|
-
let reqContent = fs.readFileSync(reqPath, "utf-8");
|
|
843
|
-
for (const reqId of reqIds) {
|
|
844
|
-
const esc = escapeRegex(reqId);
|
|
845
|
-
reqContent = reqContent.replace(
|
|
846
|
-
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${esc}\\*\\*)`, "gi"),
|
|
847
|
-
"$1x$2",
|
|
848
|
-
);
|
|
849
|
-
reqContent = reqContent.replace(
|
|
850
|
-
new RegExp(
|
|
851
|
-
`(\\|\\s*${esc}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`,
|
|
852
|
-
"gi",
|
|
853
|
-
),
|
|
854
|
-
"$1 Complete $2",
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
fs.writeFileSync(reqPath, reqContent, "utf-8");
|
|
858
|
-
requirementsUpdated = true;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
let nextPhaseNum: string | null = null,
|
|
864
|
-
nextPhaseName: string | null = null,
|
|
865
|
-
isLastPhase = true;
|
|
866
|
-
try {
|
|
867
|
-
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
868
|
-
const dirs = fs
|
|
869
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
870
|
-
.filter((e) => e.isDirectory())
|
|
871
|
-
.map((e) => e.name)
|
|
872
|
-
.filter(isDirInMilestone)
|
|
873
|
-
.sort((a, b) => comparePhaseNum(a, b));
|
|
874
|
-
for (const dir of dirs) {
|
|
875
|
-
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
876
|
-
if (dm && comparePhaseNum(dm[1], phaseNum!) > 0) {
|
|
877
|
-
nextPhaseNum = dm[1];
|
|
878
|
-
nextPhaseName = dm[2] || null;
|
|
879
|
-
isLastPhase = false;
|
|
880
|
-
break;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
} catch {
|
|
884
|
-
/* ok */
|
|
885
|
-
}
|
|
886
|
-
if (isLastPhase && fs.existsSync(roadmapPath)) {
|
|
887
|
-
try {
|
|
888
|
-
const roadmapForPhases = extractCurrentMilestone(
|
|
889
|
-
fs.readFileSync(roadmapPath, "utf-8"),
|
|
890
|
-
cwd,
|
|
891
|
-
);
|
|
892
|
-
const phasePattern =
|
|
893
|
-
/#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
894
|
-
let pm: RegExpExecArray | null;
|
|
895
|
-
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
896
|
-
if (comparePhaseNum(pm[1], phaseNum!) > 0) {
|
|
897
|
-
nextPhaseNum = pm[1];
|
|
898
|
-
nextPhaseName = pm[2]
|
|
899
|
-
.replace(/\(INSERTED\)/i, "")
|
|
900
|
-
.trim()
|
|
901
|
-
.toLowerCase()
|
|
902
|
-
.replace(/\s+/g, "-");
|
|
903
|
-
isLastPhase = false;
|
|
904
|
-
break;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
} catch {
|
|
908
|
-
/* ok */
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (fs.existsSync(statePath)) {
|
|
913
|
-
let stateContent = fs.readFileSync(statePath, "utf-8");
|
|
914
|
-
const phaseValue = nextPhaseNum || phaseNum!;
|
|
915
|
-
const existingPhaseField =
|
|
916
|
-
stateExtractField(stateContent, "Current Phase") ||
|
|
917
|
-
stateExtractField(stateContent, "Phase");
|
|
918
|
-
let newPhaseValue = String(phaseValue);
|
|
919
|
-
if (existingPhaseField) {
|
|
920
|
-
const totalMatch = existingPhaseField.match(/of\s+(\d+)/),
|
|
921
|
-
nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
|
|
922
|
-
if (totalMatch) {
|
|
923
|
-
const nameStr = nextPhaseName
|
|
924
|
-
? ` (${nextPhaseName.replace(/-/g, " ")})`
|
|
925
|
-
: nameMatch
|
|
926
|
-
? ` (${nameMatch[1]})`
|
|
927
|
-
: "";
|
|
928
|
-
newPhaseValue = `${phaseValue} of ${totalMatch[1]}${nameStr}`;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
stateContent = stateReplaceFieldWithFallback(
|
|
932
|
-
stateContent,
|
|
933
|
-
"Current Phase",
|
|
934
|
-
"Phase",
|
|
935
|
-
newPhaseValue,
|
|
936
|
-
);
|
|
937
|
-
if (nextPhaseName)
|
|
938
|
-
stateContent = stateReplaceFieldWithFallback(
|
|
939
|
-
stateContent,
|
|
940
|
-
"Current Phase Name",
|
|
941
|
-
null,
|
|
942
|
-
nextPhaseName.replace(/-/g, " "),
|
|
943
|
-
);
|
|
944
|
-
stateContent = stateReplaceFieldWithFallback(
|
|
945
|
-
stateContent,
|
|
946
|
-
"Status",
|
|
947
|
-
null,
|
|
948
|
-
isLastPhase ? "Milestone complete" : "Ready to plan",
|
|
949
|
-
);
|
|
950
|
-
stateContent = stateReplaceFieldWithFallback(
|
|
951
|
-
stateContent,
|
|
952
|
-
"Current Plan",
|
|
953
|
-
"Plan",
|
|
954
|
-
"Not started",
|
|
955
|
-
);
|
|
956
|
-
stateContent = stateReplaceFieldWithFallback(
|
|
957
|
-
stateContent,
|
|
958
|
-
"Last Activity",
|
|
959
|
-
"Last activity",
|
|
960
|
-
today,
|
|
961
|
-
);
|
|
962
|
-
stateContent = stateReplaceFieldWithFallback(
|
|
963
|
-
stateContent,
|
|
964
|
-
"Last Activity Description",
|
|
965
|
-
null,
|
|
966
|
-
`Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ""}`,
|
|
967
|
-
);
|
|
968
|
-
const completedRaw = stateExtractField(stateContent, "Completed Phases");
|
|
969
|
-
if (completedRaw) {
|
|
970
|
-
const newCompleted = parseInt(completedRaw, 10) + 1;
|
|
971
|
-
stateContent =
|
|
972
|
-
stateReplaceField(
|
|
973
|
-
stateContent,
|
|
974
|
-
"Completed Phases",
|
|
975
|
-
String(newCompleted),
|
|
976
|
-
) ?? stateContent;
|
|
977
|
-
const totalRaw = stateExtractField(stateContent, "Total Phases");
|
|
978
|
-
if (totalRaw) {
|
|
979
|
-
const totalPhases = parseInt(totalRaw, 10);
|
|
980
|
-
if (totalPhases > 0) {
|
|
981
|
-
const newPercent = Math.round((newCompleted / totalPhases) * 100);
|
|
982
|
-
stateContent =
|
|
983
|
-
stateReplaceField(stateContent, "Progress", `${newPercent}%`) ??
|
|
984
|
-
stateContent;
|
|
985
|
-
stateContent = stateContent.replace(
|
|
986
|
-
/(percent:\s*)\d+/,
|
|
987
|
-
`$1${newPercent}`,
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
writeStateMd(statePath, stateContent, cwd);
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
output(
|
|
996
|
-
{
|
|
997
|
-
completed_phase: phaseNum,
|
|
998
|
-
phase_name: phaseInfo!.phase_name,
|
|
999
|
-
plans_executed: `${summaryCount}/${planCount}`,
|
|
1000
|
-
next_phase: nextPhaseNum,
|
|
1001
|
-
next_phase_name: nextPhaseName,
|
|
1002
|
-
is_last_phase: isLastPhase,
|
|
1003
|
-
date: today,
|
|
1004
|
-
roadmap_updated: fs.existsSync(roadmapPath),
|
|
1005
|
-
state_updated: fs.existsSync(statePath),
|
|
1006
|
-
requirements_updated: requirementsUpdated,
|
|
1007
|
-
warnings,
|
|
1008
|
-
has_warnings: warnings.length > 0,
|
|
1009
|
-
},
|
|
1010
|
-
raw,
|
|
1011
|
-
);
|
|
1012
|
-
}
|