gsd-pi 2.32.0-dev.1e39869 → 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 (83) hide show
  1. package/README.md +27 -20
  2. package/dist/resource-loader.js +13 -3
  3. package/dist/resources/extensions/gsd/auto-dashboard.ts +3 -1
  4. package/dist/resources/extensions/gsd/auto-idempotency.ts +3 -2
  5. package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
  6. package/dist/resources/extensions/gsd/auto-post-unit.ts +5 -5
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +46 -44
  8. package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
  9. package/dist/resources/extensions/gsd/auto-start.ts +8 -6
  10. package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  11. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  12. package/dist/resources/extensions/gsd/auto-timers.ts +3 -2
  13. package/dist/resources/extensions/gsd/auto-verification.ts +6 -6
  14. package/dist/resources/extensions/gsd/auto-worktree.ts +5 -4
  15. package/dist/resources/extensions/gsd/auto.ts +28 -27
  16. package/dist/resources/extensions/gsd/commands-inspect.ts +2 -1
  17. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +2 -1
  18. package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
  19. package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
  20. package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
  21. package/dist/resources/extensions/gsd/error-utils.ts +6 -0
  22. package/dist/resources/extensions/gsd/export.ts +2 -1
  23. package/dist/resources/extensions/gsd/git-service.ts +3 -2
  24. package/dist/resources/extensions/gsd/guided-flow.ts +3 -2
  25. package/dist/resources/extensions/gsd/index.ts +12 -5
  26. package/dist/resources/extensions/gsd/key-manager.ts +2 -1
  27. package/dist/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  28. package/dist/resources/extensions/gsd/metrics.ts +3 -3
  29. package/dist/resources/extensions/gsd/migrate-external.ts +21 -4
  30. package/dist/resources/extensions/gsd/milestone-ids.ts +2 -1
  31. package/dist/resources/extensions/gsd/native-git-bridge.ts +2 -1
  32. package/dist/resources/extensions/gsd/parallel-merge.ts +2 -1
  33. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  34. package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  35. package/dist/resources/extensions/gsd/quick.ts +58 -3
  36. package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
  37. package/dist/resources/extensions/gsd/session-lock.ts +12 -1
  38. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  39. package/dist/resources/extensions/gsd/undo.ts +5 -7
  40. package/dist/resources/extensions/gsd/unit-id.ts +14 -0
  41. package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
  42. package/dist/resources/extensions/gsd/worktree-command.ts +8 -7
  43. package/package.json +1 -1
  44. package/src/resources/extensions/gsd/auto-dashboard.ts +3 -1
  45. package/src/resources/extensions/gsd/auto-idempotency.ts +3 -2
  46. package/src/resources/extensions/gsd/auto-observability.ts +2 -4
  47. package/src/resources/extensions/gsd/auto-post-unit.ts +5 -5
  48. package/src/resources/extensions/gsd/auto-prompts.ts +46 -44
  49. package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
  50. package/src/resources/extensions/gsd/auto-start.ts +8 -6
  51. package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  52. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  53. package/src/resources/extensions/gsd/auto-timers.ts +3 -2
  54. package/src/resources/extensions/gsd/auto-verification.ts +6 -6
  55. package/src/resources/extensions/gsd/auto-worktree.ts +5 -4
  56. package/src/resources/extensions/gsd/auto.ts +28 -27
  57. package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
  58. package/src/resources/extensions/gsd/commands-workflow-templates.ts +2 -1
  59. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
  60. package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
  61. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
  62. package/src/resources/extensions/gsd/error-utils.ts +6 -0
  63. package/src/resources/extensions/gsd/export.ts +2 -1
  64. package/src/resources/extensions/gsd/git-service.ts +3 -2
  65. package/src/resources/extensions/gsd/guided-flow.ts +3 -2
  66. package/src/resources/extensions/gsd/index.ts +12 -5
  67. package/src/resources/extensions/gsd/key-manager.ts +2 -1
  68. package/src/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  69. package/src/resources/extensions/gsd/metrics.ts +3 -3
  70. package/src/resources/extensions/gsd/migrate-external.ts +21 -4
  71. package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
  72. package/src/resources/extensions/gsd/native-git-bridge.ts +2 -1
  73. package/src/resources/extensions/gsd/parallel-merge.ts +2 -1
  74. package/src/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  75. package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  76. package/src/resources/extensions/gsd/quick.ts +58 -3
  77. package/src/resources/extensions/gsd/repo-identity.ts +22 -1
  78. package/src/resources/extensions/gsd/session-lock.ts +12 -1
  79. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  80. package/src/resources/extensions/gsd/undo.ts +5 -7
  81. package/src/resources/extensions/gsd/unit-id.ts +14 -0
  82. package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
  83. package/src/resources/extensions/gsd/worktree-command.ts +8 -7
@@ -38,6 +38,7 @@ import {
38
38
  nativeBranchDelete,
39
39
  nativeBranchExists,
40
40
  } from "./native-git-bridge.js";
41
+ import { getErrorMessage } from "./error-utils.js";
41
42
 
42
43
  // ─── Module State ──────────────────────────────────────────────────────────
43
44
 
@@ -81,7 +82,7 @@ export function runWorktreePostCreateHook(sourceDir: string, worktreeDir: string
81
82
  });
82
83
  return null;
83
84
  } catch (err) {
84
- const msg = err instanceof Error ? err.message : String(err);
85
+ const msg = getErrorMessage(err);
85
86
  return `Worktree post-create hook failed: ${msg}`;
86
87
  }
87
88
  }
@@ -141,7 +142,7 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
141
142
  // Don't store originalBase -- caller can retry or clean up.
142
143
  throw new GSDError(
143
144
  GSD_IO_ERROR,
144
- `Auto-worktree created at ${info.path} but chdir failed: ${err instanceof Error ? err.message : String(err)}`,
145
+ `Auto-worktree created at ${info.path} but chdir failed: ${getErrorMessage(err)}`,
145
146
  );
146
147
  }
147
148
 
@@ -168,7 +169,7 @@ export function teardownAutoWorktree(
168
169
  } catch (err) {
169
170
  throw new GSDError(
170
171
  GSD_IO_ERROR,
171
- `Failed to chdir back to ${originalBasePath} during teardown: ${err instanceof Error ? err.message : String(err)}`,
172
+ `Failed to chdir back to ${originalBasePath} during teardown: ${getErrorMessage(err)}`,
172
173
  );
173
174
  }
174
175
 
@@ -274,7 +275,7 @@ export function enterAutoWorktree(basePath: string, milestoneId: string): string
274
275
  } catch (err) {
275
276
  throw new GSDError(
276
277
  GSD_IO_ERROR,
277
- `Failed to enter auto-worktree at ${p}: ${err instanceof Error ? err.message : String(err)}`,
278
+ `Failed to enter auto-worktree at ${p}: ${getErrorMessage(err)}`,
278
279
  );
279
280
  }
280
281
 
@@ -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";
@@ -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).
@@ -428,7 +430,7 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
428
430
  try {
429
431
  await dispatchNextUnit(ctx, pi);
430
432
  } catch (retryErr) {
431
- const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
433
+ const message = getErrorMessage(retryErr);
432
434
  await stopAuto(ctx, pi, `Dispatch gap recovery failed: ${message}`);
433
435
  return;
434
436
  }
@@ -458,14 +460,14 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
458
460
  // ── Auto-worktree: exit worktree and reset s.basePath on stop ──
459
461
  if (s.currentMilestoneId && isInAutoWorktree(s.basePath)) {
460
462
  try {
461
- 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) }); }
462
464
  teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId, { preserveBranch: true });
463
465
  s.basePath = s.originalBasePath;
464
466
  s.gitService = createGitService(s.basePath);
