gsd-pi 2.31.2 → 2.32.0-dev.3d7932c

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 (138) hide show
  1. package/README.md +27 -20
  2. package/dist/cli.js +5 -5
  3. package/dist/resource-loader.js +13 -3
  4. package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
  5. package/dist/resources/extensions/gsd/auto-dashboard.ts +23 -27
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  7. package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
  8. package/dist/resources/extensions/gsd/auto-idempotency.ts +3 -2
  9. package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.ts +32 -37
  11. package/dist/resources/extensions/gsd/auto-prompts.ts +84 -78
  12. package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
  13. package/dist/resources/extensions/gsd/auto-start.ts +16 -12
  14. package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  16. package/dist/resources/extensions/gsd/auto-timers.ts +3 -2
  17. package/dist/resources/extensions/gsd/auto-verification.ts +6 -6
  18. package/dist/resources/extensions/gsd/auto-worktree.ts +5 -4
  19. package/dist/resources/extensions/gsd/auto.ts +82 -60
  20. package/dist/resources/extensions/gsd/commands-inspect.ts +2 -1
  21. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
  22. package/dist/resources/extensions/gsd/commands.ts +19 -0
  23. package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
  24. package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
  25. package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  26. package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
  27. package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
  28. package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
  29. package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
  30. package/dist/resources/extensions/gsd/doctor.ts +6 -0
  31. package/dist/resources/extensions/gsd/error-utils.ts +6 -0
  32. package/dist/resources/extensions/gsd/export.ts +2 -1
  33. package/dist/resources/extensions/gsd/git-service.ts +12 -2
  34. package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  35. package/dist/resources/extensions/gsd/guided-flow.ts +3 -2
  36. package/dist/resources/extensions/gsd/health-widget.ts +167 -0
  37. package/dist/resources/extensions/gsd/index.ts +18 -5
  38. package/dist/resources/extensions/gsd/key-manager.ts +2 -1
  39. package/dist/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  40. package/dist/resources/extensions/gsd/metrics.ts +3 -3
  41. package/dist/resources/extensions/gsd/migrate-external.ts +21 -4
  42. package/dist/resources/extensions/gsd/milestone-ids.ts +2 -1
  43. package/dist/resources/extensions/gsd/native-git-bridge.ts +2 -1
  44. package/dist/resources/extensions/gsd/parallel-merge.ts +2 -1
  45. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  46. package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  47. package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
  48. package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
  49. package/dist/resources/extensions/gsd/progress-score.ts +273 -0
  50. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
  51. package/dist/resources/extensions/gsd/quick.ts +61 -8
  52. package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
  53. package/dist/resources/extensions/gsd/session-lock.ts +12 -1
  54. package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  55. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  57. package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  58. package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  59. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  60. package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  61. package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
  62. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  63. package/dist/resources/extensions/gsd/undo.ts +5 -7
  64. package/dist/resources/extensions/gsd/unit-id.ts +14 -0
  65. package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
  66. package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
  67. package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
  68. package/dist/resources/extensions/gsd/worktree-command.ts +8 -7
  69. package/dist/worktree-cli.d.ts +42 -6
  70. package/dist/worktree-cli.js +88 -48
  71. package/package.json +1 -1
  72. package/packages/pi-coding-agent/package.json +1 -1
  73. package/pkg/package.json +1 -1
  74. package/src/resources/extensions/gsd/auto-constants.ts +6 -0
  75. package/src/resources/extensions/gsd/auto-dashboard.ts +23 -27
  76. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  77. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
  78. package/src/resources/extensions/gsd/auto-idempotency.ts +3 -2
  79. package/src/resources/extensions/gsd/auto-observability.ts +2 -4
  80. package/src/resources/extensions/gsd/auto-post-unit.ts +32 -37
  81. package/src/resources/extensions/gsd/auto-prompts.ts +84 -78
  82. package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
  83. package/src/resources/extensions/gsd/auto-start.ts +16 -12
  84. package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  85. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  86. package/src/resources/extensions/gsd/auto-timers.ts +3 -2
  87. package/src/resources/extensions/gsd/auto-verification.ts +6 -6
  88. package/src/resources/extensions/gsd/auto-worktree.ts +5 -4
  89. package/src/resources/extensions/gsd/auto.ts +82 -60
  90. package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
  91. package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
  92. package/src/resources/extensions/gsd/commands.ts +19 -0
  93. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
  94. package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
  95. package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  96. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
  97. package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
  98. package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
  99. package/src/resources/extensions/gsd/doctor-types.ts +14 -1
  100. package/src/resources/extensions/gsd/doctor.ts +6 -0
  101. package/src/resources/extensions/gsd/error-utils.ts +6 -0
  102. package/src/resources/extensions/gsd/export.ts +2 -1
  103. package/src/resources/extensions/gsd/git-service.ts +12 -2
  104. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  105. package/src/resources/extensions/gsd/guided-flow.ts +3 -2
  106. package/src/resources/extensions/gsd/health-widget.ts +167 -0
  107. package/src/resources/extensions/gsd/index.ts +18 -5
  108. package/src/resources/extensions/gsd/key-manager.ts +2 -1
  109. package/src/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  110. package/src/resources/extensions/gsd/metrics.ts +3 -3
  111. package/src/resources/extensions/gsd/migrate-external.ts +21 -4
  112. package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
  113. package/src/resources/extensions/gsd/native-git-bridge.ts +2 -1
  114. package/src/resources/extensions/gsd/parallel-merge.ts +2 -1
  115. package/src/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  116. package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  117. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  118. package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
  119. package/src/resources/extensions/gsd/progress-score.ts +273 -0
  120. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
  121. package/src/resources/extensions/gsd/quick.ts +61 -8
  122. package/src/resources/extensions/gsd/repo-identity.ts +22 -1
  123. package/src/resources/extensions/gsd/session-lock.ts +12 -1
  124. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  125. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  126. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  127. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  128. package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  129. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  130. package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  131. package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
  132. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  133. package/src/resources/extensions/gsd/undo.ts +5 -7
  134. package/src/resources/extensions/gsd/unit-id.ts +14 -0
  135. package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
  136. package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
  137. package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
  138. package/src/resources/extensions/gsd/worktree-command.ts +8 -7
