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:
parent
a061e3c276
commit
4f896cc561
12 changed files with 360 additions and 101 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue