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.
Files changed (81) hide show
  1. package/README.md +15 -11
  2. package/dist/resources/extensions/gsd/auto-prompts.js +171 -4
  3. package/dist/resources/extensions/gsd/doctor-providers.js +3 -0
  4. package/dist/resources/extensions/gsd/files.js +42 -7
  5. package/dist/resources/extensions/gsd/gitignore.js +16 -3
  6. package/dist/resources/extensions/gsd/guided-flow.js +67 -6
  7. package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
  8. package/dist/resources/extensions/gsd/health-widget.js +3 -86
  9. package/dist/resources/extensions/gsd/migrate-external.js +18 -1
  10. package/dist/resources/extensions/gsd/preferences.js +17 -9
  11. package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
  12. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  13. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  14. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  15. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  16. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  17. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  18. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  19. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  20. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  21. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  22. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  23. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  24. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  25. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -1
  27. package/dist/resources/extensions/gsd/state.js +41 -22
  28. package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
  29. package/dist/resources/extensions/remote-questions/status.js +4 -2
  30. package/dist/resources/extensions/remote-questions/store.js +4 -2
  31. package/dist/resources/extensions/shared/frontmatter.js +1 -1
  32. package/package.json +1 -1
  33. package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
  34. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  35. package/packages/pi-coding-agent/dist/core/skills.js +6 -1
  36. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  37. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  38. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  39. package/packages/pi-coding-agent/dist/index.js +1 -1
  40. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  41. package/packages/pi-coding-agent/src/core/skills.ts +9 -1
  42. package/packages/pi-coding-agent/src/index.ts +1 -0
  43. package/src/resources/extensions/gsd/auto-prompts.ts +213 -4
  44. package/src/resources/extensions/gsd/doctor-providers.ts +4 -0
  45. package/src/resources/extensions/gsd/files.ts +46 -8
  46. package/src/resources/extensions/gsd/gitignore.ts +17 -3
  47. package/src/resources/extensions/gsd/guided-flow.ts +67 -6
  48. package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
  49. package/src/resources/extensions/gsd/health-widget.ts +3 -89
  50. package/src/resources/extensions/gsd/migrate-external.ts +18 -1
  51. package/src/resources/extensions/gsd/preferences.ts +20 -9
  52. package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
  53. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  54. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  55. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  56. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  57. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  58. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  59. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  60. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  61. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  62. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  63. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  64. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  65. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  66. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  67. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -1
  68. package/src/resources/extensions/gsd/state.ts +38 -20
  69. package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
  70. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
  71. package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
  72. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
  73. package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
  74. package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
  75. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
  76. package/src/resources/extensions/gsd/tests/run-uat.test.ts +5 -1
  77. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
  78. package/src/resources/extensions/gsd/types.ts +10 -0
  79. package/src/resources/extensions/remote-questions/status.ts +4 -2
  80. package/src/resources/extensions/remote-questions/store.ts +4 -2
  81. 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(content);
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 = content.split('\n');
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(content, 'Goal') || '';
325
- const demo = extractBoldField(content, 'Demo') || '';
362
+ const goal = extractBoldField(body, 'Goal') || '';
363
+ const demo = extractBoldField(body, 'Demo') || '';
326
364
 
327
- const mhSection = extractSection(content, 'Must-Haves');
365
+ const mhSection = extractSection(body, 'Must-Haves');
328
366
  const mustHaves = mhSection ? parseBullets(mhSection) : [];
329
367
 
330
- const tasksSection = extractSection(content, 'Tasks');
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(content, 'Files Likely Touched');
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
- return tracked.length > 0;
108
- } catch {
109
- // Not a git repo or git not available safe to proceed
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, milestoneTitle, secretsOutputPath, inlinedTemplates: planMilestoneTemplates,
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, sliceId, sliceTitle, inlinedTemplates: planSliceTemplates,
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, sliceId, sliceTitle, inlinedTemplates: researchTemplates,
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, milestoneId, sliceId, sliceTitle, inlinedTemplates: completeSliceTemplates,
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, sliceId,
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, sliceId, taskId, taskTitle, inlinedTemplates: executeTaskTemplates,
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, readdirSync } from "node:fs";
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
- const root = gsdRoot(basePath);
37
- if (!existsSync(root)) return "none";
25
+ if (!existsSync(gsdRoot(basePath))) return "none";
38
26
 
39
- // Lightweight milestone count avoids the full detectProjectState() scan
40
- // (CI markers, Makefile targets, etc.) that is unnecessary on the 60s refresh.
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 lines = [buildExecutionHeadline(data)];
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 environment = formatEnvironmentSummary(
114
- data.environmentErrorCount,
115
- data.environmentWarningCount,
116
- );
117
- if (environment) details.push(environment);
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
- const budget = formatBudgetSummary(data);
120
- if (budget) details.push(budget);
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.eta) details.push(data.eta);
66
+ if (data.providerIssue) {
67
+ parts.push(data.providerIssue);
68
+ }
123
69
 
124
- if (details.length > 0) {
125
- lines.push(` ${details.join("")}`);
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 lines;
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 loadBaseHealthWidgetData(basePath: string): HealthWidgetData {
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 = loadBaseHealthWidgetData(basePath);
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
- const baseData = loadBaseHealthWidgetData(basePath);
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
- // Remove .gsd.migrating only after symlink is verified
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
- const GLOBAL_PREFERENCES_PATH = join(gsdHome, "preferences.md");
87
- const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
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
- const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(gsdHome, "PREFERENCES.md");
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 GLOBAL_PREFERENCES_PATH;
110
+ return globalPreferencesPath();
100
111
  }
101
112
 
102
113
  export function getLegacyGlobalGSDPreferencesPath(): string {
103
- return LEGACY_GLOBAL_PREFERENCES_PATH;
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(GLOBAL_PREFERENCES_PATH, "global")
114
- ?? loadPreferencesFile(GLOBAL_PREFERENCES_PATH_UPPERCASE, "global")
115
- ?? loadPreferencesFile(LEGACY_GLOBAL_PREFERENCES_PATH, "global");
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 vars));
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(vars)) {
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. If a `GSD Skill Preferences` block is present in system context, use it to decide which skills to load and follow during completion, without relaxing required verification or artifact rules
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.