From 2a2056bcd7b609d86b29ee56837bfc5979c2038c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Wed, 18 Mar 2026 19:12:44 -0600 Subject: [PATCH] refactor: extract getErrorMessage() helper to eliminate 65 inline duplicates (#1280) Consolidate the repeated `err instanceof Error ? err.message : String(err)` pattern into a single `getErrorMessage(err)` utility. Reduces visual noise in catch blocks across 20 files in the GSD extension. Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/auto-start.ts | 11 +++-- src/resources/extensions/gsd/auto-timers.ts | 5 +- .../extensions/gsd/auto-verification.ts | 3 +- src/resources/extensions/gsd/auto-worktree.ts | 9 ++-- src/resources/extensions/gsd/auto.ts | 47 ++++++++++--------- .../extensions/gsd/commands-inspect.ts | 3 +- .../gsd/commands-workflow-templates.ts | 3 +- src/resources/extensions/gsd/error-utils.ts | 6 +++ src/resources/extensions/gsd/export.ts | 3 +- src/resources/extensions/gsd/git-service.ts | 5 +- src/resources/extensions/gsd/guided-flow.ts | 5 +- src/resources/extensions/gsd/index.ts | 11 +++-- src/resources/extensions/gsd/key-manager.ts | 3 +- .../extensions/gsd/marketplace-discovery.ts | 7 +-- .../extensions/gsd/migrate-external.ts | 5 +- src/resources/extensions/gsd/milestone-ids.ts | 3 +- .../extensions/gsd/native-git-bridge.ts | 3 +- .../extensions/gsd/parallel-merge.ts | 3 +- .../extensions/gsd/parallel-orchestrator.ts | 3 +- src/resources/extensions/gsd/quick.ts | 3 +- .../extensions/gsd/worktree-command.ts | 15 +++--- 21 files changed, 91 insertions(+), 65 deletions(-) create mode 100644 src/resources/extensions/gsd/error-utils.ts diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index 1c18ee761..97395d768 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -63,6 +63,7 @@ import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath } from "./debug- import type { AutoSession } from "./auto/session.js"; import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs"; import { join } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; export interface BootstrapDeps { shouldUseWorktreeIsolation: () => boolean; @@ -201,11 +202,11 @@ export async function bootstrapAutoSession( if (!midMatch) continue; const mid = midMatch[1]; if (resolveMilestoneFile(base, mid, "SUMMARY")) { - try { unlinkSync(join(runtimeUnitsDir, file)); } catch (e) { debugLog("stale-unit-cleanup-failed", { file, error: e instanceof Error ? e.message : String(e) }); } + try { unlinkSync(join(runtimeUnitsDir, file)); } catch (e) { debugLog("stale-unit-cleanup-failed", { file, error: getErrorMessage(e) }); } } } } - } catch (e) { debugLog("stale-unit-dir-cleanup-failed", { error: e instanceof Error ? e.message : String(e) }); } + } catch (e) { debugLog("stale-unit-dir-cleanup-failed", { error: getErrorMessage(e) }); } let state = await deriveState(base); @@ -343,7 +344,7 @@ export async function bootstrapAutoSession( registerSigtermHandler(s.originalBasePath); } catch (err) { ctx.ui.notify( - `Auto-worktree setup failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`, + `Auto-worktree setup failed: ${getErrorMessage(err)}. Continuing in project root.`, "warning", ); } @@ -435,7 +436,7 @@ export async function bootstrapAutoSession( } } catch (err) { ctx.ui.notify( - `Secrets check error: ${err instanceof Error ? err.message : String(err)}. Continuing without secrets.`, + `Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`, "warning", ); } @@ -453,7 +454,7 @@ export async function bootstrapAutoSession( ctx.ui.notify("Removed stale .git/index.lock from prior crash.", "info"); } } - } catch (e) { debugLog("git-lock-cleanup-failed", { error: e instanceof Error ? e.message : String(e) }); } + } catch (e) { debugLog("git-lock-cleanup-failed", { error: getErrorMessage(e) }); } // Pre-flight: validate milestone queue try { diff --git a/src/resources/extensions/gsd/auto-timers.ts b/src/resources/extensions/gsd/auto-timers.ts index 3b7964811..91bd27697 100644 --- a/src/resources/extensions/gsd/auto-timers.ts +++ b/src/resources/extensions/gsd/auto-timers.ts @@ -20,6 +20,7 @@ import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js"; import { saveActivityLog } from "./activity-log.js"; import { recoverTimedOutUnit, type RecoveryContext } from "./auto-timeout-recovery.js"; import type { AutoSession } from "./auto/session.js"; +import { getErrorMessage } from "./error-utils.js"; export interface SupervisionContext { s: AutoSession; @@ -127,7 +128,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void { ); await pauseAuto(ctx, pi); } catch (err) { - const message = err instanceof Error ? err.message : String(err); + const message = getErrorMessage(err); console.error(`[idle-watchdog] Unhandled error: ${message}`); try { ctx.ui.notify(`Idle watchdog error: ${message}`, "warning"); @@ -159,7 +160,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void { ); await pauseAuto(ctx, pi); } catch (err) { - const message = err instanceof Error ? err.message : String(err); + const message = getErrorMessage(err); console.error(`[hard-timeout] Unhandled error: ${message}`); try { ctx.ui.notify(`Hard timeout error: ${message}`, "warning"); diff --git a/src/resources/extensions/gsd/auto-verification.ts b/src/resources/extensions/gsd/auto-verification.ts index 519d94136..348e34f62 100644 --- a/src/resources/extensions/gsd/auto-verification.ts +++ b/src/resources/extensions/gsd/auto-verification.ts @@ -24,6 +24,7 @@ import { writeVerificationJSON } from "./verification-evidence.js"; import { removePersistedKey } from "./auto-recovery.js"; import type { AutoSession, PendingVerificationRetry } from "./auto/session.js"; import { join } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; export interface VerificationContext { s: AutoSession; @@ -204,7 +205,7 @@ export async function runPostUnitVerification( try { await dispatchNextUnit(ctx, pi); } catch (retryDispatchErr) { - const msg = retryDispatchErr instanceof Error ? retryDispatchErr.message : String(retryDispatchErr); + const msg = getErrorMessage(retryDispatchErr); ctx.ui.notify(`Verification retry dispatch error: ${msg}`, "error"); startDispatchGapWatchdog(ctx, pi); } diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index bf856593c..3cd43d960 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -38,6 +38,7 @@ import { nativeBranchDelete, nativeBranchExists, } from "./native-git-bridge.js"; +import { getErrorMessage } from "./error-utils.js"; // ─── Module State ────────────────────────────────────────────────────────── @@ -81,7 +82,7 @@ export function runWorktreePostCreateHook(sourceDir: string, worktreeDir: string }); return null; } catch (err) { - const msg = err instanceof Error ? err.message : String(err); + const msg = getErrorMessage(err); return `Worktree post-create hook failed: ${msg}`; } } @@ -141,7 +142,7 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin // Don't store originalBase -- caller can retry or clean up. throw new GSDError( GSD_IO_ERROR, - `Auto-worktree created at ${info.path} but chdir failed: ${err instanceof Error ? err.message : String(err)}`, + `Auto-worktree created at ${info.path} but chdir failed: ${getErrorMessage(err)}`, ); } @@ -168,7 +169,7 @@ export function teardownAutoWorktree( } catch (err) { throw new GSDError( GSD_IO_ERROR, - `Failed to chdir back to ${originalBasePath} during teardown: ${err instanceof Error ? err.message : String(err)}`, + `Failed to chdir back to ${originalBasePath} during teardown: ${getErrorMessage(err)}`, ); } @@ -274,7 +275,7 @@ export function enterAutoWorktree(basePath: string, milestoneId: string): string } catch (err) { throw new GSDError( GSD_IO_ERROR, - `Failed to enter auto-worktree at ${p}: ${err instanceof Error ? err.message : String(err)}`, + `Failed to enter auto-worktree at ${p}: ${getErrorMessage(err)}`, ); } diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index dcb4881d8..1ac433bb9 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -189,6 +189,7 @@ import { NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS, } from "./auto/session.js"; import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js"; +import { getErrorMessage } from "./error-utils.js"; // ── ENCAPSULATION INVARIANT ───────────────────────────────────────────────── // ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts). @@ -428,7 +429,7 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void try { await dispatchNextUnit(ctx, pi); } catch (retryErr) { - const message = retryErr instanceof Error ? retryErr.message : String(retryErr); + const message = getErrorMessage(retryErr); await stopAuto(ctx, pi, `Dispatch gap recovery failed: ${message}`); return; } @@ -458,14 +459,14 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason // ── Auto-worktree: exit worktree and reset s.basePath on stop ── if (s.currentMilestoneId && isInAutoWorktree(s.basePath)) { try { - try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: e instanceof Error ? e.message : String(e) }); } + try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: getErrorMessage(e) }); } teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId, { preserveBranch: true }); s.basePath = s.originalBasePath; s.gitService = createGitService(s.basePath); ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info"); } catch (err) { ctx?.ui.notify( - `Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`, + `Auto-worktree teardown failed: ${getErrorMessage(err)}`, "warning", ); } @@ -476,7 +477,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason try { const { closeDatabase } = await import("./gsd-db.js"); closeDatabase(); - } catch (e) { debugLog("db-close-failed", { error: e instanceof Error ? e.message : String(e) }); } + } catch (e) { debugLog("db-close-failed", { error: getErrorMessage(e) }); } } if (s.originalBasePath) { @@ -496,7 +497,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason } if (s.basePath) { - try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error: e instanceof Error ? e.message : String(e) }); } + try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error: getErrorMessage(e) }); } } if (isDebugEnabled()) { @@ -635,7 +636,7 @@ export async function startAuto( } } catch (err) { ctx.ui.notify( - `Auto-worktree re-entry failed: ${err instanceof Error ? err.message : String(err)}. Continuing at current path.`, + `Auto-worktree re-entry failed: ${getErrorMessage(err)}. Continuing at current path.`, "warning", ); } @@ -647,13 +648,13 @@ export async function startAuto( ctx.ui.setFooter(hideFooter); ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info"); restoreHookState(s.basePath); - try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error: e instanceof Error ? e.message : String(e) }); } + try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error: getErrorMessage(e) }); } try { const report = await runGSDDoctor(s.basePath, { fix: true }); if (report.fixesApplied.length > 0) { ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info"); } - } catch (e) { debugLog("resume-doctor-failed", { error: e instanceof Error ? e.message : String(e) }); } + } catch (e) { debugLog("resume-doctor-failed", { error: getErrorMessage(e) }); } await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet); invalidateAllCaches(); @@ -700,7 +701,7 @@ export async function startAuto( } } catch (err) { ctx.ui.notify( - `Secrets check error: ${err instanceof Error ? err.message : String(err)}. Continuing without secrets.`, + `Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`, "warning", ); } @@ -807,7 +808,7 @@ export async function handleAgentEnd( try { await dispatchNextUnit(ctx, pi); } catch (dispatchErr) { - const message = dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr); + const message = getErrorMessage(dispatchErr); ctx.ui.notify( `Dispatch error after unit completion: ${message}. Retrying in ${DISPATCH_GAP_TIMEOUT_MS / 1000}s.`, "error", @@ -838,7 +839,7 @@ export async function handleAgentEnd( clearDispatchGapWatchdog(); setImmediate(() => { handleAgentEnd(ctx, pi).catch((err) => { - const msg = err instanceof Error ? err.message : String(err); + const msg = getErrorMessage(err); ctx.ui.notify(`Deferred agent_end retry failed: ${msg}`, "error"); pauseAuto(ctx, pi).catch(() => {}); }); @@ -1086,7 +1087,7 @@ async function dispatchNextUnit( ); } catch (err) { ctx.ui.notify( - `Report generation failed: ${err instanceof Error ? err.message : String(err)}`, + `Report generation failed: ${getErrorMessage(err)}`, "warning", ); } @@ -1102,7 +1103,7 @@ async function dispatchNextUnit( atomicWriteSync(file, JSON.stringify([])); } s.completedKeySet.clear(); - } catch (e) { debugLog("completed-keys-reset-failed", { error: e instanceof Error ? e.message : String(e) }); } + } catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); } // ── Worktree lifecycle on milestone transition (#616) ── if (isInAutoWorktree(s.basePath) && s.originalBasePath && shouldUseWorktreeIsolation()) { @@ -1121,7 +1122,7 @@ async function dispatchNextUnit( } } catch (err) { ctx.ui.notify( - `Milestone merge failed during transition: ${err instanceof Error ? err.message : String(err)}`, + `Milestone merge failed during transition: ${getErrorMessage(err)}`, "warning", ); if (s.originalBasePath) { @@ -1146,7 +1147,7 @@ async function dispatchNextUnit( ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info"); } catch (err) { ctx.ui.notify( - `Auto-worktree creation for ${mid} failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`, + `Auto-worktree creation for ${mid} failed: ${getErrorMessage(err)}. Continuing in project root.`, "warning", ); } @@ -1190,7 +1191,7 @@ async function dispatchNextUnit( } } catch (err) { ctx.ui.notify( - `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`, + `Milestone merge failed: ${getErrorMessage(err)}`, "warning", ); if (s.originalBasePath) { @@ -1216,7 +1217,7 @@ async function dispatchNextUnit( } } catch (err) { ctx.ui.notify( - `Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`, + `Milestone merge failed (branch mode): ${getErrorMessage(err)}`, "warning", ); } @@ -1276,7 +1277,7 @@ async function dispatchNextUnit( atomicWriteSync(file, JSON.stringify([])); } s.completedKeySet.clear(); - } catch (e) { debugLog("completed-keys-reset-failed", { error: e instanceof Error ? e.message : String(e) }); } + } catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); } // ── Milestone merge ── if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) { try { @@ -1292,7 +1293,7 @@ async function dispatchNextUnit( ); } catch (err) { ctx.ui.notify( - `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`, + `Milestone merge failed: ${getErrorMessage(err)}`, "warning", ); if (s.originalBasePath) { @@ -1318,7 +1319,7 @@ async function dispatchNextUnit( } } catch (err) { ctx.ui.notify( - `Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`, + `Milestone merge failed (branch mode): ${getErrorMessage(err)}`, "warning", ); } @@ -1417,7 +1418,7 @@ async function dispatchNextUnit( } } catch (err) { ctx.ui.notify( - `Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`, + `Secrets collection error: ${getErrorMessage(err)}. Continuing with next task.`, "warning", ); } @@ -1628,7 +1629,7 @@ async function dispatchNextUnit( ); result = await Promise.race([sessionPromise, timeoutPromise]); } catch (sessionErr) { - const msg = sessionErr instanceof Error ? sessionErr.message : String(sessionErr); + const msg = getErrorMessage(sessionErr); ctx.ui.notify(`Session creation failed: ${msg}. Retrying via watchdog.`, "error"); throw new Error(`newSession() failed: ${msg}`); } @@ -1704,7 +1705,7 @@ async function dispatchNextUnit( const { reorderForCaching } = await import("./prompt-ordering.js"); finalPrompt = reorderForCaching(finalPrompt); } catch (reorderErr) { - const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr); + const msg = getErrorMessage(reorderErr); process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`); } diff --git a/src/resources/extensions/gsd/commands-inspect.ts b/src/resources/extensions/gsd/commands-inspect.ts index 29b9b7746..7e3ac67de 100644 --- a/src/resources/extensions/gsd/commands-inspect.ts +++ b/src/resources/extensions/gsd/commands-inspect.ts @@ -5,6 +5,7 @@ */ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import { getErrorMessage } from "./error-utils.js"; export interface InspectData { schemaVersion: number | null; @@ -84,7 +85,7 @@ export async function handleInspect(ctx: ExtensionCommandContext): Promise ctx.ui.notify(formatInspectOutput(data), "info"); } catch (err) { - process.stderr.write(`gsd-db: /gsd inspect failed: ${err instanceof Error ? err.message : String(err)}\n`); + process.stderr.write(`gsd-db: /gsd inspect failed: ${getErrorMessage(err)}\n`); ctx.ui.notify("Failed to inspect GSD database. Check stderr for details.", "error"); } } diff --git a/src/resources/extensions/gsd/commands-workflow-templates.ts b/src/resources/extensions/gsd/commands-workflow-templates.ts index 6d7fc7396..e86226873 100644 --- a/src/resources/extensions/gsd/commands-workflow-templates.ts +++ b/src/resources/extensions/gsd/commands-workflow-templates.ts @@ -21,6 +21,7 @@ import { loadPrompt } from "./prompt-loader.js"; import { gsdRoot } from "./paths.js"; import { createGitService, runGit } from "./git-service.js"; import { isAutoActive, isAutoPaused } from "./auto.js"; +import { getErrorMessage } from "./error-utils.js"; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -439,7 +440,7 @@ export async function handleStart( branchCreated = true; } } catch (err) { - const message = err instanceof Error ? err.message : String(err); + const message = getErrorMessage(err); ctx.ui.notify( `Could not create branch ${branchName}: ${message}. Working on current branch.`, "warning", diff --git a/src/resources/extensions/gsd/error-utils.ts b/src/resources/extensions/gsd/error-utils.ts new file mode 100644 index 000000000..b01f17494 --- /dev/null +++ b/src/resources/extensions/gsd/error-utils.ts @@ -0,0 +1,6 @@ +/** + * Extract a human-readable message from an unknown caught value. + */ +export function getErrorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} diff --git a/src/resources/extensions/gsd/export.ts b/src/resources/extensions/gsd/export.ts index 94a813113..7bec6ae17 100644 --- a/src/resources/extensions/gsd/export.ts +++ b/src/resources/extensions/gsd/export.ts @@ -12,6 +12,7 @@ import { import type { UnitMetrics } from "./metrics.js"; import { gsdRoot } from "./paths.js"; import { formatDuration, fileLink } from "../shared/mod.js"; +import { getErrorMessage } from "./error-utils.js"; /** * Open a file in the user's default browser. @@ -226,7 +227,7 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b } } catch (err) { ctx.ui.notify( - `HTML export failed: ${err instanceof Error ? err.message : String(err)}`, + `HTML export failed: ${getErrorMessage(err)}`, "error", ); } diff --git a/src/resources/extensions/gsd/git-service.ts b/src/resources/extensions/gsd/git-service.ts index 4e57c4f25..ad2d6f3c3 100644 --- a/src/resources/extensions/gsd/git-service.ts +++ b/src/resources/extensions/gsd/git-service.ts @@ -33,6 +33,7 @@ import { nativeAddPaths, } from "./native-git-bridge.js"; import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js"; +import { getErrorMessage } from "./error-utils.js"; // ─── Types ───────────────────────────────────────────────────────────────── @@ -281,7 +282,7 @@ export function runGit(basePath: string, args: string[], options: { allowFailure }).trim(); } catch (error) { if (options.allowFailure) return ""; - const message = error instanceof Error ? error.message : String(error); + const message = getErrorMessage(error); throw new GSDError(GSD_GIT_ERROR, `git ${args.join(" ")} failed in ${basePath}: ${filterGitSvnNoise(message)}`); } } @@ -533,7 +534,7 @@ export class GitServiceImpl { execSync(command, { cwd: this.basePath, stdio: "pipe", encoding: "utf-8" }); return { passed: true, skipped: false, command }; } catch (err) { - const msg = err instanceof Error ? err.message : String(err); + const msg = getErrorMessage(err); return { passed: false, skipped: false, command, error: msg }; } } diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index a89cdca50..62fcd0d5e 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -44,6 +44,7 @@ export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js"; +import { getErrorMessage } from "./error-utils.js"; // ─── Commit Instruction Helpers ────────────────────────────────────────────── @@ -158,9 +159,9 @@ export function checkAutoStartAfterDiscuss(): boolean { pendingAutoStart = null; startAuto(ctx, pi, basePath, false, { step }).catch((err) => { - ctx.ui.notify(`Auto-start failed: ${err instanceof Error ? err.message : String(err)}`, "error"); + ctx.ui.notify(`Auto-start failed: ${getErrorMessage(err)}`, "error"); if (process.env.GSD_DEBUG) console.error('[gsd] auto start error:', err); - debugLog("auto-start-failed", { error: err instanceof Error ? err.message : String(err) }); + debugLog("auto-start-failed", { error: getErrorMessage(err) }); }); return true; } diff --git a/src/resources/extensions/gsd/index.ts b/src/resources/extensions/gsd/index.ts index b20d4ee42..e3ed5b7df 100644 --- a/src/resources/extensions/gsd/index.ts +++ b/src/resources/extensions/gsd/index.ts @@ -64,6 +64,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err import { toPosixPath } from "../shared/mod.js"; import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js"; import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js"; +import { getErrorMessage } from "./error-utils.js"; /** * Ensure the GSD database is available, auto-initializing if needed. @@ -374,7 +375,7 @@ export default function (pi: ExtensionAPI) { details: { operation: "save_decision", id }, }; } catch (err) { - const msg = err instanceof Error ? err.message : String(err); + const msg = getErrorMessage(err); process.stderr.write(`gsd-db: gsd_save_decision tool failed: ${msg}\n`); return { content: [{ type: "text" as const, text: `Error saving decision: ${msg}` }], @@ -445,7 +446,7 @@ export default function (pi: ExtensionAPI) { details: { operation: "update_requirement", id: params.id }, }; } catch (err) { - const msg = err instanceof Error ? err.message : String(err); + const msg = getErrorMessage(err); process.stderr.write(`gsd-db: gsd_update_requirement tool failed: ${msg}\n`); return { content: [{ type: "text" as const, text: `Error updating requirement: ${msg}` }], @@ -525,7 +526,7 @@ export default function (pi: ExtensionAPI) { details: { operation: "save_summary", path: relativePath, artifact_type: params.artifact_type }, }; } catch (err) { - const msg = err instanceof Error ? err.message : String(err); + const msg = getErrorMessage(err); process.stderr.write(`gsd-db: gsd_save_summary tool failed: ${msg}\n`); return { content: [{ type: "text" as const, text: `Error saving artifact: ${msg}` }], @@ -574,7 +575,7 @@ export default function (pi: ExtensionAPI) { details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, reservedCount: reservedMilestoneIds.size, uniqueEnabled }, }; } catch (err) { - const msg = err instanceof Error ? err.message : String(err); + const msg = getErrorMessage(err); return { content: [{ type: "text" as const, text: `Error generating milestone ID: ${msg}` }], isError: true, @@ -993,7 +994,7 @@ export default function (pi: ExtensionAPI) { } catch (err) { // Safety net: if handleAgentEnd throws despite its internal try-catch, // ensure auto-mode stops gracefully instead of silently stalling (#381). - const message = err instanceof Error ? err.message : String(err); + const message = getErrorMessage(err); ctx.ui.notify( `Auto-mode error in agent_end handler: ${message}. Stopping auto-mode.`, "error", diff --git a/src/resources/extensions/gsd/key-manager.ts b/src/resources/extensions/gsd/key-manager.ts index 55941265e..db67fd81b 100644 --- a/src/resources/extensions/gsd/key-manager.ts +++ b/src/resources/extensions/gsd/key-manager.ts @@ -16,6 +16,7 @@ import { getEnvApiKey } from "@gsd/pi-ai"; import { existsSync, statSync, chmodSync } from "node:fs"; import { join, dirname } from "node:path"; import { mkdirSync } from "node:fs"; +import { getErrorMessage } from "./error-utils.js"; // ─── Provider Registry ───────────────────────────────────────────────────────── @@ -552,7 +553,7 @@ export async function testProviderKey( return { provider, status: "error", message: `HTTP ${res.status}`, latencyMs }; } catch (err) { const latencyMs = Date.now() - start; - const msg = err instanceof Error ? err.message : String(err); + const msg = getErrorMessage(err); if (msg.includes("timeout") || msg.includes("AbortError")) { return { provider, status: "error", message: "timeout (15s)", latencyMs }; } diff --git a/src/resources/extensions/gsd/marketplace-discovery.ts b/src/resources/extensions/gsd/marketplace-discovery.ts index cc8a16468..41923197f 100644 --- a/src/resources/extensions/gsd/marketplace-discovery.ts +++ b/src/resources/extensions/gsd/marketplace-discovery.ts @@ -16,6 +16,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; +import { getErrorMessage } from "./error-utils.js"; // ============================================================================ // Type Definitions @@ -194,7 +195,7 @@ export function parseMarketplaceJson(repoRoot: string): } catch (err) { return { success: false, - error: `Failed to read marketplace.json: ${err instanceof Error ? err.message : String(err)}` + error: `Failed to read marketplace.json: ${getErrorMessage(err)}` }; } @@ -204,7 +205,7 @@ export function parseMarketplaceJson(repoRoot: string): } catch (err) { return { success: false, - error: `Failed to parse marketplace.json: ${err instanceof Error ? err.message : String(err)}` + error: `Failed to parse marketplace.json: ${getErrorMessage(err)}` }; } @@ -293,7 +294,7 @@ export function inspectPlugin( } } catch (err) { // Fall back to marketplace inline or derived - result.error = `Failed to parse plugin.json: ${err instanceof Error ? err.message : String(err)}`; + result.error = `Failed to parse plugin.json: ${getErrorMessage(err)}`; } } diff --git a/src/resources/extensions/gsd/migrate-external.ts b/src/resources/extensions/gsd/migrate-external.ts index 0621e0edb..31172e9ff 100644 --- a/src/resources/extensions/gsd/migrate-external.ts +++ b/src/resources/extensions/gsd/migrate-external.ts @@ -9,6 +9,7 @@ import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs"; import { join } from "node:path"; import { externalGsdRoot } from "./repo-identity.js"; +import { getErrorMessage } from "./error-utils.js"; export interface MigrationResult { migrated: boolean; @@ -47,7 +48,7 @@ export function migrateToExternalState(basePath: string): MigrationResult { return { migrated: false, error: ".gsd exists but is not a directory or symlink" }; } } catch (err) { - return { migrated: false, error: `Cannot stat .gsd: ${err instanceof Error ? err.message : String(err)}` }; + return { migrated: false, error: `Cannot stat .gsd: ${getErrorMessage(err)}` }; } const externalPath = externalGsdRoot(basePath); @@ -114,7 +115,7 @@ export function migrateToExternalState(basePath: string): MigrationResult { return { migrated: false, - error: `Migration failed: ${err instanceof Error ? err.message : String(err)}`, + error: `Migration failed: ${getErrorMessage(err)}`, }; } } diff --git a/src/resources/extensions/gsd/milestone-ids.ts b/src/resources/extensions/gsd/milestone-ids.ts index a586e679e..aec8cee67 100644 --- a/src/resources/extensions/gsd/milestone-ids.ts +++ b/src/resources/extensions/gsd/milestone-ids.ts @@ -9,6 +9,7 @@ import { randomInt } from "node:crypto"; import { readdirSync, existsSync } from "node:fs"; import { milestonesDir } from "./paths.js"; import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js"; +import { getErrorMessage } from "./error-utils.js"; // ─── Regex ────────────────────────────────────────────────────────────────── @@ -88,7 +89,7 @@ export function findMilestoneIds(basePath: string): string[] { } catch (err) { // Log why milestone scanning failed — silent [] here causes infinite loops (#456) if (existsSync(dir)) { - console.error(`[gsd] findMilestoneIds: .gsd/milestones/ exists but readdirSync failed — ${err instanceof Error ? err.message : String(err)}`); + console.error(`[gsd] findMilestoneIds: .gsd/milestones/ exists but readdirSync failed — ${getErrorMessage(err)}`); } return []; } diff --git a/src/resources/extensions/gsd/native-git-bridge.ts b/src/resources/extensions/gsd/native-git-bridge.ts index dd0958d53..22fead5c8 100644 --- a/src/resources/extensions/gsd/native-git-bridge.ts +++ b/src/resources/extensions/gsd/native-git-bridge.ts @@ -10,6 +10,7 @@ import { existsSync, readFileSync, unlinkSync, rmSync } from "node:fs"; import { join } from "node:path"; import { GSDError, GSD_GIT_ERROR } from "./errors.js"; import { GIT_NO_PROMPT_ENV } from "./git-constants.js"; +import { getErrorMessage } from "./error-utils.js"; // Issue #453: keep auto-mode bookkeeping on the stable git CLI path unless a // caller explicitly opts into the native helper. @@ -716,7 +717,7 @@ export function nativeCommit( try { return native.gitCommit(basePath, message, options?.allowEmpty); } catch (e) { - const msg = e instanceof Error ? e.message : String(e); + const msg = getErrorMessage(e); if (msg.includes("nothing to commit")) return null; throw e; } diff --git a/src/resources/extensions/gsd/parallel-merge.ts b/src/resources/extensions/gsd/parallel-merge.ts index 9df875180..835920a1f 100644 --- a/src/resources/extensions/gsd/parallel-merge.ts +++ b/src/resources/extensions/gsd/parallel-merge.ts @@ -11,6 +11,7 @@ import { mergeMilestoneToMain } from "./auto-worktree.js"; import { MergeConflictError } from "./git-service.js"; import { removeSessionStatus } from "./session-status-io.js"; import type { WorkerInfo } from "./parallel-orchestrator.js"; +import { getErrorMessage } from "./error-utils.js"; // ─── Types ───────────────────────────────────────────────────────────────── @@ -99,7 +100,7 @@ export async function mergeCompletedMilestone( return { milestoneId, success: false, - error: err instanceof Error ? err.message : String(err), + error: getErrorMessage(err), }; } } diff --git a/src/resources/extensions/gsd/parallel-orchestrator.ts b/src/resources/extensions/gsd/parallel-orchestrator.ts index 0760b78cf..66adbdf88 100644 --- a/src/resources/extensions/gsd/parallel-orchestrator.ts +++ b/src/resources/extensions/gsd/parallel-orchestrator.ts @@ -38,6 +38,7 @@ import { analyzeParallelEligibility, type ParallelCandidates, } from "./parallel-eligibility.js"; +import { getErrorMessage } from "./error-utils.js"; // ─── Types ───────────────────────────────────────────────────────────────── @@ -363,7 +364,7 @@ export async function startParallel( started.push(mid); } catch (err) { - const message = err instanceof Error ? err.message : String(err); + const message = getErrorMessage(err); errors.push({ mid, error: message }); } } diff --git a/src/resources/extensions/gsd/quick.ts b/src/resources/extensions/gsd/quick.ts index 41269abba..de8a85afd 100644 --- a/src/resources/extensions/gsd/quick.ts +++ b/src/resources/extensions/gsd/quick.ts @@ -15,6 +15,7 @@ import { join } from "node:path"; import { loadPrompt } from "./prompt-loader.js"; import { gsdRoot } from "./paths.js"; import { createGitService, runGit } from "./git-service.js"; +import { getErrorMessage } from "./error-utils.js"; // ─── Quick Task Helpers ─────────────────────────────────────────────────────── @@ -122,7 +123,7 @@ export async function handleQuick( } } catch (err) { // Branch creation failed — continue on current branch - const message = err instanceof Error ? err.message : String(err); + const message = getErrorMessage(err); ctx.ui.notify(`Could not create branch ${branchName}: ${message}. Working on current branch.`, "warning"); } } diff --git a/src/resources/extensions/gsd/worktree-command.ts b/src/resources/extensions/gsd/worktree-command.ts index 84281ab0e..d89dd3df6 100644 --- a/src/resources/extensions/gsd/worktree-command.ts +++ b/src/resources/extensions/gsd/worktree-command.ts @@ -34,6 +34,7 @@ import type { FileLineStat } from "./worktree-manager.js"; import { existsSync, realpathSync, readdirSync, rmSync, unlinkSync } from "node:fs"; import { nativeMergeAbort } from "./native-git-bridge.js"; import { join, sep } from "node:path"; +import { getErrorMessage } from "./error-utils.js"; /** * Tracks the original project root so we can switch back. @@ -370,7 +371,7 @@ async function handleCreate( "info", ); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); + const msg = getErrorMessage(error); ctx.ui.notify(`Failed to create worktree: ${msg}`, "error"); } } @@ -418,7 +419,7 @@ async function handleSwitch( "info", ); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); + const msg = getErrorMessage(error); ctx.ui.notify(`Failed to switch to worktree: ${msg}`, "error"); } } @@ -528,7 +529,7 @@ async function handleList( ctx.ui.notify(lines.join("\n"), "info"); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); + const msg = getErrorMessage(error); ctx.ui.notify(`Failed to list worktrees: ${msg}`, "error"); } } @@ -646,7 +647,7 @@ async function handleMerge( ); return; } catch (mergeErr) { - const mergeMsg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr); + const mergeMsg = getErrorMessage(mergeErr); const isConflict = /conflict/i.test(mergeMsg); if (isConflict) { @@ -703,7 +704,7 @@ async function handleMerge( "info", ); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); + const msg = getErrorMessage(error); ctx.ui.notify(`Failed to start merge: ${msg}`, "error"); } } @@ -746,7 +747,7 @@ async function handleRemove( ctx.ui.notify(`${CLR.ok("✓")} Worktree ${CLR.name(name)} removed ${CLR.muted("(branch deleted)")}.`, "info"); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); + const msg = getErrorMessage(error); ctx.ui.notify(`Failed to remove worktree: ${msg}`, "error"); } } @@ -800,7 +801,7 @@ async function handleRemoveAll( if (failed.length > 0) lines.push(`${CLR.warn("✗")} Failed: ${failed.map(n => CLR.name(n)).join(", ")}`); ctx.ui.notify(lines.join("\n"), failed.length > 0 ? "warning" : "info"); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); + const msg = getErrorMessage(error); ctx.ui.notify(`Failed to remove worktrees: ${msg}`, "error"); } }