fix(gsd): add diagnostic logging to empty catch blocks in auto-mode

Auto-mode has empty catch blocks across 11 files that silently
swallow errors. When these operations fail (DB writes, git commands,
file sync, worktree operations), the error is lost and downstream
systems see stale or inconsistent state — leading to stuck loops,
phantom milestones, and silent data loss.

Replace every empty catch with a process.stderr.write() call that
logs the operation context and error message. Format:

  gsd [filename]: <operation> failed: <error.message>

For catches already annotated with /* non-fatal */ or /* best-effort */
comments, the logging is added alongside the annotation to preserve
the original intent while making failures observable.

Adds a regression test that scans all auto-mode source files and
asserts no empty catch blocks remain.

Files modified (11):
  auto-worktree.ts, auto.ts, auto-recovery.ts, auto-prompts.ts,
  auto-dashboard.ts, auto-start.ts, auto-timers.ts, auto-post-unit.ts,
  auto-dispatch.ts, auto-unit-closeout.ts, auto/phases.ts

No behavioral changes — only diagnostic output added.

Addresses #3348, addresses #3345
This commit is contained in:
Tibsfox 2026-04-04 04:03:14 -07:00
parent a061e3c276
commit 4f896cc561
12 changed files with 360 additions and 101 deletions

View file

