diff --git a/.changeset/fix-silent-catch-blocks.md b/.changeset/fix-silent-catch-blocks.md new file mode 100644 index 0000000..b6855ef --- /dev/null +++ b/.changeset/fix-silent-catch-blocks.md @@ -0,0 +1,8 @@ +--- +"@colony/hooks": patch +"@colony/foraging": 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 4a78307..5b4d99e 100644 --- a/apps/mcp-server/src/tools/foraging.ts +++ b/apps/mcp-server/src/tools/foraging.ts @@ -95,7 +95,14 @@ function enrichForagingHits( const rows = store.storage.getObservations(hits.map((h) => h.id)); const metadataById = new Map>(); for (const row of rows) { - metadataById.set(row.id, parseMeta(row.metadata)); + if (!row.metadata) continue; + 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`, + ); + } } 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 0bb971f..368fe51 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 08c095f..61d87d6 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,10 @@ 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 +538,10 @@ 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 +682,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 +704,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; } }