@@ -105,6 +105,7 @@ import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.j
105
105
  import { GSDError, GSD_ARTIFACT_MISSING } from "./errors.js";
106
106
  import { join } from "node:path";
107
107
  import { sep as pathSep } from "node:path";
108
+ import { parseUnitId } from "./unit-id.js";
108
109
  import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync } from "node:fs";
109
110
  import { atomicWriteSync } from "./atomic-write.js";
110
111
  import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
@@ -118,7 +119,7 @@ import {
118
119
  parseSliceBranch,
119
120
  setActiveMilestoneId,
120
121
  } from "./worktree.js";
121
- import { GitServiceImpl, type TaskCommitContext } from "./git-service.js";
122
+ import { createGitService, type TaskCommitContext } from "./git-service.js";
122
123
  import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
123
124
  import { formatGitError } from "./git-self-heal.js";
124
125
  import {
@@ -189,6 +190,7 @@ import {
189
190
  NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
190
191
  } from "./auto/session.js";
191
192
  import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js";
193
+ import { getErrorMessage } from "./error-utils.js";
192
194
 
193
195
  // ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
194
196
  // ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
@@ -204,8 +206,7 @@ import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerifi
204
206
  // ─────────────────────────────────────────────────────────────────────────────
205
207
  const s = new AutoSession();
206
208
 
207
- /** Throttle STATE.md rebuilds at most once per 30 seconds */
208
- const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
209
+ import { STATE_REBUILD_MIN_INTERVAL_MS } from "./auto-constants.js";
209
210
 
210
211
  export function shouldUseWorktreeIsolation(): boolean {
211
212
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
@@ -429,7 +430,7 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
429
430
  try {
430
431
  await dispatchNextUnit(ctx, pi);
431
432
  } catch (retryErr) {
432
- const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
433
+ const message = getErrorMessage(retryErr);
433
434
  await stopAuto(ctx, pi, `Dispatch gap recovery failed: ${message}`);
434
435
  return;
435
436
  }
@@ -459,14 +460,14 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
459
460
  // ── Auto-worktree: exit worktree and reset s.basePath on stop ──
460
461
  if (s.currentMilestoneId && isInAutoWorktree(s.basePath)) {
461
462
  try {
462
- try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: e instanceof Error ? e.message : String(e) }); }
463
+ try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: getErrorMessage(e) }); }
463
464
  teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId, { preserveBranch: true });
464
465
  s.basePath = s.originalBasePath;
465
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
466
+ s.gitService = createGitService(s.basePath);
466
467
  ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
