gsd-pi 2.35.0-dev.cd3b7ea → 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
@@ -12,14 +12,16 @@
12
12
  import type { GSDState } from "./types.js";
13
13
  import type { GSDPreferences } from "./preferences.js";
14
14
  import type { UatType } from "./files.js";
15
- import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
15
+ import { loadFile, extractUatType, loadActiveOverrides, parseRoadmap } from "./files.js";
16
16
  import {
17
17
  resolveMilestoneFile,
18
18
  resolveMilestonePath,
19
19
  resolveSliceFile,
20
+ resolveSlicePath,
20
21
  resolveTaskFile,
21
22
  relSliceFile,
22
23
  buildMilestoneFileName,
24
+ buildSliceFileName,
23
25
  } from "./paths.js";
24
26
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
25
27
  import { join } from "node:path";
@@ -369,6 +371,30 @@ const DISPATCH_RULES: DispatchRule[] = [
369
371
  name: "validating-milestone → validate-milestone",
370
372
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
371
373
  if (state.phase !== "validating-milestone") return null;
374
+
375
+ // Safety guard (#1368): verify all roadmap slices have SUMMARY files before
376
+ // allowing milestone validation. If any slice lacks a summary, the milestone
377
+ // is not genuinely complete — something skipped earlier slices.
378
+ const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
379
+ const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
380
+ if (roadmapContent) {
381
+ const roadmap = parseRoadmap(roadmapContent);
382
+ const missingSlices: string[] = [];
383
+ for (const slice of roadmap.slices) {
384
+ const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
385
+ if (!summaryPath || !existsSync(summaryPath)) {
386
+ missingSlices.push(slice.id);
387
+ }
388
+ }
389
+ if (missingSlices.length > 0) {
390
+ return {
391
+ action: "stop",
392
+ reason: `Cannot validate milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. These slices may have been skipped.`,
393
+ level: "error",
394
+ };
395
+ }
396
+ }
397
+
372
398
  // Skip preference: write a minimal pass-through VALIDATION file
373
399
  if (prefs?.phases?.skip_milestone_validation) {
374
400
  const mDir = resolveMilestonePath(basePath, mid);
@@ -404,6 +430,28 @@ const DISPATCH_RULES: DispatchRule[] = [
404
430
  name: "completing-milestone → complete-milestone",
405
431
  match: async ({ state, mid, midTitle, basePath }) => {
406
432
  if (state.phase !== "completing-milestone") return null;
433
+
434
+ // Safety guard (#1368): verify all roadmap slices have SUMMARY files.
435
+ const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
436
+ const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
437
+ if (roadmapContent) {
438
+ const roadmap = parseRoadmap(roadmapContent);
439
+ const missingSlices: string[] = [];
440
+ for (const slice of roadmap.slices) {
441
+ const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
442
+ if (!summaryPath || !existsSync(summaryPath)) {
443
+ missingSlices.push(slice.id);
444
+ }
445
+ }
446
+ if (missingSlices.length > 0) {
447
+ return {
448
+ action: "stop",
449
+ reason: `Cannot complete milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. Run /gsd doctor to diagnose.`,
450
+ level: "error",
451
+ };
452
+ }
453
+ }
454
+
407
455
  return {
408
456
  action: "dispatch",
409
457
  unitType: "complete-milestone",
@@ -25,6 +25,7 @@ import type {
25
25
  import type { DispatchAction } from "./auto-dispatch.js";
26
26
  import type { WorktreeResolver } from "./worktree-resolver.js";
27
27
  import { debugLog } from "./debug-logger.js";
28
+ import type { CmuxLogLevel } from "../cmux/index.js";
28
29
 
29
30
  /**
30
31
  * Maximum total loop iterations before forced stop. Prevents runaway loops
@@ -221,6 +222,15 @@ export async function runUnit(
221
222
  s.pendingResolve = resolve;
222
223
  });
223
224
 
225
+ // Ensure cwd matches basePath before dispatch (#1389).
226
+ // async_bash and background jobs can drift cwd away from the worktree.
227
+ // Realigning here prevents commits from landing on the wrong branch.
228
+ try {
229
+ if (process.cwd() !== s.basePath) {
230
+ process.chdir(s.basePath);
231
+ }
232
+ } catch { /* non-fatal — chdir may fail if dir was removed */ }
233
+
224
234
  // ── Send the prompt ──
225
235
  debugLog("runUnit", { phase: "send-message", unitType, unitId });
226
236
 
@@ -267,6 +277,12 @@ export interface LoopDeps {
267
277
  unitId: string,
268
278
  state: GSDState,
269
279
  ) => void;
280
+ syncCmuxSidebar: (preferences: GSDPreferences | undefined, state: GSDState) => void;
281
+ logCmuxEvent: (
282
+ preferences: GSDPreferences | undefined,
283
+ message: string,
284
+ level?: CmuxLogLevel,
285
+ ) => void;
270
286
 
271
287
  // State and cache functions
272
288
  invalidateAllCaches: () => void;
@@ -344,6 +360,7 @@ export interface LoopDeps {
344
360
  getManifestStatus: (
345
361
  basePath: string,
346
362
  mid: string | undefined,
363
+ projectRoot?: string,
347
364
  ) => Promise<{ pending: unknown[] } | null>;
348
365
  collectSecretsFromManifest: (
349
366
  basePath: string,
@@ -446,6 +463,7 @@ export interface LoopDeps {
446
463
  prefs: GSDPreferences | undefined,
447
464
  verbose: boolean,
448
465
  startModel: { provider: string; id: string } | null,
466
+ retryContext?: { isRetry: boolean; previousTier?: string },
449
467
  ) => Promise<{ routing: { tier: string; modelDowngraded: boolean } | null }>;
450
468
  startUnitSupervision: (sctx: {
451
469
  s: AutoSession;
@@ -598,6 +616,7 @@ export async function autoLoop(
598
616
 
599
617
  // Derive state
600
618
  let state = await deps.deriveState(s.basePath);
619
+ deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
601
620
  let mid = state.activeMilestone?.id;
602
621
  let midTitle = state.activeMilestone?.title;
603
622
  debugLog("autoLoop", {
@@ -619,6 +638,11 @@ export async function autoLoop(
619
638
  "success",
620
639
  "milestone",
621
640
  );
641
+ deps.logCmuxEvent(
642
+ deps.loadEffectiveGSDPreferences()?.preferences,
643
+ `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
644
+ "success",
645
+ );
622
646
 
623
647
  const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
624
648
  if (vizPrefs?.auto_visualize) {
@@ -756,12 +780,18 @@ export async function autoLoop(
756
780
  "success",
757
781
  "milestone",
758
782
  );
783
+ deps.logCmuxEvent(
784
+ deps.loadEffectiveGSDPreferences()?.preferences,
785
+ "All milestones complete.",
786
+ "success",
787
+ );
759
788
  await deps.stopAuto(ctx, pi, "All milestones complete");
760
789
  } else if (state.phase === "blocked") {
761
790
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
762
791
  await deps.stopAuto(ctx, pi, blockerMsg);
763
792
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
764
793
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
794
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
765
795
  } else {
766
796
  const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
767
797
  const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
@@ -839,6 +869,11 @@ export async function autoLoop(
839
869
  "success",
840
870
  "milestone",
841
871
  );
872
+ deps.logCmuxEvent(
873
+ deps.loadEffectiveGSDPreferences()?.preferences,
874
+ `Milestone ${mid} complete.`,
875
+ "success",
876
+ );
842
877
  await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
843
878
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
844
879
  break;
@@ -860,6 +895,7 @@ export async function autoLoop(
860
895
  await deps.stopAuto(ctx, pi, blockerMsg);
861
896
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
862
897
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
898
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
863
899
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
864
900
  break;
865
901
  }
@@ -903,12 +939,14 @@ export async function autoLoop(
903
939
  "warning",
904
940
  );
905
941
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
942
+ deps.logCmuxEvent(prefs, msg, "warning");
906
943
  await deps.pauseAuto(ctx, pi);
907
944
  debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
908
945
  break;
909
946
  }
910
947
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
911
948
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
949
+ deps.logCmuxEvent(prefs, msg, "warning");
912
950
  } else if (newBudgetAlertLevel === 90) {
913
951
  s.lastBudgetAlertLevel =
914
952
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -922,6 +960,11 @@ export async function autoLoop(
922
960
  "warning",
923
961
  "budget",
924
962
  );
963
+ deps.logCmuxEvent(
964
+ prefs,
965
+ `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
966
+ "warning",
967
+ );
925
968
  } else if (newBudgetAlertLevel === 80) {
926
969
  s.lastBudgetAlertLevel =
927
970
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -935,6 +978,11 @@ export async function autoLoop(
935
978
  "warning",
936
979
  "budget",
937
980
  );
981
+ deps.logCmuxEvent(
982
+ prefs,
983
+ `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
984
+ "warning",
985
+ );
938
986
  } else if (newBudgetAlertLevel === 75) {
939
987
  s.lastBudgetAlertLevel =
940
988
  newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
@@ -948,6 +996,11 @@ export async function autoLoop(
948
996
  "info",
949
997
  "budget",
950
998
  );
999
+ deps.logCmuxEvent(
1000
+ prefs,
1001
+ `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
1002
+ "progress",
1003
+ );
951
1004
  } else if (budgetAlertLevel === 0) {
952
1005
  s.lastBudgetAlertLevel = 0;
953
1006
  }
@@ -983,7 +1036,7 @@ export async function autoLoop(
983
1036
 
984
1037
  // Secrets re-check gate
985
1038
  try {
986
- const manifestStatus = await deps.getManifestStatus(s.basePath, mid);
1039
+ const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
987
1040
  if (manifestStatus && manifestStatus.pending.length > 0) {
988
1041
  const result = await deps.collectSecretsFromManifest(
989
1042
  s.basePath,
@@ -1172,6 +1225,14 @@ export async function autoLoop(
1172
1225
  unitId,
1173
1226
  });
1174
1227
 
1228
+ // Detect retry and capture previous tier for escalation
1229
+ const isRetry = !!(
1230
+ s.currentUnit &&
1231
+ s.currentUnit.type === unitType &&
1232
+ s.currentUnit.id === unitId
1233
+ );
1234
+ const previousTier = s.currentUnitRouting?.tier;
1235
+
1175
1236
  // Closeout previous unit
1176
1237
  if (s.currentUnit) {
1177
1238
  await deps.closeoutUnit(
@@ -1325,7 +1386,7 @@ export async function autoLoop(
1325
1386
  );
1326
1387
  }
1327
1388
 
1328
- // Select and apply model
1389
+ // Select and apply model (with tier escalation on retry)
1329
1390
  const modelResult = await deps.selectAndApplyModel(
1330
1391
  ctx,
1331
1392
  pi,
@@ -1335,6 +1396,7 @@ export async function autoLoop(
1335
1396
  prefs,
1336
1397
  s.verbose,
1337
1398
  s.autoModeStartModel,
1399
+ { isRetry, previousTier },
1338
1400
  );
1339
1401
  s.currentUnitRouting =
1340
1402
  modelResult.routing as AutoSession["currentUnitRouting"];
@@ -7,8 +7,9 @@
7
7
  import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
8
8
  import type { GSDPreferences } from "./preferences.js";
9
9
  import { resolveModelWithFallbacksForUnit, resolveDynamicRoutingConfig } from "./preferences.js";
10
+ import type { ComplexityTier } from "./complexity-classifier.js";
10
11
  import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js";
11
- import { resolveModelForComplexity } from "./model-router.js";
12
+ import { resolveModelForComplexity, escalateTier } from "./model-router.js";
12
13
  import { getLedger, getProjectTotals } from "./metrics.js";
13
14
  import { unitPhaseLabel } from "./auto-dashboard.js";
14
15
 
@@ -33,6 +34,7 @@ export async function selectAndApplyModel(
33
34
  prefs: GSDPreferences | undefined,
34
35
  verbose: boolean,
35
36
  autoModeStartModel: { provider: string; id: string } | null,
37
+ retryContext?: { isRetry: boolean; previousTier?: string },
36
38
  ): Promise<ModelSelectionResult> {
37
39
  const modelConfig = resolveModelWithFallbacksForUnit(unitType);
38
40
  let routing: { tier: string; modelDowngraded: boolean } | null = null;
@@ -60,8 +62,27 @@ export async function selectAndApplyModel(
60
62
  const shouldClassify = !isHook || routingConfig.hooks !== false;
61
63
 
62
64
  if (shouldClassify) {
63
- const classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
65
+ let classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
64
66
  const availableModelIds = availableModels.map(m => m.id);
67
+
68
+ // Escalate tier on retry when escalate_on_failure is enabled (default: true)
69
+ if (
70
+ retryContext?.isRetry &&
71
+ retryContext.previousTier &&
72
+ routingConfig.escalate_on_failure !== false
73
+ ) {
74
+ const escalated = escalateTier(retryContext.previousTier as ComplexityTier);
75
+ if (escalated) {
76
+ classification = { ...classification, tier: escalated, reason: "escalated after failure" };
77
+ if (verbose) {
78
+ ctx.ui.notify(
79
+ `Tier escalation: ${retryContext.previousTier} → ${escalated} (retry after failure)`,
80
+ "info",
81
+ );
82
+ }
83
+ }
84
+ }
85
+
65
86
  const routingResult = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds);
66
87
 
67
88
  if (routingResult.wasDowngraded) {
@@ -8,6 +8,8 @@
8
8
  */
9
9
 
10
10
  import type { ExtensionContext } from "@gsd/pi-coding-agent";
11
+ import { parseUnitId } from "./unit-id.js";
12
+ import { atomicWriteSync } from "./atomic-write.js";
11
13
  import { clearUnitRuntimeRecord } from "./unit-runtime.js";
12
14
  import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
13
15
  import { isValidationTerminal } from "./state.js";
@@ -35,6 +37,7 @@ import {
35
37
  clearPathCache,
36
38
  resolveGsdRootFile,
37
39
  } from "./paths.js";
40
+ import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
38
41
  import {
39
42
  existsSync,
40
43
  mkdirSync,
@@ -499,6 +502,42 @@ export async function selfHealRuntimeRecords(
499
502
  for (const record of records) {
500
503
  const { unitType, unitId } = record;
501
504
 
505
+ // Case 0: complete-slice with SUMMARY + UAT but unchecked roadmap (#1350).
506
+ // If a complete-slice was interrupted after writing artifacts but before
507
+ // flipping the roadmap checkbox, the verification fails and the dispatch
508
+ // loop relaunches the same unit forever. Auto-fix the checkbox.
509
+ if (unitType === "complete-slice") {
510
+ const { milestone: mid, slice: sid } = parseUnitId(unitId);
511
+ if (mid && sid) {
512
+ const dir = resolveSlicePath(base, mid, sid);
513
+ if (dir) {
514
+ const summaryPath = join(dir, buildSliceFileName(sid, "SUMMARY"));
515
+ const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
516
+ if (existsSync(summaryPath) && existsSync(uatPath)) {
517
+ const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
518
+ if (roadmapFile && existsSync(roadmapFile)) {
519
+ try {
520
+ const roadmapContent = readFileSync(roadmapFile, "utf-8");
521
+ const roadmap = parseRoadmap(roadmapContent);
522
+ const slice = (roadmap.slices ?? []).find(s => s.id === sid);
523
+ if (slice && !slice.done) {
524
+ // Auto-fix: flip the checkbox using shared utility
525
+ if (markSliceDoneInRoadmap(base, mid, sid)) {
526
+ ctx.ui.notify(
527
+ `Self-heal: marked ${sid} done in roadmap (SUMMARY + UAT exist but checkbox was stale).`,
528
+ "info",
529
+ );
530
+ }
531
+ }
532
+ } catch {
533
+ // Roadmap parse failure — don't block self-heal
534
+ }
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+
502
541
  // Clear stale dispatched records (dispatched > 1h ago, process crashed)
503
542
  const age = now - (record.startedAt ?? 0);
504
543
  if (record.phase === "dispatched" && age > STALE_THRESHOLD_MS) {
@@ -20,6 +20,8 @@ import {
20
20
  resolveSkillDiscoveryMode,
21
21
  getIsolationMode,
22
22
  } from "./preferences.js";
23
+ import { ensureGsdSymlink } from "./repo-identity.js";
24
+ import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
23
25
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
24
26
  import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
25
27
  import { invalidateAllCaches } from "./cache.js";
@@ -92,6 +94,13 @@ export interface BootstrapDeps {
92
94
  * Returns false if the bootstrap aborted (e.g., guided flow returned,
93
95
  * concurrent session detected). Returns true when ready to dispatch.
94
96
  */
97
+
98
+ /** Guard: tracks consecutive bootstrap attempts that found phase === "complete".
99
+ * Prevents the recursive dialog loop described in #1348 where
100
+ * bootstrapAutoSession → showSmartEntry → checkAutoStartAfterDiscuss → startAuto
101
+ * cycles indefinitely when the discuss workflow doesn't produce a milestone. */
102
+ let _consecutiveCompleteBootstraps = 0;
103
+ const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
95
104
  export async function bootstrapAutoSession(
96
105
  s: AutoSession,
97
106
  ctx: ExtensionCommandContext,
@@ -128,7 +137,20 @@ export async function bootstrapAutoSession(
128
137
  nativeInit(base, mainBranch);
129
138
  }
130
139
 
131
- // Ensure .gitignore has baseline patterns
140
+ // Migrate legacy in-project .gsd/ to external state directory.
141
+ // Migration MUST run before ensureGitignore to avoid adding ".gsd" to
142
+ // .gitignore when .gsd/ is git-tracked (data-loss bug #1364).
143
+ recoverFailedMigration(base);
144
+ const migration = migrateToExternalState(base);
145
+ if (migration.error) {
146
+ ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
147
+ }
148
+ // Ensure symlink exists (handles fresh projects and post-migration)
149
+ ensureGsdSymlink(base);
150
+
151
+ // Ensure .gitignore has baseline patterns.
152
+ // ensureGitignore checks for git-tracked .gsd/ files and skips the
153
+ // ".gsd" pattern if the project intentionally tracks .gsd/ in git.
132
154
  const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
133
155
  const commitDocs = gitPrefs?.commit_docs;
134
156
  const manageGitignore = gitPrefs?.manage_gitignore;
@@ -286,6 +308,20 @@ export async function bootstrapAutoSession(
286
308
  if (!hasSurvivorBranch) {
287
309
  // No active work — start a new milestone via discuss flow
288
310
  if (!state.activeMilestone || state.phase === "complete") {
311
+ // Guard against recursive dialog loop (#1348):
312
+ // If we've entered this branch multiple times in quick succession,
313
+ // the discuss workflow isn't producing a milestone. Break the cycle.
314
+ _consecutiveCompleteBootstraps++;
315
+ if (_consecutiveCompleteBootstraps > MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS) {
316
+ _consecutiveCompleteBootstraps = 0;
317
+ ctx.ui.notify(
318
+ "All milestones are complete and the discussion didn't produce a new one. " +
319
+ "Run /gsd to start a new milestone manually.",
320
+ "warning",
321
+ );
322
+ return releaseLockAndReturn();
323
+ }
324
+
289
325
  const { showSmartEntry } = await import("./guided-flow.js");
290
326
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
291
327
 
@@ -296,6 +332,7 @@ export async function bootstrapAutoSession(
296
332
  postState.phase !== "complete" &&
297
333
  postState.phase !== "pre-planning"
298
334
  ) {
335
+ _consecutiveCompleteBootstraps = 0; // Successfully advanced past "complete"
299
336
  state = postState;
300
337
  } else if (
301
338
  postState.activeMilestone &&
@@ -352,6 +389,9 @@ export async function bootstrapAutoSession(
352
389
  return releaseLockAndReturn();
353
390
  }
354
391
 
392
+ // Successfully resolved an active milestone — reset the re-entry guard
393
+ _consecutiveCompleteBootstraps = 0;
394
+
355
395
  // ── Initialize session state ──
356
396
  s.active = true;
357
397
  s.stepMode = requestedStepMode;
@@ -484,7 +524,7 @@ export async function bootstrapAutoSession(
484
524
  // Secrets collection gate
485
525
  const mid = state.activeMilestone!.id;
486
526
  try {
487
- const manifestStatus = await getManifestStatus(base, mid);
527
+ const manifestStatus = await getManifestStatus(base, mid, s.originalBasePath || base);
488
528
  if (manifestStatus && manifestStatus.pending.length > 0) {
489
529
  const result = await collectSecretsFromManifest(base, mid, ctx);
490
530
  if (
@@ -127,7 +127,7 @@ import {
127
127
  formatTokenCount,
128
128
  } from "./metrics.js";
129
129
  import { join } from "node:path";
130
- import { readFileSync, existsSync, mkdirSync } from "node:fs";
130
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
131
131
  import { atomicWriteSync } from "./atomic-write.js";
132
132
  import {
133
133
  autoCommitCurrentBranch,
@@ -184,6 +184,7 @@ import {
184
184
  } from "./auto-supervisor.js";
185
185
  import { isDbAvailable } from "./gsd-db.js";
186
186
  import { countPendingCaptures } from "./captures.js";
187
+ import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
187
188
 
188
189
  // ── Extracted modules ──────────────────────────────────────────────────────
189
190
  import { startUnitSupervision } from "./auto-timers.js";
@@ -466,6 +467,7 @@ function handleLostSessionLock(ctx?: ExtensionContext): void {
466
467
  s.paused = false;
467
468
  clearUnitTimeout();
468
469
  deregisterSigtermHandler();
470
+ clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
469
471
  ctx?.ui.notify(
470
472
  "Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
471
473
  "error",
@@ -481,6 +483,7 @@ export async function stopAuto(
481
483
  reason?: string,
482
484
  ): Promise<void> {
483
485
  if (!s.active && !s.paused) return;
486
+ const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
484
487
  const reasonSuffix = reason ? ` — ${reason}` : "";
485
488
  clearUnitTimeout();
486
489
  if (lockBase()) clearLock(lockBase());
@@ -543,6 +546,13 @@ export async function stopAuto(
543
546
  }
544
547
  }
545
548
 
549
+ clearCmuxSidebar(loadedPreferences);
550
+ logCmuxEvent(
551
+ loadedPreferences,
552
+ `Auto-mode stopped${reasonSuffix || ""}.`,
553
+ reason?.startsWith("Blocked:") ? "warning" : "info",
554
+ );
555
+
546
556
  if (isDebugEnabled()) {
547
557
  const logPath = writeDebugSummary();
548
558
  if (logPath) {
@@ -554,6 +564,13 @@ export async function stopAuto(
554
564
  resetRoutingHistory();
555
565
  resetHookState();
556
566
  if (s.basePath) clearPersistedHookState(s.basePath);
567
+
568
+ // Remove paused-session metadata if present (#1383)
569
+ try {
570
+ const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
571
+ if (existsSync(pausedPath)) unlinkSync(pausedPath);
572
+ } catch { /* non-fatal */ }
573
+
557
574
  s.active = false;
558
575
  s.paused = false;
559
576
  s.stepMode = false;
@@ -607,8 +624,32 @@ export async function pauseAuto(
607
624
 
608
625
  s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
609
626
 
610
- if (lockBase()) clearLock(lockBase());
611
- if (lockBase()) releaseSessionLock(lockBase());
627
+ // Persist paused-session metadata so resume survives /exit (#1383).
628
+ // The fresh-start bootstrap checks for this file and restores worktree context.
629
+ try {
630
+ const pausedMeta = {
631
+ milestoneId: s.currentMilestoneId,
632
+ worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
633
+ originalBasePath: s.originalBasePath,
634
+ stepMode: s.stepMode,
635
+ pausedAt: new Date().toISOString(),
636
+ sessionFile: s.pausedSessionFile,
637
+ };
638
+ const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
639
+ mkdirSync(runtimeDir, { recursive: true });
640
+ writeFileSync(
641
+ join(runtimeDir, "paused-session.json"),
642
+ JSON.stringify(pausedMeta, null, 2),
643
+ "utf-8",
644
+ );
645
+ } catch {
646
+ // Non-fatal — resume will still work via full bootstrap, just without worktree context
647
+ }
648
+
649
+ if (lockBase()) {
650
+ releaseSessionLock(lockBase());
651
+ clearLock(lockBase());
652
+ }
612
653
 
613
654
  deregisterSigtermHandler();
614
655
 
@@ -677,6 +718,8 @@ function buildLoopDeps(): LoopDeps {
677
718
  pauseAuto,
678
719
  clearUnitTimeout,
679
720
  updateProgressWidget,
721
+ syncCmuxSidebar,
722
+ logCmuxEvent,
680
723
 
681
724
  // State and cache
682
725
  invalidateAllCaches,
@@ -792,6 +835,30 @@ export async function startAuto(
792
835
  base = escapeStaleWorktree(base);
793
836
 
794
837
  // If resuming from paused state, just re-activate and dispatch next unit.
838
+ // Check persisted paused-session first (#1383) — survives /exit.
839
+ if (!s.paused) {
840
+ try {
841
+ const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
842
+ if (existsSync(pausedPath)) {
843
+ const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
844
+ if (meta.milestoneId) {
845
+ s.currentMilestoneId = meta.milestoneId;
846
+ s.originalBasePath = meta.originalBasePath || base;
847
+ s.stepMode = meta.stepMode ?? requestedStepMode;
848
+ s.paused = true;
849
+ // Clean up the persisted file — we're consuming it
850
+ try { unlinkSync(pausedPath); } catch { /* non-fatal */ }
851
+ ctx.ui.notify(
852
+ `Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`,
853
+ "info",
854
+ );
855
+ }
856
+ }
857
+ } catch {
858
+ // Malformed or missing — proceed with fresh bootstrap
859
+ }
860
+ }
861
+
795
862
  if (s.paused) {
796
863
  const resumeLock = acquireSessionLock(base);
797
864
  if (!resumeLock.acquired) {
@@ -835,6 +902,7 @@ export async function startAuto(
835
902
  restoreHookState(s.basePath);
836
903
  try {
837
904
  await rebuildState(s.basePath);
905
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
838
906
  } catch (e) {
839
907
  debugLog("resume-rebuild-state-failed", {
840
908
  error: e instanceof Error ? e.message : String(e),
@@ -886,6 +954,7 @@ export async function startAuto(
886
954
  s.currentMilestoneId ?? "unknown",
887
955
  s.completedUnits.length,
888
956
  );
957
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
889
958
 
890
959
  await autoLoop(ctx, pi, s, buildLoopDeps());
891
960
  return;
@@ -910,6 +979,13 @@ export async function startAuto(
910
979
  );
911
980
  if (!ready) return;
912
981
 
982
+ try {
983
+ syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
984
+ } catch {
985
+ // Best-effort only — sidebar sync must never block auto-mode startup
986
+ }
987
+ logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
988
+
913
989
  // Dispatch the first unit
914
990
  await autoLoop(ctx, pi, s, buildLoopDeps());
915
991
  }
@@ -1145,6 +1221,9 @@ export async function dispatchHookUnit(
1145
1221
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1146
1222
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
1147
1223
 
1224
+ // Ensure cwd matches basePath before hook dispatch (#1389)
1225
+ try { if (process.cwd() !== s.basePath) process.chdir(s.basePath); } catch {}
1226
+
1148
1227
  debugLog("dispatchHookUnit", {
1149
1228
  phase: "send-message",
1150
1229
  promptLength: hookPrompt.length,