465
467
  ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
466
468
  } catch (err) {
467
469
  ctx?.ui.notify(
468
- `Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
470
+ `Auto-worktree teardown failed: ${getErrorMessage(err)}`,
469
471
  "warning",
470
472
  );
471
473
  }
@@ -476,7 +478,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
476
478
  try {
477
479
  const { closeDatabase } = await import("./gsd-db.js");
478
480
  closeDatabase();
479
- } catch (e) { debugLog("db-close-failed", { error: e instanceof Error ? e.message : String(e) }); }
481
+ } catch (e) { debugLog("db-close-failed", { error: getErrorMessage(e) }); }
480
482
  }
481
483
 
482
484
  if (s.originalBasePath) {
@@ -496,7 +498,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
496
498
  }
497
499
 
498
500
  if (s.basePath) {
499
- 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) }); }
500
502
  }
501
503
 
502
504
  if (isDebugEnabled()) {
@@ -635,7 +637,7 @@ export async function startAuto(
635
637
  }
636
638
  } catch (err) {
637
639
  ctx.ui.notify(
638
- `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.`,
639
641
  "warning",
640
642
  );
641
643
  }
@@ -647,13 +649,13 @@ export async function startAuto(
647
649
  ctx.ui.setFooter(hideFooter);
648
650
  ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
649
651
  restoreHookState(s.basePath);
650
- 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) }); }
651
653
  try {
652
654
  const report = await runGSDDoctor(s.basePath, { fix: true });
653
655
  if (report.fixesApplied.length > 0) {
654
656
  ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
655
657
  }
656
- } catch (e) { debugLog("resume-doctor-failed", { error: e instanceof Error ? e.message : String(e) }); }
658
+ } catch (e) { debugLog("resume-doctor-failed", { error: getErrorMessage(e) }); }
657
659
  await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet);
658
660
  invalidateAllCaches();
659
661
 
@@ -700,7 +702,7 @@ export async function startAuto(
700
702
  }
701
703
  } catch (err) {
702
704
  ctx.ui.notify(
703
- `Secrets check error: ${err instanceof Error ? err.message : String(err)}. Continuing without secrets.`,
705
+ `Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`,
704
706
  "warning",
705
707
  );
706
708
  }
@@ -807,7 +809,7 @@ export async function handleAgentEnd(
807
809
  try {
808
810
  await dispatchNextUnit(ctx, pi);
809
811
  } catch (dispatchErr) {
810
- const message = dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr);
812
+ const message = getErrorMessage(dispatchErr);
811
813
  ctx.ui.notify(
812
814
  `Dispatch error after unit completion: ${message}. Retrying in ${DISPATCH_GAP_TIMEOUT_MS / 1000}s.`,
813
815
  "error",
@@ -838,7 +840,7 @@ export async function handleAgentEnd(
838
840
  clearDispatchGapWatchdog();
839
841
  setImmediate(() => {
840
842
  handleAgentEnd(ctx, pi).catch((err) => {
841
- const msg = err instanceof Error ? err.message : String(err);
843
+ const msg = getErrorMessage(err);
842
844
  ctx.ui.notify(`Deferred agent_end retry failed: ${msg}`, "error");
843
845
  pauseAuto(ctx, pi).catch(() => {});
844
846
  });
@@ -1086,7 +1088,7 @@ async function dispatchNextUnit(
1086
1088
  );
1087
1089
  } catch (err) {
1088
1090
  ctx.ui.notify(
1089
- `Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
1091
+ `Report generation failed: ${getErrorMessage(err)}`,
1090
1092
  "warning",
1091
1093
  );
1092
1094
  }
@@ -1102,7 +1104,7 @@ async function dispatchNextUnit(
1102
1104
  atomicWriteSync(file, JSON.stringify([]));
1103
1105
  }
1104
1106
  s.completedKeySet.clear();
1105
- } 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) }); }
1106
1108
 
1107
1109
  // ── Worktree lifecycle on milestone transition (#616) ──