467
468
  } catch (err) {
468
469
  ctx?.ui.notify(
469
- `Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
470
+ `Auto-worktree teardown failed: ${getErrorMessage(err)}`,
470
471
  "warning",
471
472
  );
472
473
  }
@@ -477,7 +478,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
477
478
  try {
478
479
  const { closeDatabase } = await import("./gsd-db.js");
479
480
  closeDatabase();
480
- } catch (e) { debugLog("db-close-failed", { error: e instanceof Error ? e.message : String(e) }); }
481
+ } catch (e) { debugLog("db-close-failed", { error: getErrorMessage(e) }); }
481
482
  }
482
483
 
483
484
  if (s.originalBasePath) {
@@ -497,7 +498,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
497
498
  }
498
499
 
499
500
  if (s.basePath) {
500
- try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error: e instanceof Error ? e.message : String(e) }); }
501
+ try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error: getErrorMessage(e) }); }
501
502
  }
502
503
 
503
504
  if (isDebugEnabled()) {
@@ -626,17 +627,17 @@ export async function startAuto(
626
627
  if (existingWtPath) {
627
628
  const wtPath = enterAutoWorktree(s.originalBasePath, s.currentMilestoneId);
628
629
  s.basePath = wtPath;
629
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
630
+ s.gitService = createGitService(s.basePath);
630
631
  ctx.ui.notify(`Re-entered auto-worktree at ${wtPath}`, "info");
631
632
  } else {
632
633
  const wtPath = createAutoWorktree(s.originalBasePath, s.currentMilestoneId);
633
634
  s.basePath = wtPath;
634
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
635
+ s.gitService = createGitService(s.basePath);
635
636
  ctx.ui.notify(`Recreated auto-worktree at ${wtPath}`, "info");
636
637
  }
637
638
  } catch (err) {
638
639
  ctx.ui.notify(
639
- `Auto-worktree re-entry failed: ${err instanceof Error ? err.message : String(err)}. Continuing at current path.`,
640
+ `Auto-worktree re-entry failed: ${getErrorMessage(err)}. Continuing at current path.`,
640
641
  "warning",
641
642
  );
642
643
  }
@@ -648,13 +649,13 @@ export async function startAuto(
648
649
  ctx.ui.setFooter(hideFooter);
649
650
  ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
650
651
  restoreHookState(s.basePath);
651
- try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error: e instanceof Error ? e.message : String(e) }); }
652
+ try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error: getErrorMessage(e) }); }
652
653
  try {
653
654
  const report = await runGSDDoctor(s.basePath, { fix: true });
654
655
  if (report.fixesApplied.length > 0) {
655
656
  ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
656
657
  }
657
- } catch (e) { debugLog("resume-doctor-failed", { error: e instanceof Error ? e.message : String(e) }); }
658
+ } catch (e) { debugLog("resume-doctor-failed", { error: getErrorMessage(e) }); }
658
659
  await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet);
659
660
  invalidateAllCaches();
660
661
 
@@ -701,7 +702,7 @@ export async function startAuto(
701
702
  }
702
703
  } catch (err) {
703
704
  ctx.ui.notify(
704
- `Secrets check error: ${err instanceof Error ? err.message : String(err)}. Continuing without secrets.`,
705
+ `Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`,
705
706
  "warning",
706
707
  );
707
708
  }
@@ -808,7 +809,7 @@ export async function handleAgentEnd(
808
809
  try {
809
810
  await dispatchNextUnit(ctx, pi);
810
811
  } catch (dispatchErr) {
811
- const message = dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr);
812
+ const message = getErrorMessage(dispatchErr);
812
813
  ctx.ui.notify(
813
814
  `Dispatch error after unit completion: ${message}. Retrying in ${DISPATCH_GAP_TIMEOUT_MS / 1000}s.`,
814
815
  "error",
@@ -834,9 +835,12 @@ export async function handleAgentEnd(
834
835
  // permanently stalled with no unit running and no watchdog set.
835
836
  if (s.pendingAgentEndRetry) {
836
837
  s.pendingAgentEndRetry = false;
838
+ // Clear gap watchdog from the previous cycle to prevent concurrent
839
+ // dispatch when the deferred handleAgentEnd calls dispatchNextUnit (#1272).
840
+ clearDispatchGapWatchdog();
837
841
  setImmediate(() => {
838
842
  handleAgentEnd(ctx, pi).catch((err) => {
839
- const msg = err instanceof Error ? err.message : String(err);
843
+ const msg = getErrorMessage(err);
840
844
  ctx.ui.notify(`Deferred agent_end retry failed: ${msg}`, "error");
841
845
  pauseAuto(ctx, pi).catch(() => {});
842
846
  });
@@ -976,8 +980,12 @@ async function dispatchNextUnit(
976
980
  return;
977
981
  }
978
982
 
979
- // Reentrancy guard
980
- if (s.dispatching && s.skipDepth === 0) {
983
+ // Reentrancy guard — unconditional to prevent concurrent dispatch from
984
+ // gap watchdog or pendingAgentEndRetry during skip chains (#1272).
985
+ // Previously the guard was bypassed when skipDepth > 0, but the recursive
986
+ // skip chain's inner finally block resets s.dispatching = false before the
987
+ // outer call's finally runs, opening a window for concurrent entry.
988
+ if (s.dispatching) {
981
989
  debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
982
990
  return;
983
991
  }
@@ -1080,7 +1088,7 @@ async function dispatchNextUnit(
1080
1088
  );
1081
1089
  } catch (err) {
1082
1090
  ctx.ui.notify(
1083
- `Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
1091
+ `Report generation failed: ${getErrorMessage(err)}`,
1084
1092
  "warning",
1085
1093
  );
1086
1094
  }
@@ -1096,7 +1104,7 @@ async function dispatchNextUnit(
1096
1104
  atomicWriteSync(file, JSON.stringify([]));
1097
1105
  }
1098
1106
  s.completedKeySet.clear();
