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
@@ -189,30 +189,52 @@ export async function inlineGsdRootFile(
189
189
  // ─── DB-Aware Inline Helpers ──────────────────────────────────────────────
190
190
 
191
191
  /**
192
- * Inline decisions with optional milestone scoping from the DB.
193
- * Falls back to filesystem via inlineGsdRootFile when DB unavailable or empty.
192
+ * Shared DB-fallback pattern: attempt a DB query via the context-store, format
193
+ * the result, and fall back to the filesystem file when the DB is unavailable
194
+ * or the query yields no results.
195
+ *
196
+ * @param base Project root for filesystem fallback
197
+ * @param label Section heading (e.g. "Decisions")
198
+ * @param filename Filesystem fallback file (e.g. "decisions.md")
199
+ * @param queryDb Async callback receiving the dynamically-imported
200
+ * context-store module. Returns formatted markdown or null.
194
201
  */
195
- export async function inlineDecisionsFromDb(
196
- base: string, milestoneId?: string, scope?: string, level?: InlineLevel,
202
+ async function inlineFromDbOrFile(
203
+ base: string,
204
+ label: string,
205
+ filename: string,
206
+ queryDb: (cs: typeof import("./context-store.js")) => string | null,
197
207
  ): Promise<string | null> {
198
- const inlineLevel = level ?? resolveInlineLevel();
199
208
  try {
200
209
  const { isDbAvailable } = await import("./gsd-db.js");
201
210
  if (isDbAvailable()) {
202
- const { queryDecisions, formatDecisionsForPrompt } = await import("./context-store.js");
203
- const decisions = queryDecisions({ milestoneId, scope });
204
- if (decisions.length > 0) {
205
- // Use compact format for non-full levels to save ~35% tokens
206
- const formatted = inlineLevel !== "full"
207
- ? formatDecisionsCompact(decisions)
208
- : formatDecisionsForPrompt(decisions);
209
- return `### Decisions\nSource: \`.gsd/DECISIONS.md\`\n\n${formatted}`;
211
+ const contextStore = await import("./context-store.js");
212
+ const content = queryDb(contextStore);
213
+ if (content) {
214
+ return `### ${label}\nSource: \`.gsd/${filename.toUpperCase().replace(/\.MD$/i, "")}.md\`\n\n${content}`;
210
215
  }
211
216
  }
212
217
  } catch {
213
218
  // DB not available — fall through to filesystem
214
219
  }
215
- return inlineGsdRootFile(base, "decisions.md", "Decisions");
220
+ return inlineGsdRootFile(base, filename, label);
221
+ }
222
+
223
+ /**
224
+ * Inline decisions with optional milestone scoping from the DB.
225
+ * Falls back to filesystem via inlineGsdRootFile when DB unavailable or empty.
226
+ */
227
+ export async function inlineDecisionsFromDb(
228
+ base: string, milestoneId?: string, scope?: string, level?: InlineLevel,
229
+ ): Promise<string | null> {
230
+ const inlineLevel = level ?? resolveInlineLevel();
231
+ return inlineFromDbOrFile(base, "Decisions", "decisions.md", (cs) => {
232
+ const decisions = cs.queryDecisions({ milestoneId, scope });
233
+ if (decisions.length === 0) return null;
234
+ return inlineLevel !== "full"
235
+ ? formatDecisionsCompact(decisions)
236
+ : cs.formatDecisionsForPrompt(decisions);
237
+ });
216
238
  }
217
239
 
218
240
  /**
@@ -223,23 +245,13 @@ export async function inlineRequirementsFromDb(
223
245
  base: string, sliceId?: string, level?: InlineLevel,
224
246
  ): Promise<string | null> {
225
247
  const inlineLevel = level ?? resolveInlineLevel();
226
- try {
227
- const { isDbAvailable } = await import("./gsd-db.js");
228
- if (isDbAvailable()) {
229
- const { queryRequirements, formatRequirementsForPrompt } = await import("./context-store.js");
230
- const requirements = queryRequirements({ sliceId });
231
- if (requirements.length > 0) {
232
- // Use compact format for non-full levels to save ~40% tokens
233
- const formatted = inlineLevel !== "full"
234
- ? formatRequirementsCompact(requirements)
235
- : formatRequirementsForPrompt(requirements);
236
- return `### Requirements\nSource: \`.gsd/REQUIREMENTS.md\`\n\n${formatted}`;
237
- }
238
- }
239
- } catch {
240
- // DB not available — fall through to filesystem
241
- }
242
- return inlineGsdRootFile(base, "requirements.md", "Requirements");
248
+ return inlineFromDbOrFile(base, "Requirements", "requirements.md", (cs) => {
249
+ const requirements = cs.queryRequirements({ sliceId });
250
+ if (requirements.length === 0) return null;
251
+ return inlineLevel !== "full"
252
+ ? formatRequirementsCompact(requirements)
253
+ : cs.formatRequirementsForPrompt(requirements);
254
+ });
243
255
  }
244
256
 
245
257
  /**
@@ -249,19 +261,9 @@ export async function inlineRequirementsFromDb(
249
261
  export async function inlineProjectFromDb(
250
262
  base: string,
251
263
  ): Promise<string | null> {
252
- try {
253
- const { isDbAvailable } = await import("./gsd-db.js");
254
- if (isDbAvailable()) {
255
- const { queryProject } = await import("./context-store.js");
256
- const content = queryProject();
257
- if (content) {
258
- return `### Project\nSource: \`.gsd/PROJECT.md\`\n\n${content}`;
259
- }
260
- }
261
- } catch {
262
- // DB not available — fall through to filesystem
263
- }
264
- return inlineGsdRootFile(base, "project.md", "Project");
264
+ return inlineFromDbOrFile(base, "Project", "project.md", (cs) => {
265
+ return cs.queryProject();
266
+ });
265
267
  }
266
268
 
267
269
  // ─── Skill Discovery ──────────────────────────────────────────────────────
@@ -42,6 +42,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "
42
42
  import { atomicWriteSync } from "./atomic-write.js";
43
43
  import { loadJsonFileOrNull } from "./json-persistence.js";
44
44
  import { dirname, join } from "node:path";
45
+ import { parseUnitId } from "./unit-id.js";
45
46
 
46
47
  // ─── Artifact Resolution & Verification ───────────────────────────────────────
47
48
 
@@ -49,9 +50,7 @@ import { dirname, join } from "node:path";
49
50
  * Resolve the expected artifact for a unit to an absolute path.
50
51
  */
51
52
  export function resolveExpectedArtifactPath(unitType: string, unitId: string, base: string): string | null {
52
- const parts = unitId.split("/");
53
- const mid = parts[0]!;
54
- const sid = parts[1];
53
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
55
54
  switch (unitType) {
56
55
  case "research-milestone": {
57
56
  const dir = resolveMilestonePath(base, mid);
@@ -78,7 +77,6 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
78
77
  return dir ? join(dir, buildSliceFileName(sid!, "UAT-RESULT")) : null;
79
78
  }
80
79
  case "execute-task": {
81
- const tid = parts[2];
82
80
  const dir = resolveSlicePath(base, mid, sid!);
83
81
  return dir && tid ? join(dir, "tasks", buildTaskFileName(tid, "SUMMARY")) : null;
84
82
  }
@@ -167,10 +165,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
167
165
 
168
166
  // execute-task must also have its checkbox marked [x] in the slice plan
169
167
  if (unitType === "execute-task") {
170
- const parts = unitId.split("/");
171
- const mid = parts[0];
172
- const sid = parts[1];
173
- const tid = parts[2];
168
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
174
169
  if (mid && sid && tid) {
175
170
  const planAbs = resolveSliceFile(base, mid, sid, "PLAN");
176
171
  if (planAbs && existsSync(planAbs)) {
@@ -187,9 +182,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
187
182
  // but omitted T{tid}-PLAN.md files would be marked complete, causing execute-task
188
183
  // to dispatch with a missing task plan (see issue #739).
189
184
  if (unitType === "plan-slice") {
190
- const parts = unitId.split("/");
191
- const mid = parts[0];
192
- const sid = parts[1];
185
+ const { milestone: mid, slice: sid } = parseUnitId(unitId);
193
186
  if (mid && sid) {
194
187
  try {
195
188
  const planContent = readFileSync(absPath, "utf-8");
@@ -213,9 +206,8 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
213
206
  // state machine keeps returning the same complete-slice unit (roadmap still shows
214
207
  // the slice incomplete), so dispatchNextUnit recurses forever.
215
208
  if (unitType === "complete-slice") {
216
- const parts = unitId.split("/");
217
- const mid = parts[0];
218
- const sid = parts[1];
209
+ const { milestone: mid, slice: sid } = parseUnitId(unitId);
210
+
219
211
  if (mid && sid) {
220
212
  const dir = resolveSlicePath(base, mid, sid);
221
213
  if (dir) {
@@ -268,9 +260,7 @@ export function writeBlockerPlaceholder(unitType: string, unitId: string, base:
268
260
  }
269
261
 
270
262
  export function diagnoseExpectedArtifact(unitType: string, unitId: string, base: string): string | null {
271
- const parts = unitId.split("/");
272
- const mid = parts[0];
273
- const sid = parts[1];
263
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
274
264
  switch (unitType) {
275
265
  case "research-milestone":
276
266
  return `${relMilestoneFile(base, mid!, "RESEARCH")} (milestone research)`;
@@ -281,7 +271,6 @@ export function diagnoseExpectedArtifact(unitType: string, unitId: string, base:
281
271
  case "plan-slice":
282
272
  return `${relSliceFile(base, mid!, sid!, "PLAN")} (slice plan)`;
283
273
  case "execute-task": {
284
- const tid = parts[2];
285
274
  return `Task ${tid} marked [x] in ${relSliceFile(base, mid!, sid!, "PLAN")} + summary written`;
286
275
  }
287
276
  case "complete-slice":
@@ -539,10 +528,7 @@ export async function selfHealRuntimeRecords(
539
528
  * These are shown when automatic reconciliation is not possible.
540
529
  */
541
530
  export function buildLoopRemediationSteps(unitType: string, unitId: string, base: string): string | null {
542
- const parts = unitId.split("/");
543
- const mid = parts[0];
544
- const sid = parts[1];
545
- const tid = parts[2];
531
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
546
532
  switch (unitType) {
547
533
  case "execute-task": {
548
534
  if (!mid || !sid || !tid) break;
@@ -63,6 +63,8 @@ import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath } from "./debug-
63
63
  import type { AutoSession } from "./auto/session.js";
64
64
  import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
65
65
  import { join } from "node:path";
66
+ import { getErrorMessage } from "./error-utils.js";
67
+ import { parseUnitId } from "./unit-id.js";
66
68
 
67
69
  export interface BootstrapDeps {
68
70
  shouldUseWorktreeIsolation: () => boolean;
@@ -138,7 +140,7 @@ export async function bootstrapAutoSession(
138
140
  if (crashLock && crashLock.pid !== process.pid) {
139
141
  // We already hold the session lock, so no concurrent session is running.
140
142
  // The crash lock is from a dead process — recover context from it.
141
- const recoveredMid = crashLock.unitId.split("/")[0];
143
+ const recoveredMid = parseUnitId(crashLock.unitId).milestone;
142
144
  const milestoneAlreadyComplete = recoveredMid
143
145
  ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
144
146
  : false;
@@ -201,11 +203,11 @@ export async function bootstrapAutoSession(
201
203
  if (!midMatch) continue;
202
204
  const mid = midMatch[1];
203
205
  if (resolveMilestoneFile(base, mid, "SUMMARY")) {
204
- try { unlinkSync(join(runtimeUnitsDir, file)); } catch (e) { debugLog("stale-unit-cleanup-failed", { file, error: e instanceof Error ? e.message : String(e) }); }
206
+ try { unlinkSync(join(runtimeUnitsDir, file)); } catch (e) { debugLog("stale-unit-cleanup-failed", { file, error: getErrorMessage(e) }); }
205
207
  }
206
208
  }
207
209
  }
208
- } catch (e) { debugLog("stale-unit-dir-cleanup-failed", { error: e instanceof Error ? e.message : String(e) }); }
210
+ } catch (e) { debugLog("stale-unit-dir-cleanup-failed", { error: getErrorMessage(e) }); }
209
211
 
210
212
  let state = await deriveState(base);
211
213
 
@@ -343,7 +345,7 @@ export async function bootstrapAutoSession(
343
345
  registerSigtermHandler(s.originalBasePath);
344
346
  } catch (err) {
345
347
  ctx.ui.notify(
346
- `Auto-worktree setup failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
348
+ `Auto-worktree setup failed: ${getErrorMessage(err)}. Continuing in project root.`,
347
349
  "warning",
348
350
  );
349
351
  }
@@ -435,7 +437,7 @@ export async function bootstrapAutoSession(
435
437
  }
436
438
  } catch (err) {
437
439
  ctx.ui.notify(
438
- `Secrets check error: ${err instanceof Error ? err.message : String(err)}. Continuing without secrets.`,
440
+ `Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`,
439
441
  "warning",
440
442
  );
441
443
  }
@@ -453,7 +455,7 @@ export async function bootstrapAutoSession(
453
455
  ctx.ui.notify("Removed stale .git/index.lock from prior crash.", "info");
454
456
  }
455
457
  }
456
- } catch (e) { debugLog("git-lock-cleanup-failed", { error: e instanceof Error ? e.message : String(e) }); }
458
+ } catch (e) { debugLog("git-lock-cleanup-failed", { error: getErrorMessage(e) }); }
457
459
 
458
460
  // Pre-flight: validate milestone queue
459
461
  try {
@@ -39,6 +39,7 @@ import {
39
39
  import type { AutoSession } from "./auto/session.js";
40
40
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
41
41
  import { join } from "node:path";
42
+ import { parseUnitId } from "./unit-id.js";
42
43
 
43
44
  export interface StuckContext {
44
45
  s: AutoSession;
@@ -99,7 +100,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
99
100
 
100
101
  // Final reconciliation pass for execute-task
101
102
  if (unitType === "execute-task") {
102
- const [mid, sid, tid] = unitId.split("/");
103
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
103
104
  if (mid && sid && tid) {
104
105
  const status = await inspectExecuteTaskDurability(basePath, unitId);
105
106
  if (status) {
@@ -168,7 +169,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
168
169
  // Adaptive self-repair: each retry attempts a different remediation step.
169
170
  if (unitType === "execute-task") {
170
171
  const status = await inspectExecuteTaskDurability(basePath, unitId);
171
- const [mid, sid, tid] = unitId.split("/");
172
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
172
173
  if (status && mid && sid && tid) {
173
174
  if (status.summaryExists && !status.taskChecked) {
174
175
  const repaired = skipExecuteTask(basePath, mid, sid, tid, status, "self-repair", 0);
@@ -18,6 +18,7 @@ import {
18
18
  writeBlockerPlaceholder,
19
19
  } from "./auto-recovery.js";
20
20
  import { existsSync } from "node:fs";
21
+ import { parseUnitId } from "./unit-id.js";
21
22
 
22
23
  export interface RecoveryContext {
23
24
  basePath: string;
@@ -128,7 +129,7 @@ export async function recoverTimedOutUnit(
128
129
 
129
130
  // Retries exhausted — write missing durable artifacts and advance.
130
131
  const diagnostic = formatExecuteTaskRecoveryStatus(status);
131
- const [mid, sid, tid] = unitId.split("/");
132
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
132
133
  const skipped = mid && sid && tid
133
134
  ? skipExecuteTask(basePath, mid, sid, tid, status, reason, maxRecoveryAttempts)
134
135
  : false;
@@ -20,6 +20,7 @@ import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js";
20
20
  import { saveActivityLog } from "./activity-log.js";
21
21
  import { recoverTimedOutUnit, type RecoveryContext } from "./auto-timeout-recovery.js";
22
22
  import type { AutoSession } from "./auto/session.js";
23
+ import { getErrorMessage } from "./error-utils.js";
23
24
 
24
25
  export interface SupervisionContext {
25
26
  s: AutoSession;
@@ -127,7 +128,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
127
128
  );
128
129
  await pauseAuto(ctx, pi);
129
130
  } catch (err) {
130
- const message = err instanceof Error ? err.message : String(err);
131
+ const message = getErrorMessage(err);
131
132
  console.error(`[idle-watchdog] Unhandled error: ${message}`);
132
133
  try {
133
134
  ctx.ui.notify(`Idle watchdog error: ${message}`, "warning");
@@ -159,7 +160,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
159
160
  );
160
161
  await pauseAuto(ctx, pi);
161
162
  } catch (err) {
162
- const message = err instanceof Error ? err.message : String(err);
163
+ const message = getErrorMessage(err);
163
164
  console.error(`[hard-timeout] Unhandled error: ${message}`);
164
165
  try {
165
166
  ctx.ui.notify(`Hard timeout error: ${message}`, "warning");
@@ -24,6 +24,8 @@ import { writeVerificationJSON } from "./verification-evidence.js";
24
24
  import { removePersistedKey } from "./auto-recovery.js";
25
25
  import type { AutoSession, PendingVerificationRetry } from "./auto/session.js";
26
26
  import { join } from "node:path";
27
+ import { getErrorMessage } from "./error-utils.js";
28
+ import { parseUnitId } from "./unit-id.js";
27
29
 
28
30
  export interface VerificationContext {
29
31
  s: AutoSession;
@@ -57,10 +59,9 @@ export async function runPostUnitVerification(
57
59
  const prefs = effectivePrefs?.preferences;
58
60
 
59
61
  // Read task plan verify field
60
- const parts = s.currentUnit.id.split("/");
62
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
61
63
  let taskPlanVerify: string | undefined;
62
- if (parts.length >= 3) {
63
- const [mid, sid, tid] = parts;
64
+ if (mid && sid && tid) {
64
65
  const planFile = resolveSliceFile(s.basePath, mid, sid, "PLAN");
65
66
  if (planFile) {
66
67
  const planContent = await loadFile(planFile);
@@ -152,9 +153,8 @@ export async function runPostUnitVerification(
152
153
 
153
154
  // Write verification evidence JSON
154
155
  const attempt = s.verificationRetryCount.get(s.currentUnit.id) ?? 0;
155
- if (parts.length >= 3) {
156
+ if (mid && sid && tid) {
156
157
  try {
157
- const [mid, sid, tid] = parts;
158
158
  const sDir = resolveSlicePath(s.basePath, mid, sid);
159
159
  if (sDir) {
160
160
  const tasksDir = join(sDir, "tasks");
@@ -204,7 +204,7 @@ export async function runPostUnitVerification(
204
204
  try {
205
205
  await dispatchNextUnit(ctx, pi);
206
206
  } catch (retryDispatchErr) {
207
- const msg = retryDispatchErr instanceof Error ? retryDispatchErr.message : String(retryDispatchErr);
207
+ const msg = getErrorMessage(retryDispatchErr);
208
208
  ctx.ui.notify(`Verification retry dispatch error: ${msg}`, "error");
209
209
  startDispatchGapWatchdog(ctx, pi);
210
210
  }
@@ -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) {