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
@@ -11,21 +11,23 @@ import { runProviderChecks, summariseProviderIssues } from "./doctor-providers.j
11
11
  import { runEnvironmentChecks } from "./doctor-environment.js";
12
12
  import { loadEffectiveGSDPreferences } from "./preferences.js";
13
13
  import { loadLedgerFromDisk, getProjectTotals } from "./metrics.js";
14
+ import { describeNextUnit, estimateTimeRemaining, updateSliceProgressCache } from "./auto-dashboard.js";
14
15
  import { projectRoot } from "./commands.js";
16
+ import { deriveState, invalidateStateCache } from "./state.js";
17
+ import { buildHealthLines, detectHealthWidgetProjectState, } from "./health-widget-core.js";
15
18
  // ── Data loader ────────────────────────────────────────────────────────────────
16
- function loadHealthWidgetData(basePath) {
17
- let hasProject = false;
19
+ function loadBaseHealthWidgetData(basePath) {
18
20
  let budgetCeiling;
19
21
  let budgetSpent = 0;
20
22
  let providerIssue = null;
21
23
  let environmentErrorCount = 0;
22
24
  let environmentWarningCount = 0;
25
+ const projectState = detectHealthWidgetProjectState(basePath);
23
26
  try {
24
27
  const prefs = loadEffectiveGSDPreferences();
25
28
  budgetCeiling = prefs?.preferences?.budget_ceiling;
26
29
  const ledger = loadLedgerFromDisk(basePath);
27
30
  if (ledger) {
28
- hasProject = true;
29
31
  const totals = getProjectTotals(ledger.units ?? []);
30
32
  budgetSpent = totals.cost;
31
33
  }
@@ -47,7 +49,7 @@ function loadHealthWidgetData(basePath) {
47
49
  }
48
50
  catch { /* non-fatal */ }
49
51
  return {
50
- hasProject,
52
+ projectState,
51
53
  budgetCeiling,
52
54
  budgetSpent,
53
55
  providerIssue,
@@ -56,50 +58,85 @@ function loadHealthWidgetData(basePath) {
56
58
  lastRefreshed: Date.now(),
57
59
  };
58
60
  }
59
- // ── Rendering ──────────────────────────────────────────────────────────────────
60
- function formatCost(n) {
61
- return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
61
+ function compactText(text, max = 64) {
62
+ const trimmed = text.replace(/\s+/g, " ").trim();
63
+ if (trimmed.length <= max)
64
+ return trimmed;
65
+ return `${trimmed.slice(0, max - 1).trimEnd()}…`;
62
66
  }
63
- /**
64
- * Build compact health lines for the widget.
65
- * Returns a string array suitable for setWidget().
66
- */
67
- export function buildHealthLines(data) {
68
- if (!data.hasProject) {
69
- return [" GSD No project loaded — run /gsd to start"];
70
- }
71
- const parts = [];
72
- // System status signal
73
- const totalIssues = data.environmentErrorCount + data.environmentWarningCount + (data.providerIssue ? 1 : 0);
74
- if (totalIssues === 0) {
75
- parts.push(" System OK");
76
- }
77
- else if (data.environmentErrorCount > 0 || data.providerIssue?.includes("✗")) {
78
- parts.push(`✗ ${totalIssues} issue${totalIssues > 1 ? "s" : ""}`);
79
- }
80
- else {
81
- parts.push(`⚠ ${totalIssues} warning${totalIssues > 1 ? "s" : ""}`);
67
+ function summarizeExecutionStatus(state) {
68
+ switch (state.phase) {
69
+ case "blocked": return "Blocked";
70
+ case "paused": return "Paused";
71
+ case "complete": return "Complete";
72
+ case "executing": return "Executing";
73
+ case "planning": return "Planning";
74
+ case "pre-planning": return "Pre-planning";
75
+ case "summarizing": return "Summarizing";
76
+ case "validating-milestone": return "Validating";
77
+ case "completing-milestone": return "Completing";
78
+ case "needs-discussion": return "Needs discussion";
79
+ case "replanning-slice": return "Replanning";
80
+ default: return "Active";
82
81
  }
83
- // Budget
84
- if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
85
- const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
86
- parts.push(`Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`);
87
- }
88
- else if (data.budgetSpent > 0) {
89
- parts.push(`Spent: ${formatCost(data.budgetSpent)}`);
90
- }
91
- // Provider issue (if any)
92
- if (data.providerIssue) {
93
- parts.push(data.providerIssue);
82
+ }
83
+ function summarizeExecutionTarget(state) {
84
+ switch (state.phase) {
85
+ case "needs-discussion":
86
+ return state.activeMilestone ? `Discuss ${state.activeMilestone.id}` : "Discuss milestone draft";
87
+ case "pre-planning":
88
+ return state.activeMilestone ? `Plan ${state.activeMilestone.id}` : "Research & plan milestone";
89
+ case "planning":
90
+ return state.activeSlice ? `Plan ${state.activeSlice.id}` : "Plan next slice";
91
+ case "executing":
92
+ return state.activeTask ? `Execute ${state.activeTask.id}` : "Execute next task";
93
+ case "summarizing":
94
+ return state.activeSlice ? `Complete ${state.activeSlice.id}` : "Complete current slice";
95
+ case "validating-milestone":
96
+ return state.activeMilestone ? `Validate ${state.activeMilestone.id}` : "Validate milestone";
97
+ case "completing-milestone":
98
+ return state.activeMilestone ? `Complete ${state.activeMilestone.id}` : "Complete milestone";
99
+ case "replanning-slice":
100
+ return state.activeSlice ? `Replan ${state.activeSlice.id}` : "Replan current slice";
101
+ case "blocked":
102
+ return `waiting on ${compactText(state.blockers[0] ?? state.nextAction, 56)}`;
103
+ case "paused":
104
+ return compactText(state.nextAction || "waiting to resume", 56);
105
+ case "complete":
106
+ return "All milestones complete";
107
+ default:
108
+ return compactText(describeNextUnit(state).label, 56);
94
109
  }
95
- // Environment issues
96
- if (data.environmentErrorCount > 0) {
97
- parts.push(`Env: ${data.environmentErrorCount} error${data.environmentErrorCount > 1 ? "s" : ""}`);
110
+ }
111
+ async function enrichHealthWidgetData(basePath, baseData) {
112
+ if (baseData.projectState !== "active")
113
+ return baseData;
114
+ try {
115
+ invalidateStateCache();
116
+ const state = await deriveState(basePath);
117
+ if (state.activeMilestone) {
118
+ // Warm the slice-progress cache so estimateTimeRemaining() has data
119
+ updateSliceProgressCache(basePath, state.activeMilestone.id, state.activeSlice?.id);
120
+ }
121
+ return {
122
+ ...baseData,
123
+ executionPhase: state.phase,
124
+ executionStatus: summarizeExecutionStatus(state),
125
+ executionTarget: summarizeExecutionTarget(state),
126
+ nextAction: state.nextAction,
127
+ blocker: state.blockers[0] ?? null,
128
+ activeMilestoneId: state.activeMilestone?.id,
129
+ activeSliceId: state.activeSlice?.id,
130
+ activeTaskId: state.activeTask?.id,
131
+ progress: state.progress,
132
+ eta: state.phase === "blocked" || state.phase === "paused" || state.phase === "complete"
133
+ ? null
134
+ : estimateTimeRemaining(),
135
+ };
98
136
  }
99
- else if (data.environmentWarningCount > 0) {
100
- parts.push(`Env: ${data.environmentWarningCount} warning${data.environmentWarningCount > 1 ? "s" : ""}`);
137
+ catch {
138
+ return baseData;
101
139
  }
102
- return [` ${parts.join(" │ ")}`];
103
140
  }
104
141
  // ── Widget init ────────────────────────────────────────────────────────────────
105
142
  const REFRESH_INTERVAL_MS = 60_000;
@@ -112,19 +149,33 @@ export function initHealthWidget(ctx) {
112
149
  return;
113
150
  const basePath = projectRoot();
114
151
  // String-array fallback — used in RPC mode (factory is a no-op there)
115
- const initialData = loadHealthWidgetData(basePath);
152
+ const initialData = loadBaseHealthWidgetData(basePath);
116
153
  ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
117
154
  // Factory-based widget for TUI mode — replaces the string-array above
118
155
  ctx.ui.setWidget("gsd-health", (_tui, _theme) => {
119
156
  let data = initialData;
120
157
  let cachedLines;
121
- const refreshTimer = setInterval(() => {
158
+ let refreshInFlight = false;
159
+ const refresh = async () => {
160
+ if (refreshInFlight)
161
+ return;
162
+ refreshInFlight = true;
122
163
  try {
123
- data = loadHealthWidgetData(basePath);
164
+ const baseData = loadBaseHealthWidgetData(basePath);
165
+ data = await enrichHealthWidgetData(basePath, baseData);
124
166
  cachedLines = undefined;
125
167
  _tui.requestRender();
126
168
  }
127
169
  catch { /* non-fatal */ }
170
+ finally {
171
+ refreshInFlight = false;
172
+ }
173
+ };
174
+ // Fire first enrichment immediately. requestRender() inside is a no-op
175
+ // if the widget has not yet rendered, so this is safe before factory return.
176
+ void refresh();
177
+ const refreshTimer = setInterval(() => {
178
+ void refresh();
128
179
  }, REFRESH_INTERVAL_MS);
129
180
  return {
130
181
  render(_width) {
@@ -46,33 +46,22 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
46
46
  import { toPosixPath } from "../shared/mod.js";
47
47
  import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
48
48
  import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
49
- // ── Agent Instructions ────────────────────────────────────────────────────
50
- // Lightweight "always follow" files injected into every GSD agent session.
51
- // Global: ~/.gsd/agent-instructions.md Project: .gsd/agent-instructions.md
52
- // Both are loaded and concatenated (global first, project appends).
53
- function loadAgentInstructions() {
54
- const parts = [];
55
- const globalPath = join(homedir(), ".gsd", "agent-instructions.md");
56
- if (existsSync(globalPath)) {
57
- try {
58
- const content = readFileSync(globalPath, "utf-8").trim();
59
- if (content)
60
- parts.push(content);
61
- }
62
- catch { /* non-fatal — skip unreadable file */ }
63
- }
64
- const projectPath = join(process.cwd(), ".gsd", "agent-instructions.md");
65
- if (existsSync(projectPath)) {
66
- try {
67
- const content = readFileSync(projectPath, "utf-8").trim();
68
- if (content)
69
- parts.push(content);
49
+ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js";
50
+ // ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
51
+ // agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
52
+ // Pi core natively supports AGENTS.md (with CLAUDE.md fallback) per directory.
53
+ function warnDeprecatedAgentInstructions() {
54
+ const paths = [
55
+ join(homedir(), ".gsd", "agent-instructions.md"),
56
+ join(process.cwd(), ".gsd", "agent-instructions.md"),
57
+ ];
58
+ for (const p of paths) {
59
+ if (existsSync(p)) {
60
+ console.warn(`[GSD] DEPRECATED: ${p} is no longer loaded. ` +
61
+ `Migrate your instructions to AGENTS.md (or CLAUDE.md) in the same directory. ` +
62
+ `See https://github.com/gsd-build/GSD-2/issues/1492`);
70
63
  }
71
- catch { /* non-fatal — skip unreadable file */ }
72
64
  }
73
- if (parts.length === 0)
74
- return null;
75
- return parts.join("\n\n");
76
65
  }
77
66
  // ── Depth verification state ──────────────────────────────────────────────
78
67
  let depthVerificationDone = false;
@@ -140,7 +129,16 @@ export default function (pi) {
140
129
  // Pipe closed — nothing we can write; just exit cleanly
141
130
  process.exit(0);
142
131
  }
143
- // Re-throw anything that isn't EPIPE so real crashes still surface
132
+ if (err.code === "ENOENT" &&
133
+ err.syscall?.startsWith("spawn")) {
134
+ // spawn ENOENT — command not found (e.g., npx on Windows).
135
+ // This surfaces as an uncaught exception from child_process but
136
+ // is not a fatal process error. Log and continue instead of
137
+ // crashing auto-mode (#1384).
138
+ process.stderr.write(`[gsd] spawn ENOENT: ${err.path ?? "unknown"} — command not found\n`);
139
+ return;
140
+ }
141
+ // Re-throw anything that isn't EPIPE/ENOENT so real crashes still surface
144
142
  throw err;
145
143
  };
146
144
  process.on("uncaughtException", _gsdEpipeGuard);
@@ -535,6 +533,10 @@ export default function (pi) {
535
533
  const stopContextTimer = debugTime("context-inject");
536
534
  const systemContent = loadPrompt("system");
537
535
  const loadedPreferences = loadEffectiveGSDPreferences();
536
+ if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
537
+ markCmuxPromptShown();
538
+ ctx.ui.notify("cmux detected. Run /gsd cmux on to enable sidebar metadata, notifications, and visual subagent splits for this project.", "info");
539
+ }
538
540
  let preferenceBlock = "";
539
541
  if (loadedPreferences) {
540
542
  const cwd = process.cwd();
@@ -580,12 +582,8 @@ export default function (pi) {
580
582
  newSkillsBlock = formatSkillsXml(newSkills);
581
583
  }
582
584
  }
583
- // Load agent instructions (global + project)
584
- let agentInstructionsBlock = "";
585
- const agentInstructions = loadAgentInstructions();
586
- if (agentInstructions) {
587
- agentInstructionsBlock = `\n\n## Agent Instructions\n\nThe following instructions were provided by the user and must be followed in every session:\n\n${agentInstructions}`;
588
- }
585
+ // Warn if deprecated agent-instructions.md files are still present
586
+ warnDeprecatedAgentInstructions();
589
587
  const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
590
588
  // Worktree context — override the static CWD in the system prompt
591
589
  let worktreeBlock = "";
@@ -628,7 +626,7 @@ export default function (pi) {
628
626
  "Write every .gsd artifact in the worktree path above, never in the main project tree.",
629
627
  ].join("\n");
630
628
  }
631
- const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${agentInstructionsBlock}${knowledgeBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
629
+ const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
632
630
  stopContextTimer({
633
631
  systemPromptSize: fullSystem.length,
634
632
  injectionSize: injection?.length ?? 0,
@@ -5,10 +5,11 @@
5
5
  * `~/.gsd/projects/<hash>/` state directory. After migration, a
6
6
  * symlink replaces the original directory so all paths remain valid.
7
7
  */
8
- import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
8
+ import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { externalGsdRoot } from "./repo-identity.js";
11
11
  import { getErrorMessage } from "./error-utils.js";
12
+ import { hasGitTrackedGsdFiles } from "./gitignore.js";
12
13
  /**
13
14
  * Migrate a legacy in-project `.gsd/` directory to external storage.
14
15
  *
@@ -42,6 +43,27 @@ export function migrateToExternalState(basePath) {
42
43
  catch (err) {
43
44
  return { migrated: false, error: `Cannot stat .gsd: ${getErrorMessage(err)}` };
44
45
  }
46
+ // Skip if .gsd/ contains git-tracked files — the project intentionally
47
+ // keeps .gsd/ in version control and migration would destroy that.
48
+ if (hasGitTrackedGsdFiles(basePath)) {
49
+ return { migrated: false };
50
+ }
51
+ // Skip if .gsd/worktrees/ has active worktree directories (#1337).
52
+ // On Windows, active git worktrees hold OS-level directory handles that
53
+ // prevent rename/delete. Attempting migration causes EBUSY and data loss.
54
+ const worktreesDir = join(localGsd, "worktrees");
55
+ if (existsSync(worktreesDir)) {
56
+ try {
57
+ const entries = readdirSync(worktreesDir, { withFileTypes: true });
58
+ if (entries.some(e => e.isDirectory())) {
59
+ return { migrated: false };
60
+ }
61
+ }
62
+ catch {
63
+ // Can't read worktrees dir — skip migration to be safe
64
+ return { migrated: false };
65
+ }
66
+ }
45
67
  const externalPath = externalGsdRoot(basePath);
46
68
  const migratingPath = join(basePath, ".gsd.migrating");
47
69
  try {
@@ -89,7 +111,38 @@ export function migrateToExternalState(basePath) {
89
111
  }
90
112
  // Create symlink .gsd -> external path
91
113
  symlinkSync(externalPath, localGsd, "junction");
92
- // Remove .gsd.migrating
114
+ // Verify the symlink resolves correctly before removing the backup (#1377).
115
+ // On Windows, junction creation can silently succeed but resolve to the wrong
116
+ // target, or the external dir may not be accessible. If verification fails,
117
+ // restore from the backup.
118
+ try {
119
+ const resolved = realpathSync(localGsd);
120
+ const resolvedExternal = realpathSync(externalPath);
121
+ if (resolved !== resolvedExternal) {
122
+ // Symlink points to wrong target — restore backup
123
+ try {
124
+ rmSync(localGsd, { force: true });
125
+ }
126
+ catch { /* may not exist */ }
127
+ renameSync(migratingPath, localGsd);
128
+ return { migrated: false, error: `Migration verification failed: symlink resolves to ${resolved}, expected ${resolvedExternal}` };
129
+ }
130
+ // Verify we can read through the symlink
131
+ readdirSync(localGsd);
132
+ }
133
+ catch (verifyErr) {
134
+ // Symlink broken or unreadable — restore backup
135
+ try {
136
+ rmSync(localGsd, { force: true });
137
+ }
138
+ catch { /* may not exist */ }
139
+ try {
140
+ renameSync(migratingPath, localGsd);
141
+ }
142
+ catch { /* best-effort restore */ }
143
+ return { migrated: false, error: `Migration verification failed: ${getErrorMessage(verifyErr)}` };
144
+ }
145
+ // Remove .gsd.migrating only after symlink is verified
93
146
  rmSync(migratingPath, { recursive: true, force: true });
94
147
  return { migrated: true };
95
148
  }
@@ -67,8 +67,9 @@ export function findMilestoneIds(basePath) {
67
67
  .filter((d) => d.isDirectory())
68
68
  .map((d) => {
69
69
  const match = d.name.match(/^(M\d+(?:-[a-z0-9]{6})?)/);
70
- return match ? match[1] : d.name;
71
- });
70
+ return match ? match[1] : null;
71
+ })
72
+ .filter((id) => id !== null);
72
73
  // Apply custom queue order if available, else fall back to numeric sort
73
74
  const customOrder = loadQueueOrder(basePath);
74
75
  return sortByQueueOrder(ids, customOrder);
@@ -2,13 +2,22 @@
2
2
  // Cross-platform desktop notifications for auto-mode events.
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { loadEffectiveGSDPreferences } from "./preferences.js";
5
+ import { CmuxClient, emitOsc777Notification, resolveCmuxConfig } from "../cmux/index.js";
5
6
  /**
6
7
  * Send a native desktop notification. Non-blocking, non-fatal.
7
8
  * macOS: osascript, Linux: notify-send, Windows: skipped.
8
9
  */
9
10
  export function sendDesktopNotification(title, message, level = "info", kind = "complete") {
10
- if (!shouldSendDesktopNotification(kind))
11
+ const loaded = loadEffectiveGSDPreferences()?.preferences;
12
+ if (!shouldSendDesktopNotification(kind, loaded?.notifications))
11
13
  return;
14
+ const cmux = resolveCmuxConfig(loaded);
15
+ if (cmux.notifications) {
16
+ const delivered = CmuxClient.fromPreferences(loaded).notify(title, message);
17
+ if (delivered)
18
+ return;
19
+ emitOsc777Notification(title, message);
20
+ }
12
21
  try {
13
22
  const command = buildDesktopNotificationCommand(process.platform, title, message, level);
14
23
  if (!command)
@@ -9,7 +9,8 @@
9
9
  * via prefix matching, so existing projects work without migration.
10
10
  */
11
11
  import { readdirSync, existsSync, realpathSync, Dirent } from "node:fs";
12
- import { join } from "node:path";
12
+ import { join, dirname, normalize } from "node:path";
13
+ import { spawnSync } from "node:child_process";
13
14
  import { nativeScanGsdTree } from "./native-parser-bridge.js";
14
15
  import { DIR_CACHE_MAX } from "./constants.js";
15
16
  // ─── Directory Listing Cache ──────────────────────────────────────────────────
@@ -263,15 +264,81 @@ const LEGACY_GSD_ROOT_FILES = {
263
264
  OVERRIDES: "overrides.md",
264
265
  KNOWLEDGE: "knowledge.md",
265
266
  };
267
+ // ─── GSD Root Discovery ───────────────────────────────────────────────────────
268
+ const gsdRootCache = new Map();
269
+ /** Exported for tests only — do not call in production code. */
270
+ export function _clearGsdRootCache() {
271
+ gsdRootCache.clear();
272
+ }
273
+ /**
274
+ * Resolve the `.gsd` directory for a given project base path.
275
+ *
276
+ * Probe order:
277
+ * 1. basePath/.gsd — fast path (common case)
278
+ * 2. git rev-parse root — handles cwd-is-a-subdirectory
279
+ * 3. Walk up from basePath — handles moved .gsd in an ancestor (bounded by git root)
280
+ * 4. basePath/.gsd — creation fallback (init scenario)
281
+ *
282
+ * Result is cached per basePath for the process lifetime.
283
+ */
266
284
  export function gsdRoot(basePath) {
267
- const local = join(basePath, ".gsd");
285
+ const cached = gsdRootCache.get(basePath);
286
+ if (cached)
287
+ return cached;
288
+ const result = probeGsdRoot(basePath);
289
+ gsdRootCache.set(basePath, result);
290
+ return result;
291
+ }
292
+ function probeGsdRoot(rawBasePath) {
293
+ // 1. Fast path — check the input path directly
294
+ const local = join(rawBasePath, ".gsd");
295
+ if (existsSync(local))
296
+ return local;
297
+ // Resolve symlinks so path comparisons work correctly across platforms
298
+ // (e.g. macOS /var → /private/var). Use rawBasePath as fallback if not resolvable.
299
+ let basePath;
268
300
  try {
269
- const resolved = realpathSync(local);
270
- if (resolved !== local)
271
- return resolved; // symlink resolved
301
+ basePath = realpathSync.native(rawBasePath);
302
+ }
303
+ catch {
304
+ basePath = rawBasePath;
305
+ }
306
+ // 2. Git root anchor — used as both probe target and walk-up boundary
307
+ // Only walk if we're inside a git project — prevents escaping into
308
+ // unrelated filesystem territory when running outside any repo.
309
+ let gitRoot = null;
310
+ try {
311
+ const out = spawnSync("git", ["rev-parse", "--show-toplevel"], {
312
+ cwd: basePath,
313
+ encoding: "utf-8",
314
+ });
315
+ if (out.status === 0) {
316
+ const r = out.stdout.trim();
317
+ if (r)
318
+ gitRoot = normalize(r);
319
+ }
320
+ }
321
+ catch { /* git not available */ }
322
+ if (gitRoot) {
323
+ const candidate = join(gitRoot, ".gsd");
324
+ if (existsSync(candidate))
325
+ return candidate;
326
+ }
327
+ // 3. Walk up from basePath to the git root (only if we are in a subdirectory)
328
+ if (gitRoot && basePath !== gitRoot) {
329
+ let cur = dirname(basePath);
330
+ while (cur !== basePath) {
331
+ const candidate = join(cur, ".gsd");
332
+ if (existsSync(candidate))
333
+ return candidate;
334
+ if (cur === gitRoot)
335
+ break;
336
+ basePath = cur;
337
+ cur = dirname(cur);
338
+ }
272
339
  }
273
- catch { /* doesn't exist yet — fall through */ }
274
- return local; // backwards compat: unmigrated projects
340
+ // 4. Fallback for init/creation
341
+ return local;
275
342
  }
276
343
  export function milestonesDir(basePath) {
277
344
  return join(gsdRoot(basePath), "milestones");
@@ -111,10 +111,13 @@ function dequeueNextHook(basePath) {
111
111
  };
112
112
  // Build the prompt with variable substitution
113
113
  const [mid, sid, tid] = triggerUnitId.split("/");
114
- const prompt = config.prompt
114
+ let prompt = config.prompt
115
115
  .replace(/\{milestoneId\}/g, mid ?? "")
116
116
  .replace(/\{sliceId\}/g, sid ?? "")
117
117
  .replace(/\{taskId\}/g, tid ?? "");
118
+ // Inject browser safety instruction for hooks that may use browser tools (#1345).
119
+ // Vite HMR and other persistent connections prevent networkidle from resolving.
120
+ prompt += "\n\n**Browser tool safety:** Do NOT use `browser_wait_for` with `condition: \"network_idle\"` — it hangs indefinitely when dev servers keep persistent connections (Vite HMR, WebSocket). Use `selector_visible`, `text_visible`, or `delay` instead.";
118
121
  return {
119
122
  hookName: config.name,
120
123
  prompt,
@@ -47,6 +47,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
47
47
  "budget_enforcement",
48
48
  "context_pause_threshold",
49
49
  "notifications",
50
+ "cmux",
50
51
  "remote_questions",
51
52
  "git",
52
53
  "post_unit_hooks",
@@ -63,6 +64,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
63
64
  "search_provider",
64
65
  "compression_strategy",
65
66
  "context_selection",
67
+ "widget_mode",
66
68
  ]);
67
69
  /** Canonical list of all dispatch unit types. */
68
70
  export const KNOWN_UNIT_TYPES = [
@@ -14,9 +14,24 @@ export function validatePreferences(preferences) {
14
14
  const warnings = [];
15
15
  const validated = {};
16
16
  // ─── Unknown Key Detection ──────────────────────────────────────────
17
+ // Common key migration hints for pi-level settings that don't map to GSD prefs
18
+ const KEY_MIGRATION_HINTS = {
19
+ taskIsolation: 'use "git.isolation" instead (values: worktree, branch, none)',
20
+ task_isolation: 'use "git.isolation" instead (values: worktree, branch, none)',
21
+ isolation: 'use "git.isolation" instead (values: worktree, branch, none)',
22
+ manage_gitignore: 'use "git.manage_gitignore" instead',
23
+ auto_push: 'use "git.auto_push" instead',
24
+ main_branch: 'use "git.main_branch" instead',
25
+ };
17
26
  for (const key of Object.keys(preferences)) {
18
27
  if (!KNOWN_PREFERENCE_KEYS.has(key)) {
19
- warnings.push(`unknown preference key "${key}" — ignored`);
28
+ const hint = KEY_MIGRATION_HINTS[key];
29
+ if (hint) {
30
+ warnings.push(`unknown preference key "${key}" — ${hint}`);
31
+ }
32
+ else {
33
+ warnings.push(`unknown preference key "${key}" — ignored`);
34
+ }
20
35
  }
21
36
  }
22
37
  if (preferences.version !== undefined) {
@@ -210,6 +225,35 @@ export function validatePreferences(preferences) {
210
225
  errors.push("notifications must be an object");
211
226
  }
212
227
  }
228
+ // ─── Cmux ───────────────────────────────────────────────────────────────
229
+ if (preferences.cmux !== undefined) {
230
+ if (preferences.cmux && typeof preferences.cmux === "object") {
231
+ const cmux = preferences.cmux;
232
+ const validatedCmux = {};
233
+ if (cmux.enabled !== undefined)
234
+ validatedCmux.enabled = !!cmux.enabled;
235
+ if (cmux.notifications !== undefined)
236
+ validatedCmux.notifications = !!cmux.notifications;
237
+ if (cmux.sidebar !== undefined)
238
+ validatedCmux.sidebar = !!cmux.sidebar;
239
+ if (cmux.splits !== undefined)
240
+ validatedCmux.splits = !!cmux.splits;
241
+ if (cmux.browser !== undefined)
242
+ validatedCmux.browser = !!cmux.browser;
243
+ const knownCmuxKeys = new Set(["enabled", "notifications", "sidebar", "splits", "browser"]);
244
+ for (const key of Object.keys(cmux)) {
245
+ if (!knownCmuxKeys.has(key)) {
246
+ warnings.push(`unknown cmux key "${key}" — ignored`);
247
+ }
248
+ }
249
+ if (Object.keys(validatedCmux).length > 0) {
250
+ validated.cmux = validatedCmux;
251
+ }
252
+ }
253
+ else {
254
+ errors.push("cmux must be an object");
255
+ }
256
+ }
213
257
  // ─── Remote Questions ───────────────────────────────────────────────
214
258
  if (preferences.remote_questions !== undefined) {
215
259
  if (preferences.remote_questions && typeof preferences.remote_questions === "object") {
@@ -15,6 +15,7 @@ import { join } from "node:path";
15
15
  import { gsdRoot } from "./paths.js";
16
16
  import { parse as parseYaml } from "yaml";
17
17
  import { normalizeStringArray } from "../shared/mod.js";
18
+ import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
18
19
  import { MODE_DEFAULTS, } from "./preferences-types.js";
19
20
  import { validatePreferences } from "./preferences-validation.js";
20
21
  import { formatSkillRef } from "./preferences-skills.js";
@@ -79,6 +80,17 @@ export function loadEffectiveGSDPreferences() {
79
80
  ...(mergedWarnings.length > 0 ? { warnings: mergedWarnings } : {}),
80
81
  };
81
82
  }
83
+ // Apply token-profile defaults as the lowest-priority layer so that
84
+ // `token_profile: budget` sets models and phase-skips automatically.
85
+ // Explicit user preferences always override profile defaults.
86
+ const profile = result.preferences.token_profile;
87
+ if (profile) {
88
+ const profileDefaults = _resolveProfileDefaults(profile);
89
+ result = {
90
+ ...result,
91
+ preferences: mergePreferences(profileDefaults, result.preferences),
92
+ };
93
+ }
82
94
  // Apply mode defaults as the lowest-priority layer
83
95
  if (result.preferences.mode) {
84
96
  result = {
@@ -162,6 +174,9 @@ function mergePreferences(base, override) {
162
174
  notifications: (base.notifications || override.notifications)
163
175
  ? { ...(base.notifications ?? {}), ...(override.notifications ?? {}) }
164
176
  : undefined,
177
+ cmux: (base.cmux || override.cmux)
178
+ ? { ...(base.cmux ?? {}), ...(override.cmux ?? {}) }
179
+ : undefined,
165
180
  remote_questions: override.remote_questions
166
181
  ? { ...(base.remote_questions ?? {}), ...override.remote_questions }
167
182
  : base.remote_questions,