1099
- } catch (e) { debugLog("completed-keys-reset-failed", { error: e instanceof Error ? e.message : String(e) }); }
1107
+ } catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); }
1100
1108
 
1101
1109
  // ── Worktree lifecycle on milestone transition (#616) ──
1102
1110
  if (isInAutoWorktree(s.basePath) && s.originalBasePath && shouldUseWorktreeIsolation()) {
@@ -1115,7 +1123,7 @@ async function dispatchNextUnit(
1115
1123
  }
1116
1124
  } catch (err) {
1117
1125
  ctx.ui.notify(
1118
- `Milestone merge failed during transition: ${err instanceof Error ? err.message : String(err)}`,
1126
+ `Milestone merge failed during transition: ${getErrorMessage(err)}`,
1119
1127
  "warning",
1120
1128
  );
1121
1129
  if (s.originalBasePath) {
@@ -1124,7 +1132,7 @@ async function dispatchNextUnit(
1124
1132
  }
1125
1133
 
1126
1134
  s.basePath = s.originalBasePath;
1127
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1135
+ s.gitService = createGitService(s.basePath);
1128
1136
  invalidateAllCaches();
1129
1137
 
1130
1138
  state = await deriveState(s.basePath);
@@ -1136,11 +1144,11 @@ async function dispatchNextUnit(
1136
1144
  try {
1137
1145
  const wtPath = createAutoWorktree(s.basePath, mid);
1138
1146
  s.basePath = wtPath;
1139
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1147
+ s.gitService = createGitService(s.basePath);
1140
1148
  ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
1141
1149
  } catch (err) {
1142
1150
  ctx.ui.notify(
1143
- `Auto-worktree creation for ${mid} failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
1151
+ `Auto-worktree creation for ${mid} failed: ${getErrorMessage(err)}. Continuing in project root.`,
1144
1152
  "warning",
1145
1153
  );
1146
1154
  }
@@ -1176,7 +1184,7 @@ async function dispatchNextUnit(
1176
1184
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1177
1185
  const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
1178
1186
  s.basePath = s.originalBasePath;
1179
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1187
+ s.gitService = createGitService(s.basePath);
1180
1188
  ctx.ui.notify(
1181
1189
  `Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1182
1190
  "info",
@@ -1184,7 +1192,7 @@ async function dispatchNextUnit(
1184
1192
  }
1185
1193
  } catch (err) {
1186
1194
  ctx.ui.notify(
1187
- `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
1195
+ `Milestone merge failed: ${getErrorMessage(err)}`,
1188
1196
  "warning",
1189
1197
  );
1190
1198
  if (s.originalBasePath) {
@@ -1201,7 +1209,7 @@ async function dispatchNextUnit(
1201
1209
  if (roadmapPath) {
1202
1210
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1203
1211
  const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
1204
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1212
+ s.gitService = createGitService(s.basePath);
1205
1213
  ctx.ui.notify(
1206
1214
  `Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
1207
1215
  "info",
@@ -1210,7 +1218,7 @@ async function dispatchNextUnit(
1210
1218
  }
1211
1219
  } catch (err) {
1212
1220
  ctx.ui.notify(
1213
- `Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
1221
+ `Milestone merge failed (branch mode): ${getErrorMessage(err)}`,
1214
1222
  "warning",
1215
1223
  );
1216
1224
  }
@@ -1270,7 +1278,7 @@ async function dispatchNextUnit(
1270
1278
  atomicWriteSync(file, JSON.stringify([]));
1271
1279
  }
1272
1280
  s.completedKeySet.clear();
1273
- } catch (e) { debugLog("completed-keys-reset-failed", { error: e instanceof Error ? e.message : String(e) }); }
1281
+ } catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); }
1274
1282
  // ── Milestone merge ──
1275
1283
  if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {
1276
1284
  try {
@@ -1279,14 +1287,14 @@ async function dispatchNextUnit(
1279
1287
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1280
1288
  const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
1281
1289
  s.basePath = s.originalBasePath;
1282
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1290
+ s.gitService = createGitService(s.basePath);
1283
1291
  ctx.ui.notify(
1284
1292
  `Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1285
1293
  "info",
1286
1294
  );
1287
1295
  } catch (err) {
1288
1296
  ctx.ui.notify(
1289
- `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
1297
+ `Milestone merge failed: ${getErrorMessage(err)}`,
1290
1298
  "warning",
1291
1299
  );
1292
1300
  if (s.originalBasePath) {
@@ -1303,7 +1311,7 @@ async function dispatchNextUnit(
1303
1311
  if (roadmapPath) {
1304
1312
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1305
1313
  const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
1306
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1314
+ s.gitService = createGitService(s.basePath);
1307
1315
  ctx.ui.notify(
1308
1316
  `Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
1309
1317
  "info",
@@ -1312,7 +1320,7 @@ async function dispatchNextUnit(
1312
1320
  }
1313
1321
  } catch (err) {
1314
1322
  ctx.ui.notify(
1315
- `Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
1323
+ `Milestone merge failed (branch mode): ${getErrorMessage(err)}`,
1316
1324
  "warning",
1317
1325
  );
1318
1326
  }
@@ -1411,7 +1419,7 @@ async function dispatchNextUnit(
1411
1419
  }
1412
1420
  } catch (err) {
1413
1421
  ctx.ui.notify(
1414
- `Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`,
1422
+ `Secrets collection error: ${getErrorMessage(err)}. Continuing with next task.`,
1415
1423
  "warning",
1416
1424
  );
1417
1425
  }
@@ -1449,15 +1457,18 @@ async function dispatchNextUnit(
1449
1457
  }
1450
1458
 
1451
1459
  if (dispatchResult.action !== "dispatch") {
1452
- await new Promise(r => setImmediate(r));
1453
- await dispatchNextUnit(ctx, pi);
1460
+ // Defer re-dispatch to next microtask so s.dispatching is released first,
1461
+ // preventing reentrancy guard bypass during concurrent entry (#1272).
1462
+ setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
1463
+ ctx.ui.notify(`Deferred dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
1464
+ pauseAuto(ctx, pi).catch(() => {});
1465
+ }));
1454
1466
  return;
1455
1467
  }
1456
1468
 
1457
1469
  unitType = dispatchResult.unitType;
1458
1470
  unitId = dispatchResult.unitId;
1459
1471
  prompt = dispatchResult.prompt;
1460
- let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
1461
1472
 
1462
1473
  // ── Pre-dispatch hooks ──
1463
1474
  const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
@@ -1469,8 +1480,10 @@ async function dispatchNextUnit(
1469
1480
  }
1470
1481
  if (preDispatchResult.action === "skip") {
1471
1482
  ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
1472
- await new Promise(r => setImmediate(r));
1473
- await dispatchNextUnit(ctx, pi);
1483
+ setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
1484
+ ctx.ui.notify(`Deferred dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
1485
+ pauseAuto(ctx, pi).catch(() => {});
1486
+ }));
1474
1487
  return;
1475
1488
  }
1476
1489
  if (preDispatchResult.action === "replace") {
@@ -1501,9 +1514,16 @@ async function dispatchNextUnit(
1501
1514
  if (idempotencyResult.reason === "completed" || idempotencyResult.reason === "fallback-persisted" || idempotencyResult.reason === "phantom-loop-cleared" || idempotencyResult.reason === "evicted") {
1502
1515
  if (!s.active) return;
1503
1516
  s.skipDepth++;
1504
- await new Promise(r => setTimeout(r, idempotencyResult.reason === "phantom-loop-cleared" ? 50 : 150));
1505
- await dispatchNextUnit(ctx, pi);
1506
- s.skipDepth = Math.max(0, s.skipDepth - 1);
1517
+ const skipDelay = idempotencyResult.reason === "phantom-loop-cleared" ? 50 : 150;
1518
+ // Defer re-dispatch so s.dispatching is released first (#1272).
1519
+ setTimeout(() => {
1520
+ dispatchNextUnit(ctx, pi).catch(err => {
1521
+ ctx.ui.notify(`Deferred skip-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
1522
+ pauseAuto(ctx, pi).catch(() => {});
1523
+ }).finally(() => {
1524
+ s.skipDepth = Math.max(0, s.skipDepth - 1);
1525
+ });
1526
+ }, skipDelay);
1507
1527
  return;
1508
1528
  }
1509
1529
  } else if (idempotencyResult.action === "stop") {
@@ -1534,8 +1554,11 @@ async function dispatchNextUnit(
1534
1554
  return;
1535
1555
  }
1536
1556
  if (stuckResult.action === "recovered" && stuckResult.dispatchAgain) {
1537
- await new Promise(r => setImmediate(r));
1538
- await dispatchNextUnit(ctx, pi);
1557
+ // Defer re-dispatch so s.dispatching is released first (#1272).
1558
+ setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
1559
+ ctx.ui.notify(`Deferred recovery-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
1560
+ pauseAuto(ctx, pi).catch(() => {});
1561
+ }));
1539
1562
  return;
1540
1563
  }
1541
1564
 
@@ -1607,7 +1630,7 @@ async function dispatchNextUnit(
1607
1630
  );
1608
1631
  result = await Promise.race([sessionPromise, timeoutPromise]);
1609
1632
  } catch (sessionErr) {
1610
- const msg = sessionErr instanceof Error ? sessionErr.message : String(sessionErr);
1633
+ const msg = getErrorMessage(sessionErr);
1611
1634
  ctx.ui.notify(`Session creation failed: ${msg}. Retrying via watchdog.`, "error");
1612
1635
  throw new Error(`newSession() failed: ${msg}`);
1613
1636
  }
@@ -1683,7 +1706,7 @@ async function dispatchNextUnit(
1683
1706
  const { reorderForCaching } = await import("./prompt-ordering.js");
1684
1707
  finalPrompt = reorderForCaching(finalPrompt);
1685
1708
  } catch (reorderErr) {
1686
- const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
1709
+ const msg = getErrorMessage(reorderErr);
1687
1710
  process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
1688
1711
  }
1689
1712
 
@@ -1712,13 +1735,6 @@ async function dispatchNextUnit(
1712
1735
  { triggerTurn: true },
1713
1736
  );
1714
1737
 
1715
- if (pauseAfterUatDispatch) {
1716
- ctx.ui.notify(
1717
- "UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
1718
- "info",
1719
- );
1720
- await pauseAuto(ctx, pi);
1721
- }
1722
1738
  } finally {
1723
1739
  s.dispatching = false;
1724
1740
  }
@@ -1733,8 +1749,7 @@ async function dispatchNextUnit(
1733
1749
  function ensurePreconditions(
1734
1750
  unitType: string, unitId: string, base: string, state: GSDState,
1735
1751
  ): void {
1736
- const parts = unitId.split("/");
1737
- const mid = parts[0]!;
1752
+ const { milestone: mid } = parseUnitId(unitId);
1738
1753
 
1739
1754
  const mDir = resolveMilestonePath(base, mid);
1740
1755
  if (!mDir) {
@@ -1742,8 +1757,8 @@ function ensurePreconditions(
1742
1757
  mkdirSync(join(newDir, "slices"), { recursive: true });
1743
1758
  }
1744
1759
 
1745
- if (parts.length >= 2) {
1746
- const sid = parts[1]!;
1760
+ const sid = parseUnitId(unitId).slice;
1761
+ if (sid) {
1747
1762
 
1748
1763
  const mDirResolved = resolveMilestonePath(base, mid);
1749
1764
  if (mDirResolved) {
@@ -1788,6 +1803,15 @@ export {
1788
1803
  export function _getUnitConsecutiveSkips(): Map<string, number> { return s.unitConsecutiveSkips; }
1789
1804
  export function _resetUnitConsecutiveSkips(): void { s.unitConsecutiveSkips.clear(); }
1790
1805
 
1806
+ /**
1807
+ * Test-only: expose dispatching / skipDepth state for reentrancy guard tests.
1808
+ * Not part of the public API.
1809
+ */
1810
+ export function _getDispatching(): boolean { return s.dispatching; }
1811
+ export function _setDispatching(v: boolean): void { s.dispatching = v; }
1812
+ export function _getSkipDepth(): number { return s.skipDepth; }
1813
+ export function _setSkipDepth(v: number): void { s.skipDepth = v; }
1814
+
1791
1815
  /**
1792
1816
  * Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
1793
1817
  * Used for manual hook triggers via /gsd run-hook.
@@ -1874,8 +1898,6 @@ export async function dispatchHookUnit(
1874
1898
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1875
1899
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
1876
1900
 
1877
- console.log(`[dispatchHookUnit] Sending prompt of length ${hookPrompt.length}`);
1878
- console.log(`[dispatchHookUnit] Prompt preview: ${hookPrompt.substring(0, 200)}...`);
1879
1901
  pi.sendMessage(
1880
1902
  { customType: "gsd-auto", content: hookPrompt, display: true },
1881
1903
  { triggerTurn: true },
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
8
+ import { getErrorMessage } from "./error-utils.js";
8
9
 
9
10
  export interface InspectData {
10
11
  schemaVersion: number | null;
@@ -84,7 +85,7 @@ export async function handleInspect(ctx: ExtensionCommandContext): Promise<void>
84
85
 
85
86
  ctx.ui.notify(formatInspectOutput(data), "info");
86
87
  } catch (err) {
87
- process.stderr.write(`gsd-db: /gsd inspect failed: ${err instanceof Error ? err.message : String(err)}\n`);
88
+ process.stderr.write(`gsd-db: /gsd inspect failed: ${getErrorMessage(err)}\n`);
88
89
  ctx.ui.notify("Failed to inspect GSD database. Check stderr for details.", "error");
89
90
  }
90
91
  }
@@ -19,9 +19,9 @@ import {
19
19
  } from "./workflow-templates.js";
20
20
  import { loadPrompt } from "./prompt-loader.js";
21
21
  import { gsdRoot } from "./paths.js";
22
- import { GitServiceImpl, runGit } from "./git-service.js";
23
- import { loadEffectiveGSDPreferences } from "./preferences.js";
22
+ import { createGitService, runGit } from "./git-service.js";
24
23
  import { isAutoActive, isAutoPaused } from "./auto.js";
24
+ import { getErrorMessage } from "./error-utils.js";
25
25
 
26
26
  // ─── Helpers ─────────────────────────────────────────────────────────────────
27
27
 
@@ -423,9 +423,8 @@ export async function handleStart(
423
423
 
424
424
  // ─── Create git branch (unless isolation: none) ─────────────────────────
425
425
 
426
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
427
- const git = new GitServiceImpl(basePath, gitPrefs);
428
- const skipBranch = gitPrefs.isolation === "none";
426
+ const git = createGitService(basePath);
427
+ const skipBranch = git.prefs.isolation === "none";
429
428
  const slug = slugify(description || templateId);
430
429
  const branchName = `gsd/${templateId}/${slug}`;
431
430
  let branchCreated = false;
@@ -441,7 +440,7 @@ export async function handleStart(
441
440
  branchCreated = true;
442
441
  }
443
442
  } catch (err) {
444
- const message = err instanceof Error ? err.message : String(err);
443
+ const message = getErrorMessage(err);
445
444
  ctx.ui.notify(
446
445
  `Could not create branch ${branchName}: ${message}. Working on current branch.`,
447
446
  "warning",
@@ -44,6 +44,8 @@ import { handleConfig } from "./commands-config.js";
44
44
  import { handleInspect } from "./commands-inspect.js";
45
45
  import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
46
46
  import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
47
+ import { computeProgressScore, formatProgressLine } from "./progress-score.js";
48
+ import { runEnvironmentChecks } from "./doctor-environment.js";
47
49
  import { handleLogs } from "./commands-logs.js";
48
50
  import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
49
51
 
@@ -1068,6 +1070,11 @@ async function handleSetup(args: string, ctx: ExtensionCommandContext): Promise<
1068
1070
  function formatTextStatus(state: GSDState): string {
1069
1071
  const lines: string[] = ["GSD Status\n"];
1070
1072
 
1073
+ // Progress score — traffic light (#1221)
1074
+ const progressScore = computeProgressScore();
1075
+ lines.push(formatProgressLine(progressScore));
1076
+ lines.push("");
1077
+
1071
1078
  // Phase
1072
1079
  lines.push(`Phase: ${state.phase}`);
1073
1080
 
@@ -1114,5 +1121,17 @@ function formatTextStatus(state: GSDState): string {
1114
1121
  }
1115
1122
  }
1116
1123
 
1124
+ // Environment health (#1221)
1125
+ const envResults = runEnvironmentChecks(projectRoot());
1126
+ const envIssues = envResults.filter(r => r.status !== "ok");
1127
+ if (envIssues.length > 0) {
1128
+ lines.push("");
1129
+ lines.push("Environment:");
1130
+ for (const r of envIssues) {
1131
+ const icon = r.status === "error" ? "✗" : "⚠";
1132
+ lines.push(` ${icon} ${r.message}`);
1133
+ }
1134
+ }
1135
+
1117
1136
  return lines.join("\n");
1118
1137
  }
@@ -6,6 +6,7 @@ import { existsSync, readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { gsdRoot } from "./paths.js";
8
8
  import { getAdaptiveTierAdjustment } from "./routing-history.js";
9
+ import { parseUnitId } from "./unit-id.js";
9
10
 
10
11
  // ─── Types ───────────────────────────────────────────────────────────────────
11
12
 
@@ -180,15 +181,14 @@ function analyzePlanComplexity(
180
181
  basePath: string,
181
182
  ): TaskAnalysis | null {
182
183
  // Check if this is a milestone-level plan (more complex) vs single slice
183
- const parts = unitId.split("/");
184
- if (parts.length === 1) {
184
+ const { milestone: mid, slice: sid } = parseUnitId(unitId);
185
+ if (!sid) {
185
186
  // Milestone-level planning is always at least standard
186
187
  return { tier: "standard", reason: "milestone-level planning" };
187
188
  }
188
189
 
189
190
  // For slice planning, try to read the context/research to gauge complexity
190
191
  // If research exists and is large, bump to heavy
191
- const [mid, sid] = parts;
192
192
  const researchPath = join(gsdRoot(basePath), mid, "slices", sid, "RESEARCH.md");
193
193
  try {
194
194
  if (existsSync(researchPath)) {
@@ -210,10 +210,8 @@ function analyzePlanComplexity(
210
210
  */
211
211
  function extractTaskMetadata(unitId: string, basePath: string): TaskMetadata {
212
212
  const meta: TaskMetadata = {};
213
- const parts = unitId.split("/");
214
- if (parts.length !== 3) return meta;
215
-
216
- const [mid, sid, tid] = parts;
213
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
214
+ if (!mid || !sid || !tid) return meta;
217
215
  const taskPlanPath = join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-PLAN.md`);
218
216
 
219
217
  try {
@@ -98,11 +98,24 @@ export function isLockProcessAlive(lock: LockData): boolean {
98
98
 
99
99
  /** Format crash info for display or injection into a prompt. */
100
100
  export function formatCrashInfo(lock: LockData): string {
101
- return [
101
+ const lines = [
102
102
  `Previous auto-mode session was interrupted.`,
103
103
  ` Was executing: ${lock.unitType} (${lock.unitId})`,
104
104
  ` Started at: ${lock.unitStartedAt}`,
105
105
  ` Units completed before crash: ${lock.completedUnits}`,
106
106
  ` PID: ${lock.pid}`,
107
- ].join("\n");
107
+ ];
108
+
109
+ // Add recovery guidance based on what was happening when it crashed
110
+ if (lock.unitType === "starting" && lock.unitId === "bootstrap" && lock.completedUnits === 0) {
111
+ lines.push(`No work was lost. Run /gsd auto to restart.`);
112
+ } else if (lock.unitType.includes("research") || lock.unitType.includes("plan")) {
113
+ lines.push(`The ${lock.unitType} unit may be incomplete. Run /gsd auto to re-run it.`);
114
+ } else if (lock.unitType.includes("execute")) {
115
+ lines.push(`Task execution was interrupted. Run /gsd auto to resume — completed work is preserved.`);
116
+ } else if (lock.unitType.includes("complete")) {
117
+ lines.push(`Slice/milestone completion was interrupted. Run /gsd auto to finish.`);
118
+ }
119
+
120
+ return lines.join("\n");
108
121
  }
@@ -23,6 +23,8 @@ import { getActiveWorktreeName } from "./worktree-command.js";
23
23
  import { getWorkerBatches, hasActiveWorkers, type WorkerEntry } from "../subagent/worker-registry.js";
24
24
  import { formatDuration, padRight, joinColumns, centerLine, fitColumns, STATUS_GLYPH, STATUS_COLOR } from "../shared/mod.js";
25
25
  import { estimateTimeRemaining } from "./auto-dashboard.js";
26
+ import { computeProgressScore, formatProgressLine } from "./progress-score.js";
27
+ import { runEnvironmentChecks, type EnvironmentCheckResult } from "./doctor-environment.js";
26
28
 
27
29
  function unitLabel(type: string): string {
28
30
  switch (type) {
@@ -310,6 +312,15 @@ export class GSDDashboardOverlay {
310
312
  elapsedParts = th.fg("dim", `since ${this.dashData.remoteSession!.startedAt.replace("T", " ").slice(0, 19)}`);
311
313
  }
312
314
  lines.push(row(joinColumns(`${title} ${status}${worktreeTag}`, elapsedParts, contentWidth)));
315
+
316
+ // Progress score — traffic light indicator (#1221)
317
+ if (this.dashData.active || this.dashData.paused) {
318
+ const progressScore = computeProgressScore();
319
+ const progressIcon = progressScore.level === "green" ? th.fg("success", "●")
320
+ : progressScore.level === "yellow" ? th.fg("warning", "●")
321
+ : th.fg("error", "●");
322
+ lines.push(row(`${progressIcon} ${th.fg("text", progressScore.summary)}`));
323
+ }
313
324
  lines.push(blank());
314
325
 
315
326
  if (this.dashData.currentUnit) {
@@ -579,6 +590,23 @@ export class GSDDashboardOverlay {
579
590
  }
580
591
  }
581
592
 
593
+ // Environment health section (#1221) — only show issues
594
+ const envResults = runEnvironmentChecks(this.dashData.basePath || process.cwd());
595
+ const envIssues = envResults.filter(r => r.status !== "ok");
596
+ if (envIssues.length > 0) {
597
+ lines.push(blank());
598
+ lines.push(hr());
599
+ lines.push(row(th.fg("text", th.bold("Environment"))));
600
+ lines.push(blank());
601
+ for (const r of envIssues) {
602
+ const icon = r.status === "error" ? th.fg("error", "✗") : th.fg("warning", "⚠");
603
+ lines.push(row(` ${icon} ${th.fg("text", r.message)}`));
604
+ if (r.detail) {
605
+ lines.push(row(th.fg("dim", ` ${r.detail}`)));
606
+ }
607
+ }
608
+ }
609
+
582
610
  lines.push(blank());
583
611
  lines.push(hr());
584
612
  lines.push(centered(th.fg("dim", "↑↓ scroll · g/G top/end · esc close")));
@@ -5,6 +5,7 @@ import { readdirSync } from "node:fs";
5
5
  import { resolveMilestoneFile, milestonesDir } from "./paths.js";
6
6
  import { parseRoadmapSlices } from "./roadmap-slices.js";
7
7
  import { findMilestoneIds } from "./guided-flow.js";
8
+ import { parseUnitId } from "./unit-id.js";
8
9
 
9
10
  const SLICE_DISPATCH_TYPES = new Set([
10
11
  "research-slice",
@@ -39,7 +40,7 @@ function readRoadmapFromDisk(base: string, milestoneId: string): string | null {
39
40
  export function getPriorSliceCompletionBlocker(base: string, _mainBranch: string, unitType: string, unitId: string): string | null {
40
41
  if (!SLICE_DISPATCH_TYPES.has(unitType)) return null;
41
42
 
42
- const [targetMid, targetSid] = unitId.split("/");
43
+ const { milestone: targetMid, slice: targetSid } = parseUnitId(unitId);
43
44
  if (!targetMid || !targetSid) return null;
44
45
 
45
46
  // Use findMilestoneIds to respect custom queue order.