gsd-pi 2.35.0 → 2.36.0-dev.d612764

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 (194) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +7 -2
  3. package/dist/resource-loader.d.ts +1 -1
  4. package/dist/resource-loader.js +13 -1
  5. package/dist/resources/extensions/async-jobs/await-tool.js +0 -2
  6. package/dist/resources/extensions/async-jobs/job-manager.js +0 -6
  7. package/dist/resources/extensions/bg-shell/output-formatter.js +1 -19
  8. package/dist/resources/extensions/bg-shell/process-manager.js +0 -4
  9. package/dist/resources/extensions/bg-shell/types.js +0 -2
  10. package/dist/resources/extensions/cmux/index.js +321 -0
  11. package/dist/resources/extensions/context7/index.js +5 -0
  12. package/dist/resources/extensions/get-secrets-from-user.js +2 -30
  13. package/dist/resources/extensions/google-search/index.js +5 -0
  14. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  15. package/dist/resources/extensions/gsd/auto-dispatch.js +43 -1
  16. package/dist/resources/extensions/gsd/auto-loop.js +28 -3
  17. package/dist/resources/extensions/gsd/auto-model-selection.js +15 -3
  18. package/dist/resources/extensions/gsd/auto-recovery.js +35 -0
  19. package/dist/resources/extensions/gsd/auto-start.js +35 -2
  20. package/dist/resources/extensions/gsd/auto.js +75 -4
  21. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  22. package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
  23. package/dist/resources/extensions/gsd/commands-inspect.js +10 -3
  24. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  25. package/dist/resources/extensions/gsd/commands-rate.js +31 -0
  26. package/dist/resources/extensions/gsd/commands.js +94 -2
  27. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  28. package/dist/resources/extensions/gsd/doctor-environment.js +26 -17
  29. package/dist/resources/extensions/gsd/files.js +11 -2
  30. package/dist/resources/extensions/gsd/gitignore.js +54 -7
  31. package/dist/resources/extensions/gsd/guided-flow.js +8 -2
  32. package/dist/resources/extensions/gsd/health-widget-core.js +96 -0
  33. package/dist/resources/extensions/gsd/health-widget.js +97 -46
  34. package/dist/resources/extensions/gsd/index.js +31 -33
  35. package/dist/resources/extensions/gsd/migrate-external.js +55 -2
  36. package/dist/resources/extensions/gsd/milestone-ids.js +3 -2
  37. package/dist/resources/extensions/gsd/notifications.js +10 -1
  38. package/dist/resources/extensions/gsd/paths.js +74 -7
  39. package/dist/resources/extensions/gsd/post-unit-hooks.js +4 -1
  40. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  41. package/dist/resources/extensions/gsd/preferences-validation.js +45 -1
  42. package/dist/resources/extensions/gsd/preferences.js +15 -0
  43. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  44. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  45. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  46. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  47. package/dist/resources/extensions/gsd/roadmap-mutations.js +55 -0
  48. package/dist/resources/extensions/gsd/session-lock.js +53 -2
  49. package/dist/resources/extensions/gsd/state.js +2 -1
  50. package/dist/resources/extensions/gsd/templates/plan.md +8 -0
  51. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  52. package/dist/resources/extensions/gsd/worktree-resolver.js +12 -0
  53. package/dist/resources/extensions/remote-questions/remote-command.js +2 -22
  54. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  55. package/dist/resources/extensions/shared/mod.js +1 -1
  56. package/dist/resources/extensions/shared/sanitize.js +30 -0
  57. package/dist/resources/extensions/shared/terminal.js +5 -0
  58. package/dist/resources/extensions/subagent/index.js +186 -74
  59. package/dist/resources/skills/core-web-vitals/SKILL.md +1 -1
  60. package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
  61. package/dist/resources/skills/github-workflows/SKILL.md +0 -2
  62. package/dist/resources/skills/web-quality-audit/SKILL.md +0 -2
  63. package/package.json +2 -1
  64. package/packages/pi-agent-core/dist/agent.d.ts +10 -2
  65. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  66. package/packages/pi-agent-core/dist/agent.js +19 -8
  67. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  68. package/packages/pi-agent-core/src/agent.ts +31 -10
  69. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  70. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  71. package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
  72. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/agent-session.js +20 -4
  74. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/resource-loader.js +13 -2
  77. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  78. package/packages/pi-coding-agent/package.json +1 -1
  79. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -12
  80. package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
  81. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  82. package/packages/pi-tui/dist/terminal-image.js +4 -0
  83. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  84. package/packages/pi-tui/src/terminal-image.ts +5 -0
  85. package/pkg/package.json +1 -1
  86. package/src/resources/extensions/async-jobs/await-tool.ts +0 -2
  87. package/src/resources/extensions/async-jobs/job-manager.ts +0 -7
  88. package/src/resources/extensions/bg-shell/output-formatter.ts +0 -17
  89. package/src/resources/extensions/bg-shell/process-manager.ts +0 -4
  90. package/src/resources/extensions/bg-shell/types.ts +0 -12
  91. package/src/resources/extensions/cmux/index.ts +384 -0
  92. package/src/resources/extensions/context7/index.ts +7 -0
  93. package/src/resources/extensions/get-secrets-from-user.ts +2 -35
  94. package/src/resources/extensions/google-search/index.ts +7 -0
  95. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  96. package/src/resources/extensions/gsd/auto-dispatch.ts +49 -1
  97. package/src/resources/extensions/gsd/auto-loop.ts +64 -2
  98. package/src/resources/extensions/gsd/auto-model-selection.ts +23 -2
  99. package/src/resources/extensions/gsd/auto-recovery.ts +39 -0
  100. package/src/resources/extensions/gsd/auto-start.ts +42 -2
  101. package/src/resources/extensions/gsd/auto.ts +82 -3
  102. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  103. package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
  104. package/src/resources/extensions/gsd/commands-inspect.ts +10 -3
  105. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  106. package/src/resources/extensions/gsd/commands-rate.ts +55 -0
  107. package/src/resources/extensions/gsd/commands.ts +97 -2
  108. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  109. package/src/resources/extensions/gsd/doctor-environment.ts +26 -16
  110. package/src/resources/extensions/gsd/files.ts +12 -2
  111. package/src/resources/extensions/gsd/gitignore.ts +54 -7
  112. package/src/resources/extensions/gsd/guided-flow.ts +8 -2
  113. package/src/resources/extensions/gsd/health-widget-core.ts +129 -0
  114. package/src/resources/extensions/gsd/health-widget.ts +103 -59
  115. package/src/resources/extensions/gsd/index.ts +37 -32
  116. package/src/resources/extensions/gsd/migrate-external.ts +47 -2
  117. package/src/resources/extensions/gsd/milestone-ids.ts +3 -2
  118. package/src/resources/extensions/gsd/notifications.ts +10 -1
  119. package/src/resources/extensions/gsd/paths.ts +73 -7
  120. package/src/resources/extensions/gsd/post-unit-hooks.ts +5 -1
  121. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  122. package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
  123. package/src/resources/extensions/gsd/preferences.ts +18 -1
  124. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  125. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  126. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  127. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  128. package/src/resources/extensions/gsd/roadmap-mutations.ts +66 -0
  129. package/src/resources/extensions/gsd/session-lock.ts +59 -2
  130. package/src/resources/extensions/gsd/state.ts +2 -1
  131. package/src/resources/extensions/gsd/templates/plan.md +8 -0
  132. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  133. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  134. package/src/resources/extensions/gsd/tests/cmux.test.ts +98 -0
  135. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +46 -0
  136. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +20 -0
  137. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +214 -0
  138. package/src/resources/extensions/gsd/tests/health-widget.test.ts +158 -0
  139. package/src/resources/extensions/gsd/tests/paths.test.ts +113 -0
  140. package/src/resources/extensions/gsd/tests/preferences.test.ts +35 -2
  141. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +26 -0
  142. package/src/resources/extensions/gsd/tests/test-utils.ts +165 -0
  143. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +15 -0
  144. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +7 -0
  145. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +32 -0
  146. package/src/resources/extensions/gsd/worktree-resolver.ts +11 -0
  147. package/src/resources/extensions/remote-questions/remote-command.ts +2 -23
  148. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  149. package/src/resources/extensions/shared/mod.ts +1 -1
  150. package/src/resources/extensions/shared/sanitize.ts +36 -0
  151. package/src/resources/extensions/shared/terminal.ts +5 -0
  152. package/src/resources/extensions/subagent/index.ts +242 -91
  153. package/src/resources/skills/core-web-vitals/SKILL.md +1 -1
  154. package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
  155. package/src/resources/skills/github-workflows/SKILL.md +0 -2
  156. package/src/resources/skills/web-quality-audit/SKILL.md +0 -2
  157. package/dist/resources/extensions/shared/wizard-ui.js +0 -478
  158. package/dist/resources/skills/swiftui/SKILL.md +0 -208
  159. package/dist/resources/skills/swiftui/references/animations.md +0 -921
  160. package/dist/resources/skills/swiftui/references/architecture.md +0 -1561
  161. package/dist/resources/skills/swiftui/references/layout-system.md +0 -1186
  162. package/dist/resources/skills/swiftui/references/navigation.md +0 -1492
  163. package/dist/resources/skills/swiftui/references/networking-async.md +0 -214
  164. package/dist/resources/skills/swiftui/references/performance.md +0 -1706
  165. package/dist/resources/skills/swiftui/references/platform-integration.md +0 -204
  166. package/dist/resources/skills/swiftui/references/state-management.md +0 -1443
  167. package/dist/resources/skills/swiftui/references/swiftdata.md +0 -297
  168. package/dist/resources/skills/swiftui/references/testing-debugging.md +0 -247
  169. package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
  170. package/dist/resources/skills/swiftui/workflows/add-feature.md +0 -191
  171. package/dist/resources/skills/swiftui/workflows/build-new-app.md +0 -311
  172. package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
  173. package/dist/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
  174. package/dist/resources/skills/swiftui/workflows/ship-app.md +0 -203
  175. package/dist/resources/skills/swiftui/workflows/write-tests.md +0 -235
  176. package/src/resources/extensions/shared/wizard-ui.ts +0 -551
  177. package/src/resources/skills/swiftui/SKILL.md +0 -208
  178. package/src/resources/skills/swiftui/references/animations.md +0 -921
  179. package/src/resources/skills/swiftui/references/architecture.md +0 -1561
  180. package/src/resources/skills/swiftui/references/layout-system.md +0 -1186
  181. package/src/resources/skills/swiftui/references/navigation.md +0 -1492
  182. package/src/resources/skills/swiftui/references/networking-async.md +0 -214
  183. package/src/resources/skills/swiftui/references/performance.md +0 -1706
  184. package/src/resources/skills/swiftui/references/platform-integration.md +0 -204
  185. package/src/resources/skills/swiftui/references/state-management.md +0 -1443
  186. package/src/resources/skills/swiftui/references/swiftdata.md +0 -297
  187. package/src/resources/skills/swiftui/references/testing-debugging.md +0 -247
  188. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
  189. package/src/resources/skills/swiftui/workflows/add-feature.md +0 -191
  190. package/src/resources/skills/swiftui/workflows/build-new-app.md +0 -311
  191. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
  192. package/src/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
  193. package/src/resources/skills/swiftui/workflows/ship-app.md +0 -203
  194. package/src/resources/skills/swiftui/workflows/write-tests.md +0 -235