1108
1110
  if (isInAutoWorktree(s.basePath) && s.originalBasePath && shouldUseWorktreeIsolation()) {
@@ -1121,7 +1123,7 @@ async function dispatchNextUnit(
1121
1123
  }
1122
1124
  } catch (err) {
1123
1125
  ctx.ui.notify(
1124
- `Milestone merge failed during transition: ${err instanceof Error ? err.message : String(err)}`,
1126
+ `Milestone merge failed during transition: ${getErrorMessage(err)}`,
1125
1127
  "warning",
1126
1128
  );
1127
1129
  if (s.originalBasePath) {
@@ -1146,7 +1148,7 @@ async function dispatchNextUnit(
1146
1148
  ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
1147
1149
  } catch (err) {
1148
1150
  ctx.ui.notify(
1149
- `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.`,
1150
1152
  "warning",
1151
1153
  );
1152
1154
  }
@@ -1190,7 +1192,7 @@ async function dispatchNextUnit(
1190
1192
  }
1191
1193
  } catch (err) {
1192
1194
  ctx.ui.notify(
1193
- `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
1195
+ `Milestone merge failed: ${getErrorMessage(err)}`,
1194
1196
  "warning",
1195
1197
  );
1196
1198
  if (s.originalBasePath) {
@@ -1216,7 +1218,7 @@ async function dispatchNextUnit(
1216
1218
  }
1217
1219
  } catch (err) {
1218
1220
  ctx.ui.notify(
1219
- `Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
1221
+ `Milestone merge failed (branch mode): ${getErrorMessage(err)}`,
1220
1222
  "warning",
1221
1223
  );
1222
1224
  }
@@ -1276,7 +1278,7 @@ async function dispatchNextUnit(
1276
1278
  atomicWriteSync(file, JSON.stringify([]));
1277
1279
  }
1278
1280
  s.completedKeySet.clear();
1279
- } 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) }); }
1280
1282
  // ── Milestone merge ──
1281
1283
  if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {
1282
1284
  try {
@@ -1292,7 +1294,7 @@ async function dispatchNextUnit(
1292
1294
  );
1293
1295
  } catch (err) {
1294
1296
  ctx.ui.notify(
1295
- `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
1297
+ `Milestone merge failed: ${getErrorMessage(err)}`,
1296
1298
  "warning",
1297
1299
  );
1298
1300
  if (s.originalBasePath) {
@@ -1318,7 +1320,7 @@ async function dispatchNextUnit(
1318
1320
  }
1319
1321
  } catch (err) {
1320
1322
  ctx.ui.notify(
1321
- `Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
1323
+ `Milestone merge failed (branch mode): ${getErrorMessage(err)}`,
1322
1324
  "warning",
1323
1325
  );
1324
1326
  }
@@ -1417,7 +1419,7 @@ async function dispatchNextUnit(
1417
1419
  }
1418
1420
  } catch (err) {
1419
1421
  ctx.ui.notify(
1420
- `Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`,
1422
+ `Secrets collection error: ${getErrorMessage(err)}. Continuing with next task.`,
1421
1423
  "warning",
1422
1424
  );
1423
1425
  }
@@ -1628,7 +1630,7 @@ async function dispatchNextUnit(
1628
1630
  );
1629
1631
  result = await Promise.race([sessionPromise, timeoutPromise]);
1630
1632
  } catch (sessionErr) {
1631
- const msg = sessionErr instanceof Error ? sessionErr.message : String(sessionErr);
1633
+ const msg = getErrorMessage(sessionErr);
1632
1634
  ctx.ui.notify(`Session creation failed: ${msg}. Retrying via watchdog.`, "error");
1633
1635
  throw new Error(`newSession() failed: ${msg}`);
1634
1636
  }
