pi-gsd 2.0.1 → 2.0.3
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 +1533 -0
- package/dist/pi-gsd-tools.js +53 -52
- 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/state.ts
DELETED
|
@@ -1,1175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* state.ts - STATE.md operations and progression engine.
|
|
3
|
-
*
|
|
4
|
-
* Ported from lib/state.cjs. All command signatures preserved.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import fs from "fs";
|
|
8
|
-
import path from "path";
|
|
9
|
-
import {
|
|
10
|
-
escapeRegex,
|
|
11
|
-
getMilestoneInfo,
|
|
12
|
-
getMilestonePhaseFilter,
|
|
13
|
-
gsdError,
|
|
14
|
-
loadConfig,
|
|
15
|
-
normalizeMd,
|
|
16
|
-
output,
|
|
17
|
-
planningDir,
|
|
18
|
-
planningPaths,
|
|
19
|
-
} from "./core.js";
|
|
20
|
-
import { extractFrontmatter, reconstructFrontmatter } from "./frontmatter.js";
|
|
21
|
-
import { validateFieldName, validatePath } from "./security.js";
|
|
22
|
-
|
|
23
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
function getStatePath(cwd: string): string {
|
|
26
|
-
return planningPaths(cwd).state;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function stateExtractField(
|
|
30
|
-
content: string,
|
|
31
|
-
fieldName: string,
|
|
32
|
-
): string | null {
|
|
33
|
-
const escaped = escapeRegex(fieldName);
|
|
34
|
-
const boldMatch = content.match(
|
|
35
|
-
new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, "i"),
|
|
36
|
-
);
|
|
37
|
-
if (boldMatch) return boldMatch[1].trim();
|
|
38
|
-
const plainMatch = content.match(new RegExp(`^${escaped}:\\s*(.+)`, "im"));
|
|
39
|
-
return plainMatch ? plainMatch[1].trim() : null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function stateReplaceField(
|
|
43
|
-
content: string,
|
|
44
|
-
fieldName: string,
|
|
45
|
-
newValue: string,
|
|
46
|
-
): string | null {
|
|
47
|
-
const escaped = escapeRegex(fieldName);
|
|
48
|
-
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, "i");
|
|
49
|
-
if (boldPattern.test(content)) {
|
|
50
|
-
return content.replace(
|
|
51
|
-
boldPattern,
|
|
52
|
-
(_match, prefix) => `${prefix}${newValue}`,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, "im");
|
|
56
|
-
if (plainPattern.test(content)) {
|
|
57
|
-
return content.replace(
|
|
58
|
-
plainPattern,
|
|
59
|
-
(_match, prefix) => `${prefix}${newValue}`,
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function stateReplaceFieldWithFallback(
|
|
66
|
-
content: string,
|
|
67
|
-
primary: string,
|
|
68
|
-
fallback: string | null,
|
|
69
|
-
value: string,
|
|
70
|
-
): string {
|
|
71
|
-
const r1 = stateReplaceField(content, primary, value);
|
|
72
|
-
if (r1) return r1;
|
|
73
|
-
if (fallback) {
|
|
74
|
-
const r2 = stateReplaceField(content, fallback, value);
|
|
75
|
-
if (r2) return r2;
|
|
76
|
-
}
|
|
77
|
-
return content;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function updateCurrentPositionFields(
|
|
81
|
-
content: string,
|
|
82
|
-
fields: { status?: string; lastActivity?: string; plan?: string },
|
|
83
|
-
): string {
|
|
84
|
-
const posPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
85
|
-
const posMatch = content.match(posPattern);
|
|
86
|
-
if (!posMatch) return content;
|
|
87
|
-
|
|
88
|
-
let posBody = posMatch[2];
|
|
89
|
-
if (fields.status && /^Status:/m.test(posBody)) {
|
|
90
|
-
posBody = posBody.replace(/^Status:.*$/m, `Status: ${fields.status}`);
|
|
91
|
-
}
|
|
92
|
-
if (fields.lastActivity && /^Last activity:/im.test(posBody)) {
|
|
93
|
-
posBody = posBody.replace(
|
|
94
|
-
/^Last activity:.*$/im,
|
|
95
|
-
`Last activity: ${fields.lastActivity}`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
if (fields.plan && /^Plan:/m.test(posBody)) {
|
|
99
|
-
posBody = posBody.replace(/^Plan:.*$/m, `Plan: ${fields.plan}`);
|
|
100
|
-
}
|
|
101
|
-
return content.replace(posPattern, `${posMatch[1]}${posBody}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function readTextArgOrFile(
|
|
105
|
-
cwd: string,
|
|
106
|
-
value: string | null,
|
|
107
|
-
filePath: string | null,
|
|
108
|
-
label: string,
|
|
109
|
-
): string {
|
|
110
|
-
if (!filePath) return value ?? "";
|
|
111
|
-
const pathCheck = validatePath(filePath, cwd, { allowAbsolute: true });
|
|
112
|
-
if (!pathCheck.safe)
|
|
113
|
-
throw new Error(`${label} path rejected: ${pathCheck.error}`);
|
|
114
|
-
try {
|
|
115
|
-
return fs.readFileSync(pathCheck.resolved, "utf-8").trimEnd();
|
|
116
|
-
} catch {
|
|
117
|
-
throw new Error(`${label} file not found: ${filePath}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ─── Frontmatter sync ─────────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
export function stripFrontmatter(content: string): string {
|
|
124
|
-
let result = content;
|
|
125
|
-
// eslint-disable-next-line no-constant-condition
|
|
126
|
-
while (true) {
|
|
127
|
-
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, "");
|
|
128
|
-
if (stripped === result) break;
|
|
129
|
-
result = stripped;
|
|
130
|
-
}
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function buildStateFrontmatter(
|
|
135
|
-
bodyContent: string,
|
|
136
|
-
cwd?: string,
|
|
137
|
-
): import("./frontmatter.js").FrontmatterObject {
|
|
138
|
-
const currentPhase = stateExtractField(bodyContent, "Current Phase");
|
|
139
|
-
const currentPhaseName = stateExtractField(bodyContent, "Current Phase Name");
|
|
140
|
-
const currentPlan = stateExtractField(bodyContent, "Current Plan");
|
|
141
|
-
const totalPhasesRaw = stateExtractField(bodyContent, "Total Phases");
|
|
142
|
-
const totalPlansRaw = stateExtractField(bodyContent, "Total Plans in Phase");
|
|
143
|
-
const status = stateExtractField(bodyContent, "Status");
|
|
144
|
-
const progressRaw = stateExtractField(bodyContent, "Progress");
|
|
145
|
-
const lastActivity = stateExtractField(bodyContent, "Last Activity");
|
|
146
|
-
const stoppedAt =
|
|
147
|
-
stateExtractField(bodyContent, "Stopped At") ||
|
|
148
|
-
stateExtractField(bodyContent, "Stopped at");
|
|
149
|
-
const pausedAt = stateExtractField(bodyContent, "Paused At");
|
|
150
|
-
|
|
151
|
-
let milestone: string | null = null;
|
|
152
|
-
let milestoneName: string | null = null;
|
|
153
|
-
if (cwd) {
|
|
154
|
-
try {
|
|
155
|
-
const info = getMilestoneInfo(cwd);
|
|
156
|
-
milestone = info.version;
|
|
157
|
-
milestoneName = info.name;
|
|
158
|
-
} catch {
|
|
159
|
-
/* ok */
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
let totalPhases: number | null = totalPhasesRaw
|
|
164
|
-
? parseInt(totalPhasesRaw, 10)
|
|
165
|
-
: null;
|
|
166
|
-
let completedPhases: number | null = null;
|
|
167
|
-
let totalPlans: number | null = totalPlansRaw
|
|
168
|
-
? parseInt(totalPlansRaw, 10)
|
|
169
|
-
: null;
|
|
170
|
-
let completedPlans: number | null = null;
|
|
171
|
-
|
|
172
|
-
if (cwd) {
|
|
173
|
-
try {
|
|
174
|
-
const phasesDir = planningPaths(cwd).phases;
|
|
175
|
-
if (fs.existsSync(phasesDir)) {
|
|
176
|
-
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
177
|
-
const phaseDirs = fs
|
|
178
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
179
|
-
.filter((e) => e.isDirectory())
|
|
180
|
-
.map((e) => e.name)
|
|
181
|
-
.filter(isDirInMilestone);
|
|
182
|
-
let diskTotalPlans = 0,
|
|
183
|
-
diskTotalSummaries = 0,
|
|
184
|
-
diskCompletedPhases = 0;
|
|
185
|
-
for (const dir of phaseDirs) {
|
|
186
|
-
const files = fs.readdirSync(path.join(phasesDir, dir));
|
|
187
|
-
const plans = files.filter((f) => f.match(/-PLAN\.md$/i)).length;
|
|
188
|
-
const summaries = files.filter((f) =>
|
|
189
|
-
f.match(/-SUMMARY\.md$/i),
|
|
190
|
-
).length;
|
|
191
|
-
diskTotalPlans += plans;
|
|
192
|
-
diskTotalSummaries += summaries;
|
|
193
|
-
if (plans > 0 && summaries >= plans) diskCompletedPhases++;
|
|
194
|
-
}
|
|
195
|
-
totalPhases =
|
|
196
|
-
isDirInMilestone.phaseCount > 0
|
|
197
|
-
? Math.max(phaseDirs.length, isDirInMilestone.phaseCount)
|
|
198
|
-
: phaseDirs.length;
|
|
199
|
-
completedPhases = diskCompletedPhases;
|
|
200
|
-
totalPlans = diskTotalPlans;
|
|
201
|
-
completedPlans = diskTotalSummaries;
|
|
202
|
-
}
|
|
203
|
-
} catch {
|
|
204
|
-
/* ok */
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let progressPercent: number | null = null;
|
|
209
|
-
if (progressRaw) {
|
|
210
|
-
const pctMatch = progressRaw.match(/(\d+)%/);
|
|
211
|
-
if (pctMatch) progressPercent = parseInt(pctMatch[1], 10);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
let normalizedStatus = status ?? "unknown";
|
|
215
|
-
const statusLower = (status ?? "").toLowerCase();
|
|
216
|
-
if (
|
|
217
|
-
statusLower.includes("paused") ||
|
|
218
|
-
statusLower.includes("stopped") ||
|
|
219
|
-
pausedAt
|
|
220
|
-
) {
|
|
221
|
-
normalizedStatus = "paused";
|
|
222
|
-
} else if (
|
|
223
|
-
statusLower.includes("executing") ||
|
|
224
|
-
statusLower.includes("in progress")
|
|
225
|
-
) {
|
|
226
|
-
normalizedStatus = "executing";
|
|
227
|
-
} else if (
|
|
228
|
-
statusLower.includes("planning") ||
|
|
229
|
-
statusLower.includes("ready to plan")
|
|
230
|
-
) {
|
|
231
|
-
normalizedStatus = "planning";
|
|
232
|
-
} else if (statusLower.includes("discussing")) {
|
|
233
|
-
normalizedStatus = "discussing";
|
|
234
|
-
} else if (statusLower.includes("verif")) {
|
|
235
|
-
normalizedStatus = "verifying";
|
|
236
|
-
} else if (statusLower.includes("complete") || statusLower.includes("done")) {
|
|
237
|
-
normalizedStatus = "completed";
|
|
238
|
-
} else if (statusLower.includes("ready to execute")) {
|
|
239
|
-
normalizedStatus = "executing";
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const fm: import("./frontmatter.js").FrontmatterObject = { gsd_state_version: "1.0" };
|
|
243
|
-
if (milestone) fm.milestone = milestone;
|
|
244
|
-
if (milestoneName) fm.milestone_name = milestoneName;
|
|
245
|
-
if (currentPhase) fm.current_phase = currentPhase;
|
|
246
|
-
if (currentPhaseName) fm.current_phase_name = currentPhaseName;
|
|
247
|
-
if (currentPlan) fm.current_plan = currentPlan;
|
|
248
|
-
fm.status = normalizedStatus;
|
|
249
|
-
if (stoppedAt) fm.stopped_at = stoppedAt;
|
|
250
|
-
if (pausedAt) fm.paused_at = pausedAt;
|
|
251
|
-
fm.last_updated = new Date().toISOString();
|
|
252
|
-
if (lastActivity) fm.last_activity = lastActivity;
|
|
253
|
-
|
|
254
|
-
const progress: Record<string, number> = {};
|
|
255
|
-
if (totalPhases !== null) progress.total_phases = totalPhases;
|
|
256
|
-
if (completedPhases !== null) progress.completed_phases = completedPhases;
|
|
257
|
-
if (totalPlans !== null) progress.total_plans = totalPlans;
|
|
258
|
-
if (completedPlans !== null) progress.completed_plans = completedPlans;
|
|
259
|
-
if (progressPercent !== null) progress.percent = progressPercent;
|
|
260
|
-
if (Object.keys(progress).length > 0) fm.progress = progress;
|
|
261
|
-
|
|
262
|
-
return fm;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function syncStateFrontmatter(content: string, cwd?: string): string {
|
|
266
|
-
const existingFm = extractFrontmatter(content);
|
|
267
|
-
const body = stripFrontmatter(content);
|
|
268
|
-
const derivedFm = buildStateFrontmatter(body, cwd);
|
|
269
|
-
if (
|
|
270
|
-
derivedFm.status === "unknown" &&
|
|
271
|
-
existingFm.status &&
|
|
272
|
-
existingFm.status !== "unknown"
|
|
273
|
-
) {
|
|
274
|
-
derivedFm.status = existingFm.status;
|
|
275
|
-
}
|
|
276
|
-
const yamlStr = reconstructFrontmatter(derivedFm);
|
|
277
|
-
return `---\n${yamlStr}\n---\n\n${body}`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export function writeStateMd(
|
|
281
|
-
statePath: string,
|
|
282
|
-
content: string,
|
|
283
|
-
cwd?: string,
|
|
284
|
-
): void {
|
|
285
|
-
const synced = syncStateFrontmatter(content, cwd);
|
|
286
|
-
const lockPath = statePath + ".lock";
|
|
287
|
-
const maxRetries = 10;
|
|
288
|
-
const retryDelay = 200;
|
|
289
|
-
|
|
290
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
291
|
-
try {
|
|
292
|
-
const fd = fs.openSync(
|
|
293
|
-
lockPath,
|
|
294
|
-
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY,
|
|
295
|
-
);
|
|
296
|
-
fs.writeSync(fd, String(process.pid));
|
|
297
|
-
fs.closeSync(fd);
|
|
298
|
-
break;
|
|
299
|
-
} catch (err: unknown) {
|
|
300
|
-
if ((err as NodeJS.ErrnoException).code === "EEXIST") {
|
|
301
|
-
try {
|
|
302
|
-
const stat = fs.statSync(lockPath);
|
|
303
|
-
if (Date.now() - stat.mtimeMs > 10000) {
|
|
304
|
-
fs.unlinkSync(lockPath);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
} catch {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
if (i === maxRetries - 1) {
|
|
311
|
-
try {
|
|
312
|
-
fs.unlinkSync(lockPath);
|
|
313
|
-
} catch {
|
|
314
|
-
/* ok */
|
|
315
|
-
}
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
const start = Date.now();
|
|
319
|
-
const jitter = Math.floor(Math.random() * 50);
|
|
320
|
-
while (Date.now() - start < retryDelay + jitter) {
|
|
321
|
-
/* spin */
|
|
322
|
-
}
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
fs.writeFileSync(statePath, normalizeMd(synced), "utf-8");
|
|
331
|
-
} finally {
|
|
332
|
-
try {
|
|
333
|
-
fs.unlinkSync(lockPath);
|
|
334
|
-
} catch {
|
|
335
|
-
/* ok */
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
341
|
-
|
|
342
|
-
export function cmdStateLoad(cwd: string, raw: boolean): void {
|
|
343
|
-
const config = loadConfig(cwd);
|
|
344
|
-
const planDir = planningPaths(cwd).planning;
|
|
345
|
-
let stateRaw = "";
|
|
346
|
-
try {
|
|
347
|
-
stateRaw = fs.readFileSync(path.join(planDir, "STATE.md"), "utf-8");
|
|
348
|
-
} catch {
|
|
349
|
-
/* ok */
|
|
350
|
-
}
|
|
351
|
-
const configExists = fs.existsSync(path.join(planDir, "config.json"));
|
|
352
|
-
const roadmapExists = fs.existsSync(path.join(planDir, "ROADMAP.md"));
|
|
353
|
-
const stateExists = stateRaw.length > 0;
|
|
354
|
-
|
|
355
|
-
if (raw) {
|
|
356
|
-
const c = config;
|
|
357
|
-
const lines = [
|
|
358
|
-
`model_profile=${c.model_profile}`,
|
|
359
|
-
`commit_docs=${c.commit_docs}`,
|
|
360
|
-
`branching_strategy=${c.branching_strategy}`,
|
|
361
|
-
`phase_branch_template=${c.phase_branch_template}`,
|
|
362
|
-
`milestone_branch_template=${c.milestone_branch_template}`,
|
|
363
|
-
`parallelization=${c.parallelization}`,
|
|
364
|
-
`research=${c.research}`,
|
|
365
|
-
`plan_checker=${c.plan_checker}`,
|
|
366
|
-
`verifier=${c.verifier}`,
|
|
367
|
-
`config_exists=${configExists}`,
|
|
368
|
-
`roadmap_exists=${roadmapExists}`,
|
|
369
|
-
`state_exists=${stateExists}`,
|
|
370
|
-
];
|
|
371
|
-
process.stdout.write(lines.join("\n"));
|
|
372
|
-
process.exit(0);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
output({
|
|
376
|
-
config,
|
|
377
|
-
state_raw: stateRaw,
|
|
378
|
-
state_exists: stateExists,
|
|
379
|
-
roadmap_exists: roadmapExists,
|
|
380
|
-
config_exists: configExists,
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
export function cmdStateGet(
|
|
385
|
-
cwd: string,
|
|
386
|
-
section: string | undefined,
|
|
387
|
-
raw: boolean,
|
|
388
|
-
): void {
|
|
389
|
-
const statePath = planningPaths(cwd).state;
|
|
390
|
-
try {
|
|
391
|
-
const content = fs.readFileSync(statePath, "utf-8");
|
|
392
|
-
if (!section) {
|
|
393
|
-
output({ content }, raw, content);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const esc = escapeRegex(section);
|
|
397
|
-
const boldMatch = content.match(
|
|
398
|
-
new RegExp(`\\*\\*${esc}:\\*\\*\\s*(.*)`, "i"),
|
|
399
|
-
);
|
|
400
|
-
if (boldMatch) {
|
|
401
|
-
output({ [section]: boldMatch[1].trim() }, raw, boldMatch[1].trim());
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const plainMatch = content.match(new RegExp(`^${esc}:\\s*(.*)`, "im"));
|
|
405
|
-
if (plainMatch) {
|
|
406
|
-
output({ [section]: plainMatch[1].trim() }, raw, plainMatch[1].trim());
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
const sectionMatch = content.match(
|
|
410
|
-
new RegExp(`##\\s*${esc}\\s*\n([\\s\\S]*?)(?=\\n##|$)`, "i"),
|
|
411
|
-
);
|
|
412
|
-
if (sectionMatch) {
|
|
413
|
-
output(
|
|
414
|
-
{ [section]: sectionMatch[1].trim() },
|
|
415
|
-
raw,
|
|
416
|
-
sectionMatch[1].trim(),
|
|
417
|
-
);
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
output({ error: `Section or field "${section}" not found` }, raw, "");
|
|
421
|
-
} catch {
|
|
422
|
-
gsdError("STATE.md not found");
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export function cmdStatePatch(
|
|
427
|
-
cwd: string,
|
|
428
|
-
patches: Record<string, string>,
|
|
429
|
-
raw: boolean,
|
|
430
|
-
): void {
|
|
431
|
-
for (const field of Object.keys(patches)) {
|
|
432
|
-
const check = validateFieldName(field);
|
|
433
|
-
if (!check.valid) gsdError(`state patch: ${check.error}`);
|
|
434
|
-
}
|
|
435
|
-
const statePath = planningPaths(cwd).state;
|
|
436
|
-
try {
|
|
437
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
438
|
-
const results: { updated: string[]; failed: string[] } = {
|
|
439
|
-
updated: [],
|
|
440
|
-
failed: [],
|
|
441
|
-
};
|
|
442
|
-
for (const [field, value] of Object.entries(patches)) {
|
|
443
|
-
const esc = escapeRegex(field);
|
|
444
|
-
const bold = new RegExp(`(\\*\\*${esc}:\\*\\*\\s*)(.*)`, "i");
|
|
445
|
-
const plain = new RegExp(`(^${esc}:\\s*)(.*)`, "im");
|
|
446
|
-
if (bold.test(content)) {
|
|
447
|
-
content = content.replace(bold, (_m, p) => `${p}${value}`);
|
|
448
|
-
results.updated.push(field);
|
|
449
|
-
} else if (plain.test(content)) {
|
|
450
|
-
content = content.replace(plain, (_m, p) => `${p}${value}`);
|
|
451
|
-
results.updated.push(field);
|
|
452
|
-
} else {
|
|
453
|
-
results.failed.push(field);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
if (results.updated.length > 0) writeStateMd(statePath, content, cwd);
|
|
457
|
-
output(results, raw, results.updated.length > 0 ? "true" : "false");
|
|
458
|
-
} catch {
|
|
459
|
-
gsdError("STATE.md not found");
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
export function cmdStateUpdate(
|
|
464
|
-
cwd: string,
|
|
465
|
-
field: string | undefined,
|
|
466
|
-
value: string | undefined,
|
|
467
|
-
): void {
|
|
468
|
-
if (!field || value === undefined)
|
|
469
|
-
gsdError("field and value required for state update");
|
|
470
|
-
const check = validateFieldName(field);
|
|
471
|
-
if (!check.valid) gsdError(`state update: ${check.error}`);
|
|
472
|
-
const statePath = planningPaths(cwd).state;
|
|
473
|
-
try {
|
|
474
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
475
|
-
const esc = escapeRegex(field);
|
|
476
|
-
const bold = new RegExp(`(\\*\\*${esc}:\\*\\*\\s*)(.*)`, "i");
|
|
477
|
-
const plain = new RegExp(`(^${esc}:\\s*)(.*)`, "im");
|
|
478
|
-
if (bold.test(content)) {
|
|
479
|
-
content = content.replace(bold, (_m, p) => `${p}${value}`);
|
|
480
|
-
writeStateMd(statePath, content, cwd);
|
|
481
|
-
output({ updated: true });
|
|
482
|
-
} else if (plain.test(content)) {
|
|
483
|
-
content = content.replace(plain, (_m, p) => `${p}${value}`);
|
|
484
|
-
writeStateMd(statePath, content, cwd);
|
|
485
|
-
output({ updated: true });
|
|
486
|
-
} else {
|
|
487
|
-
output({
|
|
488
|
-
updated: false,
|
|
489
|
-
reason: `Field "${field}" not found in STATE.md`,
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
} catch {
|
|
493
|
-
output({ updated: false, reason: "STATE.md not found" });
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
export function cmdStateAdvancePlan(cwd: string, raw: boolean): void {
|
|
498
|
-
const statePath = planningPaths(cwd).state;
|
|
499
|
-
if (!fs.existsSync(statePath)) {
|
|
500
|
-
output({ error: "STATE.md not found" }, raw);
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
504
|
-
const today = new Date().toISOString().split("T")[0];
|
|
505
|
-
|
|
506
|
-
const legacyPlan = stateExtractField(content, "Current Plan");
|
|
507
|
-
const legacyTotal = stateExtractField(content, "Total Plans in Phase");
|
|
508
|
-
const planField = stateExtractField(content, "Plan");
|
|
509
|
-
|
|
510
|
-
let currentPlan: number,
|
|
511
|
-
totalPlans: number,
|
|
512
|
-
useCompoundFormat = false;
|
|
513
|
-
|
|
514
|
-
if (legacyPlan && legacyTotal) {
|
|
515
|
-
currentPlan = parseInt(legacyPlan, 10);
|
|
516
|
-
totalPlans = parseInt(legacyTotal, 10);
|
|
517
|
-
} else if (planField) {
|
|
518
|
-
currentPlan = parseInt(planField, 10);
|
|
519
|
-
const ofMatch = planField.match(/of\s+(\d+)/);
|
|
520
|
-
totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
|
|
521
|
-
useCompoundFormat = true;
|
|
522
|
-
} else {
|
|
523
|
-
output({ error: "Cannot parse plan fields from STATE.md" }, raw);
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (isNaN(currentPlan) || isNaN(totalPlans)) {
|
|
528
|
-
output(
|
|
529
|
-
{
|
|
530
|
-
error:
|
|
531
|
-
"Cannot parse Current Plan or Total Plans in Phase from STATE.md",
|
|
532
|
-
},
|
|
533
|
-
raw,
|
|
534
|
-
);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (currentPlan >= totalPlans) {
|
|
539
|
-
content = stateReplaceFieldWithFallback(
|
|
540
|
-
content,
|
|
541
|
-
"Status",
|
|
542
|
-
null,
|
|
543
|
-
"Phase complete - ready for verification",
|
|
544
|
-
);
|
|
545
|
-
content = stateReplaceFieldWithFallback(
|
|
546
|
-
content,
|
|
547
|
-
"Last Activity",
|
|
548
|
-
"Last activity",
|
|
549
|
-
today,
|
|
550
|
-
);
|
|
551
|
-
content = updateCurrentPositionFields(content, {
|
|
552
|
-
status: "Phase complete - ready for verification",
|
|
553
|
-
lastActivity: today,
|
|
554
|
-
});
|
|
555
|
-
writeStateMd(statePath, content, cwd);
|
|
556
|
-
output(
|
|
557
|
-
{
|
|
558
|
-
advanced: false,
|
|
559
|
-
reason: "last_plan",
|
|
560
|
-
current_plan: currentPlan,
|
|
561
|
-
total_plans: totalPlans,
|
|
562
|
-
status: "ready_for_verification",
|
|
563
|
-
},
|
|
564
|
-
raw,
|
|
565
|
-
"false",
|
|
566
|
-
);
|
|
567
|
-
} else {
|
|
568
|
-
const newPlan = currentPlan + 1;
|
|
569
|
-
let planDisplayValue: string;
|
|
570
|
-
if (useCompoundFormat && planField) {
|
|
571
|
-
planDisplayValue = planField.replace(/^\d+/, String(newPlan));
|
|
572
|
-
content = stateReplaceField(content, "Plan", planDisplayValue) ?? content;
|
|
573
|
-
} else {
|
|
574
|
-
planDisplayValue = `${newPlan} of ${totalPlans}`;
|
|
575
|
-
content =
|
|
576
|
-
stateReplaceField(content, "Current Plan", String(newPlan)) ?? content;
|
|
577
|
-
}
|
|
578
|
-
content = stateReplaceFieldWithFallback(
|
|
579
|
-
content,
|
|
580
|
-
"Status",
|
|
581
|
-
null,
|
|
582
|
-
"Ready to execute",
|
|
583
|
-
);
|
|
584
|
-
content = stateReplaceFieldWithFallback(
|
|
585
|
-
content,
|
|
586
|
-
"Last Activity",
|
|
587
|
-
"Last activity",
|
|
588
|
-
today,
|
|
589
|
-
);
|
|
590
|
-
content = updateCurrentPositionFields(content, {
|
|
591
|
-
status: "Ready to execute",
|
|
592
|
-
lastActivity: today,
|
|
593
|
-
plan: planDisplayValue,
|
|
594
|
-
});
|
|
595
|
-
writeStateMd(statePath, content, cwd);
|
|
596
|
-
output(
|
|
597
|
-
{
|
|
598
|
-
advanced: true,
|
|
599
|
-
previous_plan: currentPlan,
|
|
600
|
-
current_plan: newPlan,
|
|
601
|
-
total_plans: totalPlans,
|
|
602
|
-
},
|
|
603
|
-
raw,
|
|
604
|
-
"true",
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
export function cmdStateRecordMetric(
|
|
610
|
-
cwd: string,
|
|
611
|
-
options: {
|
|
612
|
-
phase?: string | null;
|
|
613
|
-
plan?: string | null;
|
|
614
|
-
duration?: string | null;
|
|
615
|
-
tasks?: string | null;
|
|
616
|
-
files?: string | null;
|
|
617
|
-
},
|
|
618
|
-
raw: boolean,
|
|
619
|
-
): void {
|
|
620
|
-
const statePath = planningPaths(cwd).state;
|
|
621
|
-
if (!fs.existsSync(statePath)) {
|
|
622
|
-
output({ error: "STATE.md not found" }, raw);
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
const { phase, plan, duration, tasks, files } = options;
|
|
626
|
-
if (!phase || !plan || !duration) {
|
|
627
|
-
output({ error: "phase, plan, and duration required" }, raw);
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
631
|
-
const metricsPattern =
|
|
632
|
-
/(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
|
|
633
|
-
const metricsMatch = content.match(metricsPattern);
|
|
634
|
-
if (metricsMatch) {
|
|
635
|
-
let tableBody = metricsMatch[2].trimEnd();
|
|
636
|
-
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks ?? "-"} tasks | ${files ?? "-"} files |`;
|
|
637
|
-
tableBody =
|
|
638
|
-
!tableBody.trim() || tableBody.includes("None yet")
|
|
639
|
-
? newRow
|
|
640
|
-
: tableBody + "\n" + newRow;
|
|
641
|
-
content = content.replace(
|
|
642
|
-
metricsPattern,
|
|
643
|
-
(_m, header) => `${header}${tableBody}\n`,
|
|
644
|
-
);
|
|
645
|
-
writeStateMd(statePath, content, cwd);
|
|
646
|
-
output({ recorded: true, phase, plan, duration }, raw, "true");
|
|
647
|
-
} else {
|
|
648
|
-
output(
|
|
649
|
-
{
|
|
650
|
-
recorded: false,
|
|
651
|
-
reason: "Performance Metrics section not found in STATE.md",
|
|
652
|
-
},
|
|
653
|
-
raw,
|
|
654
|
-
"false",
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
export function cmdStateUpdateProgress(cwd: string, raw: boolean): void {
|
|
660
|
-
const statePath = planningPaths(cwd).state;
|
|
661
|
-
if (!fs.existsSync(statePath)) {
|
|
662
|
-
output({ error: "STATE.md not found" }, raw);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
666
|
-
const phasesDir = planningPaths(cwd).phases;
|
|
667
|
-
let totalPlans = 0,
|
|
668
|
-
totalSummaries = 0;
|
|
669
|
-
if (fs.existsSync(phasesDir)) {
|
|
670
|
-
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
671
|
-
const phaseDirs = fs
|
|
672
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
673
|
-
.filter((e) => e.isDirectory())
|
|
674
|
-
.map((e) => e.name)
|
|
675
|
-
.filter(isDirInMilestone);
|
|
676
|
-
for (const dir of phaseDirs) {
|
|
677
|
-
const fls = fs.readdirSync(path.join(phasesDir, dir));
|
|
678
|
-
totalPlans += fls.filter((f) => f.match(/-PLAN\.md$/i)).length;
|
|
679
|
-
totalSummaries += fls.filter((f) => f.match(/-SUMMARY\.md$/i)).length;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
const percent =
|
|
683
|
-
totalPlans > 0
|
|
684
|
-
? Math.min(100, Math.round((totalSummaries / totalPlans) * 100))
|
|
685
|
-
: 0;
|
|
686
|
-
const barWidth = 10;
|
|
687
|
-
const filled = Math.round((percent / 100) * barWidth);
|
|
688
|
-
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
|
|
689
|
-
const progressStr = `[${bar}] ${percent}%`;
|
|
690
|
-
const boldPat = /(\*\*Progress:\*\*\s*).*/i;
|
|
691
|
-
const plainPat = /^(Progress:\s*).*/im;
|
|
692
|
-
if (boldPat.test(content)) {
|
|
693
|
-
content = content.replace(boldPat, (_m, p) => `${p}${progressStr}`);
|
|
694
|
-
writeStateMd(statePath, content, cwd);
|
|
695
|
-
output(
|
|
696
|
-
{
|
|
697
|
-
updated: true,
|
|
698
|
-
percent,
|
|
699
|
-
completed: totalSummaries,
|
|
700
|
-
total: totalPlans,
|
|
701
|
-
bar: progressStr,
|
|
702
|
-
},
|
|
703
|
-
raw,
|
|
704
|
-
progressStr,
|
|
705
|
-
);
|
|
706
|
-
} else if (plainPat.test(content)) {
|
|
707
|
-
content = content.replace(plainPat, (_m, p) => `${p}${progressStr}`);
|
|
708
|
-
writeStateMd(statePath, content, cwd);
|
|
709
|
-
output(
|
|
710
|
-
{
|
|
711
|
-
updated: true,
|
|
712
|
-
percent,
|
|
713
|
-
completed: totalSummaries,
|
|
714
|
-
total: totalPlans,
|
|
715
|
-
bar: progressStr,
|
|
716
|
-
},
|
|
717
|
-
raw,
|
|
718
|
-
progressStr,
|
|
719
|
-
);
|
|
720
|
-
} else {
|
|
721
|
-
output(
|
|
722
|
-
{ updated: false, reason: "Progress field not found in STATE.md" },
|
|
723
|
-
raw,
|
|
724
|
-
"false",
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
export function cmdStateAddDecision(
|
|
730
|
-
cwd: string,
|
|
731
|
-
options: {
|
|
732
|
-
phase?: string | null;
|
|
733
|
-
summary?: string | null;
|
|
734
|
-
summary_file?: string | null;
|
|
735
|
-
rationale?: string | null;
|
|
736
|
-
rationale_file?: string | null;
|
|
737
|
-
},
|
|
738
|
-
raw: boolean,
|
|
739
|
-
): void {
|
|
740
|
-
const statePath = planningPaths(cwd).state;
|
|
741
|
-
if (!fs.existsSync(statePath)) {
|
|
742
|
-
output({ error: "STATE.md not found" }, raw);
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
const { phase, summary, summary_file, rationale, rationale_file } = options;
|
|
746
|
-
let summaryText: string | null = null,
|
|
747
|
-
rationaleText = "";
|
|
748
|
-
try {
|
|
749
|
-
summaryText = readTextArgOrFile(
|
|
750
|
-
cwd,
|
|
751
|
-
summary ?? null,
|
|
752
|
-
summary_file ?? null,
|
|
753
|
-
"summary",
|
|
754
|
-
);
|
|
755
|
-
rationaleText = readTextArgOrFile(
|
|
756
|
-
cwd,
|
|
757
|
-
rationale ?? "",
|
|
758
|
-
rationale_file ?? null,
|
|
759
|
-
"rationale",
|
|
760
|
-
);
|
|
761
|
-
} catch (err) {
|
|
762
|
-
output({ added: false, reason: (err as Error).message }, raw, "false");
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
if (!summaryText) {
|
|
766
|
-
output({ error: "summary required" }, raw);
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
770
|
-
const entry = `- [Phase ${phase ?? "?"}]: ${summaryText}${rationaleText ? ` - ${rationaleText}` : ""}`;
|
|
771
|
-
const sectionPattern =
|
|
772
|
-
/(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
773
|
-
const match = content.match(sectionPattern);
|
|
774
|
-
if (match) {
|
|
775
|
-
let body = match[2]
|
|
776
|
-
.replace(/None yet\.?\s*\n?/gi, "")
|
|
777
|
-
.replace(/No decisions yet\.?\s*\n?/gi, "");
|
|
778
|
-
body = body.trimEnd() + "\n" + entry + "\n";
|
|
779
|
-
content = content.replace(
|
|
780
|
-
sectionPattern,
|
|
781
|
-
(_m, header) => `${header}${body}`,
|
|
782
|
-
);
|
|
783
|
-
writeStateMd(statePath, content, cwd);
|
|
784
|
-
output({ added: true, decision: entry }, raw, "true");
|
|
785
|
-
} else {
|
|
786
|
-
output(
|
|
787
|
-
{ added: false, reason: "Decisions section not found in STATE.md" },
|
|
788
|
-
raw,
|
|
789
|
-
"false",
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
export function cmdStateAddBlocker(
|
|
795
|
-
cwd: string,
|
|
796
|
-
options: { text?: string | null; text_file?: string | null } | string,
|
|
797
|
-
raw: boolean,
|
|
798
|
-
): void {
|
|
799
|
-
const statePath = planningPaths(cwd).state;
|
|
800
|
-
if (!fs.existsSync(statePath)) {
|
|
801
|
-
output({ error: "STATE.md not found" }, raw);
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
const blockerOptions =
|
|
805
|
-
typeof options === "object" && options !== null
|
|
806
|
-
? options
|
|
807
|
-
: { text: options };
|
|
808
|
-
let blockerText: string | null = null;
|
|
809
|
-
try {
|
|
810
|
-
blockerText = readTextArgOrFile(
|
|
811
|
-
cwd,
|
|
812
|
-
blockerOptions.text ?? null,
|
|
813
|
-
blockerOptions.text_file ?? null,
|
|
814
|
-
"blocker",
|
|
815
|
-
);
|
|
816
|
-
} catch (err) {
|
|
817
|
-
output({ added: false, reason: (err as Error).message }, raw, "false");
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
if (!blockerText) {
|
|
821
|
-
output({ error: "text required" }, raw);
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
825
|
-
const entry = `- ${blockerText}`;
|
|
826
|
-
const sectionPattern =
|
|
827
|
-
/(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
828
|
-
const match = content.match(sectionPattern);
|
|
829
|
-
if (match) {
|
|
830
|
-
let body = match[2]
|
|
831
|
-
.replace(/None\.?\s*\n?/gi, "")
|
|
832
|
-
.replace(/None yet\.?\s*\n?/gi, "");
|
|
833
|
-
body = body.trimEnd() + "\n" + entry + "\n";
|
|
834
|
-
content = content.replace(
|
|
835
|
-
sectionPattern,
|
|
836
|
-
(_m, header) => `${header}${body}`,
|
|
837
|
-
);
|
|
838
|
-
writeStateMd(statePath, content, cwd);
|
|
839
|
-
output({ added: true, blocker: blockerText }, raw, "true");
|
|
840
|
-
} else {
|
|
841
|
-
output(
|
|
842
|
-
{ added: false, reason: "Blockers section not found in STATE.md" },
|
|
843
|
-
raw,
|
|
844
|
-
"false",
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
export function cmdStateResolveBlocker(
|
|
850
|
-
cwd: string,
|
|
851
|
-
text: string | null,
|
|
852
|
-
raw: boolean,
|
|
853
|
-
): void {
|
|
854
|
-
const statePath = planningPaths(cwd).state;
|
|
855
|
-
if (!fs.existsSync(statePath)) {
|
|
856
|
-
output({ error: "STATE.md not found" }, raw);
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
if (!text) {
|
|
860
|
-
output({ error: "text required" }, raw);
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
864
|
-
const sectionPattern =
|
|
865
|
-
/(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
866
|
-
const match = content.match(sectionPattern);
|
|
867
|
-
if (match) {
|
|
868
|
-
const lines = match[2].split("\n");
|
|
869
|
-
const filtered = lines.filter(
|
|
870
|
-
(l) =>
|
|
871
|
-
!l.startsWith("- ") || !l.toLowerCase().includes(text.toLowerCase()),
|
|
872
|
-
);
|
|
873
|
-
let newBody = filtered.join("\n");
|
|
874
|
-
if (!newBody.trim() || !newBody.includes("- ")) newBody = "None\n";
|
|
875
|
-
content = content.replace(
|
|
876
|
-
sectionPattern,
|
|
877
|
-
(_m, header) => `${header}${newBody}`,
|
|
878
|
-
);
|
|
879
|
-
writeStateMd(statePath, content, cwd);
|
|
880
|
-
output({ resolved: true, blocker: text }, raw, "true");
|
|
881
|
-
} else {
|
|
882
|
-
output(
|
|
883
|
-
{ resolved: false, reason: "Blockers section not found in STATE.md" },
|
|
884
|
-
raw,
|
|
885
|
-
"false",
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
export function cmdStateRecordSession(
|
|
891
|
-
cwd: string,
|
|
892
|
-
options: { stopped_at?: string | null; resume_file?: string | null },
|
|
893
|
-
raw: boolean,
|
|
894
|
-
): void {
|
|
895
|
-
const statePath = planningPaths(cwd).state;
|
|
896
|
-
if (!fs.existsSync(statePath)) {
|
|
897
|
-
output({ error: "STATE.md not found" }, raw);
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
901
|
-
const now = new Date().toISOString();
|
|
902
|
-
const updated: string[] = [];
|
|
903
|
-
const tryReplace = (field: string): void => {
|
|
904
|
-
const r = stateReplaceField(content, field, now);
|
|
905
|
-
if (r) {
|
|
906
|
-
content = r;
|
|
907
|
-
updated.push(field);
|
|
908
|
-
}
|
|
909
|
-
};
|
|
910
|
-
tryReplace("Last session");
|
|
911
|
-
tryReplace("Last Date");
|
|
912
|
-
if (options.stopped_at) {
|
|
913
|
-
const r =
|
|
914
|
-
stateReplaceField(content, "Stopped At", options.stopped_at) ??
|
|
915
|
-
stateReplaceField(content, "Stopped at", options.stopped_at);
|
|
916
|
-
if (r) {
|
|
917
|
-
content = r;
|
|
918
|
-
updated.push("Stopped At");
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
const resumeFile = options.resume_file ?? "None";
|
|
922
|
-
const r =
|
|
923
|
-
stateReplaceField(content, "Resume File", resumeFile) ??
|
|
924
|
-
stateReplaceField(content, "Resume file", resumeFile);
|
|
925
|
-
if (r) {
|
|
926
|
-
content = r;
|
|
927
|
-
updated.push("Resume File");
|
|
928
|
-
}
|
|
929
|
-
if (updated.length > 0) {
|
|
930
|
-
writeStateMd(statePath, content, cwd);
|
|
931
|
-
output({ recorded: true, updated }, raw, "true");
|
|
932
|
-
} else {
|
|
933
|
-
output(
|
|
934
|
-
{ recorded: false, reason: "No session fields found in STATE.md" },
|
|
935
|
-
raw,
|
|
936
|
-
"false",
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
export function cmdStateSnapshot(cwd: string, raw: boolean): void {
|
|
942
|
-
const statePath = getStatePath(cwd);
|
|
943
|
-
if (!fs.existsSync(statePath)) {
|
|
944
|
-
output({ error: "STATE.md not found" }, raw);
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
const content = fs.readFileSync(statePath, "utf-8");
|
|
948
|
-
const get = (f: string) => stateExtractField(content, f);
|
|
949
|
-
|
|
950
|
-
const decisions: Array<{
|
|
951
|
-
phase: string;
|
|
952
|
-
summary: string;
|
|
953
|
-
rationale: string;
|
|
954
|
-
}> = [];
|
|
955
|
-
const decisionsMatch = content.match(
|
|
956
|
-
/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i,
|
|
957
|
-
);
|
|
958
|
-
if (decisionsMatch) {
|
|
959
|
-
for (const row of decisionsMatch[1]
|
|
960
|
-
.trim()
|
|
961
|
-
.split("\n")
|
|
962
|
-
.filter((r) => r.includes("|"))) {
|
|
963
|
-
const cells = row
|
|
964
|
-
.split("|")
|
|
965
|
-
.map((c) => c.trim())
|
|
966
|
-
.filter(Boolean);
|
|
967
|
-
if (cells.length >= 3)
|
|
968
|
-
decisions.push({
|
|
969
|
-
phase: cells[0],
|
|
970
|
-
summary: cells[1],
|
|
971
|
-
rationale: cells[2],
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const blockers: string[] = [];
|
|
977
|
-
const blockersMatch = content.match(
|
|
978
|
-
/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i,
|
|
979
|
-
);
|
|
980
|
-
if (blockersMatch) {
|
|
981
|
-
for (const item of blockersMatch[1].match(/^-\s+(.+)$/gm) ?? []) {
|
|
982
|
-
blockers.push(item.replace(/^-\s+/, "").trim());
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
const session = {
|
|
987
|
-
last_date: null as string | null,
|
|
988
|
-
stopped_at: null as string | null,
|
|
989
|
-
resume_file: null as string | null,
|
|
990
|
-
};
|
|
991
|
-
const sessionMatch = content.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i);
|
|
992
|
-
if (sessionMatch) {
|
|
993
|
-
const s = sessionMatch[1];
|
|
994
|
-
session.last_date =
|
|
995
|
-
(s.match(/\*\*Last Date:\*\*\s*(.+)/i) ??
|
|
996
|
-
s.match(/^Last Date:\s*(.+)/im))?.[1]?.trim() ?? null;
|
|
997
|
-
session.stopped_at =
|
|
998
|
-
(s.match(/\*\*Stopped At:\*\*\s*(.+)/i) ??
|
|
999
|
-
s.match(/^Stopped At:\s*(.+)/im))?.[1]?.trim() ?? null;
|
|
1000
|
-
session.resume_file =
|
|
1001
|
-
(s.match(/\*\*Resume File:\*\*\s*(.+)/i) ??
|
|
1002
|
-
s.match(/^Resume File:\s*(.+)/im))?.[1]?.trim() ?? null;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
output(
|
|
1006
|
-
{
|
|
1007
|
-
current_phase: get("Current Phase"),
|
|
1008
|
-
current_phase_name: get("Current Phase Name"),
|
|
1009
|
-
total_phases: get("Total Phases")
|
|
1010
|
-
? parseInt(get("Total Phases")!, 10)
|
|
1011
|
-
: null,
|
|
1012
|
-
current_plan: get("Current Plan"),
|
|
1013
|
-
total_plans_in_phase: get("Total Plans in Phase")
|
|
1014
|
-
? parseInt(get("Total Plans in Phase")!, 10)
|
|
1015
|
-
: null,
|
|
1016
|
-
status: get("Status"),
|
|
1017
|
-
progress_percent: get("Progress")
|
|
1018
|
-
? parseInt(get("Progress")!.replace("%", ""), 10)
|
|
1019
|
-
: null,
|
|
1020
|
-
last_activity: get("Last Activity"),
|
|
1021
|
-
last_activity_desc: get("Last Activity Description"),
|
|
1022
|
-
decisions,
|
|
1023
|
-
blockers,
|
|
1024
|
-
paused_at: get("Paused At"),
|
|
1025
|
-
session,
|
|
1026
|
-
},
|
|
1027
|
-
raw,
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
export function cmdStateJson(cwd: string, raw: boolean): void {
|
|
1032
|
-
const statePath = planningPaths(cwd).state;
|
|
1033
|
-
if (!fs.existsSync(statePath)) {
|
|
1034
|
-
output({ error: "STATE.md not found" }, raw, "STATE.md not found");
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
const content = fs.readFileSync(statePath, "utf-8");
|
|
1038
|
-
const fm = extractFrontmatter(content);
|
|
1039
|
-
if (!fm || Object.keys(fm).length === 0) {
|
|
1040
|
-
const body = stripFrontmatter(content);
|
|
1041
|
-
const built = buildStateFrontmatter(body, cwd);
|
|
1042
|
-
output(built, raw, JSON.stringify(built, null, 2));
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
output(fm, raw, JSON.stringify(fm, null, 2));
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
export function cmdStateBeginPhase(
|
|
1049
|
-
cwd: string,
|
|
1050
|
-
phaseNumber: string | null,
|
|
1051
|
-
phaseName: string | null,
|
|
1052
|
-
planCount: number | null,
|
|
1053
|
-
raw: boolean,
|
|
1054
|
-
): void {
|
|
1055
|
-
const statePath = planningPaths(cwd).state;
|
|
1056
|
-
if (!fs.existsSync(statePath)) {
|
|
1057
|
-
output({ error: "STATE.md not found" }, raw);
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
let content = fs.readFileSync(statePath, "utf-8");
|
|
1061
|
-
const today = new Date().toISOString().split("T")[0];
|
|
1062
|
-
const updated: string[] = [];
|
|
1063
|
-
|
|
1064
|
-
const trySet = (field: string, value: string) => {
|
|
1065
|
-
const r = stateReplaceField(content, field, value);
|
|
1066
|
-
if (r) {
|
|
1067
|
-
content = r;
|
|
1068
|
-
updated.push(field);
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
|
-
|
|
1072
|
-
trySet("Status", `Executing Phase ${phaseNumber}`);
|
|
1073
|
-
trySet("Last Activity", today);
|
|
1074
|
-
trySet("Last Activity Description", `Phase ${phaseNumber} execution started`);
|
|
1075
|
-
trySet("Current Phase", String(phaseNumber));
|
|
1076
|
-
if (phaseName) trySet("Current Phase Name", phaseName);
|
|
1077
|
-
trySet("Current Plan", "1");
|
|
1078
|
-
if (planCount) trySet("Total Plans in Phase", String(planCount));
|
|
1079
|
-
|
|
1080
|
-
const focusLabel = phaseName
|
|
1081
|
-
? `Phase ${phaseNumber} - ${phaseName}`
|
|
1082
|
-
: `Phase ${phaseNumber}`;
|
|
1083
|
-
const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
|
|
1084
|
-
if (focusPattern.test(content)) {
|
|
1085
|
-
content = content.replace(focusPattern, (_m, p) => `${p}${focusLabel}`);
|
|
1086
|
-
updated.push("Current focus");
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
1090
|
-
const positionMatch = content.match(positionPattern);
|
|
1091
|
-
if (positionMatch) {
|
|
1092
|
-
const header = positionMatch[1];
|
|
1093
|
-
let posBody = positionMatch[2];
|
|
1094
|
-
const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ""} - EXECUTING`;
|
|
1095
|
-
posBody = /^Phase:/m.test(posBody)
|
|
1096
|
-
? posBody.replace(/^Phase:.*$/m, newPhase)
|
|
1097
|
-
: newPhase + "\n" + posBody;
|
|
1098
|
-
const newPlan = `Plan: 1 of ${planCount ?? "?"}`;
|
|
1099
|
-
posBody = /^Plan:/m.test(posBody)
|
|
1100
|
-
? posBody.replace(/^Plan:.*$/m, newPlan)
|
|
1101
|
-
: posBody.replace(/^(Phase:.*$)/m, `$1\n${newPlan}`);
|
|
1102
|
-
if (/^Status:/m.test(posBody))
|
|
1103
|
-
posBody = posBody.replace(
|
|
1104
|
-
/^Status:.*$/m,
|
|
1105
|
-
`Status: Executing Phase ${phaseNumber}`,
|
|
1106
|
-
);
|
|
1107
|
-
if (/^Last activity:/im.test(posBody))
|
|
1108
|
-
posBody = posBody.replace(
|
|
1109
|
-
/^Last activity:.*$/im,
|
|
1110
|
-
`Last activity: ${today} -- Phase ${phaseNumber} execution started`,
|
|
1111
|
-
);
|
|
1112
|
-
content = content.replace(positionPattern, `${header}${posBody}`);
|
|
1113
|
-
updated.push("Current Position");
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
if (updated.length > 0) writeStateMd(statePath, content, cwd);
|
|
1117
|
-
output(
|
|
1118
|
-
{
|
|
1119
|
-
updated,
|
|
1120
|
-
phase: phaseNumber,
|
|
1121
|
-
phase_name: phaseName ?? null,
|
|
1122
|
-
plan_count: planCount ?? null,
|
|
1123
|
-
},
|
|
1124
|
-
raw,
|
|
1125
|
-
updated.length > 0 ? "true" : "false",
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
export function cmdSignalWaiting(
|
|
1130
|
-
cwd: string,
|
|
1131
|
-
type: string | null,
|
|
1132
|
-
question: string | null,
|
|
1133
|
-
options: string | null,
|
|
1134
|
-
phase: string | null,
|
|
1135
|
-
raw: boolean,
|
|
1136
|
-
): void {
|
|
1137
|
-
const gsdDir = fs.existsSync(path.join(cwd, ".gsd"))
|
|
1138
|
-
? path.join(cwd, ".gsd")
|
|
1139
|
-
: planningDir(cwd);
|
|
1140
|
-
const waitingPath = path.join(gsdDir, "WAITING.json");
|
|
1141
|
-
const signal = {
|
|
1142
|
-
status: "waiting",
|
|
1143
|
-
type: type ?? "decision_point",
|
|
1144
|
-
question: question ?? null,
|
|
1145
|
-
options: options ? options.split("|").map((o) => o.trim()) : [],
|
|
1146
|
-
since: new Date().toISOString(),
|
|
1147
|
-
phase: phase ?? null,
|
|
1148
|
-
};
|
|
1149
|
-
try {
|
|
1150
|
-
fs.mkdirSync(gsdDir, { recursive: true });
|
|
1151
|
-
fs.writeFileSync(waitingPath, JSON.stringify(signal, null, 2), "utf-8");
|
|
1152
|
-
output({ signaled: true, path: waitingPath }, raw, "true");
|
|
1153
|
-
} catch (e) {
|
|
1154
|
-
output({ signaled: false, error: (e as Error).message }, raw, "false");
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
export function cmdSignalResume(cwd: string, raw: boolean): void {
|
|
1159
|
-
const paths = [
|
|
1160
|
-
path.join(cwd, ".gsd", "WAITING.json"),
|
|
1161
|
-
path.join(planningDir(cwd), "WAITING.json"),
|
|
1162
|
-
];
|
|
1163
|
-
let removed = false;
|
|
1164
|
-
for (const p of paths) {
|
|
1165
|
-
if (fs.existsSync(p)) {
|
|
1166
|
-
try {
|
|
1167
|
-
fs.unlinkSync(p);
|
|
1168
|
-
removed = true;
|
|
1169
|
-
} catch {
|
|
1170
|
-
/* ok */
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
output({ resumed: true, removed }, raw, removed ? "true" : "false");
|
|
1175
|
-
}
|