@@ -23,6 +23,7 @@ import {
23
23
  } from "./paths.js";
24
24
  import { join } from "node:path";
25
25
  import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
26
+ import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
26
27
  import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
27
28
  import { ensureGitignore, ensurePreferences, untrackRuntimeFiles } from "./gitignore.js";
28
29
  import { loadEffectiveGSDPreferences } from "./preferences.js";
@@ -191,7 +192,7 @@ type UIContext = ExtensionContext;
191
192
  * This is the only way the wizard triggers work — everything else is the LLM's job.
192
193
  */
193
194
  function dispatchWorkflow(pi: ExtensionAPI, note: string, customType = "gsd-run"): void {
194
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
195
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
195
196
  const workflow = readFileSync(workflowPath, "utf-8");
196
197
 
197
198
  pi.sendMessage(
@@ -516,8 +517,13 @@ export async function showDiscuss(
516
517
  // If all pending slices are discussed, notify and exit instead of looping
517
518
  const allDiscussed = pendingSlices.every(s => discussedMap.get(s.id));
518
519
  if (allDiscussed) {
520
+ const lockData = readSessionLockData(basePath);
521
+ const remoteAutoRunning = lockData && lockData.pid !== process.pid && isSessionLockProcessAlive(lockData);
522
+ const nextStep = remoteAutoRunning
523
+ ? "Auto-mode is already running — use /gsd status to check progress."
524
+ : "Run /gsd to start planning.";
519
525
  ctx.ui.notify(
520
- `All ${pendingSlices.length} slices discussed. Run /gsd to start planning.`,
526
+ `All ${pendingSlices.length} slices discussed. ${nextStep}`,
521
527
  "info",
522
528
  );
523
529
  return;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Pure GSD health widget logic.
3
+ *
4
+ * Separates project-state detection and line rendering from the widget's
5
+ * runtime integrations so the regressions can be tested directly.
6
+ */
7
+
8
+ import { existsSync, readdirSync } from "node:fs";
9
+ import { gsdRoot } from "./paths.js";
10
+ import { join } from "node:path";
11
+ import type { GSDState, Phase } from "./types.js";
12
+
13
+ export type HealthWidgetProjectState = "none" | "initialized" | "active";
14
+
15
+ export interface HealthWidgetData {
16
+ projectState: HealthWidgetProjectState;
17
+ budgetCeiling: number | undefined;
18
+ budgetSpent: number;
19
+ providerIssue: string | null;
20
+ environmentErrorCount: number;
21
+ environmentWarningCount: number;
22
+ 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
+ }
34
+
35
+ export function detectHealthWidgetProjectState(basePath: string): HealthWidgetProjectState {
36
+ const root = gsdRoot(basePath);
37
+ if (!existsSync(root)) return "none";
38
+
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";
50
+ }
51
+
52
+ function formatCost(n: number): string {
53
+ return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
54
+ }
55
+
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
+ /**
93
+ * Build compact health lines for the widget.
94
+ * Returns a string array suitable for setWidget().
95
+ */
96
+ export function buildHealthLines(data: HealthWidgetData): string[] {
97
+ if (data.projectState === "none") {
98
+ return [" GSD No project loaded — run /gsd to start"];
99
+ }
100
+
101
+ if (data.projectState === "initialized") {
102
+ return [" GSD Project initialized — run /gsd to continue setup"];
103
+ }
104
+
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);
112
+
113
+ const environment = formatEnvironmentSummary(
114
+ data.environmentErrorCount,
115
+ data.environmentWarningCount,
116
+ );
117
+ if (environment) details.push(environment);
118
+
119
+ const budget = formatBudgetSummary(data);
120
+ if (budget) details.push(budget);
121
+
122
+ if (data.eta) details.push(data.eta);
123
+
124
+ if (details.length > 0) {
125
+ lines.push(` ${details.join(" │ ")}`);
126
+ }
127
+
128
+ return lines;
129
+ }
@@ -9,41 +9,37 @@
9
9
  */
10
10
 
11
11
  import type { ExtensionContext } from "@gsd/pi-coding-agent";
12
+ import type { GSDState } from "./types.js";
12
13
  import { runProviderChecks, summariseProviderIssues } from "./doctor-providers.js";
13
14
  import { runEnvironmentChecks } from "./doctor-environment.js";
14
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
15
16
  import { loadLedgerFromDisk, getProjectTotals } from "./metrics.js";
17
+ import { describeNextUnit, estimateTimeRemaining, updateSliceProgressCache } from "./auto-dashboard.js";
16
18
  import { projectRoot } from "./commands.js";
17
-
18
- // ── Types ──────────────────────────────────────────────────────────────────────
19
-
20
- interface HealthWidgetData {
21
- hasProject: boolean;
22
- budgetCeiling: number | undefined;
23
- budgetSpent: number;
24
- providerIssue: string | null; // compact summary from summariseProviderIssues()
25
- environmentErrorCount: number;
26
- environmentWarningCount: number;
27
- lastRefreshed: number;
28
- }
19
+ import { deriveState, invalidateStateCache } from "./state.js";
20
+ import {
21
+ buildHealthLines,
22
+ detectHealthWidgetProjectState,
23
+ type HealthWidgetData,
24
+ } from "./health-widget-core.js";
29
25
 
30
26
  // ── Data loader ────────────────────────────────────────────────────────────────
31
27
 
32
- function loadHealthWidgetData(basePath: string): HealthWidgetData {
33
- let hasProject = false;
28
+ function loadBaseHealthWidgetData(basePath: string): HealthWidgetData {
34
29
  let budgetCeiling: number | undefined;
35
30
  let budgetSpent = 0;
36
31
  let providerIssue: string | null = null;
37
32
  let environmentErrorCount = 0;
38
33
  let environmentWarningCount = 0;
39
34
 
35
+ const projectState = detectHealthWidgetProjectState(basePath);
36
+
40
37
  try {
41
38
  const prefs = loadEffectiveGSDPreferences();
42
39
  budgetCeiling = prefs?.preferences?.budget_ceiling;
43
40
 
44
41
  const ledger = loadLedgerFromDisk(basePath);
45
42
  if (ledger) {
46
- hasProject = true;
47
43
  const totals = getProjectTotals(ledger.units ?? []);
48
44
  budgetSpent = totals.cost;
49
45
  }
@@ -63,7 +59,7 @@ function loadHealthWidgetData(basePath: string): HealthWidgetData {
63
59
  } catch { /* non-fatal */ }
64
60
 
65
61
  return {
66
- hasProject,
62
+ projectState,
67
63
  budgetCeiling,
68
64
  budgetSpent,
69
65
  providerIssue,
@@ -73,54 +69,88 @@ function loadHealthWidgetData(basePath: string): HealthWidgetData {
73
69
  };
74
70
  }
75
71
 
76
- // ── Rendering ──────────────────────────────────────────────────────────────────
77
-
78
- function formatCost(n: number): string {
79
- return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
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()}…`;
80
76
  }
81
77
 
82
- /**
83
- * Build compact health lines for the widget.
84
- * Returns a string array suitable for setWidget().
85
- */
86
- export function buildHealthLines(data: HealthWidgetData): string[] {
87
- if (!data.hasProject) {
88
- return [" GSD No project loaded — run /gsd to start"];
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";
89
92
  }
93
+ }
90
94
 
91
- const parts: string[] = [];
92
-
93
- // System status signal
94
- const totalIssues = data.environmentErrorCount + data.environmentWarningCount + (data.providerIssue ? 1 : 0);
95
- if (totalIssues === 0) {
96
- parts.push(" System OK");
97
- } else if (data.environmentErrorCount > 0 || data.providerIssue?.includes("")) {
98
- parts.push(`✗ ${totalIssues} issue${totalIssues > 1 ? "s" : ""}`);
99
- } else {
100
- parts.push(`⚠ ${totalIssues} warning${totalIssues > 1 ? "s" : ""}`);
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);
101
121
  }
122
+ }
102
123
 
103
- // Budget
104
- if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
105
- const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
106
- parts.push(`Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`);
107
- } else if (data.budgetSpent > 0) {
108
- parts.push(`Spent: ${formatCost(data.budgetSpent)}`);
109
- }
124
+ async function enrichHealthWidgetData(basePath: string, baseData: HealthWidgetData): Promise<HealthWidgetData> {
125
+ if (baseData.projectState !== "active") return baseData;
110
126
 
111
- // Provider issue (if any)
112
- if (data.providerIssue) {
113
- parts.push(data.providerIssue);
114
- }
127
+ try {
128
+ invalidateStateCache();
129
+ const state = await deriveState(basePath);
115
130
 
116
- // Environment issues
117
- if (data.environmentErrorCount > 0) {
118
- parts.push(`Env: ${data.environmentErrorCount} error${data.environmentErrorCount > 1 ? "s" : ""}`);
119
- } else if (data.environmentWarningCount > 0) {
120
- parts.push(`Env: ${data.environmentWarningCount} warning${data.environmentWarningCount > 1 ? "s" : ""}`);
121
- }
131
+ if (state.activeMilestone) {
132
+ // Warm the slice-progress cache so estimateTimeRemaining() has data
133
+ updateSliceProgressCache(basePath, state.activeMilestone.id, state.activeSlice?.id);
134
+ }
122
135
 
123
- return [` ${parts.join(" │ ")}`];
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
+ }
124
154
  }
125
155
 
126
156
  // ── Widget init ────────────────────────────────────────────────────────────────
@@ -137,20 +167,34 @@ export function initHealthWidget(ctx: ExtensionContext): void {
137
167
  const basePath = projectRoot();
138
168
 
139
169
  // String-array fallback — used in RPC mode (factory is a no-op there)
140
- const initialData = loadHealthWidgetData(basePath);
170
+ const initialData = loadBaseHealthWidgetData(basePath);
141
171
  ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
142
172
 
143
173
  // Factory-based widget for TUI mode — replaces the string-array above
144
174
  ctx.ui.setWidget("gsd-health", (_tui, _theme) => {
145
175
  let data = initialData;
146
176
  let cachedLines: string[] | undefined;
177
+ let refreshInFlight = false;
147
178
 
148
- const refreshTimer = setInterval(() => {
179
+ const refresh = async () => {
180
+ if (refreshInFlight) return;
181
+ refreshInFlight = true;
149
182
  try {
150
- data = loadHealthWidgetData(basePath);
183
+ const baseData = loadBaseHealthWidgetData(basePath);
184
+ data = await enrichHealthWidgetData(basePath, baseData);
151
185
  cachedLines = undefined;
152
186
  _tui.requestRender();
153
- } catch { /* non-fatal */ }
187
+ } catch { /* non-fatal */ } finally {
188
+ refreshInFlight = false;
189
+ }
190
+ };
191
+
192
+ // Fire first enrichment immediately. requestRender() inside is a no-op
193
+ // if the widget has not yet rendered, so this is safe before factory return.
194
+ void refresh();
195
+
196
+ const refreshTimer = setInterval(() => {
197
+ void refresh();
154
198
  }, REFRESH_INTERVAL_MS);
155
199
 
156
200
  return {
@@ -65,33 +65,26 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
65
65
  import { toPosixPath } from "../shared/mod.js";
66
66
  import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
67
67
  import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
68
+ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js";
68
69
 
69
- // ── Agent Instructions ────────────────────────────────────────────────────
70
- // Lightweight "always follow" files injected into every GSD agent session.
71
- // Global: ~/.gsd/agent-instructions.md Project: .gsd/agent-instructions.md
72
- // Both are loaded and concatenated (global first, project appends).
70
+ // ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
71
+ // agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
72
+ // Pi core natively supports AGENTS.md (with CLAUDE.md fallback) per directory.
73
73
 
74
- function loadAgentInstructions(): string | null {
75
- const parts: string[] = [];
76
-
77
- const globalPath = join(homedir(), ".gsd", "agent-instructions.md");
78
- if (existsSync(globalPath)) {
79
- try {
80
- const content = readFileSync(globalPath, "utf-8").trim();
81
- if (content) parts.push(content);
82
- } catch { /* non-fatal skip unreadable file */ }
83
- }
84
-
85
- const projectPath = join(process.cwd(), ".gsd", "agent-instructions.md");
86
- if (existsSync(projectPath)) {
87
- try {
88
- const content = readFileSync(projectPath, "utf-8").trim();
89
- if (content) parts.push(content);
90
- } catch { /* non-fatal — skip unreadable file */ }
74
+ function warnDeprecatedAgentInstructions(): void {
75
+ const paths = [
76
+ join(homedir(), ".gsd", "agent-instructions.md"),
77
+ join(process.cwd(), ".gsd", "agent-instructions.md"),
78
+ ];
79
+ for (const p of paths) {
80
+ if (existsSync(p)) {
81
+ console.warn(
82
+ `[GSD] DEPRECATED: ${p} is no longer loaded. ` +
83
+ `Migrate your instructions to AGENTS.md (or CLAUDE.md) in the same directory. ` +
84
+ `See https://github.com/gsd-build/GSD-2/issues/1492`,
85
+ );
86
+ }
91
87
  }
92
-
93
- if (parts.length === 0) return null;
94
- return parts.join("\n\n");
95
88
  }
96
89
 
97
90
  // ── Depth verification state ──────────────────────────────────────────────
@@ -175,7 +168,16 @@ export default function (pi: ExtensionAPI) {
175
168
  // Pipe closed — nothing we can write; just exit cleanly
176
169
  process.exit(0);
177
170
  }
178
- // Re-throw anything that isn't EPIPE so real crashes still surface
171
+ if ((err as NodeJS.ErrnoException).code === "ENOENT" &&
172
+ (err as any).syscall?.startsWith("spawn")) {
173
+ // spawn ENOENT — command not found (e.g., npx on Windows).
174
+ // This surfaces as an uncaught exception from child_process but
175
+ // is not a fatal process error. Log and continue instead of
176
+ // crashing auto-mode (#1384).
177
+ process.stderr.write(`[gsd] spawn ENOENT: ${(err as any).path ?? "unknown"} — command not found\n`);
178
+ return;
179
+ }
180
+ // Re-throw anything that isn't EPIPE/ENOENT so real crashes still surface
179
181
  throw err;
180
182
  };
181
183
  process.on("uncaughtException", _gsdEpipeGuard);
@@ -622,6 +624,13 @@ export default function (pi: ExtensionAPI) {
622
624
  const stopContextTimer = debugTime("context-inject");
623
625
  const systemContent = loadPrompt("system");
624
626
  const loadedPreferences = loadEffectiveGSDPreferences();
627
+ if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
628
+ markCmuxPromptShown();
629
+ ctx.ui.notify(
630
+ "cmux detected. Run /gsd cmux on to enable sidebar metadata, notifications, and visual subagent splits for this project.",
631
+ "info",
632
+ );
633
+ }
625
634
  let preferenceBlock = "";
626
635
  if (loadedPreferences) {
627
636
  const cwd = process.cwd();
@@ -673,12 +682,8 @@ export default function (pi: ExtensionAPI) {
673
682
  }
674
683
  }
675
684
 
676
- // Load agent instructions (global + project)
677
- let agentInstructionsBlock = "";
678
- const agentInstructions = loadAgentInstructions();
679
- if (agentInstructions) {
680
- agentInstructionsBlock = `\n\n## Agent Instructions\n\nThe following instructions were provided by the user and must be followed in every session:\n\n${agentInstructions}`;
681
- }
685
+ // Warn if deprecated agent-instructions.md files are still present
686
+ warnDeprecatedAgentInstructions();
682
687
 
683
688
  const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
684
689
 
@@ -723,7 +728,7 @@ export default function (pi: ExtensionAPI) {
723
728
  ].join("\n");
724
729
  }