@@ -1704,7 +1706,7 @@ async function dispatchNextUnit(
1704
1706
  const { reorderForCaching } = await import("./prompt-ordering.js");
1705
1707
  finalPrompt = reorderForCaching(finalPrompt);
1706
1708
  } catch (reorderErr) {
1707
- const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
1709
+ const msg = getErrorMessage(reorderErr);
1708
1710
  process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
1709
1711
  }
1710
1712
 
@@ -1747,8 +1749,7 @@ async function dispatchNextUnit(
1747
1749
  function ensurePreconditions(
1748
1750
  unitType: string, unitId: string, base: string, state: GSDState,
1749
1751
  ): void {
1750
- const parts = unitId.split("/");
1751
- const mid = parts[0]!;
1752
+ const { milestone: mid } = parseUnitId(unitId);
1752
1753
 
1753
1754
  const mDir = resolveMilestonePath(base, mid);
1754
1755
  if (!mDir) {
@@ -1756,8 +1757,8 @@ function ensurePreconditions(
1756
1757
  mkdirSync(join(newDir, "slices"), { recursive: true });
1757
1758
  }
1758
1759
 
1759
- if (parts.length >= 2) {
1760
- const sid = parts[1]!;
1760
+ const sid = parseUnitId(unitId).slice;
1761
+ if (sid) {
1761
1762
 
1762
1763
  const mDirResolved = resolveMilestonePath(base, mid);
1763
1764
  if (mDirResolved) {
@@ -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
  }
@@ -21,6 +21,7 @@ import { loadPrompt } from "./prompt-loader.js";
21
21
  import { gsdRoot } from "./paths.js";
22
22
  import { createGitService, runGit } from "./git-service.js";
23
23
  import { isAutoActive, isAutoPaused } from "./auto.js";
24
+ import { getErrorMessage } from "./error-utils.js";
24
25
 
25
26
  // ─── Helpers ─────────────────────────────────────────────────────────────────
26
27
 
@@ -439,7 +440,7 @@ export async function handleStart(
439
440
  branchCreated = true;
440
441
  }
441
442
  } catch (err) {
442
- const message = err instanceof Error ? err.message : String(err);
443
+ const message = getErrorMessage(err);
443
444
  ctx.ui.notify(
444
445
  `Could not create branch ${branchName}: ${message}. Working on current branch.`,
445
446
  "warning",
@@ -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
  }
@@ -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.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Extract a human-readable message from an unknown caught value.
3
+ */
4
+ export function getErrorMessage(err: unknown): string {
5
+ return err instanceof Error ? err.message : String(err);
6
+ }
@@ -12,6 +12,7 @@ import {
12
12
  import type { UnitMetrics } from "./metrics.js";
13
13
  import { gsdRoot } from "./paths.js";
14
14
  import { formatDuration, fileLink } from "../shared/mod.js";
15
+ import { getErrorMessage } from "./error-utils.js";
15
16
 
16
17
  /**
17
18
  * Open a file in the user's default browser.
@@ -226,7 +227,7 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
226
227
  }
227
228
  } catch (err) {
228
229
  ctx.ui.notify(
229
- `HTML export failed: ${err instanceof Error ? err.message : String(err)}`,
230
+ `HTML export failed: ${getErrorMessage(err)}`,
230
231
  "error",
231
232
  );
232
233
  }
@@ -33,6 +33,7 @@ import {
33
33
  nativeAddPaths,
34
34
  } from "./native-git-bridge.js";
35
35
  import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
36
+ import { getErrorMessage } from "./error-utils.js";
36
37
 
37
38
  // ─── Types ─────────────────────────────────────────────────────────────────
38
39
 
@@ -281,7 +282,7 @@ export function runGit(basePath: string, args: string[], options: { allowFailure
281
282
  }).trim();
282
283
  } catch (error) {
283
284
  if (options.allowFailure) return "";
284
- const message = error instanceof Error ? error.message : String(error);
285
+ const message = getErrorMessage(error);
285
286
  throw new GSDError(GSD_GIT_ERROR, `git ${args.join(" ")} failed in ${basePath}: ${filterGitSvnNoise(message)}`);
286
287
  }
287
288
  }
@@ -533,7 +534,7 @@ export class GitServiceImpl {
533
534
  execSync(command, { cwd: this.basePath, stdio: "pipe", encoding: "utf-8" });
534
535
  return { passed: true, skipped: false, command };
535
536
  } catch (err) {
536
- const msg = err instanceof Error ? err.message : String(err);
537
+ const msg = getErrorMessage(err);
537
538
  return { passed: false, skipped: false, command, error: msg };
538
539
  }
539
540
  }
@@ -44,6 +44,7 @@ export {
44
44
  showQueue, handleQueueReorder, showQueueAdd,
45
45
  buildExistingMilestonesContext,
46
46
  } from "./guided-flow-queue.js";
47
+ import { getErrorMessage } from "./error-utils.js";
47
48
 
48
49
  // ─── Commit Instruction Helpers ──────────────────────────────────────────────
49
50
 
@@ -158,9 +159,9 @@ export function checkAutoStartAfterDiscuss(): boolean {
158
159
 
159
160
  pendingAutoStart = null;
160
161
  startAuto(ctx, pi, basePath, false, { step }).catch((err) => {
161
- ctx.ui.notify(`Auto-start failed: ${err instanceof Error ? err.message : String(err)}`, "error");
162
+ ctx.ui.notify(`Auto-start failed: ${getErrorMessage(err)}`, "error");
162
163
  if (process.env.GSD_DEBUG) console.error('[gsd] auto start error:', err);
163
- debugLog("auto-start-failed", { error: err instanceof Error ? err.message : String(err) });
164
+ debugLog("auto-start-failed", { error: getErrorMessage(err) });
164
165
  });
165
166
  return true;
166
167
  }
@@ -64,6 +64,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
64
64
  import { toPosixPath } from "../shared/mod.js";
65
65
  import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
66
66
  import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
67
+ import { getErrorMessage } from "./error-utils.js";
67
68
 
68
69
  /**
69
70
  * Ensure the GSD database is available, auto-initializing if needed.
@@ -374,7 +375,7 @@ export default function (pi: ExtensionAPI) {
374
375
  details: { operation: "save_decision", id },
375
376
  };
376
377
  } catch (err) {
377
- const msg = err instanceof Error ? err.message : String(err);
378
+ const msg = getErrorMessage(err);
378
379
  process.stderr.write(`gsd-db: gsd_save_decision tool failed: ${msg}\n`);
379
380
  return {
380
381
  content: [{ type: "text" as const, text: `Error saving decision: ${msg}` }],
@@ -445,7 +446,7 @@ export default function (pi: ExtensionAPI) {
445
446
  details: { operation: "update_requirement", id: params.id },
446
447
  };
447
448
  } catch (err) {
448
- const msg = err instanceof Error ? err.message : String(err);
449
+ const msg = getErrorMessage(err);
449
450
  process.stderr.write(`gsd-db: gsd_update_requirement tool failed: ${msg}\n`);
450
451
  return {
451
452
  content: [{ type: "text" as const, text: `Error updating requirement: ${msg}` }],
@@ -525,7 +526,7 @@ export default function (pi: ExtensionAPI) {
525
526
  details: { operation: "save_summary", path: relativePath, artifact_type: params.artifact_type },
526
527
  };
527
528
  } catch (err) {
528
- const msg = err instanceof Error ? err.message : String(err);
529
+ const msg = getErrorMessage(err);
529
530
  process.stderr.write(`gsd-db: gsd_save_summary tool failed: ${msg}\n`);
530
531
  return {
531
532
  content: [{ type: "text" as const, text: `Error saving artifact: ${msg}` }],
@@ -574,7 +575,7 @@ export default function (pi: ExtensionAPI) {
574
575
  details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, reservedCount: reservedMilestoneIds.size, uniqueEnabled },
575
576
  };
576
577
  } catch (err) {
577
- const msg = err instanceof Error ? err.message : String(err);
578
+ const msg = getErrorMessage(err);
578
579
  return {
579
580
  content: [{ type: "text" as const, text: `Error generating milestone ID: ${msg}` }],
580
581
  isError: true,
@@ -795,6 +796,12 @@ export default function (pi: ExtensionAPI) {
795
796
 
796
797
  // ── agent_end: auto-mode advancement or auto-start after discuss ───────────
797
798
  pi.on("agent_end", async (event, ctx: ExtensionContext) => {
799
+ // Clean up quick-task branch if one just completed (#1269)
800
+ try {
801
+ const { cleanupQuickBranch } = await import("./quick.js");
802
+ cleanupQuickBranch();
803
+ } catch { /* non-fatal */ }
804
+
798
805
  // If discuss phase just finished, start auto-mode
799
806
  if (checkAutoStartAfterDiscuss()) {
800
807
  depthVerifiedMilestones.clear();
@@ -987,7 +994,7 @@ export default function (pi: ExtensionAPI) {
987
994
  } catch (err) {
988
995
  // Safety net: if handleAgentEnd throws despite its internal try-catch,
989
996
  // ensure auto-mode stops gracefully instead of silently stalling (#381).
990
- const message = err instanceof Error ? err.message : String(err);
997
+ const message = getErrorMessage(err);
991
998
  ctx.ui.notify(
992
999
  `Auto-mode error in agent_end handler: ${message}. Stopping auto-mode.`,
993
1000
  "error",
@@ -16,6 +16,7 @@ import { getEnvApiKey } from "@gsd/pi-ai";
16
16
  import { existsSync, statSync, chmodSync } from "node:fs";
17
17
  import { join, dirname } from "node:path";
18
18
  import { mkdirSync } from "node:fs";
19
+ import { getErrorMessage } from "./error-utils.js";
19
20
 
20
21
  // ─── Provider Registry ─────────────────────────────────────────────────────────
21
22
 
@@ -552,7 +553,7 @@ export async function testProviderKey(
552
553
  return { provider, status: "error", message: `HTTP ${res.status}`, latencyMs };
553
554
  } catch (err) {
554
555
  const latencyMs = Date.now() - start;
555
- const msg = err instanceof Error ? err.message : String(err);
556
+ const msg = getErrorMessage(err);
556
557
  if (msg.includes("timeout") || msg.includes("AbortError")) {
557
558
  return { provider, status: "error", message: "timeout (15s)", latencyMs };
558
559
  }
@@ -16,6 +16,7 @@
16
16
 
17
17
  import * as fs from 'node:fs';
18
18
  import * as path from 'node:path';
19
+ import { getErrorMessage } from "./error-utils.js";
19
20
 
20
21
  // ============================================================================
21
22
  // Type Definitions
@@ -194,7 +195,7 @@ export function parseMarketplaceJson(repoRoot: string):
194
195
  } catch (err) {
195
196
  return {
196
197
  success: false,
197
- error: `Failed to read marketplace.json: ${err instanceof Error ? err.message : String(err)}`
198
+ error: `Failed to read marketplace.json: ${getErrorMessage(err)}`
198
199
  };
199
200
  }
200
201
 
@@ -204,7 +205,7 @@ export function parseMarketplaceJson(repoRoot: string):
204
205
  } catch (err) {
205
206
  return {
206
207
  success: false,
207
- error: `Failed to parse marketplace.json: ${err instanceof Error ? err.message : String(err)}`
208
+ error: `Failed to parse marketplace.json: ${getErrorMessage(err)}`
208
209
  };
209
210
  }
210
211
 
@@ -293,7 +294,7 @@ export function inspectPlugin(
293
294
  }
294
295
  } catch (err) {
295
296
  // Fall back to marketplace inline or derived
296
- result.error = `Failed to parse plugin.json: ${err instanceof Error ? err.message : String(err)}`;
297
+ result.error = `Failed to parse plugin.json: ${getErrorMessage(err)}`;
297
298
  }
298
299
  }
299
300
 
@@ -18,6 +18,7 @@ import type { ExtensionContext } from "@gsd/pi-coding-agent";
18
18
  import { gsdRoot } from "./paths.js";
19
19
  import { getAndClearSkills } from "./skill-telemetry.js";
20
20
  import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
21
+ import { parseUnitId } from "./unit-id.js";
21
22
 
22
23
  // Re-export from shared — canonical implementation lives in format-utils.
23
24
  export { formatTokenCount } from "../shared/mod.js";
@@ -290,9 +291,8 @@ export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
290
291
  export function aggregateBySlice(units: UnitMetrics[]): SliceAggregate[] {
291
292
  const map = new Map<string, SliceAggregate>();
292
293
  for (const u of units) {
293
- const parts = u.id.split("/");
294
- // Slice ID is parts[0]/parts[1] if it exists, else parts[0]
295
- const sliceId = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
294
+ const { milestone, slice } = parseUnitId(u.id);
295
+ const sliceId = slice ? `${milestone}/${slice}` : milestone;
296
296
  let agg = map.get(sliceId);
297
297
  if (!agg) {
298
298
  agg = { sliceId, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
@@ -9,6 +9,7 @@
9
9
  import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { externalGsdRoot } from "./repo-identity.js";
12
+ import { getErrorMessage } from "./error-utils.js";
12
13
 
13
14
  export interface MigrationResult {
14
15
  migrated: boolean;
@@ -47,7 +48,7 @@ export function migrateToExternalState(basePath: string): MigrationResult {
47
48
  return { migrated: false, error: ".gsd exists but is not a directory or symlink" };
48
49
  }
49
50
  } catch (err) {
50
- return { migrated: false, error: `Cannot stat .gsd: ${err instanceof Error ? err.message : String(err)}` };
51
+ return { migrated: false, error: `Cannot stat .gsd: ${getErrorMessage(err)}` };
51
52
  }
52
53
 
53
54
  const externalPath = externalGsdRoot(basePath);
@@ -57,8 +58,24 @@ export function migrateToExternalState(basePath: string): MigrationResult {
57
58
  // mkdir -p the external dir
58
59
  mkdirSync(externalPath, { recursive: true });
59
60
 
60
- // Rename .gsd -> .gsd.migrating (atomic lock)
61
- renameSync(localGsd, migratingPath);
61
+ // Rename .gsd -> .gsd.migrating (atomic lock).
62
+ // On Windows, NTFS may reject rename with EPERM if file descriptors are
63
+ // open (VS Code watchers, antivirus on-access scan). Fall back to
64
+ // copy+delete (#1292).
65
+ try {
66
+ renameSync(localGsd, migratingPath);
67
+ } catch (renameErr: any) {
68
+ if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
69
+ try {
70
+ cpSync(localGsd, migratingPath, { recursive: true, force: true });
71
+ rmSync(localGsd, { recursive: true, force: true });
72
+ } catch (copyErr) {
73
+ return { migrated: false, error: `Migration rename/copy failed: ${copyErr instanceof Error ? copyErr.message : String(copyErr)}` };
74
+ }
75
+ } else {
76
+ throw renameErr;
77
+ }
78
+ }
62
79
 
63
80
  // Copy contents to external dir, skipping worktrees/
64
81
  const entries = readdirSync(migratingPath, { withFileTypes: true });
@@ -98,7 +115,7 @@ export function migrateToExternalState(basePath: string): MigrationResult {
98
115
 
99
116
  return {
100
117
  migrated: false,
101
- error: `Migration failed: ${err instanceof Error ? err.message : String(err)}`,
118
+ error: `Migration failed: ${getErrorMessage(err)}`,
102
119
  };
103
120
  }
104
121
  }