diff --git a/src/resources/extensions/gsd/activity-log.ts b/src/resources/extensions/gsd/activity-log.ts index 3e58543ec..fd235d121 100644 --- a/src/resources/extensions/gsd/activity-log.ts +++ b/src/resources/extensions/gsd/activity-log.ts @@ -30,7 +30,8 @@ function scanNextSequence(activityDir: string): number { const match = f.match(SEQ_PREFIX_RE); if (match) maxSeq = Math.max(maxSeq, parseInt(match[1], 10)); } - } catch { + } catch (e) { + void e; /* directory not readable — start at 1 */ return 1; } return maxSeq + 1; @@ -100,8 +101,9 @@ export function saveActivityLog( writeFileSync(filePath, content, "utf-8"); state.nextSeq += 1; state.lastSnapshotKeyByUnit.set(unitKey, key); - } catch { + } catch (e) { // Don't let logging failures break auto-mode + void e; } } diff --git a/src/resources/extensions/gsd/auto-recovery.ts b/src/resources/extensions/gsd/auto-recovery.ts index 6ac6c1dd5..4fd9848c6 100644 --- a/src/resources/extensions/gsd/auto-recovery.ts +++ b/src/resources/extensions/gsd/auto-recovery.ts @@ -149,7 +149,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s const roadmap = parseRoadmap(roadmapContent); const slice = roadmap.slices.find(s => s.id === sid); if (slice && !slice.done) return false; - } catch { /* corrupt roadmap — be lenient and treat as verified */ } + } catch (e) { /* corrupt roadmap — be lenient and treat as verified */ void e; } } } } @@ -273,7 +273,7 @@ export function persistCompletedKey(base: string, key: string): void { if (existsSync(file)) { keys = JSON.parse(readFileSync(file, "utf-8")); } - } catch { /* corrupt file — start fresh */ } + } catch (e) { /* corrupt file — start fresh */ void e; } if (!keys.includes(key)) { keys.push(key); // Atomic write: tmp file + rename prevents partial writes on crash @@ -292,7 +292,7 @@ export function removePersistedKey(base: string, key: string): void { keys = keys.filter(k => k !== key); writeFileSync(file, JSON.stringify(keys), "utf-8"); } - } catch { /* non-fatal */ } + } catch (e) { /* non-fatal: removePersistedKey failure */ void e; } } /** Load all completed unit keys from disk into the in-memory set. */ @@ -303,7 +303,7 @@ export function loadPersistedKeys(base: string, target: Set): void { const keys: string[] = JSON.parse(readFileSync(file, "utf-8")); for (const k of keys) target.add(k); } - } catch { /* non-fatal */ } + } catch (e) { /* non-fatal: loadPersistedKeys failure */ void e; } } // ─── Merge State Reconciliation ─────────────────────────────────────────────── @@ -394,8 +394,9 @@ export async function selfHealRuntimeRecords( if (healed > 0) { ctx.ui.notify(`Self-heal: cleared ${healed} stale runtime record(s).`, "info"); } - } catch { + } catch (e) { // Non-fatal — self-heal should never block auto-mode start + void e; } } diff --git a/src/resources/extensions/gsd/crash-recovery.ts b/src/resources/extensions/gsd/crash-recovery.ts index bb9bd6d6c..d58f903e4 100644 --- a/src/resources/extensions/gsd/crash-recovery.ts +++ b/src/resources/extensions/gsd/crash-recovery.ts @@ -50,7 +50,7 @@ export function writeLock( sessionFile, }; writeFileSync(lockPath(basePath), JSON.stringify(data, null, 2), "utf-8"); - } catch { /* non-fatal */ } + } catch (e) { /* non-fatal: lock write failure */ void e; } } /** Remove the lock file on clean stop. */ @@ -58,7 +58,7 @@ export function clearLock(basePath: string): void { try { const p = lockPath(basePath); if (existsSync(p)) unlinkSync(p); - } catch { /* non-fatal */ } + } catch (e) { /* non-fatal: lock clear failure */ void e; } } /** Check if a crash lock exists and return its data. */ @@ -68,7 +68,8 @@ export function readCrashLock(basePath: string): LockData | null { if (!existsSync(p)) return null; const raw = readFileSync(p, "utf-8"); return JSON.parse(raw) as LockData; - } catch { + } catch (e) { + /* non-fatal: corrupt or unreadable lock file */ void e; return null; } } diff --git a/src/resources/extensions/gsd/errors.ts b/src/resources/extensions/gsd/errors.ts new file mode 100644 index 000000000..ac30bb714 --- /dev/null +++ b/src/resources/extensions/gsd/errors.ts @@ -0,0 +1,31 @@ +/** + * GSD Error Types — Typed error hierarchy for diagnostics and crash recovery. + * + * All GSD-specific errors extend GSDError, which carries a stable `code` + * string suitable for programmatic matching. Error codes are defined as + * constants so callers can switch on them without string-matching. + */ + +// ─── Error Codes ────────────────────────────────────────────────────────────── + +export const GSD_STALE_STATE = "GSD_STALE_STATE"; +export const GSD_LOCK_HELD = "GSD_LOCK_HELD"; +export const GSD_DISPATCH_FAILED = "GSD_DISPATCH_FAILED"; +export const GSD_TIMEOUT = "GSD_TIMEOUT"; +export const GSD_ARTIFACT_MISSING = "GSD_ARTIFACT_MISSING"; +export const GSD_GIT_ERROR = "GSD_GIT_ERROR"; +export const GSD_MERGE_CONFLICT = "GSD_MERGE_CONFLICT"; +export const GSD_PARSE_ERROR = "GSD_PARSE_ERROR"; +export const GSD_IO_ERROR = "GSD_IO_ERROR"; + +// ─── Base Error ─────────────────────────────────────────────────────────────── + +export class GSDError extends Error { + readonly code: string; + + constructor(code: string, message: string, options?: ErrorOptions) { + super(message, options); + this.name = "GSDError"; + this.code = code; + } +} diff --git a/src/resources/extensions/gsd/git-service.ts b/src/resources/extensions/gsd/git-service.ts index 966ef6d3e..86f7be281 100644 --- a/src/resources/extensions/gsd/git-service.ts +++ b/src/resources/extensions/gsd/git-service.ts @@ -22,6 +22,7 @@ import { nativeBranchExists, nativeHasChanges, } from "./native-git-bridge.js"; +import { GSDError, GSD_MERGE_CONFLICT } from "./errors.js"; // ─── Types ───────────────────────────────────────────────────────────────── @@ -48,7 +49,7 @@ export interface CommitOptions { * The working tree is left in a conflicted state (no reset) so the * caller can dispatch a fix-merge session to resolve it. */ -export class MergeConflictError extends Error { +export class MergeConflictError extends GSDError { readonly conflictedFiles: string[]; readonly strategy: "squash" | "merge"; readonly branch: string; @@ -61,6 +62,7 @@ export class MergeConflictError extends Error { mainBranch: string, ) { super( + GSD_MERGE_CONFLICT, `${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" ` + `failed with conflicts in ${conflictedFiles.length} non-.gsd file(s): ${conflictedFiles.join(", ")}`, );