725
730
 
726
- const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${agentInstructionsBlock}${knowledgeBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
731
+ const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
727
732
  stopContextTimer({
728
733
  systemPromptSize: fullSystem.length,
729
734
  injectionSize: injection?.length ?? 0,
@@ -6,10 +6,11 @@
6
6
  * symlink replaces the original directory so all paths remain valid.
7
7
  */
8
8
 
9
- import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
9
+ import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { externalGsdRoot } from "./repo-identity.js";
12
12
  import { getErrorMessage } from "./error-utils.js";
13
+ import { hasGitTrackedGsdFiles } from "./gitignore.js";
13
14
 
14
15
  export interface MigrationResult {
15
16
  migrated: boolean;
@@ -51,6 +52,28 @@ export function migrateToExternalState(basePath: string): MigrationResult {
51
52
  return { migrated: false, error: `Cannot stat .gsd: ${getErrorMessage(err)}` };
52
53
  }
53
54
 
55
+ // Skip if .gsd/ contains git-tracked files — the project intentionally
56
+ // keeps .gsd/ in version control and migration would destroy that.
57
+ if (hasGitTrackedGsdFiles(basePath)) {
58
+ return { migrated: false };
59
+ }
60
+
61
+ // Skip if .gsd/worktrees/ has active worktree directories (#1337).
62
+ // On Windows, active git worktrees hold OS-level directory handles that
63
+ // prevent rename/delete. Attempting migration causes EBUSY and data loss.
64
+ const worktreesDir = join(localGsd, "worktrees");
65
+ if (existsSync(worktreesDir)) {
66
+ try {
67
+ const entries = readdirSync(worktreesDir, { withFileTypes: true });
68
+ if (entries.some(e => e.isDirectory())) {
69
+ return { migrated: false };
70
+ }
71
+ } catch {
72
+ // Can't read worktrees dir — skip migration to be safe
73
+ return { migrated: false };
74
+ }
75
+ }
76
+
54
77
  const externalPath = externalGsdRoot(basePath);
55
78
  const migratingPath = join(basePath, ".gsd.migrating");
56
79
 
@@ -99,7 +122,29 @@ export function migrateToExternalState(basePath: string): MigrationResult {
99
122
  // Create symlink .gsd -> external path
100
123
  symlinkSync(externalPath, localGsd, "junction");
101
124
 
102
- // Remove .gsd.migrating
125
+ // Verify the symlink resolves correctly before removing the backup (#1377).
126
+ // On Windows, junction creation can silently succeed but resolve to the wrong
127
+ // target, or the external dir may not be accessible. If verification fails,
128
+ // restore from the backup.
129
+ try {
130
+ const resolved = realpathSync(localGsd);
131
+ const resolvedExternal = realpathSync(externalPath);
132
+ if (resolved !== resolvedExternal) {
133
+ // Symlink points to wrong target — restore backup
134
+ try { rmSync(localGsd, { force: true }); } catch { /* may not exist */ }
135
+ renameSync(migratingPath, localGsd);
136
+ return { migrated: false, error: `Migration verification failed: symlink resolves to ${resolved}, expected ${resolvedExternal}` };
137
+ }
138
+ // Verify we can read through the symlink
139
+ readdirSync(localGsd);
140
+ } catch (verifyErr) {
141
+ // Symlink broken or unreadable — restore backup
142
+ try { rmSync(localGsd, { force: true }); } catch { /* may not exist */ }
143
+ try { renameSync(migratingPath, localGsd); } catch { /* best-effort restore */ }
144
+ return { migrated: false, error: `Migration verification failed: ${getErrorMessage(verifyErr)}` };
145
+ }
146
+
147
+ // Remove .gsd.migrating only after symlink is verified
103
148
  rmSync(migratingPath, { recursive: true, force: true });
104
149
 
105
150
  return { migrated: true };
@@ -80,8 +80,9 @@ export function findMilestoneIds(basePath: string): string[] {
80
80
  .filter((d) => d.isDirectory())
81
81
  .map((d) => {
82
82
  const match = d.name.match(/^(M\d+(?:-[a-z0-9]{6})?)/);
83
- return match ? match[1] : d.name;
84
- });
83
+ return match ? match[1] : null;
84
+ })
85
+ .filter((id): id is string => id !== null);
85
86
 
86
87
  // Apply custom queue order if available, else fall back to numeric sort
87
88
  const customOrder = loadQueueOrder(basePath);
@@ -4,6 +4,7 @@
4
4
  import { execFileSync } from "node:child_process";
5
5
  import type { NotificationPreferences } from "./types.js";
6
6
  import { loadEffectiveGSDPreferences } from "./preferences.js";
7
+ import { CmuxClient, emitOsc777Notification, resolveCmuxConfig } from "../cmux/index.js";
7
8
 
8
9
  export type NotifyLevel = "info" | "success" | "warning" | "error";
9
10
  export type NotificationKind = "complete" | "error" | "budget" | "milestone" | "attention";
@@ -23,7 +24,15 @@ export function sendDesktopNotification(
23
24
  level: NotifyLevel = "info",
24
25
  kind: NotificationKind = "complete",
25
26
  ): void {
26
- if (!shouldSendDesktopNotification(kind)) return;
27
+ const loaded = loadEffectiveGSDPreferences()?.preferences;
28
+ if (!shouldSendDesktopNotification(kind, loaded?.notifications)) return;
29
+
30
+ const cmux = resolveCmuxConfig(loaded);
31
+ if (cmux.notifications) {
32
+ const delivered = CmuxClient.fromPreferences(loaded).notify(title, message);
33
+ if (delivered) return;
34
+ emitOsc777Notification(title, message);
35
+ }
27
36
 
28
37
  try {
29
38
  const command = buildDesktopNotificationCommand(process.platform, title, message, level);