From 7a2c57c6fed522499d14e12037797a05fa42d3eb Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 8 May 2026 19:40:33 +0200 Subject: [PATCH 1/2] fix(hooks,foraging,mcp-server): surface silent catch failures Rule #9: no silent failures. Every empty catch in session-start, scanner, and the foraging MCP tool now either logs [colony] : to stderr or carries a one-line comment explaining why tolerance is intentional (fs-stat races on missing entries, best-effort marker cleanup). A whole session 43/43 MCP call failures could previously vanish with no trace. --- .changeset/fix-silent-catch-blocks.md | 9 ++++++ apps/mcp-server/src/tools/foraging.ts | 4 ++- packages/foraging/src/scanner.ts | 18 ++++++++--- packages/hooks/src/handlers/session-start.ts | 32 ++++++++++++++------ 4 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 .changeset/fix-silent-catch-blocks.md diff --git a/.changeset/fix-silent-catch-blocks.md b/.changeset/fix-silent-catch-blocks.md new file mode 100644 index 00000000..e872835a --- /dev/null +++ b/.changeset/fix-silent-catch-blocks.md @@ -0,0 +1,9 @@ +--- +"@colony/hooks": patch +"@colony/foraging": patch +"@colony/mcp-server": patch +--- + +Surface silent `catch {}` failures to stderr (rule #9). + +Every empty catch in `session-start`, `scanner`, and the foraging MCP tool now either logs a `[colony] : ` line to stderr or carries a one-line comment explaining why silence is intentional (fs-stat races, missing-directory guards, best-effort cleanup). Previously a whole session's 43/43 MCP call failures could vanish with no trace. diff --git a/apps/mcp-server/src/tools/foraging.ts b/apps/mcp-server/src/tools/foraging.ts index 129b36ca..b020b46b 100644 --- a/apps/mcp-server/src/tools/foraging.ts +++ b/apps/mcp-server/src/tools/foraging.ts @@ -97,7 +97,9 @@ function enrichForagingHits( if (!row.metadata) continue; try { metadataById.set(row.id, JSON.parse(row.metadata) as Record); - } catch {} + } catch (err) { + process.stderr.write(`[colony] enrichForagingHits: observation ${row.id} metadata parse failed: ${(err as Error)?.message ?? err}\n`); + } } return hits.map((h) => { const md = metadataById.get(h.id); diff --git a/packages/foraging/src/scanner.ts b/packages/foraging/src/scanner.ts index 0bb971f4..368fe517 100644 --- a/packages/foraging/src/scanner.ts +++ b/packages/foraging/src/scanner.ts @@ -295,7 +295,9 @@ export function scanExamplesFs(opts: ScanFsOptions): ScanFsResult { let names: string[]; try { names = readdirSync(examplesDir); - } catch { + } catch (err) { + // examples/ directory may not exist on repos that haven't set up foraging yet. + process.stderr.write(`[colony] scanExamplesFs: ${(err as Error)?.message ?? err}\n`); return { scanned: [] }; } names.sort(); @@ -307,7 +309,9 @@ export function scanExamplesFs(opts: ScanFsOptions): ScanFsResult { let isDir = false; try { isDir = statSync(abs_path).isDirectory(); - } catch {} + } catch { + // Tolerant: entry disappeared between readdir and stat (race or dangling symlink); skip it. + } if (!isDir) continue; if (example_name === COCOINDEX_EXAMPLE_NAME && isLargeCocoindexExample(abs_path)) { @@ -441,7 +445,9 @@ function compactExistingPaths(abs_path: string, paths: readonly string[]): strin try { const st = statSync(abs); out.add(st.isDirectory() ? `${p.replace(/\/$/, '')}/` : p); - } catch {} + } catch { + // Tolerant: path listed in spec does not exist in this repo's working copy; omit it. + } } return Array.from(out).sort(); } @@ -460,7 +466,9 @@ function computeRufloContentHash( let st: Stats | null = null; try { st = statSync(abs); - } catch {} + } catch { + // Tolerant: filetree path may not exist in all repo variants; skip for hash contribution. + } if (!st) continue; hash.update(`${rel}\t${st.size}\n`); if (st.isFile()) { @@ -476,6 +484,7 @@ function fileExists(abs: string): boolean { try { return statSync(abs).isFile(); } catch { + // Tolerant: stat failure (ENOENT, EACCES) means file is absent from this working copy. return false; } } @@ -484,6 +493,7 @@ function directoryExists(abs: string): boolean { try { return statSync(abs).isDirectory(); } catch { + // Tolerant: stat failure (ENOENT, EACCES) means directory is absent from this working copy. return false; } } diff --git a/packages/hooks/src/handlers/session-start.ts b/packages/hooks/src/handlers/session-start.ts index 08c095fd..b155b5ef 100644 --- a/packages/hooks/src/handlers/session-start.ts +++ b/packages/hooks/src/handlers/session-start.ts @@ -146,9 +146,10 @@ function kickForagingScan(store: MemoryStore, input: HookInput): void { if (!claimForagingSessionStartScan(settings, cwd)) return; try { spawnNodeScript(cli, ['foraging', 'scan', '--cwd', cwd]); - } catch { + } catch (err) { releaseForagingSessionStartScan(settings, cwd); // Best-effort. Foraging is not load-bearing for the hook's primary job. + console.error(`[colony] kickForagingScan: ${(err as Error)?.message ?? err}`); } } @@ -180,7 +181,9 @@ function releaseForagingSessionStartScan(settings: Settings, cwd: string): void const markerPath = foragingSessionStartMarkerPath(settings, cwd); try { if (existsSync(markerPath)) unlinkSync(markerPath); - } catch {} + } catch { + // Tolerant: marker cleanup is best-effort; a stale marker just delays the next scan. + } } function foragingSessionStartMarkerPath(settings: Settings, cwd: string): string { @@ -194,6 +197,7 @@ function readForagingSessionStartMarker(markerPath: string): number | null { const value = parsed.last_started_at; return typeof value === 'number' && Number.isFinite(value) ? value : null; } catch { + // Tolerant: missing or malformed marker is treated as no prior scan; scan will proceed. return null; } } @@ -237,7 +241,8 @@ export function buildReadyClaimNudgePreface( let plans: ReturnType; try { plans = listPlans(store, { repo_root: detected.repo_root, limit: 50 }); - } catch { + } catch (err) { + console.error(`[colony] buildReadyClaimNudgePreface: ${(err as Error)?.message ?? err}`); return ''; } let unclaimed = 0; @@ -276,7 +281,8 @@ export function buildAttentionBudgetSection( agent, repo_root: detected.repo_root, }); - } catch { + } catch (err) { + console.error(`[colony] buildAttentionBudgetSection: ${(err as Error)?.message ?? err}`); return ''; } const budget = applyAttentionBudget(inbox); @@ -378,7 +384,8 @@ export function buildTaskPreface( repo_root: detected.repo_root, include_stalled_lanes: false, }).unread_messages; - } catch { + } catch (err) { + console.error(`[colony] buildTaskPreface unreadMessages: ${(err as Error)?.message ?? err}`); unreadMessages = []; } const others = thread.participants().filter((p) => p.session_id !== input.session_id); @@ -506,7 +513,8 @@ export async function buildSuggestionPreface( let queryEmbedding: Float32Array; try { queryEmbedding = await embedder.embed(suggestionQuery(input, detected.branch)); - } catch { + } catch (err) { + console.error(`[colony] buildSuggestionPreface embed: ${(err as Error)?.message ?? err}`); return ''; } @@ -518,7 +526,8 @@ export async function buildSuggestionPreface( exclude_task_ids: [thread.task_id], min_similarity: thresholds.SIMILARITY_FLOOR, }); - } catch { + } catch (err) { + console.error(`[colony] buildSuggestionPreface findSimilarTasks: ${(err as Error)?.message ?? err}`); return ''; } const top = similarTasks[0]; @@ -527,7 +536,8 @@ export async function buildSuggestionPreface( let payload: unknown; try { payload = core.buildSuggestionPayload(store, similarTasks); - } catch { + } catch (err) { + console.error(`[colony] buildSuggestionPreface buildSuggestionPayload: ${(err as Error)?.message ?? err}`); return ''; } if (!isSuggestionPayload(payload)) return ''; @@ -668,7 +678,8 @@ async function resolveSuggestionEmbedder(store: MemoryStore): Promise {}, }); - } catch { + } catch (err) { + console.error(`[colony] resolveSuggestionEmbedder: ${(err as Error)?.message ?? err}`); cachedSuggestionEmbedder = null; } return cachedSuggestionEmbedder; @@ -689,7 +700,8 @@ async function loadSuggestionCore(): Promise { findSimilarTasks: core.findSimilarTasks, buildSuggestionPayload: core.buildSuggestionPayload, }; - } catch { + } catch (err) { + console.error(`[colony] loadSuggestionCore: ${(err as Error)?.message ?? err}`); return null; } } From b4b0c8e200226ec0b857578e9b98ba33836cac80 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 8 May 2026 19:44:49 +0200 Subject: [PATCH 2/2] fix: apply biome format to long catch log lines; fix changeset package names Wrap two overlong process.stderr.write/console.error calls to satisfy biome line-length rule. Remove @colony/mcp-server from changeset (private package) and drop to the two publishable packages. --- .changeset/fix-silent-catch-blocks.md | 1 - apps/mcp-server/src/tools/foraging.ts | 4 +++- packages/hooks/src/handlers/session-start.ts | 8 ++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.changeset/fix-silent-catch-blocks.md b/.changeset/fix-silent-catch-blocks.md index e872835a..b6855efe 100644 --- a/.changeset/fix-silent-catch-blocks.md +++ b/.changeset/fix-silent-catch-blocks.md @@ -1,7 +1,6 @@ --- "@colony/hooks": patch "@colony/foraging": patch -"@colony/mcp-server": patch --- Surface silent `catch {}` failures to stderr (rule #9). diff --git a/apps/mcp-server/src/tools/foraging.ts b/apps/mcp-server/src/tools/foraging.ts index b020b46b..d37c8d3b 100644 --- a/apps/mcp-server/src/tools/foraging.ts +++ b/apps/mcp-server/src/tools/foraging.ts @@ -98,7 +98,9 @@ function enrichForagingHits( try { metadataById.set(row.id, JSON.parse(row.metadata) as Record); } catch (err) { - process.stderr.write(`[colony] enrichForagingHits: observation ${row.id} metadata parse failed: ${(err as Error)?.message ?? err}\n`); + process.stderr.write( + `[colony] enrichForagingHits: observation ${row.id} metadata parse failed: ${(err as Error)?.message ?? err}\n`, + ); } } return hits.map((h) => { diff --git a/packages/hooks/src/handlers/session-start.ts b/packages/hooks/src/handlers/session-start.ts index b155b5ef..61d87d65 100644 --- a/packages/hooks/src/handlers/session-start.ts +++ b/packages/hooks/src/handlers/session-start.ts @@ -527,7 +527,9 @@ export async function buildSuggestionPreface( min_similarity: thresholds.SIMILARITY_FLOOR, }); } catch (err) { - console.error(`[colony] buildSuggestionPreface findSimilarTasks: ${(err as Error)?.message ?? err}`); + console.error( + `[colony] buildSuggestionPreface findSimilarTasks: ${(err as Error)?.message ?? err}`, + ); return ''; } const top = similarTasks[0]; @@ -537,7 +539,9 @@ export async function buildSuggestionPreface( try { payload = core.buildSuggestionPayload(store, similarTasks); } catch (err) { - console.error(`[colony] buildSuggestionPreface buildSuggestionPayload: ${(err as Error)?.message ?? err}`); + console.error( + `[colony] buildSuggestionPreface buildSuggestionPayload: ${(err as Error)?.message ?? err}`, + ); return ''; } if (!isSuggestionPayload(payload)) return '';