@ -285,8 +285,9 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
taskDetails = dbTasks.map(t => ({ id: t.id, title: t.title, done: t.status === "complete" || t.status === "done" }));
}
}
} catch {
} catch (err) {
// Non-fatal — just omit task count
process.stderr.write(`gsd [auto-dashboard]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -297,8 +298,9 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
activeSliceTasks,
taskDetails,
};
} catch {
} catch (err) {
// Non-fatal — widget just won't show progress bar
process.stderr.write(`gsd [auto-dashboard]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -332,8 +334,9 @@ function refreshLastCommit(basePath: string): void {
};
}
lastCommitFetchedAt = Date.now();
} catch {
} catch (err) {
// Non-fatal — just skip last commit display
process.stderr.write(`gsd [auto-dashboard]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -376,7 +379,9 @@ function ensureWidgetModeLoaded(): void {
if (saved && WIDGET_MODES.includes(saved as WidgetMode)) {
widgetMode = saved as WidgetMode;
}
} catch { /* non-fatal — use default */ }
} catch (err) { /* non-fatal — use default */
process.stderr.write(`gsd [auto-dashboard]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
/** Persist widget mode to global preferences YAML. */
@ -395,7 +400,9 @@ function persistWidgetMode(mode: WidgetMode): void {
content = content.trimEnd() + "\n" + line + "\n";
}
writeFileSync(prefsPath, content, "utf-8");
} catch { /* non-fatal — mode still set in memory */ }
} catch (err) { /* non-fatal — mode still set in memory */
process.stderr.write(`gsd [auto-dashboard]: file write failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
/** Cycle to the next widget mode. Returns the new mode. */
@ -458,7 +465,9 @@ export function updateProgressWidget(
// Cache git branch at widget creation time (not per render)
let cachedBranch: string | null = null;
try { cachedBranch = getCurrentBranch(accessors.getBasePath()); } catch { /* not in git repo */ }
try { cachedBranch = getCurrentBranch(accessors.getBasePath()); } catch (err) { /* not in git repo */
process.stderr.write(`gsd [auto-dashboard]: git branch detection failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// Cache short pwd (last 2 path segments only) + worktree/branch info
let widgetPwd: string;
@ -519,7 +528,9 @@ export function updateProgressWidget(
}
refreshRtkLabel();
cachedLines = undefined;
} catch { /* non-fatal */ }
} catch (err) { /* non-fatal */
process.stderr.write(`gsd [auto-dashboard]: DB status update failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}, 15_000);
return {
@ -878,3 +889,4 @@ function padToWidth(s: string, colWidth: number): string {
if (vis >= colWidth) return truncateToWidth(s, colWidth, "…");
return s + " ".repeat(colWidth - vis);
}

View file

@ -712,7 +712,9 @@ export const DISPATCH_RULES: DispatchRule[] = [
}
}
}
} catch { /* fall through — don't block on DB errors */ }
} catch (err) { /* fall through — don't block on DB errors */
process.stderr.write(`gsd [auto-dispatch]: lock cleanup failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return {
action: "dispatch",
@ -754,8 +756,9 @@ export async function resolveDispatch(
try {
const registry = getRegistry();
return await registry.evaluateDispatch(ctx);
} catch {
} catch (err) {
// Registry not initialized — fall back to inline loop
process.stderr.write(`gsd [auto-dispatch]: dispatch failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
for (const rule of DISPATCH_RULES) {
@ -779,3 +782,4 @@ export async function resolveDispatch(
export function getDispatchRuleNames(): string[] {
return DISPATCH_RULES.map((r) => r.name);
}

View file

@ -279,8 +279,9 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
try {
const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
} catch {
} catch (err) {
// GitHub sync not available — skip
process.stderr.write(`gsd [auto-post-unit]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
taskContext = {
@ -732,3 +733,4 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
return "continue";
}

View file

@ -198,7 +198,9 @@ export async function inlineDependencySummaries(
}
// If slice not found in DB, fall through to file-based parsing
}
} catch { /* fall through */ }
} catch (err) { /* fall through */
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// If DB didn't provide depends, fall back to roadmap parsing
if (!depends) {
@ -276,8 +278,9 @@ export async function inlineDecisionsFromDb(
return `### Decisions\nSource: \`.gsd/DECISIONS.md\`\n\n${formatted}`;
}
}
} catch {
} catch (err) {
// DB not available — fall through to filesystem
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return inlineGsdRootFile(base, "decisions.md", "Decisions");
}
@ -303,8 +306,9 @@ export async function inlineRequirementsFromDb(
return `### Requirements\nSource: \`.gsd/REQUIREMENTS.md\`\n\n${formatted}`;
}
}
} catch {
} catch (err) {
// DB not available — fall through to filesystem
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return inlineGsdRootFile(base, "requirements.md", "Requirements");
}
@ -325,8 +329,9 @@ export async function inlineProjectFromDb(
return `### Project\nSource: \`.gsd/PROJECT.md\`\n\n${content}`;
}
}
} catch {
} catch (err) {
// DB not available — fall through to filesystem
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return inlineGsdRootFile(base, "project.md", "Project");
}
@ -486,8 +491,9 @@ export function buildSkillActivationBlock(params: {
for (const skillName of taskPlan.frontmatter.skills_used) {
matched.add(normalizeSkillReference(skillName));
}
} catch {
} catch (err) {
// Non-fatal — malformed task plan should not break prompt construction
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -736,7 +742,9 @@ export async function checkNeedsReassessment(
return { sliceId: lastCompleted };
}
}
} catch { /* fall through */ }
} catch (err) { /* fall through */
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// File-based fallback using roadmap checkboxes
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
@ -802,7 +810,9 @@ export async function checkNeedsRunUat(
return { sliceId: sid, uatType };
}
}
} catch { /* fall through */ }
} catch (err) { /* fall through */
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// File-based fallback using roadmap checkboxes
if (!prefs?.uat_dispatch) return null;
@ -1312,7 +1322,9 @@ export async function buildCompleteMilestonePrompt(
if (isDbAvailable()) {
sliceIds = getMilestoneSlices(mid).map(s => s.id);
}
} catch { /* fall through */ }
} catch (err) { /* fall through */
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// File-based fallback: parse roadmap for slice IDs when DB has no data
if (sliceIds.length === 0 && roadmapPath) {
const roadmapContent = await loadFile(roadmapPath);
@ -1393,7 +1405,9 @@ export async function buildValidateMilestonePrompt(
}
}
}
} catch { /* fall through */ }
} catch (err) { /* fall through */
process.stderr.write(`gsd [auto-prompts]: git push failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// Inline all slice summaries and UAT results
let valSliceIds: string[] = [];
@ -1402,7 +1416,9 @@ export async function buildValidateMilestonePrompt(
if (isDbAvailable()) {
valSliceIds = getMilestoneSlices(mid).map(s => s.id);
}
} catch { /* fall through */ }
} catch (err) { /* fall through */
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// File-based fallback: parse roadmap for slice IDs when DB has no data
if (valSliceIds.length === 0 && roadmapPath) {
const roadmapContent = await loadFile(roadmapPath);
@ -1541,8 +1557,9 @@ export async function buildReplanSlicePrompt(
`- **${c.id}**: "${c.text}" — ${c.rationale ?? "no rationale"}`
).join("\n");
}
} catch {
} catch (err) {
// Non-fatal — captures module may not be available
process.stderr.write(`gsd [auto-prompts]: capture count failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return loadPrompt("replan-slice", {
@ -1642,8 +1659,9 @@ export async function buildReassessRoadmapPrompt(
`- **${c.id}**: "${c.text}" — ${c.rationale ?? "deferred during triage"}`
).join("\n");
}
} catch {
} catch (err) {
// Non-fatal — captures module may not be available
process.stderr.write(`gsd [auto-prompts]: capture count failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
const reassessCommitInstruction = "Do not commit — .gsd/ planning docs are managed externally and not tracked in git.";
@ -1859,7 +1877,9 @@ export async function buildRewriteDocsPrompt(
.filter(t => t.status !== "complete" && t.status !== "done")
.map(t => ({ id: t.id }));
}
} catch { /* fall through */ }
} catch (err) { /* fall through */
process.stderr.write(`gsd [auto-prompts]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
if (!incompleteTasks) {
// DB unavailable — no task data to inline
@ -1911,3 +1931,4 @@ export async function buildRewriteDocsPrompt(
overridesPath: relGsdRootFile("OVERRIDES"),
});
}

View file

@ -109,8 +109,9 @@ function detectMainBranch(basePath: string): string {
encoding: "utf-8",
});
if (result.trim()) return "main";
} catch {
} catch (err) {
// main doesn't exist
process.stderr.write(`gsd [auto-recovery]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
try {
const result = execFileSync("git", ["rev-parse", "--verify", "master"], {
@ -119,8 +120,9 @@ function detectMainBranch(basePath: string): string {
encoding: "utf-8",
});
if (result.trim()) return "master";
} catch {
} catch (err) {
// master doesn't exist either
process.stderr.write(`gsd [auto-recovery]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return "main"; // default fallback
}
@ -144,8 +146,9 @@ function getChangedFilesSinceBranch(basePath: string, targetBranch: string): str
).trim();
return result ? result.split("\n").filter(Boolean) : [];
}
} catch {
} catch (err) {
// merge-base failed — fall back
process.stderr.write(`gsd [auto-recovery]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// Fallback: check last 20 commits
@ -246,8 +249,9 @@ export function verifyExpectedArtifact(
for (const gid of gateIds) {
if (pendingIds.has(gid)) return false;
}
} catch {
} catch (err) {
// DB unavailable — treat as verified to avoid blocking
process.stderr.write(`gsd [auto-recovery]: dispatch failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return true;
}
@ -335,8 +339,9 @@ export function verifyExpectedArtifact(
}
}
}
} catch {
} catch (err) {
// Parse failure — don't block; slice plan may have non-standard format
process.stderr.write(`gsd [auto-recovery]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
@ -418,7 +423,9 @@ export function writeBlockerPlaceholder(
if (unitType === "execute-task" && isDbAvailable()) {
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
if (mid && sid && tid) {
try { updateTaskStatus(mid, sid, tid, "complete", new Date().toISOString()); } catch { /* non-fatal */ }
try { updateTaskStatus(mid, sid, tid, "complete", new Date().toISOString()); } catch (err) { /* non-fatal */
process.stderr.write(`gsd [auto-recovery]: DB status update failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
@ -439,20 +446,23 @@ function abortAndResetMerge(
if (hasMergeHead) {
try {
nativeMergeAbort(basePath);
} catch {
} catch (err) {
/* best-effort */
process.stderr.write(`gsd [auto-recovery]: git merge-abort failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
} else if (squashMsgPath) {
try {
unlinkSync(squashMsgPath);
} catch {
} catch (err) {
/* best-effort */
process.stderr.write(`gsd [auto-recovery]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
try {
nativeResetHard(basePath);
} catch {
} catch (err) {
/* best-effort */
process.stderr.write(`gsd [auto-recovery]: git reset failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -592,3 +602,4 @@ export function buildLoopRemediationSteps(
}
return null;
}

View file

@ -112,8 +112,9 @@ async function openProjectDbIfPresent(basePath: string): Promise<void> {
try {
const { openDatabase } = await import("./gsd-db.js");
openDatabase(gsdDbPath);
} catch {
} catch (err) {
/* non-fatal — DB lifecycle block below will retry */
process.stderr.write(`gsd [auto-start]: DB open failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -213,8 +214,9 @@ export async function bootstrapAutoSession(
try {
nativeAddAll(base);
nativeCommit(base, "chore: init gsd");
} catch {
} catch (err) {
/* nothing to commit */
process.stderr.write(`gsd [auto-start]: mkdir failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -724,8 +726,9 @@ export async function bootstrapAutoSession(
}
}
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-start]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return true;
@ -735,3 +738,4 @@ export async function bootstrapAutoSession(
throw err;
}
}

View file

@ -99,8 +99,9 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
}
}
}
} catch {
} catch (err) {
// Non-fatal — fall through with no estimate
process.stderr.write(`gsd [auto-timers]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
const estimateMinutes = taskEstimate ? parseEstimateMinutes(taskEstimate) : null;
@ -219,7 +220,9 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
resolveAgentEndCancelled({ message: `Idle watchdog error: ${message}`, category: "idle", isTransient: true });
try {
ctx.ui.notify(`Idle watchdog error: ${message}`, "warning");
} catch { /* best effort */ }
} catch (err) { /* best effort */
process.stderr.write(`gsd [auto-timers]: notification failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}, 15000);
@ -253,7 +256,9 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
resolveAgentEndCancelled({ message: `Hard timeout error: ${message}`, category: "timeout", isTransient: true });
try {
ctx.ui.notify(`Hard timeout error: ${message}`, "warning");
} catch { /* best effort */ }
} catch (err) { /* best effort */
process.stderr.write(`gsd [auto-timers]: notification failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}, hardTimeoutMs);
@ -311,3 +316,4 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
}
}, 15_000);
}

View file

@ -41,8 +41,11 @@ export async function closeoutUnit(
if (process.env.GSD_DEBUG) console.error(`[gsd] memory extraction failed for ${unitType}/${unitId}:`, err);
});
}
} catch { /* non-fatal */ }
} catch (err) { /* non-fatal */
process.stderr.write(`gsd [auto-unit-closeout]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
return activityFile ?? undefined;
}

View file

@ -153,16 +153,19 @@ function forceOverwriteAssessmentsWithVerdict(
// Source has a verdict — force-copy into worktree
mkdirSync(dstSliceDir, { recursive: true });
safeCopy(srcFile, join(dstSliceDir, fileEntry.name), { force: true });
} catch {
} catch (err) {
/* non-fatal per file */
process.stderr.write(`gsd [auto-worktree]: mkdir failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
} catch {
} catch (err) {
/* non-fatal per slice */
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -182,8 +185,9 @@ function clearProjectRootStateFiles(basePath: string, milestoneId: string): void
for (const file of transientFiles) {
try {
unlinkSync(file);
} catch {
} catch (err) {
/* non-fatal — file may not exist */
process.stderr.write(`gsd [auto-worktree]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -211,14 +215,16 @@ function clearProjectRootStateFiles(basePath: string, milestoneId: string): void
for (const f of untrackedOutput.split("\n").filter(Boolean)) {
try {
unlinkSync(join(basePath, f));
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
}
} catch {
} catch (err) {
/* non-fatal — git command may fail if not in repo */
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
@ -313,8 +319,9 @@ export function syncProjectRootToWorktree(
unlinkSync(wtDb);
}
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -480,13 +487,15 @@ export function cleanStaleRuntimeUnits(
try {
unlinkSync(join(runtimeUnitsDir, file));
cleaned++;
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return cleaned;
}
@ -528,8 +537,9 @@ export function syncGsdStateToWorktree(
try {
cpSync(src, dst);
synced.push(f);
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
@ -548,8 +558,9 @@ export function syncGsdStateToWorktree(
try {
cpSync(src, dst);
synced.push(file);
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
break;
}
@ -578,8 +589,9 @@ export function syncGsdStateToWorktree(
try {
cpSync(srcDir, dstDir, { recursive: true });
synced.push(`milestones/${mid}/`);
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
} else {
// Milestone directory exists but may be missing files (stale snapshot).
@ -598,8 +610,9 @@ export function syncGsdStateToWorktree(
cpSync(srcFile, dstFile);
synced.push(`milestones/${mid}/${f}`);
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
@ -611,8 +624,9 @@ export function syncGsdStateToWorktree(
try {
cpSync(srcSlicesDir, dstSlicesDir, { recursive: true });
synced.push(`milestones/${mid}/slices/`);
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
} else if (existsSync(srcSlicesDir) && existsSync(dstSlicesDir)) {
// Both exist — sync missing slice directories
@ -628,19 +642,22 @@ export function syncGsdStateToWorktree(
try {
cpSync(srcSlice, dstSlice, { recursive: true });
synced.push(`milestones/${mid}/slices/${sid}/`);
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -692,8 +709,9 @@ export function syncWorktreeStateBack(
try {
reconcileWorktreeDb(mainDb, wtLocalDb);
synced.push("gsd.db (pre-upgrade reconcile)");
} catch {
} catch (err) {
// Non-fatal — file sync below is the fallback
process.stderr.write(`gsd [auto-worktree]: DB reconciliation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -710,8 +728,9 @@ export function syncWorktreeStateBack(
try {
cpSync(src, dst, { force: true });
synced.push(f);
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
@ -731,8 +750,9 @@ export function syncWorktreeStateBack(
for (const mid of wtMilestones) {
syncMilestoneDir(wtGsd, mainGsd, mid, synced);
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return { synced };
@ -756,12 +776,14 @@ function syncDirFiles(
try {
cpSync(join(srcDir, entry.name), join(dstDir, entry.name), { force: true });
synced.push(`${prefix}${entry.name}`);
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
} catch {
} catch (err) {
/* non-fatal — srcDir may not be readable */
process.stderr.write(`gsd [auto-worktree]: git push failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -804,8 +826,9 @@ function syncMilestoneDir(
syncDirFiles(wtTasksDir, mainTasksDir, isMd, synced, `milestones/${mid}/slices/${sid}/tasks/`);
}
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: mkdir failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
// ─── Worktree Post-Create Hook (#597) ────────────────────────────────────────
@ -837,7 +860,9 @@ export function runWorktreePostCreateHook(
return `Worktree post-create hook not found: ${resolved}`;
}
if (process.platform === "win32") {
try { resolved = realpathSync.native(resolved); } catch { /* keep original */ }
try { resolved = realpathSync.native(resolved); } catch (err) { /* keep original */
process.stderr.write(`gsd [auto-worktree]: realpath failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
try {
@ -921,8 +946,9 @@ function reconcilePlanCheckboxes(
results.push(full);
}
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: git push failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return results;
}
@ -972,8 +998,9 @@ function reconcilePlanCheckboxes(
if (changed) {
try {
atomicWriteSync(dstFile, updated, "utf-8");
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: file write failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
@ -1158,8 +1185,9 @@ export function teardownAutoWorktree(
// Attempt a direct filesystem removal as a fallback
try {
rmSync(wtDir, { recursive: true, force: true });
} catch {
} catch (err) {
// Non-fatal — the warning above tells the user how to clean up
process.stderr.write(`gsd [auto-worktree]: file removal failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
@ -1359,8 +1387,9 @@ export function mergeMilestoneToMain(
if (!isSamePath(worktreeDbPath, mainDbPath)) {
reconcileWorktreeDb(mainDbPath, worktreeDbPath);
}
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto-worktree]: DB reconciliation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -1515,9 +1544,10 @@ export function mergeMilestoneToMain(
);
stashed = true;
}
} catch {
} catch (err) {
// Stash failure is non-fatal — proceed without stash and let the merge
// report the dirty tree if it fails.
process.stderr.write(`gsd [auto-worktree]: git stash failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// 7a. Shelter queued milestone directories before the squash merge (#2505).
@ -1538,9 +1568,13 @@ export function mergeMilestoneToMain(
try {
mkdirSync(milestonesDir, { recursive: true });
cpSync(join(shelterDir, dirName), join(milestonesDir, dirName), { recursive: true, force: true });
} catch { /* best-effort */ }
} catch (err) { /* best-effort */
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
try { rmSync(shelterDir, { recursive: true, force: true }); } catch (err) { /* best-effort */
process.stderr.write(`gsd [auto-worktree]: shelter cleanup failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
try { rmSync(shelterDir, { recursive: true, force: true }); } catch { /* best-effort */ }
};
try {
@ -1557,13 +1591,15 @@ export function mergeMilestoneToMain(
cpSync(srcDir, dstDir, { recursive: true, force: true });
rmSync(srcDir, { recursive: true, force: true });
shelteredDirs.push(entry.name);
} catch {
} catch (err) {
// Non-fatal — if shelter fails, the merge may still succeed
process.stderr.write(`gsd [auto-worktree]: file copy failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
} catch {
} catch (err) {
// Non-fatal — proceed with merge; untracked files may block it
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// 7b. Clean up stale merge state before attempting squash merge (#2912).
@ -1577,7 +1613,9 @@ export function mergeMilestoneToMain(
const p = join(gitDir_, f);
if (existsSync(p)) unlinkSync(p);
}
} catch { /* best-effort */ }
} catch (err) { /* best-effort */
process.stderr.write(`gsd [auto-worktree]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// 8. Squash merge — auto-resolve .gsd/ state file conflicts (#530)
const mergeResult = nativeMergeSquash(originalBasePath_, milestoneBranch);
@ -1595,7 +1633,9 @@ export function mergeMilestoneToMain(
const p = join(gitDir_, f);
if (existsSync(p)) unlinkSync(p);
}
} catch { /* best-effort */ }
} catch (err) { /* best-effort */
process.stderr.write(`gsd [auto-worktree]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// Pop stash before throwing so local work is not lost.
if (stashed) {
@ -1605,7 +1645,9 @@ export function mergeMilestoneToMain(
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf-8",
});
} catch { /* stash pop conflict is non-fatal */ }
} catch (err) { /* stash pop conflict is non-fatal */
process.stderr.write(`gsd [auto-worktree]: git stash failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
restoreShelter();
// Restore cwd so the caller is not stranded on the integration branch
@ -1657,14 +1699,18 @@ export function mergeMilestoneToMain(
// Abort merge state so MERGE_HEAD is not left on disk (#2912).
// libgit2's merge creates MERGE_HEAD even for squash merges; if left
// dangling, subsequent merges fail and doctor reports corrupt state.
try { nativeMergeAbort(originalBasePath_); } catch { /* best-effort */ }
try { nativeMergeAbort(originalBasePath_); } catch (err) { /* best-effort */
process.stderr.write(`gsd [auto-worktree]: git merge-abort failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
try {
const gitDir_ = resolveGitDir(originalBasePath_);
for (const f of ["SQUASH_MSG", "MERGE_MSG", "MERGE_HEAD"]) {
const p = join(gitDir_, f);
if (existsSync(p)) unlinkSync(p);
}
} catch { /* best-effort */ }
} catch (err) { /* best-effort */
process.stderr.write(`gsd [auto-worktree]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// Pop stash before throwing so local work is not lost (#2151).
if (stashed) {
@ -1674,7 +1720,9 @@ export function mergeMilestoneToMain(
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf-8",
});
} catch { /* stash pop conflict is non-fatal */ }
} catch (err) { /* stash pop conflict is non-fatal */
process.stderr.write(`gsd [auto-worktree]: git stash failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
restoreShelter();
throw new MergeConflictError(
@ -1704,7 +1752,9 @@ export function mergeMilestoneToMain(
const p = join(gitDir_, f);
if (existsSync(p)) unlinkSync(p);
}
} catch { /* best-effort */ }
} catch (err) { /* best-effort */
process.stderr.write(`gsd [auto-worktree]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// 9a-ii. Restore stashed files now that the merge+commit is complete (#2151).
// Pop after commit so stashed changes do not interfere with the squash merge
@ -1752,7 +1802,9 @@ export function mergeMilestoneToMain(
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf-8",
});
} catch { /* stash may already be consumed */ }
} catch (err) { /* stash may already be consumed */
process.stderr.write(`gsd [auto-worktree]: git stash failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
} else {
// Non-.gsd conflicts remain — leave stash for manual resolution
logWarning("reconcile", "Stash pop conflict on non-.gsd files after merge", {
@ -1822,8 +1874,9 @@ export function mergeMilestoneToMain(
encoding: "utf-8",
});
pushed = true;
} catch {
} catch (err) {
// Push failure is non-fatal
process.stderr.write(`gsd [auto-worktree]: git push failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -1852,8 +1905,9 @@ export function mergeMilestoneToMain(
encoding: "utf-8",
});
prCreated = true;
} catch {
} catch (err) {
// PR creation failure is non-fatal — gh may not be installed or authenticated
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -1891,15 +1945,17 @@ export function mergeMilestoneToMain(
branch: null as unknown as string,
deleteBranch: false,
});
} catch {
} catch (err) {
// Best-effort -- worktree dir may already be gone
process.stderr.write(`gsd [auto-worktree]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// 13. Delete milestone branch (after worktree removal so ref is unlocked)
try {
nativeBranchDelete(originalBasePath_, milestoneBranch);
} catch {
} catch (err) {
// Best-effort
process.stderr.write(`gsd [auto-worktree]: git branch-delete failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// 14. Clear module state
@ -1908,3 +1964,4 @@ export function mergeMilestoneToMain(
return { commitMessage, pushed, prCreated, codeFilesChanged };
}

View file

@ -316,8 +316,9 @@ export function getAutoDashboardData(): AutoDashboardData {
if (s.basePath) {
pendingCaptureCount = countPendingCaptures(s.basePath);
}
} catch {
} catch (err) {
// Non-fatal — captures module may not be loaded
process.stderr.write(`gsd [auto]: capture count failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
return {
active: s.active,
@ -565,8 +566,9 @@ function cleanupAfterLoopExit(ctx: ExtensionContext): void {
try {
if (lockBase()) clearLock(lockBase());
if (lockBase()) releaseSessionLock(lockBase());
} catch {
} catch (err) {
/* best-effort — mirror stopAuto cleanup */
process.stderr.write(`gsd [auto]: lock cleanup failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
ctx.ui.setStatus("gsd-auto", undefined);
@ -578,8 +580,9 @@ function cleanupAfterLoopExit(ctx: ExtensionContext): void {
s.basePath = s.originalBasePath;
try {
process.chdir(s.basePath);
} catch {
} catch (err) {
/* best-effort */
process.stderr.write(`gsd [auto]: chdir failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
}
@ -651,8 +654,9 @@ export async function stopAuto(
} else {
milestoneComplete = true;
}
} catch {
} catch (err) {
// Non-fatal — fall through to preserveBranch path
process.stderr.write(`gsd [auto]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
if (milestoneComplete) {
@ -687,8 +691,9 @@ export async function stopAuto(
s.basePath = s.originalBasePath;
try {
process.chdir(s.basePath);
} catch {
} catch (err) {
/* best-effort */
process.stderr.write(`gsd [auto]: chdir failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
} catch (e) {
@ -760,7 +765,9 @@ export async function stopAuto(
try {
const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
if (existsSync(pausedPath)) unlinkSync(pausedPath);
} catch { /* non-fatal */ }
} catch (err) { /* non-fatal */
process.stderr.write(`gsd [auto]: file unlink failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// ── Step 13: Restore original model (before reset clears IDs) ──
try {
@ -794,7 +801,9 @@ export async function stopAuto(
const { closeBrowser } = await import("../browser-tools/lifecycle.js");
await closeBrowser();
}
} catch { /* non-fatal: browser-tools may not be loaded */ }
} catch (err) { /* non-fatal: browser-tools may not be loaded */
process.stderr.write(`gsd [auto]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// External cleanup (not covered by session reset)
clearInFlightTools();
@ -852,16 +861,18 @@ export async function pauseAuto(
JSON.stringify(pausedMeta, null, 2),
"utf-8",
);
} catch {
} catch (err) {
// Non-fatal — resume will still work via full bootstrap, just without worktree context
process.stderr.write(`gsd [auto]: file write failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
// Close out the current unit so its runtime record doesn't stay at "dispatched"
if (s.currentUnit && ctx) {
try {
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt);
} catch {
} catch (err) {
// Non-fatal — best-effort closeout on pause
process.stderr.write(`gsd [auto]: dispatch failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
s.currentUnit = null;
}
@ -1085,7 +1096,9 @@ export async function startAuto(
s.originalBasePath = meta.originalBasePath || base;
s.stepMode = meta.stepMode ?? requestedStepMode;
s.paused = true;
try { unlinkSync(pausedPath); } catch { /* non-fatal */ }
try { unlinkSync(pausedPath); } catch (err) { /* non-fatal */
process.stderr.write(`gsd [auto]: pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
ctx.ui.notify(
`Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`,
"info",
@ -1096,7 +1109,9 @@ export async function startAuto(
const summaryFile = resolveMilestoneFile(base, meta.milestoneId, "SUMMARY");
if (!mDir || summaryFile) {
// Stale milestone — clean up and fall through to fresh bootstrap
try { unlinkSync(pausedPath); } catch { /* non-fatal */ }
try { unlinkSync(pausedPath); } catch (err) { /* non-fatal */
process.stderr.write(`gsd [auto]: pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
ctx.ui.notify(
`Paused milestone ${meta.milestoneId} is ${!mDir ? "missing" : "already complete"}. Starting fresh.`,
"info",
@ -1107,7 +1122,9 @@ export async function startAuto(
s.stepMode = meta.stepMode ?? requestedStepMode;
s.paused = true;
// Clean up the persisted file — we're consuming it
try { unlinkSync(pausedPath); } catch { /* non-fatal */ }
try { unlinkSync(pausedPath); } catch (err) { /* non-fatal */
process.stderr.write(`gsd [auto]: pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
ctx.ui.notify(
`Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`,
"info",
@ -1115,8 +1132,9 @@ export async function startAuto(
}
}
}
} catch {
} catch (err) {
// Malformed or missing — proceed with fresh bootstrap
process.stderr.write(`gsd [auto]: operation failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
@ -1242,8 +1260,9 @@ export async function startAuto(
try {
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
} catch {
} catch (err) {
// Best-effort only — sidebar sync must never block auto-mode startup
process.stderr.write(`gsd [auto]: cmux sync failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
@ -1415,8 +1434,9 @@ export async function dispatchHookUnit(
if (match) {
try {
await pi.setModel(match);
} catch {
} catch (err) {
/* non-fatal */
process.stderr.write(`gsd [auto]: dispatch failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
} else {
ctx.ui.notify(
@ -1453,7 +1473,9 @@ export async function dispatchHookUnit(
ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
// Ensure cwd matches basePath before hook dispatch (#1389)
try { if (process.cwd() !== s.basePath) process.chdir(s.basePath); } catch {}
try { if (process.cwd() !== s.basePath) process.chdir(s.basePath); } catch (err) {
process.stderr.write(`gsd [auto]: chdir failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
debugLog("dispatchHookUnit", {
phase: "send-message",
@ -1475,3 +1497,4 @@ export {
buildLoopRemediationSteps,
} from "./auto-recovery.js";
export { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";

View file

@ -1261,7 +1261,9 @@ export async function runUnitPhase(
blockers: [],
nextSteps: [],
});
} catch { /* non-fatal — anchor is advisory */ }
} catch (err) { /* non-fatal — anchor is advisory */
process.stderr.write(`gsd [phases]: phase anchor failed: ${err instanceof Error ? err.message : String(err)}\n`);
}
}
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "unit-end", data: { unitType, unitId, status: unitResult.status, artifactVerified, ...(unitResult.errorContext ? { errorContext: unitResult.errorContext } : {}) }, causedBy: { flowId: ic.flowId, seq: unitStartSeq } });
@ -1384,3 +1386,4 @@ export async function runFinalize(
return { action: "next", data: undefined as void };
}

View file

@ -0,0 +1,113 @@
/**
* Verify that auto-mode catch blocks emit diagnostic output instead of
* silently swallowing errors (#3348, #3345).
*
* This test scans the auto-mode source files and asserts that no empty
* catch blocks remain every catch must contain at least one statement
* beyond comments.
*/
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync, readdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const gsdDir = join(__dirname, "..");
function getAutoModeFiles(): string[] {
const files: string[] = [];
// Top-level auto*.ts files
for (const f of readdirSync(gsdDir)) {
if (f.startsWith("auto") && f.endsWith(".ts") && !f.endsWith(".test.ts")) {
files.push(join(gsdDir, f));
}
}
// auto/ subdirectory
const autoSubDir = join(gsdDir, "auto");
for (const f of readdirSync(autoSubDir)) {
if (f.endsWith(".ts") && !f.endsWith(".test.ts")) {
files.push(join(autoSubDir, f));
}
}
return files;
}
/**
* Scan a file for empty catch blocks catches whose body contains
* only whitespace and/or comments but no executable statements.
*/
function findEmptyCatches(filePath: string): Array<{ line: number; text: string }> {
const content = readFileSync(filePath, "utf-8");
const lines = content.split("\n");
const results: Array<{ line: number; text: string }> = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Match catch block opening
if (!/\}\s*catch\s*(\([^)]*\))?\s*\{/.test(line)) continue;
// Inline single-line catch: } catch { ... }
const inlineMatch = line.match(/\}\s*catch\s*(\([^)]*\))?\s*\{(.*)\}\s*;?\s*$/);
if (inlineMatch) {
const body = inlineMatch[2].trim();
// Check if body is only comments
const stripped = body.replace(/\/\*.*?\*\//g, "").replace(/\/\/.*/g, "").trim();
if (!stripped) {
results.push({ line: i + 1, text: line.trim() });
}
continue;
}
// Multi-line catch — scan until matching }
let j = i + 1;
let depth = 1;
const bodyLines: string[] = [];
while (j < lines.length && depth > 0) {
for (const ch of lines[j]) {
if (ch === "{") depth++;
else if (ch === "}") depth--;
}
bodyLines.push(lines[j].trim());
j++;
}
// Check if body (excluding closing brace) has any executable statements
const meaningful = bodyLines.slice(0, -1).filter(
(l) => l && !l.startsWith("//") && !l.startsWith("/*") && !l.startsWith("*") && l !== "}",
);
if (meaningful.length === 0) {
results.push({ line: i + 1, text: line.trim() });
}
}
return results;
}
describe("auto-mode diagnostic catch blocks (#3348)", () => {
test("no empty catch blocks remain in auto-mode files", () => {
const files = getAutoModeFiles();
assert.ok(files.length > 0, "should find auto-mode source files");
const violations: string[] = [];
for (const file of files) {
const empties = findEmptyCatches(file);
for (const empty of empties) {
const rel = file.replace(gsdDir + "/", "");
violations.push(`${rel}:${empty.line}${empty.text}`);
}
}
assert.equal(
violations.length,
0,
`Found ${violations.length} empty catch block(s) in auto-mode files:\n${violations.join("\n")}`,
);
});
});