gsd-pi 2.38.0-dev.7209774 → 2.38.0-dev.785052f
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/README.md +15 -11
- package/dist/resources/extensions/gsd/auto-prompts.js +171 -4
- package/dist/resources/extensions/gsd/doctor-providers.js +3 -0
- package/dist/resources/extensions/gsd/files.js +42 -7
- package/dist/resources/extensions/gsd/gitignore.js +16 -3
- package/dist/resources/extensions/gsd/guided-flow.js +67 -6
- package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
- package/dist/resources/extensions/gsd/health-widget.js +3 -86
- package/dist/resources/extensions/gsd/migrate-external.js +18 -1
- package/dist/resources/extensions/gsd/preferences.js +17 -9
- package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -1
- package/dist/resources/extensions/gsd/state.js +41 -22
- package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/dist/resources/extensions/remote-questions/status.js +4 -2
- package/dist/resources/extensions/remote-questions/store.js +4 -2
- package/dist/resources/extensions/shared/frontmatter.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +6 -1
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/skills.ts +9 -1
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +213 -4
- package/src/resources/extensions/gsd/doctor-providers.ts +4 -0
- package/src/resources/extensions/gsd/files.ts +46 -8
- package/src/resources/extensions/gsd/gitignore.ts +17 -3
- package/src/resources/extensions/gsd/guided-flow.ts +67 -6
- package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
- package/src/resources/extensions/gsd/health-widget.ts +3 -89
- package/src/resources/extensions/gsd/migrate-external.ts +18 -1
- package/src/resources/extensions/gsd/preferences.ts +20 -9
- package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +1 -1
- package/src/resources/extensions/gsd/state.ts +38 -20
- package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
- package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
- package/src/resources/extensions/gsd/types.ts +10 -0
- package/src/resources/extensions/remote-questions/status.ts +4 -2
- package/src/resources/extensions/remote-questions/store.ts +4 -2
- package/src/resources/extensions/shared/frontmatter.ts +1 -1
|
@@ -11,7 +11,7 @@ import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
|
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
13
|
Roadmap, BoundaryMapEntry,
|
|
14
|
-
SlicePlan, TaskPlanEntry,
|
|
14
|
+
SlicePlan, TaskPlanEntry, TaskPlanFile, TaskPlanFrontmatter,
|
|
15
15
|
Summary, SummaryFrontmatter, SummaryRequires, FileModified,
|
|
16
16
|
Continue, ContinueFrontmatter, ContinueStatus,
|
|
17
17
|
RequirementCounts,
|
|
@@ -277,14 +277,52 @@ export function formatSecretsManifest(manifest: SecretsManifest): string {
|
|
|
277
277
|
|
|
278
278
|
// ─── Slice Plan Parser ─────────────────────────────────────────────────────
|
|
279
279
|
|
|
280
|
+
function normalizeTaskPlanFrontmatter(frontmatter: Record<string, unknown>): TaskPlanFrontmatter {
|
|
281
|
+
const estimatedStepsRaw = frontmatter.estimated_steps;
|
|
282
|
+
const estimatedFilesRaw = frontmatter.estimated_files;
|
|
283
|
+
const skillsUsedRaw = frontmatter.skills_used;
|
|
284
|
+
|
|
285
|
+
const parseOptionalNumber = (value: unknown): number | undefined => {
|
|
286
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
287
|
+
if (typeof value === 'string' && value.trim()) {
|
|
288
|
+
const parsed = parseInt(value, 10);
|
|
289
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const estimated_steps = parseOptionalNumber(estimatedStepsRaw);
|
|
295
|
+
const estimated_files = parseOptionalNumber(estimatedFilesRaw);
|
|
296
|
+
const skills_used = Array.isArray(skillsUsedRaw)
|
|
297
|
+
? skillsUsedRaw.map(v => String(v).trim()).filter(Boolean)
|
|
298
|
+
: typeof skillsUsedRaw === 'string' && skillsUsedRaw.trim()
|
|
299
|
+
? [skillsUsedRaw.trim()]
|
|
300
|
+
: [];
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
...(estimated_steps !== undefined ? { estimated_steps } : {}),
|
|
304
|
+
...(estimated_files !== undefined ? { estimated_files } : {}),
|
|
305
|
+
skills_used,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function parseTaskPlanFile(content: string): TaskPlanFile {
|
|
310
|
+
const [fmLines] = splitFrontmatter(content);
|
|
311
|
+
const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
|
|
312
|
+
return {
|
|
313
|
+
frontmatter: normalizeTaskPlanFrontmatter(fm),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
280
317
|
export function parsePlan(content: string): SlicePlan {
|
|
281
318
|
return cachedParse(content, 'plan', _parsePlanImpl);
|
|
282
319
|
}
|
|
283
320
|
|
|
284
321
|
function _parsePlanImpl(content: string): SlicePlan {
|
|
285
322
|
const stopTimer = debugTime("parse-plan");
|
|
323
|
+
const [, body] = splitFrontmatter(content);
|
|
286
324
|
// Try native parser first for better performance
|
|
287
|
-
const nativeResult = nativeParsePlanFile(
|
|
325
|
+
const nativeResult = nativeParsePlanFile(body);
|
|
288
326
|
if (nativeResult) {
|
|
289
327
|
stopTimer({ native: true });
|
|
290
328
|
return {
|
|
@@ -306,7 +344,7 @@ function _parsePlanImpl(content: string): SlicePlan {
|
|
|
306
344
|
};
|
|
307
345
|
}
|
|
308
346
|
|
|
309
|
-
const lines =
|
|
347
|
+
const lines = body.split('\n');
|
|
310
348
|
|
|
311
349
|
const h1 = lines.find(l => l.startsWith('# '));
|
|
312
350
|
let id = '';
|
|
@@ -321,13 +359,13 @@ function _parsePlanImpl(content: string): SlicePlan {
|
|
|
321
359
|
}
|
|
322
360
|
}
|
|
323
361
|
|
|
324
|
-
const goal = extractBoldField(
|
|
325
|
-
const demo = extractBoldField(
|
|
362
|
+
const goal = extractBoldField(body, 'Goal') || '';
|
|
363
|
+
const demo = extractBoldField(body, 'Demo') || '';
|
|
326
364
|
|
|
327
|
-
const mhSection = extractSection(
|
|
365
|
+
const mhSection = extractSection(body, 'Must-Haves');
|
|
328
366
|
const mustHaves = mhSection ? parseBullets(mhSection) : [];
|
|
329
367
|
|
|
330
|
-
const tasksSection = extractSection(
|
|
368
|
+
const tasksSection = extractSection(body, 'Tasks');
|
|
331
369
|
const tasks: TaskPlanEntry[] = [];
|
|
332
370
|
|
|
333
371
|
if (tasksSection) {
|
|
@@ -375,7 +413,7 @@ function _parsePlanImpl(content: string): SlicePlan {
|
|
|
375
413
|
if (currentTask) tasks.push(currentTask);
|
|
376
414
|
}
|
|
377
415
|
|
|
378
|
-
const filesSection = extractSection(
|
|
416
|
+
const filesSection = extractSection(body, 'Files Likely Touched');
|
|
379
417
|
const filesLikelyTouched = filesSection ? parseBullets(filesSection) : [];
|
|
380
418
|
|
|
381
419
|
const result = { id, title, goal, demo, mustHaves, tasks, filesLikelyTouched };
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
10
11
|
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
12
|
import { nativeRmCached, nativeLsFiles } from "./native-git-bridge.js";
|
|
12
13
|
import { gsdRoot } from "./paths.js";
|
|
14
|
+
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* GSD runtime patterns for git index cleanup.
|
|
@@ -104,10 +106,22 @@ export function hasGitTrackedGsdFiles(basePath: string): boolean {
|
|
|
104
106
|
// Check if git tracks any files under .gsd/
|
|
105
107
|
try {
|
|
106
108
|
const tracked = nativeLsFiles(basePath, ".gsd");
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
109
|
+
if (tracked.length > 0) return true;
|
|
110
|
+
|
|
111
|
+
// nativeLsFiles swallows git failures and returns []. An empty result
|
|
112
|
+
// could mean "nothing tracked" OR "git failed silently". Verify git is
|
|
113
|
+
// reachable before trusting the empty result — if it isn't, fail safe
|
|
114
|
+
// by assuming files ARE tracked to prevent data loss.
|
|
115
|
+
execFileSync("git", ["rev-parse", "--git-dir"], {
|
|
116
|
+
cwd: basePath,
|
|
117
|
+
stdio: "pipe",
|
|
118
|
+
env: GIT_NO_PROMPT_ENV,
|
|
119
|
+
});
|
|
120
|
+
|
|
110
121
|
return false;
|
|
122
|
+
} catch {
|
|
123
|
+
// git unavailable, index locked, or repo corrupt — fail safe
|
|
124
|
+
return true;
|
|
111
125
|
}
|
|
112
126
|
}
|
|
113
127
|
|
|
@@ -10,6 +10,7 @@ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@g
|
|
|
10
10
|
import { showNextAction } from "../shared/mod.js";
|
|
11
11
|
import { loadFile, parseRoadmap } from "./files.js";
|
|
12
12
|
import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
|
|
13
|
+
import { buildSkillActivationBlock } from "./auto-prompts.js";
|
|
13
14
|
import { deriveState } from "./state.js";
|
|
14
15
|
import { invalidateAllCaches } from "./cache.js";
|
|
15
16
|
import { startAuto } from "./auto.js";
|
|
@@ -1124,7 +1125,16 @@ export async function showSmartEntry(
|
|
|
1124
1125
|
].join("\n\n---\n\n");
|
|
1125
1126
|
const secretsOutputPath = relMilestoneFile(basePath, milestoneId, "SECRETS");
|
|
1126
1127
|
await dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
|
|
1127
|
-
milestoneId,
|
|
1128
|
+
milestoneId,
|
|
1129
|
+
milestoneTitle,
|
|
1130
|
+
secretsOutputPath,
|
|
1131
|
+
inlinedTemplates: planMilestoneTemplates,
|
|
1132
|
+
skillActivation: buildSkillActivationBlock({
|
|
1133
|
+
base: basePath,
|
|
1134
|
+
milestoneId,
|
|
1135
|
+
milestoneTitle,
|
|
1136
|
+
extraContext: [planMilestoneTemplates],
|
|
1137
|
+
}),
|
|
1128
1138
|
}), "gsd-run", ctx, "plan-milestone");
|
|
1129
1139
|
} else if (choice === "discuss") {
|
|
1130
1140
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
@@ -1254,14 +1264,34 @@ export async function showSmartEntry(
|
|
|
1254
1264
|
inlineTemplate("task-plan", "Task Plan"),
|
|
1255
1265
|
].join("\n\n---\n\n");
|
|
1256
1266
|
await dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
|
|
1257
|
-
milestoneId,
|
|
1267
|
+
milestoneId,
|
|
1268
|
+
sliceId,
|
|
1269
|
+
sliceTitle,
|
|
1270
|
+
inlinedTemplates: planSliceTemplates,
|
|
1271
|
+
skillActivation: buildSkillActivationBlock({
|
|
1272
|
+
base: basePath,
|
|
1273
|
+
milestoneId,
|
|
1274
|
+
sliceId,
|
|
1275
|
+
sliceTitle,
|
|
1276
|
+
extraContext: [planSliceTemplates],
|
|
1277
|
+
}),
|
|
1258
1278
|
}), "gsd-run", ctx, "plan-slice");
|
|
1259
1279
|
} else if (choice === "discuss") {
|
|
1260
1280
|
await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "plan-slice");
|
|
1261
1281
|
} else if (choice === "research") {
|
|
1262
1282
|
const researchTemplates = inlineTemplate("research", "Research");
|
|
1263
1283
|
await dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
|
|
1264
|
-
milestoneId,
|
|
1284
|
+
milestoneId,
|
|
1285
|
+
sliceId,
|
|
1286
|
+
sliceTitle,
|
|
1287
|
+
inlinedTemplates: researchTemplates,
|
|
1288
|
+
skillActivation: buildSkillActivationBlock({
|
|
1289
|
+
base: basePath,
|
|
1290
|
+
milestoneId,
|
|
1291
|
+
sliceId,
|
|
1292
|
+
sliceTitle,
|
|
1293
|
+
extraContext: [researchTemplates],
|
|
1294
|
+
}),
|
|
1265
1295
|
}), "gsd-run", ctx, "research-slice");
|
|
1266
1296
|
} else if (choice === "status") {
|
|
1267
1297
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -1305,7 +1335,18 @@ export async function showSmartEntry(
|
|
|
1305
1335
|
inlineTemplate("uat", "UAT"),
|
|
1306
1336
|
].join("\n\n---\n\n");
|
|
1307
1337
|
await dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
|
|
1308
|
-
workingDirectory: basePath,
|
|
1338
|
+
workingDirectory: basePath,
|
|
1339
|
+
milestoneId,
|
|
1340
|
+
sliceId,
|
|
1341
|
+
sliceTitle,
|
|
1342
|
+
inlinedTemplates: completeSliceTemplates,
|
|
1343
|
+
skillActivation: buildSkillActivationBlock({
|
|
1344
|
+
base: basePath,
|
|
1345
|
+
milestoneId,
|
|
1346
|
+
sliceId,
|
|
1347
|
+
sliceTitle,
|
|
1348
|
+
extraContext: [completeSliceTemplates],
|
|
1349
|
+
}),
|
|
1309
1350
|
}), "gsd-run", ctx, "complete-slice");
|
|
1310
1351
|
} else if (choice === "status") {
|
|
1311
1352
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -1370,12 +1411,32 @@ export async function showSmartEntry(
|
|
|
1370
1411
|
if (choice === "execute") {
|
|
1371
1412
|
if (hasInterrupted) {
|
|
1372
1413
|
await dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
|
|
1373
|
-
milestoneId,
|
|
1414
|
+
milestoneId,
|
|
1415
|
+
sliceId,
|
|
1416
|
+
skillActivation: buildSkillActivationBlock({
|
|
1417
|
+
base: basePath,
|
|
1418
|
+
milestoneId,
|
|
1419
|
+
sliceId,
|
|
1420
|
+
taskId,
|
|
1421
|
+
taskTitle,
|
|
1422
|
+
}),
|
|
1374
1423
|
}), "gsd-run", ctx, "execute-task");
|
|
1375
1424
|
} else {
|
|
1376
1425
|
const executeTaskTemplates = inlineTemplate("task-summary", "Task Summary");
|
|
1377
1426
|
await dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
|
|
1378
|
-
milestoneId,
|
|
1427
|
+
milestoneId,
|
|
1428
|
+
sliceId,
|
|
1429
|
+
taskId,
|
|
1430
|
+
taskTitle,
|
|
1431
|
+
inlinedTemplates: executeTaskTemplates,
|
|
1432
|
+
skillActivation: buildSkillActivationBlock({
|
|
1433
|
+
base: basePath,
|
|
1434
|
+
milestoneId,
|
|
1435
|
+
sliceId,
|
|
1436
|
+
taskId,
|
|
1437
|
+
taskTitle,
|
|
1438
|
+
extraContext: [executeTaskTemplates],
|
|
1439
|
+
}),
|
|
1379
1440
|
}), "gsd-run", ctx, "execute-task");
|
|
1380
1441
|
}
|
|
1381
1442
|
} else if (choice === "status") {
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
* runtime integrations so the regressions can be tested directly.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { detectProjectState } from "./detection.js";
|
|
9
10
|
import { gsdRoot } from "./paths.js";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import type { GSDState, Phase } from "./types.js";
|
|
12
11
|
|
|
13
12
|
export type HealthWidgetProjectState = "none" | "initialized" | "active";
|
|
14
13
|
|
|
@@ -20,75 +19,19 @@ export interface HealthWidgetData {
|
|
|
20
19
|
environmentErrorCount: number;
|
|
21
20
|
environmentWarningCount: number;
|
|
22
21
|
lastRefreshed: number;
|
|
23
|
-
executionPhase?: Phase;
|
|
24
|
-
executionStatus?: string;
|
|
25
|
-
executionTarget?: string;
|
|
26
|
-
nextAction?: string;
|
|
27
|
-
blocker?: string | null;
|
|
28
|
-
activeMilestoneId?: string;
|
|
29
|
-
activeSliceId?: string;
|
|
30
|
-
activeTaskId?: string;
|
|
31
|
-
progress?: GSDState["progress"];
|
|
32
|
-
eta?: string | null;
|
|
33
22
|
}
|
|
34
23
|
|
|
35
24
|
export function detectHealthWidgetProjectState(basePath: string): HealthWidgetProjectState {
|
|
36
|
-
|
|
37
|
-
if (!existsSync(root)) return "none";
|
|
25
|
+
if (!existsSync(gsdRoot(basePath))) return "none";
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const milestonesDir = join(root, "milestones");
|
|
43
|
-
if (existsSync(milestonesDir)) {
|
|
44
|
-
const entries = readdirSync(milestonesDir, { withFileTypes: true });
|
|
45
|
-
if (entries.some(e => e.isDirectory())) return "active";
|
|
46
|
-
}
|
|
47
|
-
} catch { /* non-fatal */ }
|
|
48
|
-
|
|
49
|
-
return "initialized";
|
|
27
|
+
const { state } = detectProjectState(basePath);
|
|
28
|
+
return state === "v2-gsd" ? "active" : "initialized";
|
|
50
29
|
}
|
|
51
30
|
|
|
52
31
|
function formatCost(n: number): string {
|
|
53
32
|
return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
|
|
54
33
|
}
|
|
55
34
|
|
|
56
|
-
function formatProgress(progress?: GSDState["progress"]): string | null {
|
|
57
|
-
if (!progress) return null;
|
|
58
|
-
|
|
59
|
-
const parts: string[] = [];
|
|
60
|
-
parts.push(`M ${progress.milestones.done}/${progress.milestones.total}`);
|
|
61
|
-
if (progress.slices) parts.push(`S ${progress.slices.done}/${progress.slices.total}`);
|
|
62
|
-
if (progress.tasks) parts.push(`T ${progress.tasks.done}/${progress.tasks.total}`);
|
|
63
|
-
return parts.length > 0 ? `Progress: ${parts.join(" · ")}` : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function formatEnvironmentSummary(errorCount: number, warningCount: number): string | null {
|
|
67
|
-
if (errorCount <= 0 && warningCount <= 0) return null;
|
|
68
|
-
|
|
69
|
-
const parts: string[] = [];
|
|
70
|
-
if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
71
|
-
if (warningCount > 0) parts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
72
|
-
return `Env: ${parts.join(", ")}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function formatBudgetSummary(data: HealthWidgetData): string | null {
|
|
76
|
-
if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
|
|
77
|
-
const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
|
|
78
|
-
return `Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`;
|
|
79
|
-
}
|
|
80
|
-
if (data.budgetSpent > 0) {
|
|
81
|
-
return `Spent: ${formatCost(data.budgetSpent)}`;
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function buildExecutionHeadline(data: HealthWidgetData): string {
|
|
87
|
-
const status = data.executionStatus ?? "Active project";
|
|
88
|
-
const target = data.executionTarget ?? data.blocker ?? "loading status…";
|
|
89
|
-
return ` GSD ${status}${target ? ` - ${target}` : ""}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
35
|
/**
|
|
93
36
|
* Build compact health lines for the widget.
|
|
94
37
|
* Returns a string array suitable for setWidget().
|
|
@@ -102,28 +45,33 @@ export function buildHealthLines(data: HealthWidgetData): string[] {
|
|
|
102
45
|
return [" GSD Project initialized — run /gsd to continue setup"];
|
|
103
46
|
}
|
|
104
47
|
|
|
105
|
-
const
|
|
106
|
-
const details: string[] = [];
|
|
107
|
-
|
|
108
|
-
const progress = formatProgress(data.progress);
|
|
109
|
-
if (progress) details.push(progress);
|
|
110
|
-
|
|
111
|
-
if (data.providerIssue) details.push(data.providerIssue);
|
|
48
|
+
const parts: string[] = [];
|
|
112
49
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
50
|
+
const totalIssues = data.environmentErrorCount + data.environmentWarningCount + (data.providerIssue ? 1 : 0);
|
|
51
|
+
if (totalIssues === 0) {
|
|
52
|
+
parts.push("● System OK");
|
|
53
|
+
} else if (data.environmentErrorCount > 0 || data.providerIssue?.includes("✗")) {
|
|
54
|
+
parts.push(`✗ ${totalIssues} issue${totalIssues > 1 ? "s" : ""}`);
|
|
55
|
+
} else {
|
|
56
|
+
parts.push(`⚠ ${totalIssues} warning${totalIssues > 1 ? "s" : ""}`);
|
|
57
|
+
}
|
|
118
58
|
|
|
119
|
-
|
|
120
|
-
|
|
59
|
+
if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
|
|
60
|
+
const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
|
|
61
|
+
parts.push(`Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`);
|
|
62
|
+
} else if (data.budgetSpent > 0) {
|
|
63
|
+
parts.push(`Spent: ${formatCost(data.budgetSpent)}`);
|
|
64
|
+
}
|
|
121
65
|
|
|
122
|
-
if (data.
|
|
66
|
+
if (data.providerIssue) {
|
|
67
|
+
parts.push(data.providerIssue);
|
|
68
|
+
}
|
|
123
69
|
|
|
124
|
-
if (
|
|
125
|
-
|
|
70
|
+
if (data.environmentErrorCount > 0) {
|
|
71
|
+
parts.push(`Env: ${data.environmentErrorCount} error${data.environmentErrorCount > 1 ? "s" : ""}`);
|
|
72
|
+
} else if (data.environmentWarningCount > 0) {
|
|
73
|
+
parts.push(`Env: ${data.environmentWarningCount} warning${data.environmentWarningCount > 1 ? "s" : ""}`);
|
|
126
74
|
}
|
|
127
75
|
|
|
128
|
-
return
|
|
76
|
+
return [` ${parts.join(" │ ")}`];
|
|
129
77
|
}
|
|
@@ -16,7 +16,6 @@ import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
|
16
16
|
import { loadLedgerFromDisk, getProjectTotals } from "./metrics.js";
|
|
17
17
|
import { describeNextUnit, estimateTimeRemaining, updateSliceProgressCache } from "./auto-dashboard.js";
|
|
18
18
|
import { projectRoot } from "./commands.js";
|
|
19
|
-
import { deriveState, invalidateStateCache } from "./state.js";
|
|
20
19
|
import {
|
|
21
20
|
buildHealthLines,
|
|
22
21
|
detectHealthWidgetProjectState,
|
|
@@ -25,7 +24,7 @@ import {
|
|
|
25
24
|
|
|
26
25
|
// ── Data loader ────────────────────────────────────────────────────────────────
|
|
27
26
|
|
|
28
|
-
function
|
|
27
|
+
function loadHealthWidgetData(basePath: string): HealthWidgetData {
|
|
29
28
|
let budgetCeiling: number | undefined;
|
|
30
29
|
let budgetSpent = 0;
|
|
31
30
|
let providerIssue: string | null = null;
|
|
@@ -69,90 +68,6 @@ function loadBaseHealthWidgetData(basePath: string): HealthWidgetData {
|
|
|
69
68
|
};
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
function compactText(text: string, max = 64): string {
|
|
73
|
-
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
74
|
-
if (trimmed.length <= max) return trimmed;
|
|
75
|
-
return `${trimmed.slice(0, max - 1).trimEnd()}…`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function summarizeExecutionStatus(state: GSDState): string {
|
|
79
|
-
switch (state.phase) {
|
|
80
|
-
case "blocked": return "Blocked";
|
|
81
|
-
case "paused": return "Paused";
|
|
82
|
-
case "complete": return "Complete";
|
|
83
|
-
case "executing": return "Executing";
|
|
84
|
-
case "planning": return "Planning";
|
|
85
|
-
case "pre-planning": return "Pre-planning";
|
|
86
|
-
case "summarizing": return "Summarizing";
|
|
87
|
-
case "validating-milestone": return "Validating";
|
|
88
|
-
case "completing-milestone": return "Completing";
|
|
89
|
-
case "needs-discussion": return "Needs discussion";
|
|
90
|
-
case "replanning-slice": return "Replanning";
|
|
91
|
-
default: return "Active";
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function summarizeExecutionTarget(state: GSDState): string {
|
|
96
|
-
switch (state.phase) {
|
|
97
|
-
case "needs-discussion":
|
|
98
|
-
return state.activeMilestone ? `Discuss ${state.activeMilestone.id}` : "Discuss milestone draft";
|
|
99
|
-
case "pre-planning":
|
|
100
|
-
return state.activeMilestone ? `Plan ${state.activeMilestone.id}` : "Research & plan milestone";
|
|
101
|
-
case "planning":
|
|
102
|
-
return state.activeSlice ? `Plan ${state.activeSlice.id}` : "Plan next slice";
|
|
103
|
-
case "executing":
|
|
104
|
-
return state.activeTask ? `Execute ${state.activeTask.id}` : "Execute next task";
|
|
105
|
-
case "summarizing":
|
|
106
|
-
return state.activeSlice ? `Complete ${state.activeSlice.id}` : "Complete current slice";
|
|
107
|
-
case "validating-milestone":
|
|
108
|
-
return state.activeMilestone ? `Validate ${state.activeMilestone.id}` : "Validate milestone";
|
|
109
|
-
case "completing-milestone":
|
|
110
|
-
return state.activeMilestone ? `Complete ${state.activeMilestone.id}` : "Complete milestone";
|
|
111
|
-
case "replanning-slice":
|
|
112
|
-
return state.activeSlice ? `Replan ${state.activeSlice.id}` : "Replan current slice";
|
|
113
|
-
case "blocked":
|
|
114
|
-
return `waiting on ${compactText(state.blockers[0] ?? state.nextAction, 56)}`;
|
|
115
|
-
case "paused":
|
|
116
|
-
return compactText(state.nextAction || "waiting to resume", 56);
|
|
117
|
-
case "complete":
|
|
118
|
-
return "All milestones complete";
|
|
119
|
-
default:
|
|
120
|
-
return compactText(describeNextUnit(state).label, 56);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function enrichHealthWidgetData(basePath: string, baseData: HealthWidgetData): Promise<HealthWidgetData> {
|
|
125
|
-
if (baseData.projectState !== "active") return baseData;
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
invalidateStateCache();
|
|
129
|
-
const state = await deriveState(basePath);
|
|
130
|
-
|
|
131
|
-
if (state.activeMilestone) {
|
|
132
|
-
// Warm the slice-progress cache so estimateTimeRemaining() has data
|
|
133
|
-
updateSliceProgressCache(basePath, state.activeMilestone.id, state.activeSlice?.id);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
...baseData,
|
|
138
|
-
executionPhase: state.phase,
|
|
139
|
-
executionStatus: summarizeExecutionStatus(state),
|
|
140
|
-
executionTarget: summarizeExecutionTarget(state),
|
|
141
|
-
nextAction: state.nextAction,
|
|
142
|
-
blocker: state.blockers[0] ?? null,
|
|
143
|
-
activeMilestoneId: state.activeMilestone?.id,
|
|
144
|
-
activeSliceId: state.activeSlice?.id,
|
|
145
|
-
activeTaskId: state.activeTask?.id,
|
|
146
|
-
progress: state.progress,
|
|
147
|
-
eta: state.phase === "blocked" || state.phase === "paused" || state.phase === "complete"
|
|
148
|
-
? null
|
|
149
|
-
: estimateTimeRemaining(),
|
|
150
|
-
};
|
|
151
|
-
} catch {
|
|
152
|
-
return baseData;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
71
|
// ── Widget init ────────────────────────────────────────────────────────────────
|
|
157
72
|
|
|
158
73
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
@@ -167,7 +82,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
|
|
|
167
82
|
const basePath = projectRoot();
|
|
168
83
|
|
|
169
84
|
// String-array fallback — used in RPC mode (factory is a no-op there)
|
|
170
|
-
const initialData =
|
|
85
|
+
const initialData = loadHealthWidgetData(basePath);
|
|
171
86
|
ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
|
|
172
87
|
|
|
173
88
|
// Factory-based widget for TUI mode — replaces the string-array above
|
|
@@ -180,8 +95,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
|
|
|
180
95
|
if (refreshInFlight) return;
|
|
181
96
|
refreshInFlight = true;
|
|
182
97
|
try {
|
|
183
|
-
|
|
184
|
-
data = await enrichHealthWidgetData(basePath, baseData);
|
|
98
|
+
data = loadHealthWidgetData(basePath);
|
|
185
99
|
cachedLines = undefined;
|
|
186
100
|
_tui.requestRender();
|
|
187
101
|
} catch { /* non-fatal */ } finally {
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* symlink replaces the original directory so all paths remain valid.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
9
10
|
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
import { externalGsdRoot } from "./repo-identity.js";
|
|
12
13
|
import { getErrorMessage } from "./error-utils.js";
|
|
13
14
|
import { hasGitTrackedGsdFiles } from "./gitignore.js";
|
|
15
|
+
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
14
16
|
|
|
15
17
|
export interface MigrationResult {
|
|
16
18
|
migrated: boolean;
|
|
@@ -144,7 +146,22 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
144
146
|
return { migrated: false, error: `Migration verification failed: ${getErrorMessage(verifyErr)}` };
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
//
|
|
149
|
+
// Clean the git index — any .gsd/* files tracked before migration now
|
|
150
|
+
// sit behind the symlink and git can't follow it, causing them to show
|
|
151
|
+
// as deleted. Remove them from the index so the working tree stays clean.
|
|
152
|
+
// --ignore-unmatch makes this a no-op on fresh projects with no tracked .gsd/.
|
|
153
|
+
try {
|
|
154
|
+
execFileSync("git", ["rm", "-r", "--cached", "--ignore-unmatch", ".gsd"], {
|
|
155
|
+
cwd: basePath,
|
|
156
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
157
|
+
env: GIT_NO_PROMPT_ENV,
|
|
158
|
+
timeout: 10_000,
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
// Non-fatal — git may be unavailable or nothing was tracked
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Remove .gsd.migrating only after symlink is verified and index is clean
|
|
148
165
|
rmSync(migratingPath, { recursive: true, force: true });
|
|
149
166
|
|
|
150
167
|
return { migrated: true };
|
|
@@ -14,7 +14,6 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
|
|
17
|
-
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
18
17
|
import { gsdRoot } from "./paths.js";
|
|
19
18
|
import { parse as parseYaml } from "yaml";
|
|
20
19
|
import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
|
|
@@ -83,24 +82,36 @@ export {
|
|
|
83
82
|
|
|
84
83
|
// ─── Path Constants & Getters ───────────────────────────────────────────────
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
function gsdHome(): string {
|
|
86
|
+
return process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function globalPreferencesPath(): string {
|
|
90
|
+
return join(gsdHome(), "preferences.md");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function legacyGlobalPreferencesPath(): string {
|
|
94
|
+
return join(homedir(), ".pi", "agent", "gsd-preferences.md");
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
function projectPreferencesPath(): string {
|
|
89
98
|
return join(gsdRoot(process.cwd()), "preferences.md");
|
|
90
99
|
}
|
|
91
100
|
// Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
|
|
92
101
|
// Check uppercase as a fallback so those files aren't silently ignored.
|
|
93
|
-
|
|
102
|
+
function globalPreferencesPathUppercase(): string {
|
|
103
|
+
return join(gsdHome(), "PREFERENCES.md");
|
|
104
|
+
}
|
|
94
105
|
function projectPreferencesPathUppercase(): string {
|
|
95
106
|
return join(gsdRoot(process.cwd()), "PREFERENCES.md");
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
export function getGlobalGSDPreferencesPath(): string {
|
|
99
|
-
return
|
|
110
|
+
return globalPreferencesPath();
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
export function getLegacyGlobalGSDPreferencesPath(): string {
|
|
103
|
-
return
|
|
114
|
+
return legacyGlobalPreferencesPath();
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
export function getProjectGSDPreferencesPath(): string {
|
|
@@ -110,9 +121,9 @@ export function getProjectGSDPreferencesPath(): string {
|
|
|
110
121
|
// ─── Loading ────────────────────────────────────────────────────────────────
|
|
111
122
|
|
|
112
123
|
export function loadGlobalGSDPreferences(): LoadedGSDPreferences | null {
|
|
113
|
-
return loadPreferencesFile(
|
|
114
|
-
?? loadPreferencesFile(
|
|
115
|
-
?? loadPreferencesFile(
|
|
124
|
+
return loadPreferencesFile(globalPreferencesPath(), "global")
|
|
125
|
+
?? loadPreferencesFile(globalPreferencesPathUppercase(), "global")
|
|
126
|
+
?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
export function loadProjectGSDPreferences(): LoadedGSDPreferences | null {
|
|
@@ -78,6 +78,11 @@ export function loadPrompt(name: string, vars: Record<string, string> = {}): str
|
|
|
78
78
|
templateCache.set(name, content);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
const effectiveVars = {
|
|
82
|
+
skillActivation: "If a `GSD Skill Preferences` block is present in system context, use it and the `<available_skills>` catalog in your system prompt to decide which skills to load and follow for this unit, without relaxing required verification or artifact rules.",
|
|
83
|
+
...vars,
|
|
84
|
+
};
|
|
85
|
+
|
|
81
86
|
// Check BEFORE substitution: find all {{varName}} placeholders the template
|
|
82
87
|
// declares and verify every one has a value in vars. Checking after substitution
|
|
83
88
|
// would also flag {{...}} patterns injected by inlined content (e.g. template
|
|
@@ -86,7 +91,7 @@ export function loadPrompt(name: string, vars: Record<string, string> = {}): str
|
|
|
86
91
|
if (declared) {
|
|
87
92
|
const missing = [...new Set(declared)]
|
|
88
93
|
.map(m => m.slice(2, -2))
|
|
89
|
-
.filter(key => !(key in
|
|
94
|
+
.filter(key => !(key in effectiveVars));
|
|
90
95
|
if (missing.length > 0) {
|
|
91
96
|
throw new GSDError(
|
|
92
97
|
GSD_PARSE_ERROR,
|
|
@@ -97,7 +102,7 @@ export function loadPrompt(name: string, vars: Record<string, string> = {}): str
|
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
for (const [key, value] of Object.entries(
|
|
105
|
+
for (const [key, value] of Object.entries(effectiveVars)) {
|
|
101
106
|
content = content.replaceAll(`{{${key}}}`, value);
|
|
102
107
|
}
|
|
103
108
|
|
|
@@ -16,7 +16,7 @@ All relevant context has been preloaded below — the roadmap, all slice summari
|
|
|
16
16
|
|
|
17
17
|
Then:
|
|
18
18
|
1. Use the **Milestone Summary** output template from the inlined context above
|
|
19
|
-
2.
|
|
19
|
+
2. {{skillActivation}}
|
|
20
20
|
3. Verify each **success criterion** from the milestone definition in `{{roadmapPath}}`. For each criterion, confirm it was met with specific evidence from slice summaries, test results, or observable behavior. List any criterion that was NOT met.
|
|
21
21
|
4. Verify the milestone's **definition of done** — all slices are `[x]`, all slice summaries exist, and any cross-slice integration points work correctly.
|
|
22
22
|
5. Validate **requirement status transitions**. For each requirement that changed status during this milestone, confirm the transition is supported by evidence. Requirements can move between Active, Validated, Deferred, Blocked, or Out of Scope — but only with proof.
|