From cca13737b60a6efa92c9d144a4500aca4ca6542a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 26 Jul 2026 12:01:19 -0700 Subject: [PATCH] FN-8603: reduce steady-state diagnostic log noise Route routine core, engine, and dashboard diagnostics through debug-gated shared loggers. - Demote steady-state diagnostic sites while preserving warnings and errors for actionable failures. - Add cross-package severity contracts and manifest coverage for demoted log sites. - Document logging severity guidance and add a patch changeset. Files changed: .changeset/fn-8603-log-severity.md | 7 ++ docs/diagnostics.md | 20 ++++-- .../__tests__/log-severity-spam-contract.test.ts | 71 ++++++++++++++++++ packages/core/src/activity-analytics.ts | 5 +- packages/core/src/ai-summarize.ts | 61 +++++++--------- packages/core/src/async-mission-store.ts | 5 +- packages/core/src/async-secrets-store.ts | 7 +- packages/core/src/central-core.ts | 17 ++--- packages/core/src/docker-provisioning.ts | 13 ++-- packages/core/src/index.ts | 1 + packages/core/src/master-key.ts | 9 ++- packages/core/src/memory-compaction.ts | 29 ++++---- packages/core/src/memory-insights.ts | 7 +- packages/core/src/migration-orchestrator.ts | 7 +- packages/core/src/mission-store.ts | 5 +- packages/core/src/node-discovery.ts | 7 +- packages/core/src/notification/dispatcher.ts | 9 ++- .../core/src/plugins/bundled-plugin-install.ts | 11 +-- packages/core/src/reflection-store.ts | 5 +- packages/core/src/secrets-store.ts | 7 +- packages/core/src/task-store/agent-logs.ts | 21 +++--- packages/core/src/task-store/async-events.ts | 5 +- packages/core/src/task-store/async-maintenance.ts | 7 +- packages/core/src/task-store/comments-ops.ts | 7 +- packages/core/src/task-store/task-mutation-ops.ts | 11 +-- packages/core/src/task-store/workflow-integrity.ts | 9 ++- packages/core/src/types/merge-policy.ts | 5 +- packages/core/src/usage-events.ts | 5 +- .../__tests__/log-severity-spam-contract.test.ts | 48 +++++++++++++ packages/dashboard/src/ai-refine.ts | 5 +- packages/dashboard/src/ai-session-diagnostics.ts | 10 +-- packages/dashboard/src/chat.ts | 8 ++- packages/dashboard/src/devserver-manager.ts | 9 ++- packages/dashboard/src/file-service.ts | 5 +- packages/dashboard/src/github-tracking-comments.ts | 7 +- .../dashboard/src/github-tracking-reconciler.ts | 5 +- packages/dashboard/src/github-tracking-state.ts | 5 +- packages/dashboard/src/gitlab-lifecycle.ts | 5 +- packages/dashboard/src/insights-routes.ts | 9 ++- packages/dashboard/src/issue-image-attachments.ts | 5 +- packages/dashboard/src/knowledge-index.ts | 5 +- packages/dashboard/src/plugin-routes.ts | 7 +- packages/dashboard/src/routes/board-workflows.ts | 5 +- packages/dashboard/src/routes/context.ts | 5 +- .../dashboard/src/routes/register-auth-routes.ts | 13 ++-- .../routes/register-docker-provisioning-routes.ts | 7 +- .../dashboard/src/routes/register-git-github.ts | 21 +++--- packages/dashboard/src/routes/register-gitlab.ts | 7 +- .../src/routes/register-session-diff-routes.ts | 9 ++- .../src/routes/register-settings-memory-routes.ts | 7 +- .../src/routes/register-setup-activity-routes.ts | 7 +- .../dashboard/src/routes/register-signal-routes.ts | 5 +- .../src/routes/register-task-workflow-routes.ts | 11 +-- packages/dashboard/src/runtime-logger.ts | 11 +-- packages/dashboard/src/server.ts | 7 +- packages/dashboard/src/sse.ts | 8 ++- packages/dashboard/src/terminal-service.ts | 34 ++++----- packages/dashboard/src/view-chunk-manifest.ts | 5 +- .../engine/src/__tests__/log-severity-manifest.ts | 83 ++++++++++++++++++++++ .../__tests__/log-severity-spam-contract.test.ts | 40 ++++++++++- .../src/__tests__/logger-debug-gating.test.ts | 7 +- packages/engine/src/goal-anchoring-audit.ts | 5 +- packages/engine/src/plugin-runner.ts | 44 ++++++------ packages/engine/src/pty-native.ts | 9 ++- .../engine/src/runtimes/child-process-worker.ts | 4 +- packages/engine/src/self-healing.ts | 12 ++-- packages/engine/src/worktree-hooks.ts | 10 ++- 67 files changed, 632 insertions(+), 250 deletions(-) Fusion-Task-Id: FN-8603 Fusion-Task-Lineage: 53901db6-1af2-4bd7-b5ea-49507e048ef2 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8603-log-severity.md | 7 ++ docs/diagnostics.md | 20 +++-- .../log-severity-spam-contract.test.ts | 71 ++++++++++++++++ packages/core/src/activity-analytics.ts | 5 +- packages/core/src/ai-summarize.ts | 61 ++++++-------- packages/core/src/async-mission-store.ts | 5 +- packages/core/src/async-secrets-store.ts | 7 +- packages/core/src/central-core.ts | 17 ++-- packages/core/src/docker-provisioning.ts | 13 ++- packages/core/src/index.ts | 1 + packages/core/src/master-key.ts | 9 +- packages/core/src/memory-compaction.ts | 29 ++++--- packages/core/src/memory-insights.ts | 7 +- packages/core/src/migration-orchestrator.ts | 7 +- packages/core/src/mission-store.ts | 5 +- packages/core/src/node-discovery.ts | 7 +- packages/core/src/notification/dispatcher.ts | 9 +- .../src/plugins/bundled-plugin-install.ts | 11 ++- packages/core/src/reflection-store.ts | 5 +- packages/core/src/secrets-store.ts | 7 +- packages/core/src/task-store/agent-logs.ts | 21 +++-- packages/core/src/task-store/async-events.ts | 5 +- .../core/src/task-store/async-maintenance.ts | 7 +- packages/core/src/task-store/comments-ops.ts | 7 +- .../core/src/task-store/task-mutation-ops.ts | 11 ++- .../core/src/task-store/workflow-integrity.ts | 9 +- packages/core/src/types/merge-policy.ts | 5 +- packages/core/src/usage-events.ts | 5 +- .../log-severity-spam-contract.test.ts | 48 +++++++++++ packages/dashboard/src/ai-refine.ts | 5 +- .../dashboard/src/ai-session-diagnostics.ts | 10 +-- packages/dashboard/src/chat.ts | 8 +- packages/dashboard/src/devserver-manager.ts | 9 +- packages/dashboard/src/file-service.ts | 5 +- .../dashboard/src/github-tracking-comments.ts | 7 +- .../src/github-tracking-reconciler.ts | 5 +- .../dashboard/src/github-tracking-state.ts | 5 +- packages/dashboard/src/gitlab-lifecycle.ts | 5 +- packages/dashboard/src/insights-routes.ts | 9 +- .../dashboard/src/issue-image-attachments.ts | 5 +- packages/dashboard/src/knowledge-index.ts | 5 +- packages/dashboard/src/plugin-routes.ts | 7 +- .../dashboard/src/routes/board-workflows.ts | 5 +- packages/dashboard/src/routes/context.ts | 5 +- .../src/routes/register-auth-routes.ts | 13 +-- .../register-docker-provisioning-routes.ts | 7 +- .../src/routes/register-git-github.ts | 21 +++-- .../dashboard/src/routes/register-gitlab.ts | 7 +- .../routes/register-session-diff-routes.ts | 9 +- .../routes/register-settings-memory-routes.ts | 7 +- .../routes/register-setup-activity-routes.ts | 7 +- .../src/routes/register-signal-routes.ts | 5 +- .../routes/register-task-workflow-routes.ts | 11 ++- packages/dashboard/src/runtime-logger.ts | 11 +-- packages/dashboard/src/server.ts | 7 +- packages/dashboard/src/sse.ts | 8 +- packages/dashboard/src/terminal-service.ts | 34 ++++---- packages/dashboard/src/view-chunk-manifest.ts | 5 +- .../src/__tests__/log-severity-manifest.ts | 83 +++++++++++++++++++ .../log-severity-spam-contract.test.ts | 40 ++++++++- .../src/__tests__/logger-debug-gating.test.ts | 7 +- packages/engine/src/goal-anchoring-audit.ts | 5 +- packages/engine/src/plugin-runner.ts | 44 +++++----- packages/engine/src/pty-native.ts | 9 +- .../src/runtimes/child-process-worker.ts | 4 +- packages/engine/src/self-healing.ts | 12 +-- packages/engine/src/worktree-hooks.ts | 10 +-- 67 files changed, 632 insertions(+), 250 deletions(-) create mode 100644 .changeset/fn-8603-log-severity.md create mode 100644 packages/core/src/__tests__/log-severity-spam-contract.test.ts create mode 100644 packages/dashboard/src/__tests__/log-severity-spam-contract.test.ts create mode 100644 packages/engine/src/__tests__/log-severity-manifest.ts diff --git a/.changeset/fn-8603-log-severity.md b/.changeset/fn-8603-log-severity.md new file mode 100644 index 0000000000..06b2fe4846 --- /dev/null +++ b/.changeset/fn-8603-log-severity.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Reduce routine diagnostic noise in the operator log view. +category: fix +dev: Routes engine, core, and dashboard-server diagnostics through severity-aware shared loggers. diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 4cd3985795..8b23b15fca 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -1,10 +1,17 @@ # Diagnostics -## Debug-level engine logs (`FUSION_DEBUG`) +## Debug-level diagnostics (`FUSION_DEBUG`) -Engine subsystem loggers (`createLogger` in `packages/engine/src/logger.ts`) expose a `debug()` level for steady-state per-poll chatter. It is **off by default** so the TUI log pane and engine stderr show state *changes* rather than the scheduler reprinting its resting state every poll. +Engine and core subsystem loggers (`createLogger`) expose a `debug()` level for routine diagnostics. It is **off by default** so the TUI log pane and engine stderr show state *changes* rather than repeated resting-state chatter. -Opt in per subsystem with the logger prefix: +| Severity | Use it for | +| --- | --- | +| `debug()` | Repeated poll/sweep lines with unchanged state, expected skips and no-ops, per-item progress, expected-and-handled failures (fallback/retry/optional dependency), and diagnostics already recorded as run-audit events. | +| `log()` | A state transition or operator-visible event. | +| `warn()` | Handled degradation that needs eventual operator attention. | +| `error()` | An unrecovered failure requiring operator action; do not use it when code recovered or scheduled a retry. | + +Most new diagnostic sites should therefore start at `debug()` and only be promoted when they meet a higher-severity rule. `debug()` output is enabled per subsystem with the logger prefix: ```bash FUSION_DEBUG=scheduler # one subsystem @@ -14,12 +21,11 @@ FUSION_DEBUG=1 # everything (also: true, all, *) The variable is re-read per call, so it can be toggled on a long-lived process without recreating loggers. Debug lines emit under the `info` severity marker and render like any other info line. -Currently debug-gated: +Currently debug-gated classes include local/default routing, capacity and re-entrancy skips, poll/sweep no-actions, per-step success/progress, optional integration probes, and successful verification bookkeeping. State-changing recovery and dispatch outcomes remain visible. -- `Task routed to node=local (source=local)` — routing to a **remote** node stays at info; only the local default is demoted. -- `Hold release for deferred — no reservable slot for ` — being at capacity is the expected steady state, not an event. +Dashboard server code uses the core logger only: `import { createLogger } from "@fusion/core";`. Do not import an engine logger, use a relative cross-package logger path, or add a dashboard-local logger implementation. -Guidance for new log sites: if a line repeats on every scheduler poll while nothing changed, it belongs at `debug()`. Anything reporting a transition, a rejection, or something needing operator action stays at `log()`/`warn()`/`error()`. +`packages/engine/src/__tests__/log-severity-manifest.ts` and package contract tests pin individual demotions and the no-bare-console rule. This makes an accidental severity reversion a CI failure instead of an operator-visible log flood. ## Goal injection diagnostics (`[goal-injection]`) diff --git a/packages/core/src/__tests__/log-severity-spam-contract.test.ts b/packages/core/src/__tests__/log-severity-spam-contract.test.ts new file mode 100644 index 0000000000..a96b5cf662 --- /dev/null +++ b/packages/core/src/__tests__/log-severity-spam-contract.test.ts @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { createLogger } from "../logger.js"; +import { logSeverityManifest } from "../../../engine/src/__tests__/log-severity-manifest.js"; + +/* +FNXC:EngineDiagnostics 2026-08-01-10:46: +Core mirrors engine FUSION_DEBUG behavior; both implementations must preserve +silent-by-default routine diagnostics and the TUI's info severity marker. +*/ +describe("core logger debug gating", () => { + afterEach(() => { delete process.env.FUSION_DEBUG; vi.restoreAllMocks(); }); + + it.each(["1", "true", "all", "*", "core-test", "other,core-test"]) + ("emits an info-marked debug line when FUSION_DEBUG=%s", (value) => { + process.env.FUSION_DEBUG = value; + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + createLogger("core-test").debug("routine"); + expect(spy).toHaveBeenCalledOnce(); + expect(String(spy.mock.calls[0]?.[0])).toContain("\0fnlvl=info\0[core-test] routine"); + }); + + it("is silent when unset or its prefix is absent", () => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + createLogger("core-test").debug("routine"); + process.env.FUSION_DEBUG = "other"; + createLogger("core-test").debug("routine"); + expect(spy).not.toHaveBeenCalled(); + }); +}); + +/* +FNXC:EngineDiagnostics 2026-08-01-11:12: +FN-8603 requires core diagnostics to use the shared logger rather than bare +console output, preserving severity markers and FUSION_DEBUG gating. +*/ +function sourceFiles(root: string): string[] { + return readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const path = join(root, entry.name); + if (entry.isDirectory()) return entry.name === "__tests__" || entry.name === "__test-utils__" ? [] : sourceFiles(path); + return entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") ? [path] : []; + }); +} + +function withoutComments(source: string): string { + return source.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, ""); +} + +describe("core diagnostic source contract", () => { + // Logger implementation is the sole adapter allowed to write severity-marked console output. + const bareConsoleAllowlist = new Set([join(__dirname, "..", "logger.ts")]); + + it("pins each core demotion to one debug call site, not any louder severity", () => { + for (const entry of logSeverityManifest.filter((entry) => entry.pkg === "core")) { + const source = readFileSync(join(__dirname, "..", entry.file), "utf8"); + const matchingCalls = source.split("\n").filter((line) => line.includes(entry.anchor) && /\.(debug|log|warn|error)\(|console\.(log|warn|error)\(/.test(line)); + expect(matchingCalls, `${entry.file}: ${entry.anchor}`).toHaveLength(1); + const [line] = matchingCalls; + expect(line).toContain(`.${entry.severity}(`); + expect(line).not.toMatch(/\.(log|warn|error)\(|console\.(log|warn|error)\(/); + } + }); + + it("routes production diagnostics through createLogger", () => { + for (const file of sourceFiles(join(__dirname, ".."))) { + if (bareConsoleAllowlist.has(file)) continue; + expect(withoutComments(readFileSync(file, "utf8")), file).not.toMatch(/console\.(log|warn|error)\(/); + } + }); +}); diff --git a/packages/core/src/activity-analytics.ts b/packages/core/src/activity-analytics.ts index 8bc18684b0..502a7a60b6 100644 --- a/packages/core/src/activity-analytics.ts +++ b/packages/core/src/activity-analytics.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-activity-analytics"); import { sql } from "drizzle-orm"; import type { Database } from "./db.js"; import type { AsyncDataLayer } from "./postgres/data-layer.js"; @@ -925,7 +928,7 @@ export async function aggregateMonitorMetrics( // (it previously sat outside any try/catch), but log: a real failure here // (permissions, schema drift, bad bind) must not masquerade as "0 deploys". deployments = 0; - console.warn("[fusion] monitor metrics: deployments count failed in PG mode, reporting 0:", err); + severityAuditLog.warn("[fusion] monitor metrics: deployments count failed in PG mode, reporting 0:", err); } try { const openedFrom = query.from ? sql`AND opened_at >= ${query.from}` : sql``; diff --git a/packages/core/src/ai-summarize.ts b/packages/core/src/ai-summarize.ts index 8a06d7339f..8afc25d1c2 100644 --- a/packages/core/src/ai-summarize.ts +++ b/packages/core/src/ai-summarize.ts @@ -14,6 +14,9 @@ import { getFnAgent, type AgentMessage } from "./ai-engine-loader.js"; import { detectContentLanguage, localeDisplayName } from "./detect-content-language.js"; import { DANGLING_TAIL_STOPWORDS, stripDanglingTail, stripEmptyPlaceholders } from "./task-title-id-drift.js"; +import { createLogger } from "./logger.js"; + +const log = createLogger("ai-summarize"); // ── Constants ─────────────────────────────────────────────────────────────── @@ -144,7 +147,7 @@ function cleanupExpiredRateLimits(): void { } if (cleanedRateLimits > 0) { - console.log(`[ai-summarize] Cleanup: removed ${cleanedRateLimits} rate limit entries`); + log.debug(`Cleanup: removed ${cleanedRateLimits} rate limit entries`); } } @@ -208,9 +211,6 @@ export function validateDescription(description: unknown): string { // ── AI Integration ─────────────────────────────────────────────────────────── -/** Debug flag for AI operations */ -const DEBUG = process.env.FUSION_DEBUG_AI === "true"; - function isConfiguredModelNotFoundError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); return /Configured model .+ was not found in the pi model registry/.test(message); @@ -260,15 +260,15 @@ async function runTitleSummarizer( agentOptions.defaultModelId = modelId; } - if (DEBUG) console.log("[ai-summarize] Creating agent session..."); + log.debug("Creating agent session..."); const agentResult = await createFnAgent(agentOptions); if (!agentResult?.session) { - if (DEBUG) console.log("[ai-summarize] Failed to initialize AI agent - no session"); + log.debug("Failed to initialize AI agent - no session"); throw new AiServiceError("Failed to initialize AI agent"); } - if (DEBUG) console.log("[ai-summarize] Agent session created, sending prompt..."); + log.debug("Agent session created, sending prompt..."); try { // Wrap the user-supplied description in a delimiter so the model treats it @@ -291,18 +291,16 @@ async function runTitleSummarizer( // Check for session errors (pi SDK stores errors in state.error, does not throw) if (agentResult.session.state?.error) { const errorMsg = agentResult.session.state.error; - if (DEBUG) console.log(`[ai-summarize] Session error: ${errorMsg}`); + log.debug(`Session error: ${errorMsg}`); throw new AiServiceError(`AI session error: ${errorMsg}`); } - if (DEBUG) console.log("[ai-summarize] Prompt sent, extracting response from messages..."); + log.debug("Prompt sent, extracting response from messages..."); const messages: AgentMessage[] = agentResult.session.state?.messages ?? []; const assistantMessages = messages.filter((m: AgentMessage) => m.role === "assistant"); - if (DEBUG) { - console.log(`[ai-summarize] Total messages: ${messages.length}, Assistant messages: ${assistantMessages.length}`); - } + log.debug(`Total messages: ${messages.length}, Assistant messages: ${assistantMessages.length}`); const lastMessage = assistantMessages.pop(); @@ -321,22 +319,22 @@ async function runTitleSummarizer( } } - if (DEBUG) console.log(`[ai-summarize] Extracted raw title: "${title}"`); + log.debug(`Extracted raw title: "${title}"`); const sanitized = sanitizeTitle(title); if (!sanitized) { - if (DEBUG) console.log("[ai-summarize] AI returned empty/unusable response"); + log.debug("AI returned empty/unusable response"); throw new AiServiceError("AI returned empty response"); } - if (DEBUG) console.log(`[ai-summarize] Title generation successful: "${sanitized}"`); + log.debug(`Title generation successful: "${sanitized}"`); return sanitized; } catch (err) { if (err instanceof AiServiceError) { throw err; } const message = err instanceof Error ? err.message : "AI processing failed"; - if (DEBUG) console.log(`[ai-summarize] Unexpected error: ${message}`); + log.debug(`Unexpected error: ${message}`); throw new AiServiceError(message); } finally { // Ensure session is disposed even on error @@ -369,7 +367,7 @@ export async function summarizeTitle( const createFnAgent = await getFnAgent(); if (!createFnAgent) { - if (DEBUG) console.log("[ai-summarize] AI engine not available"); + log.debug("AI engine not available"); throw new AiServiceError("AI engine not available"); } @@ -381,18 +379,15 @@ export async function summarizeTitle( } const staleModel = formatConfiguredModel(provider, modelId); - console.warn( - `[ai-summarize] Configured title summarizer model ${staleModel} was not found in the pi model registry; ` - + "retrying with automatic model resolution.", + log.warn( + `Configured title summarizer model ${staleModel} was not found in the pi model registry; retrying with automatic model resolution.`, ); try { return await runTitleSummarizer(createFnAgent, description, rootDir); } catch (retryError) { const message = retryError instanceof Error ? retryError.message : String(retryError); - console.warn( - `[ai-summarize] Automatic title summarizer fallback after stale model ${staleModel} failed: ${message}`, - ); + log.warn(`Automatic title summarizer fallback after stale model ${staleModel} failed: ${message}`); return null; } } @@ -635,7 +630,7 @@ export async function summarizeCommitBody( try { const createFnAgent = await getFnAgent(); if (!createFnAgent) { - if (DEBUG) console.log("[ai-summarize] AI engine not available for commit body"); + log.debug("AI engine not available for commit body"); return null; } @@ -663,7 +658,7 @@ export async function summarizeCommitBody( if (aborter.signal.aborted) return null; if (session.state?.error) { - if (DEBUG) console.log(`[ai-summarize] Commit-body session error: ${session.state.error}`); + log.debug(`Commit-body session error: ${session.state.error}`); return null; } @@ -690,10 +685,8 @@ export async function summarizeCommitBody( } return body; } catch (err) { - if (DEBUG) { - const message = err instanceof Error ? err.message : String(err); - console.log(`[ai-summarize] Commit-body generation failed: ${message}`); - } + const message = err instanceof Error ? err.message : String(err); + log.debug(`Commit-body generation failed: ${message}`); return null; } finally { clearTimeout(timer); @@ -818,7 +811,7 @@ export async function summarizeCommitSubject( try { const createFnAgent = await getFnAgent(); if (!createFnAgent) { - if (DEBUG) console.log("[ai-summarize] AI engine not available for commit subject"); + log.debug("AI engine not available for commit subject"); return null; } @@ -846,7 +839,7 @@ export async function summarizeCommitSubject( if (aborter.signal.aborted) return null; if (session.state?.error) { - if (DEBUG) console.log(`[ai-summarize] Commit-subject session error: ${session.state.error}`); + log.debug(`Commit-subject session error: ${session.state.error}`); return null; } @@ -868,10 +861,8 @@ export async function summarizeCommitSubject( return sanitizeCommitSubject(raw); } catch (err) { - if (DEBUG) { - const message = err instanceof Error ? err.message : String(err); - console.log(`[ai-summarize] Commit-subject generation failed: ${message}`); - } + const message = err instanceof Error ? err.message : String(err); + log.debug(`Commit-subject generation failed: ${message}`); return null; } finally { clearTimeout(timer); diff --git a/packages/core/src/async-mission-store.ts b/packages/core/src/async-mission-store.ts index a0d5134927..65e0937d55 100644 --- a/packages/core/src/async-mission-store.ts +++ b/packages/core/src/async-mission-store.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-async-mission-store"); /** * Event-emitting PostgreSQL MissionStore facade. * @@ -977,7 +980,7 @@ export class AsyncMissionStore extends EventEmitter { try { await this.triageSlice(id); } catch (err) { - console.error(`[AsyncMissionStore] Auto-triage failed for slice ${id}:`, err); + severityAuditLog.error(`[AsyncMissionStore] Auto-triage failed for slice ${id}:`, err); } } this.emit("slice:activated", updated); diff --git a/packages/core/src/async-secrets-store.ts b/packages/core/src/async-secrets-store.ts index cef7d4dbe2..999668d87c 100644 --- a/packages/core/src/async-secrets-store.ts +++ b/packages/core/src/async-secrets-store.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-async-secrets-store"); /** * Async Drizzle SecretsStore helpers (U6 satellite-central-archive-db). * @@ -466,7 +469,7 @@ export class AsyncSecretsStore { try { this.options.auditEmitter(event); } catch (error) { - console.warn("[async-secrets-store] audit emitter failed", error); + severityAuditLog.warn("[async-secrets-store] audit emitter failed", error); } } @@ -564,7 +567,7 @@ export class AsyncSecretsStore { }); } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.warn(`[async-secrets-store] failed to reveal env exportable secret ${row.scope}:${row.key}: ${message}`); + severityAuditLog.warn(`[async-secrets-store] failed to reveal env exportable secret ${row.scope}:${row.key}: ${message}`); } }; diff --git a/packages/core/src/central-core.ts b/packages/core/src/central-core.ts index f8ee1f1af0..0442407b6c 100644 --- a/packages/core/src/central-core.ts +++ b/packages/core/src/central-core.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-central-core"); /** * CentralCore — Main API for fn's multi-project central infrastructure. * @@ -274,19 +277,19 @@ export class CentralCore extends EventEmitter { private readonly onDiscoveryNodeDiscovered = (node: DiscoveredNode): void => { void this.handleDiscoveryNodeDiscovered(node).catch((error) => { - console.warn("[central-core] Failed to process discovered node", error); + severityAuditLog.warn("[central-core] Failed to process discovered node", error); }); }; private readonly onDiscoveryNodeUpdated = (node: DiscoveredNode): void => { void this.handleDiscoveryNodeUpdated(node).catch((error) => { - console.warn("[central-core] Failed to process discovery node update", error); + severityAuditLog.warn("[central-core] Failed to process discovery node update", error); }); }; private readonly onDiscoveryNodeLost = (name: string): void => { void this.handleDiscoveryNodeLost(name).catch((error) => { - console.warn("[central-core] Failed to process discovery node loss", error); + severityAuditLog.warn("[central-core] Failed to process discovery node loss", error); }); }; @@ -362,7 +365,7 @@ export class CentralCore extends EventEmitter { } await this.markLocalNodeOffline().catch((error) => { - console.warn("[central-core] Failed to persist local node offline during close", error); + severityAuditLog.warn("[central-core] Failed to persist local node offline during close", error); }); // FNXC:CentralCore 2026-06-26-12:30: In backend mode there is no SQLite @@ -560,7 +563,7 @@ export class CentralCore extends EventEmitter { if (this.backendMode) { // FNXC:CentralCore 2026-06-26-12:30: Backend mode delegates to PostgreSQL. await asyncCentralCore.insertProjectRow(this.asyncLayer!, project, now); - console.log( + severityAuditLog.log( `[central] reattached project ${project.id} at ${project.path} using stored identity (createdAt=${now})`, ); this.emit("project:reattached", project, "identity-recovered"); @@ -569,9 +572,7 @@ export class CentralCore extends EventEmitter { this.insertProjectRow(project, now); this.db!.bumpLastModified(); - console.log( - `[central] reattached project ${project.id} at ${project.path} using stored identity (createdAt=${now})`, - ); + severityAuditLog.debug(`[central] local reattached project ${project.id} at ${project.path} using stored identity (createdAt=${now})`); this.emit("project:reattached", project, "identity-recovered"); return project; } diff --git a/packages/core/src/docker-provisioning.ts b/packages/core/src/docker-provisioning.ts index e04ceeac6e..b8241c5c62 100644 --- a/packages/core/src/docker-provisioning.ts +++ b/packages/core/src/docker-provisioning.ts @@ -7,12 +7,9 @@ import type { DockerProvisionResult, } from "./types.js"; import { DockerClientService } from "./docker-client.js"; +import { createLogger } from "./logger.js"; -const log = { - info: (...args: unknown[]) => console.log("[docker-provisioning]", ...args), - error: (...args: unknown[]) => console.error("[docker-provisioning]", ...args), - warn: (...args: unknown[]) => console.warn("[docker-provisioning]", ...args), -}; +const log = createLogger("docker-provisioning"); /** * Service for provisioning Docker-based Fusion nodes. @@ -37,7 +34,7 @@ export class DockerProvisioningService { // Step 1: Pull or validate image if (input.imageConfig.pullImage) { - log.info(`Pulling image ${imageRef}...`); + log.debug(`Pulling image ${imageRef}...`); try { const authOptions = input.imageConfig.registryUsername || input.imageConfig.registryPassword @@ -197,7 +194,7 @@ export class DockerProvisioningService { } const durationMs = Date.now() - startTime; - log.info(`Container ${containerName} provisioned successfully in ${durationMs}ms`); + log.debug(`Container ${containerName} provisioned successfully in ${durationMs}ms`); return { success: true, @@ -242,7 +239,7 @@ export class DockerProvisioningService { } await container.remove({ force: true, v: removeVolumes }); - log.info(`Container ${containerId} deprovisioned`); + log.debug(`Container ${containerId} deprovisioned`); return { success: true }; } catch (error) { const message = error instanceof Error ? error.message : String(error); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 73f16cf9a3..fc1c56f507 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2644,5 +2644,6 @@ export type { LanguageFamily, DetectedContentLanguage } from "./detect-content-l export { promoteResearchFinding } from "./research-feature-promotion.js"; export type { ResearchFeaturePromotionInput } from "./research-feature-promotion.js"; export { getTotalAgentActiveMs, startPlanningSegment, finalizePlanningSegment } from "./task-timing.js"; +export { createLogger, type Logger } from "./logger.js"; export { ACTIVE_WORKFLOW_WORK_ITEM_STATES } from "./types.js"; export * from "./task-document-concurrency.js"; diff --git a/packages/core/src/master-key.ts b/packages/core/src/master-key.ts index 9d22e8fe4c..b28e9d8cf0 100644 --- a/packages/core/src/master-key.ts +++ b/packages/core/src/master-key.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-master-key"); import { randomBytes } from "node:crypto"; import * as fs from "node:fs/promises"; import { join } from "node:path"; @@ -119,7 +122,7 @@ export class MasterKeyManager { if (afterRace) { return { key: afterRace, backend: "keychain" }; } - console.warn("master key keychain unavailable; using file backend"); + severityAuditLog.warn("master key keychain unavailable; using file backend"); } } @@ -166,7 +169,7 @@ export class MasterKeyManager { if (error instanceof MasterKeyCorruptError) { throw error; } - console.warn("master key keychain unavailable; using file backend"); + severityAuditLog.warn("master key keychain unavailable; using file backend"); return null; } } @@ -218,7 +221,7 @@ export class MasterKeyManager { ); return true; } catch { - console.warn("master key keychain unavailable; using file backend"); + severityAuditLog.warn("master key keychain unavailable; using file backend"); return false; } } diff --git a/packages/core/src/memory-compaction.ts b/packages/core/src/memory-compaction.ts index cbd1711616..d671a2ab36 100644 --- a/packages/core/src/memory-compaction.ts +++ b/packages/core/src/memory-compaction.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-memory-compaction"); /** * AI Memory Compaction Service * @@ -50,8 +53,6 @@ Your job is to compress the provided project memory markdown into a shorter vers Return only the compacted markdown content.`; /** Debug flag for AI operations */ -const DEBUG = process.env.FUSION_DEBUG_AI === "true"; - // ── Custom Errors ─────────────────────────────────────────────────────────── export class AiServiceError extends Error { @@ -81,7 +82,7 @@ export async function compactMemoryWithAi( ): Promise { const createFnAgent = await getFnAgent(); if (!createFnAgent) { - if (DEBUG) console.log("[memory-compaction] AI engine not available"); + severityAuditLog.debug("[memory-compaction] AI engine not available"); throw new AiServiceError("AI engine not available"); } @@ -103,7 +104,7 @@ export async function compactMemoryWithAi( agentOptions.defaultModelId = modelId; } - if (DEBUG) console.log("[memory-compaction] Creating agent session..."); + severityAuditLog.debug("[memory-compaction] Creating agent session..."); /* * FNXC:McpConfig 2026-06-26-00:00: * Memory compaction lives in @fusion/core and cannot import engine MCP resolution without introducing a core→engine cycle. This readonly session intentionally runs without configured MCP servers until a neutral store/secrets seam exists in core. @@ -111,11 +112,11 @@ export async function compactMemoryWithAi( const agentResult = await createFnAgent(agentOptions); if (!agentResult?.session) { - if (DEBUG) console.log("[memory-compaction] Failed to initialize AI agent - no session"); + severityAuditLog.debug("[memory-compaction] Failed to initialize AI agent - no session"); throw new AiServiceError("Failed to initialize AI agent"); } - if (DEBUG) console.log("[memory-compaction] Agent session created, sending prompt..."); + severityAuditLog.debug("[memory-compaction] Agent session created, sending prompt..."); try { // Send the memory content to the agent @@ -124,18 +125,16 @@ export async function compactMemoryWithAi( // Check for session errors (pi SDK stores errors in state.error, does not throw) if (agentResult.session.state?.error) { const errorMsg = agentResult.session.state.error; - if (DEBUG) console.log(`[memory-compaction] Session error: ${errorMsg}`); + severityAuditLog.debug(`[memory-compaction] Session error: ${errorMsg}`); throw new AiServiceError(`AI session error: ${errorMsg}`); } - if (DEBUG) console.log("[memory-compaction] Prompt sent, extracting response from messages..."); + severityAuditLog.debug("[memory-compaction] Prompt sent, extracting response from messages..."); const messages: AgentMessage[] = agentResult.session.state?.messages ?? []; const assistantMessages = messages.filter((m: AgentMessage) => m.role === "assistant"); - if (DEBUG) { - console.log(`[memory-compaction] Total messages: ${messages.length}, Assistant messages: ${assistantMessages.length}`); - } + severityAuditLog.debug(`[memory-compaction] Total messages: ${messages.length}, Assistant messages: ${assistantMessages.length}`); const lastMessage = assistantMessages.pop(); @@ -154,21 +153,21 @@ export async function compactMemoryWithAi( } } - if (DEBUG) console.log(`[memory-compaction] Extracted compacted content length: ${compacted.length}`); + severityAuditLog.debug(`[memory-compaction] Extracted compacted content length: ${compacted.length}`); if (!compacted) { - if (DEBUG) console.log("[memory-compaction] AI returned empty response"); + severityAuditLog.debug("[memory-compaction] AI returned empty response"); throw new AiServiceError("AI returned empty response"); } - if (DEBUG) console.log("[memory-compaction] Memory compaction successful"); + severityAuditLog.debug("[memory-compaction] Memory compaction successful"); return compacted; } catch (err) { if (err instanceof AiServiceError) { throw err; } const message = err instanceof Error ? err.message : "AI processing failed"; - if (DEBUG) console.log(`[memory-compaction] Unexpected error: ${message}`); + severityAuditLog.debug(`[memory-compaction] Unexpected error: ${message}`); throw new AiServiceError(message); } finally { // Ensure session is disposed even on error diff --git a/packages/core/src/memory-insights.ts b/packages/core/src/memory-insights.ts index 16dafe83e2..35f279b549 100644 --- a/packages/core/src/memory-insights.ts +++ b/packages/core/src/memory-insights.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-memory-insights"); /** * Two-Stage Memory System with Automated Insight Extraction * @@ -1726,7 +1729,7 @@ export async function processAndAuditInsightExtraction( updatedAt: new Date().toISOString(), }); } catch (err) { - console.error( + severityAuditLog.error( `[memory-audit] Failed to persist audit state: ${err instanceof Error ? err.message : String(err)}`, ); } @@ -1740,7 +1743,7 @@ export async function processAndAuditInsightExtraction( await writeMemoryAudit(rootDir, auditMarkdown); } catch (err) { // Best-effort: log but don't throw - console.error( + severityAuditLog.error( `[memory-audit] Failed to write audit report: ${err instanceof Error ? err.message : String(err)}`, ); } diff --git a/packages/core/src/migration-orchestrator.ts b/packages/core/src/migration-orchestrator.ts index d6cf6a5214..af308df372 100644 --- a/packages/core/src/migration-orchestrator.ts +++ b/packages/core/src/migration-orchestrator.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-migration-orchestrator"); /** * Migration Orchestrator — Coordinates auto-migration from single-project to multi-project mode. * @@ -16,7 +19,7 @@ * if (await orchestrator.needsMigration()) { * // Run migration with auto-registration * const result = await orchestrator.runMigration({ autoRegister: true }); - * console.log(`Registered ${result.projectsRegistered.length} projects`); + * severityAuditLog.debug(`Registered ${result.projectsRegistered.length} projects`); * } * ``` */ @@ -281,7 +284,7 @@ export class MigrationOrchestrator { registered.push(activeProject); } catch (err) { // Log but continue with other projects - console.warn(`[migration] Failed to register ${project.path}:`, (err as Error).message); + severityAuditLog.warn(`[migration] Failed to register ${project.path}:`, (err as Error).message); } } diff --git a/packages/core/src/mission-store.ts b/packages/core/src/mission-store.ts index 9f4f3a0aa9..5fee8bae2d 100644 --- a/packages/core/src/mission-store.ts +++ b/packages/core/src/mission-store.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-mission-store"); /** * MissionStore - Data layer for the Missions hierarchy system. * @@ -2086,7 +2089,7 @@ export class MissionStore extends EventEmitter { await this.triageSlice(id); } catch (err) { // Log but don't fail — triage failures shouldn't block slice activation - console.error(`[MissionStore] Auto-triage failed for slice ${id}:`, err); + severityAuditLog.error(`[MissionStore] Auto-triage failed for slice ${id}:`, err); } } diff --git a/packages/core/src/node-discovery.ts b/packages/core/src/node-discovery.ts index 2c0fba58b0..45bdf577cc 100644 --- a/packages/core/src/node-discovery.ts +++ b/packages/core/src/node-discovery.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-node-discovery"); import { EventEmitter } from "node:events"; import os from "node:os"; import { Bonjour, type Browser, type Service } from "bonjour-service"; @@ -362,11 +365,11 @@ export class NodeDiscovery extends EventEmitter { private warn(message: string, error?: unknown): void { if (error) { - console.warn(`[node-discovery] ${message}`, error); + severityAuditLog.warn(`[node-discovery] ${message}`, error); return; } - console.warn(`[node-discovery] ${message}`); + severityAuditLog.warn(`[node-discovery] ${message}`); } private reportError(message: string, error: unknown): void { diff --git a/packages/core/src/notification/dispatcher.ts b/packages/core/src/notification/dispatcher.ts index d150298382..bc72b6d0b3 100644 --- a/packages/core/src/notification/dispatcher.ts +++ b/packages/core/src/notification/dispatcher.ts @@ -1,3 +1,6 @@ +import { createLogger } from "../logger.js"; + +const severityAuditLog = createLogger("core-dispatcher"); import type { NotificationProvider } from "./provider.js"; import type { NotificationDispatcherConfig, @@ -38,7 +41,7 @@ export class NotificationDispatcher { return await provider.sendNotification(event, payload); } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.warn( + severityAuditLog.warn( `[notification-dispatcher] Provider ${providerId} failed for event ${event}: ${message}`, ); return { success: false, providerId, error: message }; @@ -60,7 +63,7 @@ export class NotificationDispatcher { await provider.initialize(this.config as Record); } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.warn( + severityAuditLog.warn( `[notification-dispatcher] Provider ${provider.getProviderId()} initialization failed: ${message}`, ); } @@ -79,7 +82,7 @@ export class NotificationDispatcher { await provider.shutdown(); } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.warn( + severityAuditLog.warn( `[notification-dispatcher] Provider ${provider.getProviderId()} shutdown failed: ${message}`, ); } diff --git a/packages/core/src/plugins/bundled-plugin-install.ts b/packages/core/src/plugins/bundled-plugin-install.ts index 1ee82ad715..90737a3500 100644 --- a/packages/core/src/plugins/bundled-plugin-install.ts +++ b/packages/core/src/plugins/bundled-plugin-install.ts @@ -1,3 +1,6 @@ +import { createLogger } from "../logger.js"; + +const severityAuditLog = createLogger("core-bundled-plugin-install"); /** * FNXC:PluginLoader 2026-07-07-00:00: * Bundled-plugin auto-install is host-agnostic in @fusion/core. Hosts (the CLI's @@ -120,7 +123,7 @@ export async function ensureBundledPluginInstalled( const entryPath = resolvePluginEntryPath(bundledDir); if (!entryPath) { - console.warn(`[plugins] Bundled plugin "${pluginId}" is missing a loadable entry file in ${bundledDir}`); + severityAuditLog.warn(`[plugins] Bundled plugin "${pluginId}" is missing a loadable entry file in ${bundledDir}`); return "missing-bundle"; } @@ -134,7 +137,7 @@ export async function ensureBundledPluginInstalled( try { await pluginLoader.loadPlugin(existingPlugin.id); } catch (err) { - console.warn("[plugins] failed to load bundled plugin", existingPlugin.id, err); + severityAuditLog.warn("[plugins] failed to load bundled plugin", existingPlugin.id, err); } } return "already-installed"; @@ -149,7 +152,7 @@ export async function ensureBundledPluginInstalled( try { await pluginLoader.loadPlugin(existingPlugin.id); } catch (err) { - console.warn("[plugins] failed to load bundled plugin", existingPlugin.id, err); + severityAuditLog.warn("[plugins] failed to load bundled plugin", existingPlugin.id, err); } } @@ -165,7 +168,7 @@ export async function ensureBundledPluginInstalled( try { await pluginLoader.loadPlugin(plugin.id); } catch (err) { - console.warn("[plugins] failed to load bundled plugin", plugin.id, err); + severityAuditLog.warn("[plugins] failed to load bundled plugin", plugin.id, err); } } diff --git a/packages/core/src/reflection-store.ts b/packages/core/src/reflection-store.ts index fd799042fd..541d5867e6 100644 --- a/packages/core/src/reflection-store.ts +++ b/packages/core/src/reflection-store.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-reflection-store"); import { randomUUID } from "node:crypto"; import { EventEmitter } from "node:events"; import { existsSync } from "node:fs"; @@ -253,7 +256,7 @@ export class ReflectionStore extends EventEmitter { try { reflections.push(JSON.parse(line) as AgentReflection); } catch (error) { - console.warn( + severityAuditLog.warn( `[ReflectionStore] Skipping malformed reflection line ${index + 1} for ${agentId}`, error, ); diff --git a/packages/core/src/secrets-store.ts b/packages/core/src/secrets-store.ts index 3ea29a1ad9..f75bfb2f52 100644 --- a/packages/core/src/secrets-store.ts +++ b/packages/core/src/secrets-store.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-secrets-store"); import { randomUUID } from "node:crypto"; import type { Database as ProjectDatabase } from "./db.js"; import type { CentralDatabase } from "./central-db.js"; @@ -131,7 +134,7 @@ export class SecretsStore { try { this.options.auditEmitter(event); } catch (error) { - console.warn("[secrets-store] audit emitter failed", error); + severityAuditLog.warn("[secrets-store] audit emitter failed", error); } } @@ -203,7 +206,7 @@ export class SecretsStore { }); } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.warn(`[secrets-store] failed to reveal env exportable secret ${row.scope}:${row.key}: ${message}`); + severityAuditLog.warn(`[secrets-store] failed to reveal env exportable secret ${row.scope}:${row.key}: ${message}`); } }; diff --git a/packages/core/src/task-store/agent-logs.ts b/packages/core/src/task-store/agent-logs.ts index 2a11c1d090..0910ab09c8 100644 --- a/packages/core/src/task-store/agent-logs.ts +++ b/packages/core/src/task-store/agent-logs.ts @@ -1,3 +1,6 @@ +import { createLogger } from "../logger.js"; + +const severityAuditLog = createLogger("core-agent-logs"); /** * agent-logs operations. * @@ -50,7 +53,7 @@ export function flushAgentLogBufferImpl(store: TaskStore): void { validEntries = batch.filter((entry) => liveTaskIds.has(entry.taskId)); const dropped = batch.length - validEntries.length; if (dropped > 0) { - console.warn( + severityAuditLog.warn( `[fusion] Dropped ${dropped} buffered agent log entries for deleted tasks (${store.fusionDir})`, ); } @@ -84,7 +87,7 @@ export function flushAgentLogBufferImpl(store: TaskStore): void { ), ); } catch (err) { - console.warn("[fusion] Failed to scan goal citations from agent_log:", err); + severityAuditLog.warn("[fusion] Failed to scan goal citations from agent_log:", err); } } } @@ -97,10 +100,10 @@ export function flushAgentLogBufferImpl(store: TaskStore): void { // fire-and-forget agent-log path. try { void Promise.resolve(store.recordGoalCitations(citationInputs)).catch((err) => { - console.warn("[fusion] Failed to record goal citations from agent_log batch:", err); + severityAuditLog.warn("[fusion] Failed to record goal citations from agent_log batch:", err); }); } catch (err) { - console.warn("[fusion] Failed to record goal citations from agent_log batch:", err); + severityAuditLog.warn("[fusion] Failed to record goal citations from agent_log batch:", err); } } if (!store.backendMode) { @@ -117,7 +120,7 @@ export function flushAgentLogBufferImpl(store: TaskStore): void { try { store.flushAgentLogBuffer(); } catch (err) { - console.error(`[fusion] Retry agent log flush failed (${store.fusionDir}):`, err); + severityAuditLog.error(`[fusion] Retry agent log flush failed (${store.fusionDir}):`, err); } }, TaskStore.AGENT_LOG_FLUSH_MS); store.agentLogFlushTimer.unref(); @@ -152,7 +155,7 @@ export async function appendAgentLogBatchImpl(store: TaskStore, entries: Array<{ validEntries = normalizedEntries.filter((entry) => liveTaskIds.has(entry.taskId)); const dropped = normalizedEntries.length - validEntries.length; if (dropped > 0) { - console.warn(`[fusion] Dropped ${dropped} batch agent log entries for deleted tasks (${store.fusionDir})`); + severityAuditLog.warn(`[fusion] Dropped ${dropped} batch agent log entries for deleted tasks (${store.fusionDir})`); } } @@ -192,7 +195,7 @@ export async function appendAgentLogBatchImpl(store: TaskStore, entries: Array<{ ), ); } catch (err) { - console.warn("[fusion] Failed to scan goal citations from agent log batch:", err); + severityAuditLog.warn("[fusion] Failed to scan goal citations from agent log batch:", err); } } } @@ -201,10 +204,10 @@ export async function appendAgentLogBatchImpl(store: TaskStore, entries: Array<{ // promise so a citation-write failure is not an unhandled rejection. try { void Promise.resolve(store.recordGoalCitations(citationInputs)).catch((err) => { - console.warn("[fusion] Failed to record goal citations from appendAgentLogBatch:", err); + severityAuditLog.warn("[fusion] Failed to record goal citations from appendAgentLogBatch:", err); }); } catch (err) { - console.warn("[fusion] Failed to record goal citations from appendAgentLogBatch:", err); + severityAuditLog.warn("[fusion] Failed to record goal citations from appendAgentLogBatch:", err); } } if (validEntries.length > 0 && !store.backendMode) { diff --git a/packages/core/src/task-store/async-events.ts b/packages/core/src/task-store/async-events.ts index 64da44e72f..48ef5ea658 100644 --- a/packages/core/src/task-store/async-events.ts +++ b/packages/core/src/task-store/async-events.ts @@ -1,3 +1,6 @@ +import { createLogger } from "../logger.js"; + +const severityAuditLog = createLogger("core-async-events"); /** * Async Drizzle goal-citation / usage-event / plugin-activation helpers (U14). * @@ -252,7 +255,7 @@ export async function emitUsageEvent( }); return true; } catch (err) { - console.warn("[fusion] emitUsageEvent skipped a malformed/failed event:", err); + severityAuditLog.warn("[fusion] emitUsageEvent skipped a malformed/failed event:", err); return false; } } diff --git a/packages/core/src/task-store/async-maintenance.ts b/packages/core/src/task-store/async-maintenance.ts index c9f5fc6f6b..87488adf8d 100644 --- a/packages/core/src/task-store/async-maintenance.ts +++ b/packages/core/src/task-store/async-maintenance.ts @@ -1,3 +1,6 @@ +import { createLogger } from "../logger.js"; + +const severityAuditLog = createLogger("core-async-maintenance"); import { sql } from "drizzle-orm"; import type { AsyncDataLayer } from "../postgres/data-layer.js"; import { pruneAgentLogFiles as pruneAgentLogFileEntries } from "../agent-log-file-store.js"; @@ -31,7 +34,7 @@ export async function pruneAgentLogFilesAsync( } const boundProjectId = layer.projectId?.trim(); if (!boundProjectId) { - console.warn("[fusion] PostgreSQL agent-log-file pruning is using the legacy unscoped project sentinel because asyncLayer.projectId is missing"); + severityAuditLog.warn("[fusion] PostgreSQL agent-log-file pruning is using the legacy unscoped project sentinel because asyncLayer.projectId is missing"); } const projectId = boundProjectId || "__legacy_unscoped__"; const rows = (await layer.db.execute( @@ -64,7 +67,7 @@ export async function pruneOperationalLogsAsync( Operational retention should normally be project-bound. Preserve the legacy sentinel fallback for compatibility, but make every unbound maintenance pass visible before it can target legacy-unscoped rows. */ if (!boundProjectId) { - console.warn("[fusion] PostgreSQL operational maintenance is using the legacy unscoped project sentinel because asyncLayer.projectId is missing"); + severityAuditLog.warn("[fusion] PostgreSQL operational maintenance is using the legacy unscoped project sentinel because asyncLayer.projectId is missing"); } const projectId = boundProjectId || "__legacy_unscoped__"; const cutoff = new Date(Date.now() - retentionMs).toISOString(); diff --git a/packages/core/src/task-store/comments-ops.ts b/packages/core/src/task-store/comments-ops.ts index 8bd9ef2ec8..3d58999c7f 100644 --- a/packages/core/src/task-store/comments-ops.ts +++ b/packages/core/src/task-store/comments-ops.ts @@ -1,3 +1,6 @@ +import { createLogger } from "../logger.js"; + +const severityAuditLog = createLogger("core-comments-ops"); /** * comments-ops operations. * @@ -260,7 +263,7 @@ export async function upsertTaskDocumentImpl(store: TaskStore, taskId: string, i void store.recordGoalCitations(citationInputs); } } catch (err) { - console.warn("[fusion] Failed to scan/record goal citations from task document:", err); + severityAuditLog.warn("[fusion] Failed to scan/record goal citations from task document:", err); } return document; } @@ -359,7 +362,7 @@ export async function upsertTaskDocumentImpl(store: TaskStore, taskId: string, i store.recordGoalCitations(citationInputs); } } catch (err) { - console.warn("[fusion] Failed to scan/record goal citations from task document:", err); + severityAuditLog.warn("[fusion] Failed to scan/record goal citations from task document:", err); } return document; diff --git a/packages/core/src/task-store/task-mutation-ops.ts b/packages/core/src/task-store/task-mutation-ops.ts index 4ea4fb4e71..efb316a03e 100644 --- a/packages/core/src/task-store/task-mutation-ops.ts +++ b/packages/core/src/task-store/task-mutation-ops.ts @@ -1,3 +1,6 @@ +import { createLogger } from "../logger.js"; + +const severityAuditLog = createLogger("core-task-mutation-ops"); /** * FNXC:CodeOrganization 2026-07-21-12:00: * Domain rename from remaining-ops-2: task JSON/config writes, atomic updates, tracking reconcile, @@ -1087,7 +1090,7 @@ export async function addAttachmentImpl(store: TaskStore, id: string, filename: }, }); } catch (err) { - console.warn( + severityAuditLog.warn( `[fusion:store] Skipping artifact bridge for attachment ${attachmentResult.filename} on task ${id}: ${err instanceof Error ? err.message : String(err)}`, ); } @@ -1565,7 +1568,7 @@ export async function closeImpl(store: TaskStore): Promise { } catch (err) { // Best-effort flush — entries for deleted tasks will fail FK check. // Log the error instead of silently swallowing it. - console.warn(`[fusion] Could not flush remaining agent log entries on close:`, err); + severityAuditLog.warn(`[fusion] Could not flush remaining agent log entries on close:`, err); } } // Cancel any retry timer armed by a failed flush — the DB is about to close. @@ -1594,7 +1597,7 @@ export async function closeImpl(store: TaskStore): Promise { try { await secretsCentralCore.close(); } catch (err) { - console.warn(`[fusion] Could not close secrets central core on TaskStore close:`, err); + severityAuditLog.warn(`[fusion] Could not close secrets central core on TaskStore close:`, err); } } store.secretsStore = null; @@ -1610,7 +1613,7 @@ export async function closeImpl(store: TaskStore): Promise { try { pluginStore.close(); } catch (err) { - console.warn(`[fusion] Could not close plugin store on TaskStore close:`, err); + severityAuditLog.warn(`[fusion] Could not close plugin store on TaskStore close:`, err); } } // FNXC:RuntimeBackendInjection 2026-06-24-14:30: diff --git a/packages/core/src/task-store/workflow-integrity.ts b/packages/core/src/task-store/workflow-integrity.ts index 797a1bbab9..081decdb6f 100644 --- a/packages/core/src/task-store/workflow-integrity.ts +++ b/packages/core/src/task-store/workflow-integrity.ts @@ -1,3 +1,6 @@ +import { createLogger } from "../logger.js"; + +const severityAuditLog = createLogger("core-workflow-integrity"); /** * workflow-integrity operations. * @@ -101,7 +104,7 @@ export async function appendAgentLogImpl(store: TaskStore, taskId: string, text: // where an uncaught throw exits the process. The catch blocks exist // precisely to keep a failed flush from crashing the caller/process, so // they must not themselves dereference `store.db`. - console.warn( + severityAuditLog.warn( `[fusion] Dropped ${dropCount} buffered agent log entries — backlog cap reached (${store.fusionDir})`, ); } @@ -122,7 +125,7 @@ export async function appendAgentLogImpl(store: TaskStore, taskId: string, text: store.flushAgentLogBuffer(); } catch (err) { // Size-triggered flush failed — log but don't crash the caller. - console.error(`[fusion] Size-triggered agent log flush failed (${store.fusionDir}):`, err); + severityAuditLog.error(`[fusion] Size-triggered agent log flush failed (${store.fusionDir}):`, err); } } else if (!store.agentLogFlushTimer) { store.agentLogFlushTimer = setTimeout( @@ -131,7 +134,7 @@ export async function appendAgentLogImpl(store: TaskStore, taskId: string, text: store.flushAgentLogBuffer(); } catch (err) { // Timer-triggered flush failed — log but don't crash the process. - console.error(`[fusion] Timer-triggered agent log flush failed (${store.fusionDir}):`, err); + severityAuditLog.error(`[fusion] Timer-triggered agent log flush failed (${store.fusionDir}):`, err); } }, TaskStore.AGENT_LOG_FLUSH_MS, diff --git a/packages/core/src/types/merge-policy.ts b/packages/core/src/types/merge-policy.ts index 77bc5880eb..d133f56bde 100644 --- a/packages/core/src/types/merge-policy.ts +++ b/packages/core/src/types/merge-policy.ts @@ -1,3 +1,6 @@ +import { createLogger } from "../logger.js"; + +const severityAuditLog = createLogger("core-merge-policy"); /** * Merger strategy, conflict, audit, and auto-recovery policy types + normalizers. * @@ -28,7 +31,7 @@ export function normalizeMergeIntegrationWorktreeMode( if (value === "cwd-main") { if (!warnedLegacyCwdMain) { warnedLegacyCwdMain = true; - console.warn("[merger] settings.mergeIntegrationWorktree=cwd-main is legacy; normalized to cwd-integration-branch"); + severityAuditLog.warn("[merger] settings.mergeIntegrationWorktree=cwd-main is legacy; normalized to cwd-integration-branch"); } return "cwd-integration-branch"; } diff --git a/packages/core/src/usage-events.ts b/packages/core/src/usage-events.ts index ba0e554819..5948296a44 100644 --- a/packages/core/src/usage-events.ts +++ b/packages/core/src/usage-events.ts @@ -1,3 +1,6 @@ +import { createLogger } from "./logger.js"; + +const severityAuditLog = createLogger("core-usage-events"); import type { Database } from "./db.js"; /** @@ -229,7 +232,7 @@ export function emitUsageEvent(db: Database, event: UsageEventInput): boolean { ); return true; } catch (err) { - console.warn("[fusion] emitUsageEvent skipped a malformed/failed event:", err); + severityAuditLog.warn("[fusion] emitUsageEvent skipped a malformed/failed event:", err); return false; } } diff --git a/packages/dashboard/src/__tests__/log-severity-spam-contract.test.ts b/packages/dashboard/src/__tests__/log-severity-spam-contract.test.ts new file mode 100644 index 0000000000..7f8de41ee1 --- /dev/null +++ b/packages/dashboard/src/__tests__/log-severity-spam-contract.test.ts @@ -0,0 +1,48 @@ +/* +FNXC:EngineDiagnostics 2026-08-01-11:12: +FN-8603 keeps dashboard-server diagnostics on @fusion/core's shared logger so +routine output is debug-gated and no dashboard-local console sink can drift. +*/ +import { describe, expect, it } from "vitest"; +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { logSeverityManifest } from "../../../engine/src/__tests__/log-severity-manifest.js"; + +function sourceFiles(root: string): string[] { + return readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const path = join(root, entry.name); + if (entry.isDirectory()) return entry.name === "__tests__" ? [] : sourceFiles(path); + return entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") ? [path] : []; + }); +} + +function withoutComments(source: string): string { + return source.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, ""); +} + +describe("dashboard-server diagnostic source contract", () => { + it("pins each dashboard demotion to one debug call site, not any louder severity", () => { + for (const entry of logSeverityManifest.filter((entry) => entry.pkg === "dashboard")) { + const source = readFileSync(join(__dirname, "..", entry.file), "utf8"); + const matchingLines = source.split("\n").filter((line) => line.includes(entry.anchor)); + expect(matchingLines, `${entry.file}: ${entry.anchor}`).toHaveLength(1); + const [line] = matchingLines; + expect(line).toContain(`.${entry.severity}(`); + expect(line).not.toMatch(/\.(log|warn|error)\(|console\.(log|warn|error)\(/); + } + }); + + it("has no bare production console diagnostics", () => { + for (const file of sourceFiles(join(__dirname, ".."))) { + expect(withoutComments(readFileSync(file, "utf8")), file).not.toMatch(/console\.(log|warn|error)\(/); + } + }); + + it("never imports createLogger from engine or a cross-package relative path", () => { + for (const file of sourceFiles(join(__dirname, ".."))) { + const source = readFileSync(file, "utf8"); + expect(source, file).not.toMatch(/import\s*\{[^}]*\bcreateLogger\b[^}]*\}\s*from\s*["']@fusion\/engine["']/); + expect(source, file).not.toMatch(/import\s*\{[^}]*\bcreateLogger\b[^}]*\}\s*from\s*["'][^"']*(?:\.\.\/){2,}[^"']*["']/); + } + }); +}); diff --git a/packages/dashboard/src/ai-refine.ts b/packages/dashboard/src/ai-refine.ts index e0909a866a..0626d131c6 100644 --- a/packages/dashboard/src/ai-refine.ts +++ b/packages/dashboard/src/ai-refine.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-ai-refine"); /** * AI Text Refinement Service * @@ -193,7 +196,7 @@ function cleanupExpiredRateLimits(): void { } if (cleanedRateLimits > 0) { - console.log(`[ai-refine] Cleanup: removed ${cleanedRateLimits} rate limit entries`); + severityAuditLog.debug(`[ai-refine] Cleanup: removed ${cleanedRateLimits} rate limit entries`); } } diff --git a/packages/dashboard/src/ai-session-diagnostics.ts b/packages/dashboard/src/ai-session-diagnostics.ts index d380d04d08..e97fe72962 100644 --- a/packages/dashboard/src/ai-session-diagnostics.ts +++ b/packages/dashboard/src/ai-session-diagnostics.ts @@ -1,3 +1,4 @@ +import { createLogger } from "@fusion/core"; /** * Shared AI-Session Diagnostics Helper * @@ -186,18 +187,17 @@ let _sink: DiagnosticsSink = defaultSink; * All methods are non-throwing (safe to call even if console is mocked). */ function defaultSink(level: DiagnosticsLevel, scope: string, message: string, context: DiagnosticsContext): void { - const prefix = `[${scope}]`; - const logArgs = [prefix, message, context]; + const log = createLogger(scope); try { switch (level) { case "info": - console.log(...logArgs); + log.log(message, context); break; case "warn": - console.warn(...logArgs); + log.warn(message, context); break; case "error": - console.error(...logArgs); + log.error(message, context); break; } } catch { diff --git a/packages/dashboard/src/chat.ts b/packages/dashboard/src/chat.ts index 5ae60b4017..9976b20f0f 100644 --- a/packages/dashboard/src/chat.ts +++ b/packages/dashboard/src/chat.ts @@ -36,6 +36,7 @@ import { resolveEffectiveAgentPermissionPolicy, summarizeTitle, FUSION_RUNTIME_SELF_AWARENESS, + createLogger, } from "@fusion/core"; import { EventEmitter } from "node:events"; import { randomUUID } from "node:crypto"; @@ -106,15 +107,16 @@ interface DiagnosticsLogger { error(message: string, ...args: unknown[]): void; } +const chatLog = createLogger("dashboard-chat"); const defaultDiagnostics: DiagnosticsLogger = { log(message: string, ...args: unknown[]) { - console.log(`[chat] ${message}`, ...args); + chatLog.log(message, ...args); }, warn(message: string, ...args: unknown[]) { - console.warn(`[chat] ${message}`, ...args); + chatLog.warn(message, ...args); }, error(message: string, ...args: unknown[]) { - console.error(`[chat] ${message}`, ...args); + chatLog.error(message, ...args); }, }; diff --git a/packages/dashboard/src/devserver-manager.ts b/packages/dashboard/src/devserver-manager.ts index 2ef2f517b4..418b16d217 100644 --- a/packages/dashboard/src/devserver-manager.ts +++ b/packages/dashboard/src/devserver-manager.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-devserver-manager"); import { spawn, type ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; import { createConnection } from "node:net"; @@ -116,7 +119,7 @@ export class DevServerManager extends EventEmitter { return; } - console.error("[devserver] process error", { + severityAuditLog.error("[devserver] process error", { id: config.id, error: error.message, }); @@ -339,7 +342,7 @@ export class DevServerManager extends EventEmitter { child.kill("SIGTERM"); } } catch (error) { - console.warn("[devserver] failed to send SIGTERM", { error }); + severityAuditLog.warn("[devserver] failed to send SIGTERM", { error }); } } @@ -353,7 +356,7 @@ export class DevServerManager extends EventEmitter { child.kill("SIGKILL"); } } catch (error) { - console.warn("[devserver] failed to send SIGKILL", { error }); + severityAuditLog.warn("[devserver] failed to send SIGKILL", { error }); } } } diff --git a/packages/dashboard/src/file-service.ts b/packages/dashboard/src/file-service.ts index e87e4e0511..6cdcf3edf7 100644 --- a/packages/dashboard/src/file-service.ts +++ b/packages/dashboard/src/file-service.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-file-service"); import { join, resolve, relative, dirname, basename } from "node:path"; import { readdir, readFile as fsReadFile, writeFile as fsWriteFile, stat, copyFile as fsCopyFile, rename as fsRename, rm as fsRm, mkdir, access } from "node:fs/promises"; import type { Dirent } from "node:fs"; @@ -1154,7 +1157,7 @@ export async function scanMarkdownFiles( } catch (err: unknown) { const error = err as NodeJS.ErrnoException; if (error.code !== "ENOENT") { - console.warn( + severityAuditLog.warn( `[scanMarkdownFiles] failed to read directory ${directoryPath}: ${error.message ?? String(err)}`, ); } diff --git a/packages/dashboard/src/github-tracking-comments.ts b/packages/dashboard/src/github-tracking-comments.ts index 49dc1d9052..aee8efa304 100644 --- a/packages/dashboard/src/github-tracking-comments.ts +++ b/packages/dashboard/src/github-tracking-comments.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-github-tracking-comments"); import type { GlobalSettings, MergeDetails, ProjectSettings, Task, TaskStore } from "@fusion/core"; import { deriveTitleFromDescription } from "./github-tracking.js"; import { GitHubClient } from "./github.js"; @@ -214,7 +217,7 @@ export class GitHubTrackingCommentService { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes(`Task ${taskId} not found`)) { - console.warn(`[github-tracking-comments] Unable to write log entry for deleted task ${taskId}: ${message}`); + severityAuditLog.warn(`[github-tracking-comments] Unable to write log entry for deleted task ${taskId}: ${message}`); return; } throw error; @@ -309,7 +312,7 @@ export class GitHubTrackingCommentService { "Posted GitHub tracking comment", `${owner}/${repo}#${number} (${event.to})`, ); - console.warn( + severityAuditLog.warn( `[github-tracking-comments] Posted in-progress comment for ${event.task.id}, but failed to persist its marker: ${markerError instanceof Error ? markerError.message : String(markerError)}`, ); return; diff --git a/packages/dashboard/src/github-tracking-reconciler.ts b/packages/dashboard/src/github-tracking-reconciler.ts index c0f5e68677..7b6915a5b1 100644 --- a/packages/dashboard/src/github-tracking-reconciler.ts +++ b/packages/dashboard/src/github-tracking-reconciler.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-github-tracking-reconciler"); import type { GlobalSettings, ProjectSettings, TaskSourceIssue, TaskStore } from "@fusion/core"; import { resolveGithubTrackingAuth } from "./github-auth.js"; import { GitHubClient } from "./github.js"; @@ -38,7 +41,7 @@ export class GitHubTrackingReconciler { try { await fn(); } catch (err) { - console.warn( + severityAuditLog.warn( `[github-tracking-reconcile] ${label} pass failed (other passes still run): ${err instanceof Error ? err.message : String(err)}`, ); } diff --git a/packages/dashboard/src/github-tracking-state.ts b/packages/dashboard/src/github-tracking-state.ts index ebe7f9d8d7..070657347e 100644 --- a/packages/dashboard/src/github-tracking-state.ts +++ b/packages/dashboard/src/github-tracking-state.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-github-tracking-state"); import type { GithubIssueAction, GlobalSettings, ProjectSettings, Task, TaskStore } from "@fusion/core"; import { GitHubClient } from "./github.js"; import { resolveGithubTrackingAuth } from "./github-auth.js"; @@ -261,7 +264,7 @@ export class GitHubTrackingStateService { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes(`Task ${taskId} not found`)) { - console.warn(`[github-tracking-state] Unable to write log entry for deleted task ${taskId}: ${message}`); + severityAuditLog.warn(`[github-tracking-state] Unable to write log entry for deleted task ${taskId}: ${message}`); return; } throw error; diff --git a/packages/dashboard/src/gitlab-lifecycle.ts b/packages/dashboard/src/gitlab-lifecycle.ts index e571b68987..e2073b314b 100644 --- a/packages/dashboard/src/gitlab-lifecycle.ts +++ b/packages/dashboard/src/gitlab-lifecycle.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-gitlab-lifecycle"); import type { GlobalSettings, ProjectSettings, Task, TaskGitLabTrackedItem, TaskStore } from "@fusion/core"; import { GitLabClient } from "./gitlab.js"; import { resolveGitlabAuth } from "./gitlab-auth.js"; @@ -50,7 +53,7 @@ export async function safeLogGitLabEntry(store: TaskStore, taskId: string, messa } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes(`Task ${taskId} not found`)) { - console.warn(`[gitlab-lifecycle] Unable to write log entry for deleted task ${taskId}: ${message}`); + severityAuditLog.warn(`[gitlab-lifecycle] Unable to write log entry for deleted task ${taskId}: ${message}`); return; } throw error; diff --git a/packages/dashboard/src/insights-routes.ts b/packages/dashboard/src/insights-routes.ts index b90fdef42f..7dec6bb4ad 100644 --- a/packages/dashboard/src/insights-routes.ts +++ b/packages/dashboard/src/insights-routes.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-insights-routes"); /** * Insights REST API Routes * @@ -310,7 +313,7 @@ export function createInsightsRouter(store: TaskStore, options?: ServerOptions): graceMs: ORPHAN_GRACE_MS, source: "startup", }).catch((error) => { - console.warn("[insight-sweeper] startup sweep failed", error); + severityAuditLog.warn("[insight-sweeper] startup sweep failed", error); }); const { dispose: disposeSweeper } = startInsightRunSweeper({ @@ -566,7 +569,7 @@ export function createInsightsRouter(store: TaskStore, options?: ServerOptions): source: "drive_by", }); } catch (error) { - console.warn("[insight-sweeper] drive-by sweep failed", error); + severityAuditLog.warn("[insight-sweeper] drive-by sweep failed", error); } const runs = await store.listRuns(options); @@ -593,7 +596,7 @@ export function createInsightsRouter(store: TaskStore, options?: ServerOptions): source: "drive_by", }); } catch (error) { - console.warn("[insight-sweeper] drive-by sweep failed", error); + severityAuditLog.warn("[insight-sweeper] drive-by sweep failed", error); } const run = await store.getRun(id); diff --git a/packages/dashboard/src/issue-image-attachments.ts b/packages/dashboard/src/issue-image-attachments.ts index 3102019946..089c6ccee7 100644 --- a/packages/dashboard/src/issue-image-attachments.ts +++ b/packages/dashboard/src/issue-image-attachments.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-issue-image-attachments"); import { runGhAsync, isGhAvailable, isGhAuthenticated, type TaskStore } from "@fusion/core"; /* @@ -294,7 +297,7 @@ export async function importIssueImageAttachments( attached++; } catch (err) { failed++; - console.warn( + severityAuditLog.warn( `[fusion:issue-import] Skipping image ${url} for task ${taskId}: ${err instanceof Error ? err.message : String(err)}`, ); } diff --git a/packages/dashboard/src/knowledge-index.ts b/packages/dashboard/src/knowledge-index.ts index 4a0e7d8cbf..94e530515d 100644 --- a/packages/dashboard/src/knowledge-index.ts +++ b/packages/dashboard/src/knowledge-index.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-knowledge-index"); /** * Persistent knowledge index (U14). * @@ -254,7 +257,7 @@ export async function refreshKnowledgeForTask( if (!layer) throw new Error("Knowledge refresh requires the project PostgreSQL AsyncDataLayer"); return (await upsertKnowledgePageAsync(layer, input)).page; } catch (err) { - console.warn(`[knowledge-index] refresh skipped for task ${taskId}:`, err); + severityAuditLog.warn(`[knowledge-index] refresh skipped for task ${taskId}:`, err); return null; } } diff --git a/packages/dashboard/src/plugin-routes.ts b/packages/dashboard/src/plugin-routes.ts index 07fc6ba871..5288b3a07e 100644 --- a/packages/dashboard/src/plugin-routes.ts +++ b/packages/dashboard/src/plugin-routes.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-plugin-routes"); /** * Plugin REST API Routes * @@ -98,7 +101,7 @@ export async function loadRegistryManifest(): Promise { // registry manifest as data at request time and degrade to an empty registry // when the packaged manifest is missing or malformed so startup never fails at // module load with ERR_IMPORT_ATTRIBUTE_MISSING. - console.warn("[dashboard/plugins] Registry manifest unavailable; serving an empty plugin registry", error); + severityAuditLog.warn("[dashboard/plugins] Registry manifest unavailable; serving an empty plugin registry", error); cachedRegistryManifest = {}; return cachedRegistryManifest; } @@ -427,7 +430,7 @@ export function createPluginRouter( await pluginLoader.loadPlugin(plugin.id); } catch (loadErr) { // Log but don't fail - the plugin is registered, just not loaded - console.error(`[plugin-routes] Failed to load plugin ${plugin.id}:`, loadErr); + severityAuditLog.error(`[plugin-routes] Failed to load plugin ${plugin.id}:`, loadErr); } } diff --git a/packages/dashboard/src/routes/board-workflows.ts b/packages/dashboard/src/routes/board-workflows.ts index 2e6e2dc48c..c277fc5b3d 100644 --- a/packages/dashboard/src/routes/board-workflows.ts +++ b/packages/dashboard/src/routes/board-workflows.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-board-workflows"); /** * Board multi-lane payload assembly (U9, R16/R17). * @@ -206,7 +209,7 @@ export async function buildBoardWorkflowsPayload( // Older/partial test stores may not expose definition listing; the referenced // workflow set above is still sufficient for task rendering. Production // failures are logged so empty workflow definitions do not disappear silently. - console.warn("[board-workflows] listWorkflowDefinitions failed; using referenced workflows only", err); + severityAuditLog.warn("[board-workflows] listWorkflowDefinitions failed; using referenced workflows only", err); } const workflows: BoardWorkflowDefinition[] = []; diff --git a/packages/dashboard/src/routes/context.ts b/packages/dashboard/src/routes/context.ts index a789e9fb2f..66bf0bb83a 100644 --- a/packages/dashboard/src/routes/context.ts +++ b/packages/dashboard/src/routes/context.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-context"); import { Router, type Request } from "express"; import { resolve, sep } from "node:path"; import { @@ -134,7 +137,7 @@ export function warnLaunchDirFallbackOnce(options?: ServerOptions): void { if (logger?.warn) { logger.warn(message); } else { - console.warn(message); + severityAuditLog.warn(message); } } diff --git a/packages/dashboard/src/routes/register-auth-routes.ts b/packages/dashboard/src/routes/register-auth-routes.ts index 0843546716..1aaaf7836c 100644 --- a/packages/dashboard/src/routes/register-auth-routes.ts +++ b/packages/dashboard/src/routes/register-auth-routes.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-register-auth-routes"); import type { Request } from "express"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -887,7 +890,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => { try { options.onUseClaudeCliToggled(prev, next); } catch (hookErr) { - console.warn( + severityAuditLog.warn( `[auth/claude-cli] onUseClaudeCliToggled callback threw: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`, ); } @@ -952,7 +955,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => { try { options.onUseDroidCliToggled(prev, next); } catch (hookErr) { - console.warn( + severityAuditLog.warn( `[auth/droid-cli] onUseDroidCliToggled callback threw: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`, ); } @@ -1366,7 +1369,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => { try { options.onUseLlamaCppToggled(prev, next); } catch (hookErr) { - console.warn( + severityAuditLog.warn( `[auth/llama-cpp] onUseLlamaCppToggled callback threw: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`, ); } @@ -1477,7 +1480,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => { void inputPromise.catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); if (message !== "cancelled") { - console.warn(`[auth/login] manual OAuth input promise rejected for ${provider}: ${message}`); + severityAuditLog.warn(`[auth/login] manual OAuth input promise rejected for ${provider}: ${message}`); } }); const pendingLogin: PendingLogin = { @@ -1583,7 +1586,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => { rejectAuthInfo(error); if (error.message !== "cancelled") { lastLoginError.set(provider, error.message); - console.error(`[auth/login] background login failed for ${provider}: ${error.message}`); + severityAuditLog.error(`[auth/login] background login failed for ${provider}: ${error.message}`); } }) .finally(() => { diff --git a/packages/dashboard/src/routes/register-docker-provisioning-routes.ts b/packages/dashboard/src/routes/register-docker-provisioning-routes.ts index 407cc7261b..f8193b0118 100644 --- a/packages/dashboard/src/routes/register-docker-provisioning-routes.ts +++ b/packages/dashboard/src/routes/register-docker-provisioning-routes.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-register-docker-provisioning-routes"); import type { DockerHostConfig, DockerProvisionInput } from "@fusion/core"; import { ApiError, badRequest } from "../api-error.js"; import type { ApiRouteRegistrar } from "./types.js"; @@ -177,7 +180,7 @@ export const registerDockerProvisioningRoutes: ApiRouteRegistrar = (ctx) => { }); } catch (metaError) { // Non-fatal: node is registered but Docker metadata couldn't be persisted - console.warn( + severityAuditLog.warn( "[docker-provisioning] Failed to persist managed Docker node metadata:", metaError instanceof Error ? metaError.message : String(metaError), ); @@ -188,7 +191,7 @@ export const registerDockerProvisioningRoutes: ApiRouteRegistrar = (ctx) => { } } catch (registerError) { // Container is running but unregistered — log warning, return result with error - console.warn( + severityAuditLog.warn( "[docker-provisioning] Container created but node registration failed:", registerError instanceof Error ? registerError.message : String(registerError), ); diff --git a/packages/dashboard/src/routes/register-git-github.ts b/packages/dashboard/src/routes/register-git-github.ts index cb1c4f666a..6817407504 100644 --- a/packages/dashboard/src/routes/register-git-github.ts +++ b/packages/dashboard/src/routes/register-git-github.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-register-git-github"); import { type NextFunction, type Request, type Response } from "express"; import { isAbsolute, resolve } from "node:path"; import { realpathSync } from "node:fs"; @@ -1466,7 +1469,7 @@ export async function pullGitBranch(cwd?: string, options?: PullGitBranchOptions // advanced, so downstream stash-pop and audit emission proceed. // The user's worktree just stays at its prior sha, matching today's // behavior. Logged loudly so the failure is visible. - console.warn( + severityAuditLog.warn( `[integration-pull] taskId=${taskId} worktree sync to ${localIntegrationTip.slice(0, 8)} failed (continuing): ${err instanceof Error ? err.message : String(err)}`, ); }); @@ -2404,7 +2407,7 @@ export async function refreshPrInBackground( directMergeCommitStrategy: options?.directMergeCommitStrategy, }); } catch (err) { - console.error("[pr-conflict-diagnostics]", err); + severityAuditLog.error("[pr-conflict-diagnostics]", err); } } else { conflictDiagnostics = undefined; @@ -2647,7 +2650,7 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void { reconcileSweepOffsetByStore.set(projectStore, nextOffset); } catch (err) { // runSweep isolates per-pass failures internally; this guards only unexpected orchestration errors. - console.warn( + severityAuditLog.warn( `[github-tracking-reconcile] sweep orchestration error: ${err instanceof Error ? err.message : String(err)}`, ); } finally { @@ -2985,7 +2988,7 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void { res.json({ ...status, ...extended }); } catch (extErr: unknown) { const message = extErr instanceof Error ? extErr.message : String(extErr); - console.warn(`[git-status] extended computation failed; returning basic status: ${message}`); + severityAuditLog.warn(`[git-status] extended computation failed; returning basic status: ${message}`); res.json(status); } } catch (err: unknown) { @@ -4147,7 +4150,7 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void { const detail = await client.getIssueDetail(owner, repo, issueNumber); issueImageBodies.push(...detail.comments.map((comment) => comment.body)); } catch (err) { - console.warn( + severityAuditLog.warn( `[fusion:github-import] Could not fetch comments for ${owner}/${repo}#${issueNumber}; importing body images only: ${err instanceof Error ? err.message : String(err)}`, ); } @@ -4168,7 +4171,7 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void { } catch (error) { // FNXC:IssueImportAttachments 2026-07-15-14:10: Post-create audit // telemetry is best-effort; never turn a stored task into a failed import. - console.warn(`[fusion:github-import] Could not log image attachments for ${task.id}: ${error instanceof Error ? error.message : String(error)}`); + severityAuditLog.warn(`[fusion:github-import] Could not log image attachments for ${task.id}: ${error instanceof Error ? error.message : String(error)}`); } } @@ -4453,7 +4456,7 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void { const detail = await githubClient.getIssueDetail(owner, repo, issueNumber); batchImageBodies.push(...detail.comments.map((comment) => comment.body)); } catch (err) { - console.warn( + severityAuditLog.warn( `[fusion:github-import] Could not fetch comments for ${owner}/${repo}#${issueNumber}; importing body images only: ${err instanceof Error ? err.message : String(err)}`, ); } @@ -4477,7 +4480,7 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void { sourceUrl, ); } catch (error) { - console.warn(`[fusion:github-import] Could not log image attachments for ${task.id}: ${error instanceof Error ? error.message : String(error)}`); + severityAuditLog.warn(`[fusion:github-import] Could not log image attachments for ${task.id}: ${error instanceof Error ? error.message : String(error)}`); } } @@ -5869,7 +5872,7 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void { directMergeCommitStrategy: settings.directMergeCommitStrategy, }); } catch (err) { - console.error("[pr-conflict-diagnostics]", err); + severityAuditLog.error("[pr-conflict-diagnostics]", err); } } else { conflictDiagnostics = undefined; diff --git a/packages/dashboard/src/routes/register-gitlab.ts b/packages/dashboard/src/routes/register-gitlab.ts index d39c39e27b..cb6ed8005c 100644 --- a/packages/dashboard/src/routes/register-gitlab.ts +++ b/packages/dashboard/src/routes/register-gitlab.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-register-gitlab"); import { badRequest, unauthorized, ApiError } from "../api-error.js"; import { resolveGitlabAuth } from "../gitlab-auth.js"; import { @@ -124,7 +127,7 @@ async function importItem(ctx: ApiRoutesContext, req: Parameters ownSet.has(file.path)); } catch (err) { - console.warn( + severityAuditLog.warn( `[diff] FN-5154 attribution failed for ${task.id}: ${(err as Error).message}; falling back to unrestricted range`, ); return rebaseRangeFiles; @@ -1088,7 +1091,7 @@ export function registerSessionDiffRoutes(router: Router, deps: SessionDiffRoute if (rebaseDiffSpec) { diffSpec = rebaseDiffSpec; } else { - console.warn(`[diff] done task ${task.id}: mergeDetails.rebaseBaseSha ${rebaseBaseSha} is not ancestor of ${sha}; falling back to single-commit diff`); + severityAuditLog.warn(`[diff] done task ${task.id}: mergeDetails.rebaseBaseSha ${rebaseBaseSha} is not ancestor of ${sha}; falling back to single-commit diff`); try { diffSpec = await resolveCommitDiffSpec(sha, rootDir); } catch { @@ -1283,7 +1286,7 @@ export function registerSessionDiffRoutes(router: Router, deps: SessionDiffRoute if (rebaseDiffSpec) { diffSpec = rebaseDiffSpec; } else { - console.warn(`[file-diffs] done task ${task.id}: mergeDetails.rebaseBaseSha ${rebaseBaseSha} is not ancestor of ${sha}; falling back to single-commit diff`); + severityAuditLog.warn(`[file-diffs] done task ${task.id}: mergeDetails.rebaseBaseSha ${rebaseBaseSha} is not ancestor of ${sha}; falling back to single-commit diff`); try { diffSpec = await resolveCommitDiffSpec(sha, rootDir); } catch { diff --git a/packages/dashboard/src/routes/register-settings-memory-routes.ts b/packages/dashboard/src/routes/register-settings-memory-routes.ts index 505e33ac9d..5e460fdc79 100644 --- a/packages/dashboard/src/routes/register-settings-memory-routes.ts +++ b/packages/dashboard/src/routes/register-settings-memory-routes.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-register-settings-memory-routes"); import { DEFAULT_GLOBAL_SETTINGS, GLOBAL_SETTINGS_KEYS, @@ -246,7 +249,7 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin if (process.arch === "x64") { return "cloudflared-linux-amd64"; } - console.warn(`[remote-access] Unsupported Linux architecture '${process.arch}' for cloudflared; falling back to amd64`); + severityAuditLog.warn(`[remote-access] Unsupported Linux architecture '${process.arch}' for cloudflared; falling back to amd64`); return "cloudflared-linux-amd64"; } @@ -257,7 +260,7 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin if (process.arch === "x64") { return "cloudflared-darwin-amd64"; } - console.warn(`[remote-access] Unsupported macOS architecture '${process.arch}' for cloudflared; falling back to amd64`); + severityAuditLog.warn(`[remote-access] Unsupported macOS architecture '${process.arch}' for cloudflared; falling back to amd64`); return "cloudflared-darwin-amd64"; } diff --git a/packages/dashboard/src/routes/register-setup-activity-routes.ts b/packages/dashboard/src/routes/register-setup-activity-routes.ts index e9c496aad0..c56d3e32f2 100644 --- a/packages/dashboard/src/routes/register-setup-activity-routes.ts +++ b/packages/dashboard/src/routes/register-setup-activity-routes.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-register-setup-activity-routes"); import type { ActivityEventType } from "@fusion/core"; import { ApiError, badRequest, rethrowAsApiError } from "../api-error.js"; import type { ApiRouteRegistrar } from "./types.js"; @@ -194,7 +197,7 @@ router.get("/first-run-status", async (_req, res) => { const hasProjects = detectedProjects.length > 0; const singleProjectPath = detectedProjects.length === 1 ? detectedProjects[0].path : null; - console.warn( + severityAuditLog.warn( `[routes:first-run-status] Falling back to detected projects after central DB error: ${ error instanceof Error ? error.message : String(error) }`, @@ -241,7 +244,7 @@ router.get("/setup-state", async (_req, res) => { state = await detector.detectFirstRunState(central); projects = await central.listProjects(); } catch (error) { - console.warn( + severityAuditLog.warn( `[routes:setup-state] Unable to read central DB state: ${error instanceof Error ? error.message : String(error)}`, ); } finally { diff --git a/packages/dashboard/src/routes/register-signal-routes.ts b/packages/dashboard/src/routes/register-signal-routes.ts index 444626bbaa..7d150e76b6 100644 --- a/packages/dashboard/src/routes/register-signal-routes.ts +++ b/packages/dashboard/src/routes/register-signal-routes.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-register-signal-routes"); import type { Request, Response } from "express"; import type { Task, TaskStore } from "@fusion/core"; import { ingestIncidentSignal, resolveIncident } from "../monitor-store.js"; @@ -242,7 +245,7 @@ export async function ingestSignal(deps: SignalIngestDeps): Promise | null { const metadata = event.metadata; if (!metadata || typeof metadata !== "object") { - console.warn(`[merge-advance-events] dropping run-audit event ${event.id}: missing metadata`); + severityAuditLog.warn(`[merge-advance-events] dropping run-audit event ${event.id}: missing metadata`); return null; } const candidate = metadata as { @@ -434,11 +437,11 @@ function extractMergeAdvanceEvent(event: RunAuditEvent): Omit= this.maxSessions) { - console.error(`Max sessions (${this.maxSessions}) reached, refusing new session`); + terminalLog.warn(`Max sessions (${this.maxSessions}) reached, refusing new session`); return { success: false, code: "max_sessions", @@ -499,7 +501,7 @@ export class TerminalService extends EventEmitter { // Validate shell is allowed if (!this.isAllowedShell(shell)) { - console.error(`Shell not allowed: ${shell}`); + terminalLog.warn(`Shell not allowed: ${shell}`); return { success: false, code: "invalid_shell", @@ -552,7 +554,7 @@ export class TerminalService extends EventEmitter { try { pty = await loadPtyModule(); } catch (loadErr) { - console.error(`[terminal] Failed to load PTY module: ${loadErr}`); + terminalLog.error(`Failed to load PTY module: ${loadErr}`); return { success: false, code: "pty_load_failed", @@ -616,8 +618,8 @@ export class TerminalService extends EventEmitter { break; } catch (spawnError) { lastSpawnError = spawnError; - console.error( - `[createSession] PTY spawn failed (${attempt.reason}) for ${attempt.shell} ${attempt.args.join(" ")}:`, + terminalLog.warn( + `PTY spawn failed (${attempt.reason}) for ${attempt.shell} ${attempt.args.join(" ")}:`, spawnError, spawnDiagnostics, ); @@ -625,7 +627,7 @@ export class TerminalService extends EventEmitter { } if (!ptyProcess) { - console.error(`[createSession] All PTY spawn attempts failed`, lastSpawnError, spawnDiagnostics); + terminalLog.error("All PTY spawn attempts failed", lastSpawnError, spawnDiagnostics); return { success: false, code: "pty_spawn_failed", @@ -813,13 +815,13 @@ export class TerminalService extends EventEmitter { const session = this.sessions.get(sessionId); if (!session) { - console.warn(`Session ${sessionId} not found`); + terminalLog.debug(`Session ${sessionId} not found`); return false; } // Reject data with null bytes if (data.includes("\0")) { - console.warn(`Rejecting input with null byte to session ${sessionId}`); + terminalLog.warn(`Rejecting input with null byte to session ${sessionId}`); return false; } @@ -845,7 +847,7 @@ export class TerminalService extends EventEmitter { const session = this.sessions.get(sessionId); if (!session) { - console.warn(`Session ${sessionId} not found for resize`); + terminalLog.debug(`Session ${sessionId} not found for resize`); return false; } @@ -883,7 +885,7 @@ export class TerminalService extends EventEmitter { return true; } catch (error) { - console.error(`Error resizing session ${sessionId}:`, error); + terminalLog.warn(`Error resizing session ${sessionId}:`, error); session.resizeInProgress = false; return false; } @@ -943,7 +945,7 @@ export class TerminalService extends EventEmitter { return true; } catch (error) { - console.error(`Error killing session ${sessionId}:`, error); + terminalLog.warn(`Error killing session ${sessionId}:`, error); this.resolveReady(session); this.sessions.delete(sessionId); return false; diff --git a/packages/dashboard/src/view-chunk-manifest.ts b/packages/dashboard/src/view-chunk-manifest.ts index e254ab9d83..057e277e7a 100644 --- a/packages/dashboard/src/view-chunk-manifest.ts +++ b/packages/dashboard/src/view-chunk-manifest.ts @@ -1,3 +1,6 @@ +import { createLogger } from "@fusion/core"; + +const severityAuditLog = createLogger("dashboard-view-chunk-manifest"); import { existsSync, readFileSync, statSync } from "node:fs"; import { join, resolve } from "node:path"; @@ -53,7 +56,7 @@ function warnOnce(set: Set, key: string, message: string): void { return; } set.add(key); - console.warn(message); + severityAuditLog.warn(message); } export function loadViewChunkManifest(clientDir: string): Record { diff --git a/packages/engine/src/__tests__/log-severity-manifest.ts b/packages/engine/src/__tests__/log-severity-manifest.ts new file mode 100644 index 0000000000..34fd211de6 --- /dev/null +++ b/packages/engine/src/__tests__/log-severity-manifest.ts @@ -0,0 +1,83 @@ +/* +FNXC:EngineDiagnostics 2026-08-01-10:46: +FN-8603 requires routine and expected diagnostic sites to default to debug so a +single accidental severity reversion cannot flood the operator log pane. +*/ +export type SeverityManifestEntry = { + pkg: "engine" | "core" | "dashboard"; + file: string; + anchor: string; + priorSeverity: "log" | "warn" | "error" | "console"; + severity: "debug" | "warn"; +}; + +export const logSeverityManifest: SeverityManifestEntry[] = [ + { pkg: "engine", file: "plugin-runner.ts", anchor: "condition evaluated false", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Executor runtime environment event: ${event}", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Tool execution event: ${event}", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Runtime instantiation event: ${event}", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "debug: (...args: unknown[]) => this.log.debug(prefix, ...args)", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Tools cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Routes cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "UI slots cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "UI contributions cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Runtimes cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "CLI provider contributions cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Skills cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "MCP servers cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Workflow steps cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Workflow extensions cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Workflow step templates cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Plugin traits cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Prompt contributions cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "plugin-runner.ts", anchor: "Setup cache invalidated", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "self-healing.ts", anchor: "Cleaned ${cleaned} stale AI merge temp worktree(s)", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "self-healing.ts", anchor: "cleanup-old-chats\" removed stale data", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "self-healing.ts", anchor: "cleanup-old-mail\" removed stale data", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "self-healing.ts", anchor: "Auto-archiving ${stale.length}", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "self-healing.ts", anchor: "auto-archive: archived", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "self-healing.ts", anchor: "Auto-archived ${archived} stale done task(s)", priorSeverity: "log", severity: "debug" }, + { pkg: "engine", file: "pty-native.ts", anchor: "Pre-loaded native module via dlopen", priorSeverity: "console", severity: "debug" }, + { pkg: "engine", file: "pty-native.ts", anchor: "dlopen pre-load failed (continuing)", priorSeverity: "console", severity: "debug" }, + { pkg: "engine", file: "goal-anchoring-audit.ts", anchor: "goal retrieval audit emission skipped", priorSeverity: "console", severity: "debug" }, + { pkg: "engine", file: "runtimes/child-process-worker.ts", anchor: "Child process worker starting", priorSeverity: "log", severity: "debug" }, + { pkg: "core", file: "central-core.ts", anchor: "local reattached project ${project.id}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "docker-provisioning.ts", anchor: "Pulling image ${imageRef}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "docker-provisioning.ts", anchor: "provisioned successfully in ${durationMs}ms", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "docker-provisioning.ts", anchor: "deprovisioned", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Cleanup: removed ${cleanedRateLimits} rate limit entries", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Creating agent session...", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Failed to initialize AI agent - no session", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Agent session created, sending prompt...", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Session error: ${errorMsg}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Prompt sent, extracting response from messages...", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Total messages: ${messages.length}, Assistant messages: ${assistantMessages.length}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Extracted raw title: \"${title}\"", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "AI returned empty/unusable response", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Title generation successful: \"${sanitized}\"", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Unexpected error: ${message}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "log.debug(\"AI engine not available\");", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "AI engine not available for commit body", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Commit-body session error: ${session.state.error}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Commit-body generation failed: ${message}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "AI engine not available for commit subject", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Commit-subject session error: ${session.state.error}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "ai-summarize.ts", anchor: "Commit-subject generation failed: ${message}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "severityAuditLog.debug(\"[memory-compaction] AI engine not available\");", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "Creating agent session", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "Failed to initialize AI agent - no session", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "Agent session created, sending prompt", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "Session error: ${errorMsg}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "Prompt sent, extracting response", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "Total messages: ${messages.length}", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "Extracted compacted content length", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "AI returned empty response", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "Memory compaction successful", priorSeverity: "console", severity: "debug" }, + { pkg: "core", file: "memory-compaction.ts", anchor: "Unexpected error: ${message}", priorSeverity: "console", severity: "debug" }, + { pkg: "dashboard", file: "ai-refine.ts", anchor: "Cleanup: removed ${cleanedRateLimits}", priorSeverity: "console", severity: "debug" }, + { pkg: "dashboard", file: "sse.ts", anchor: "sseLog.debug(message)", priorSeverity: "console", severity: "debug" }, + { pkg: "dashboard", file: "terminal-service.ts", anchor: "Working directory is not a directory: ${cwd}", priorSeverity: "warn", severity: "debug" }, + { pkg: "dashboard", file: "terminal-service.ts", anchor: "Working directory does not exist: ${cwd}", priorSeverity: "warn", severity: "debug" }, + { pkg: "dashboard", file: "terminal-service.ts", anchor: "terminalLog.debug(`Session ${sessionId} not found`);", priorSeverity: "warn", severity: "debug" }, + { pkg: "dashboard", file: "terminal-service.ts", anchor: "Session ${sessionId} not found for resize", priorSeverity: "warn", severity: "debug" }, +]; diff --git a/packages/engine/src/__tests__/log-severity-spam-contract.test.ts b/packages/engine/src/__tests__/log-severity-spam-contract.test.ts index 94128a88e9..cfa9ae6960 100644 --- a/packages/engine/src/__tests__/log-severity-spam-contract.test.ts +++ b/packages/engine/src/__tests__/log-severity-spam-contract.test.ts @@ -5,9 +5,10 @@ * shipped call-site severity for known TUI flood classes. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { readFileSync } from "node:fs"; +import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { createLogger } from "../logger.js"; +import { logSeverityManifest } from "./log-severity-manifest.js"; const engineSrc = join(__dirname, ".."); @@ -15,7 +16,44 @@ function readSrc(relative: string): string { return readFileSync(join(engineSrc, relative), "utf8"); } +function sourceFiles(root: string): string[] { + return readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const path = join(root, entry.name); + if (entry.isDirectory()) return entry.name === "__tests__" ? [] : sourceFiles(path); + return entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") ? [path] : []; + }); +} + +function withoutComments(source: string): string { + return source.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, ""); +} + +/* +FNXC:EngineDiagnostics 2026-08-01-10:46: +FN-8603 pins every demoted engine call-site by a stable message anchor. The +source check rejects a reversion to log, warn, error, or bare console output. +*/ describe("log severity spam contract (source)", () => { + it("keeps every manifest entry at its audited severity", () => { + for (const entry of logSeverityManifest.filter((entry) => entry.pkg === "engine")) { + const src = readSrc(entry.file); + const matchingLines = src.split("\n").filter((line) => line.includes(entry.anchor)); + expect(matchingLines, `${entry.file}: ${entry.anchor}`).toHaveLength(1); + const [line] = matchingLines; + expect(line).toContain(`.${entry.severity}(`); + expect(line).not.toMatch(/\.(log|warn|error)\(|console\.(log|warn|error)\(/); + } + }); + // Logger implementation is the sole adapter allowed to write severity-marked console output. + const bareConsoleAllowlist = new Set([join(engineSrc, "logger.ts")]); + + it("routes production diagnostics through createLogger", () => { + for (const file of sourceFiles(engineSrc)) { + if (bareConsoleAllowlist.has(file)) continue; + expect(withoutComments(readFileSync(file, "utf8")), file).not.toMatch(/console\.(log|warn|error)\(/); + } + }); + it("maintenance batch per-step success uses debug, not log", () => { const src = readSrc("self-healing.ts"); expect(src).toMatch(/log\.debug\(`Maintenance batch 1 step "\$\{fn\.name\}" succeeded`\)/); diff --git a/packages/engine/src/__tests__/logger-debug-gating.test.ts b/packages/engine/src/__tests__/logger-debug-gating.test.ts index 60568083ec..6ea4b2036f 100644 --- a/packages/engine/src/__tests__/logger-debug-gating.test.ts +++ b/packages/engine/src/__tests__/logger-debug-gating.test.ts @@ -18,12 +18,12 @@ describe("createLogger debug gating", () => { expect(spy).not.toHaveBeenCalled(); }); - it("emits debug output for a subsystem named in the FUSION_DEBUG list", () => { + it("emits an info-marked debug line for a subsystem named in the FUSION_DEBUG list", () => { process.env.FUSION_DEBUG = "scheduler,merger"; const spy = vi.spyOn(console, "error").mockImplementation(() => {}); createLogger("scheduler").debug("steady-state chatter"); expect(spy).toHaveBeenCalledOnce(); - expect(spy.mock.calls[0]?.[0]).toContain("[scheduler] steady-state chatter"); + expect(spy.mock.calls[0]?.[0]).toContain("\0fnlvl=info\0[scheduler] steady-state chatter"); }); it("suppresses debug output for a subsystem absent from the FUSION_DEBUG list", () => { @@ -33,11 +33,12 @@ describe("createLogger debug gating", () => { expect(spy).not.toHaveBeenCalled(); }); - it.each(["1", "true", "all", "*"])("emits debug output for every subsystem when FUSION_DEBUG=%s", (value) => { + it.each(["1", "true", "all", "*"])("emits an info-marked debug line for every subsystem when FUSION_DEBUG=%s", (value) => { process.env.FUSION_DEBUG = value; const spy = vi.spyOn(console, "error").mockImplementation(() => {}); createLogger("scheduler").debug("steady-state chatter"); expect(spy).toHaveBeenCalledOnce(); + expect(spy.mock.calls[0]?.[0]).toContain("\0fnlvl=info\0[scheduler] steady-state chatter"); }); it("re-reads FUSION_DEBUG per call so toggling does not require a new logger", () => { diff --git a/packages/engine/src/goal-anchoring-audit.ts b/packages/engine/src/goal-anchoring-audit.ts index 8373f02d26..0082ca0037 100644 --- a/packages/engine/src/goal-anchoring-audit.ts +++ b/packages/engine/src/goal-anchoring-audit.ts @@ -1,6 +1,9 @@ import type { TaskStore } from "@fusion/core"; import type { RunAuditor } from "./run-audit.js"; +import { createLogger } from "./logger.js"; + +const log = createLogger("goal-anchoring-audit"); /** Goal context was injected into heartbeat/executor prompts for the Slice 2 cite-rate experiment. */ export const GOAL_INJECTION_APPLIED = "goal:injection-applied"; @@ -88,6 +91,6 @@ export function emitGoalRetrievalAudit( }, }); } catch (error) { - console.warn("[fusion-extension] goal retrieval audit emission skipped", error); + log.debug("goal retrieval audit emission skipped", error); } } diff --git a/packages/engine/src/plugin-runner.ts b/packages/engine/src/plugin-runner.ts index 3f1b417d80..f45d17d8ea 100644 --- a/packages/engine/src/plugin-runner.ts +++ b/packages/engine/src/plugin-runner.ts @@ -818,7 +818,7 @@ export class PluginRunner { settings, logger: this.createPluginLogger(pluginId), emitEvent: (event: string, data: unknown) => { - this.log.log(`[plugin:${pluginId}] Event: ${event}`, data); + this.log.debug(`[plugin:${pluginId}] Executor runtime environment event: ${event}`, data); }, }; @@ -902,8 +902,8 @@ export class PluginRunner { if (!evaluation.included) { if (evaluation.reason) { this.log.warn(`Excluded prompt contribution for plugin ${pluginId} on surface ${surface}: ${evaluation.reason}`); - } else if (process.env.DEBUG?.includes("plugins")) { - this.log.log(`Excluded prompt contribution for plugin ${pluginId} on surface ${surface}: condition evaluated false`); + } else { + this.log.debug(`Excluded prompt contribution for plugin ${pluginId} on surface ${surface}: condition evaluated false`); } continue; } @@ -1257,7 +1257,7 @@ export class PluginRunner { settings, logger: this.createPluginLogger(plugin.manifest.id), emitEvent: (event: string, data: unknown) => { - this.log.log(`[plugin:${plugin.manifest.id}] Event: ${event}`, data); + this.log.debug(`[plugin:${plugin.manifest.id}] Tool execution event: ${event}`, data); }, }; } @@ -1286,7 +1286,7 @@ export class PluginRunner { settings, logger: this.createPluginLogger(pluginId), emitEvent: (event: string, data: unknown) => { - this.log.log(`[plugin:${pluginId}] Event: ${event}`, data); + this.log.debug(`[plugin:${pluginId}] Runtime instantiation event: ${event}`, data); }, }; } @@ -1330,11 +1330,7 @@ export class PluginRunner { info: (...args: unknown[]) => this.log.log(prefix, ...args), warn: (...args: unknown[]) => this.log.warn(prefix, ...args), error: (...args: unknown[]) => this.log.error(prefix, ...args), - debug: (...args: unknown[]) => { - if (process.env.DEBUG?.includes("plugins")) { - this.log.log(prefix, ...args); - } - }, + debug: (...args: unknown[]) => this.log.debug(prefix, ...args), }; } @@ -1345,7 +1341,7 @@ export class PluginRunner { */ private invalidateToolsCache(): void { this.toolsCacheVersion++; - this.log.log(`Tools cache invalidated (version: ${this.toolsCacheVersion})`); + this.log.debug(`Tools cache invalidated (version: ${this.toolsCacheVersion})`); } /** @@ -1353,7 +1349,7 @@ export class PluginRunner { */ private invalidateRoutesCache(): void { this.routesCacheVersion++; - this.log.log(`Routes cache invalidated (version: ${this.routesCacheVersion})`); + this.log.debug(`Routes cache invalidated (version: ${this.routesCacheVersion})`); } /** @@ -1361,12 +1357,12 @@ export class PluginRunner { */ private invalidateUiSlotsCache(): void { this.uiSlotsCacheVersion++; - this.log.log(`UI slots cache invalidated (version: ${this.uiSlotsCacheVersion})`); + this.log.debug(`UI slots cache invalidated (version: ${this.uiSlotsCacheVersion})`); } private invalidateUiContributionsCache(): void { this.uiContributionsCacheVersion++; - this.log.log(`UI contributions cache invalidated (version: ${this.uiContributionsCacheVersion})`); + this.log.debug(`UI contributions cache invalidated (version: ${this.uiContributionsCacheVersion})`); } /** @@ -1374,43 +1370,43 @@ export class PluginRunner { */ private invalidateRuntimesCache(): void { this.runtimesCacheVersion++; - this.log.log(`Runtimes cache invalidated (version: ${this.runtimesCacheVersion})`); + this.log.debug(`Runtimes cache invalidated (version: ${this.runtimesCacheVersion})`); } private invalidateCliProviderContributionsCache(): void { this.cliProviderContributionsCacheVersion++; - this.log.log(`CLI provider contributions cache invalidated (version: ${this.cliProviderContributionsCacheVersion})`); + this.log.debug(`CLI provider contributions cache invalidated (version: ${this.cliProviderContributionsCacheVersion})`); } private invalidateSkillsCache(): void { this.skillsCacheVersion++; - this.log.log(`Skills cache invalidated (version: ${this.skillsCacheVersion})`); + this.log.debug(`Skills cache invalidated (version: ${this.skillsCacheVersion})`); } private invalidateMcpServersCache(): void { this.mcpServersCacheVersion++; - this.log.log(`MCP servers cache invalidated (version: ${this.mcpServersCacheVersion})`); + this.log.debug(`MCP servers cache invalidated (version: ${this.mcpServersCacheVersion})`); } private invalidateWorkflowStepsCache(): void { this.workflowStepsCacheVersion++; - this.log.log(`Workflow steps cache invalidated (version: ${this.workflowStepsCacheVersion})`); + this.log.debug(`Workflow steps cache invalidated (version: ${this.workflowStepsCacheVersion})`); } private invalidateWorkflowExtensionsCache(): void { this.workflowExtensionsCacheVersion++; - this.log.log(`Workflow extensions cache invalidated (version: ${this.workflowExtensionsCacheVersion})`); + this.log.debug(`Workflow extensions cache invalidated (version: ${this.workflowExtensionsCacheVersion})`); this.syncPluginWorkflowExtensions(); } private invalidateWorkflowStepTemplatesCache(): void { this.workflowStepTemplatesCacheVersion++; - this.log.log(`Workflow step templates cache invalidated (version: ${this.workflowStepTemplatesCacheVersion})`); + this.log.debug(`Workflow step templates cache invalidated (version: ${this.workflowStepTemplatesCacheVersion})`); } private invalidateTraitsCache(): void { this.traitsCacheVersion++; - this.log.log(`Plugin traits cache invalidated (version: ${this.traitsCacheVersion})`); + this.log.debug(`Plugin traits cache invalidated (version: ${this.traitsCacheVersion})`); // Re-register/deregister plugin traits in the core registry to match the // newly-loaded/unloaded set (mirrors the workflow-step contribution flow). this.syncPluginTraits(); @@ -1420,12 +1416,12 @@ export class PluginRunner { private invalidatePromptContributionsCache(): void { this.promptContributionsCacheVersion++; - this.log.log(`Prompt contributions cache invalidated (version: ${this.promptContributionsCacheVersion})`); + this.log.debug(`Prompt contributions cache invalidated (version: ${this.promptContributionsCacheVersion})`); } private invalidateSetupCache(): void { this.setupCacheVersion++; - this.log.log(`Setup cache invalidated (version: ${this.setupCacheVersion})`); + this.log.debug(`Setup cache invalidated (version: ${this.setupCacheVersion})`); } // ── Store Event Subscriptions ──────────────────────────────────── diff --git a/packages/engine/src/pty-native.ts b/packages/engine/src/pty-native.ts index 1a8564e72b..96fb89195e 100644 --- a/packages/engine/src/pty-native.ts +++ b/packages/engine/src/pty-native.ts @@ -11,6 +11,9 @@ import * as fs from "node:fs"; import { createRequire } from "node:module"; import { join, dirname } from "node:path"; +import { createLogger } from "./logger.js"; + +const log = createLogger("terminal"); // Detect if we're running as a Bun-compiled binary // @ts-expect-error - Bun global is only available in Bun runtime @@ -137,7 +140,7 @@ export function ensureNodePtyNativePermissions(): void { } catch (err) { // Keep diagnostics for the native module path since missing/invalid perms // here are more likely to prevent PTY startup. - console.warn("[terminal] Failed to repair node-pty native permissions:", { + log.warn("Failed to repair node-pty native permissions:", { nativeDir, error: err instanceof Error ? err.message : String(err), }); @@ -180,10 +183,10 @@ export async function loadPtyModule(): Promise { const nativeModule: { exports?: unknown } = { exports: {} }; // process.dlopen is a Node internal API process.dlopen(nativeModule, nativePath); - console.log("[terminal] Pre-loaded native module via dlopen"); + log.debug("Pre-loaded native module via dlopen"); } catch (dlopenErr) { // dlopen failed - log but continue, normal import might still work - console.log("[terminal] dlopen pre-load failed (continuing):", dlopenErr); + log.debug("dlopen pre-load failed (continuing):", dlopenErr); } } } diff --git a/packages/engine/src/runtimes/child-process-worker.ts b/packages/engine/src/runtimes/child-process-worker.ts index 62782df2f9..be90f36061 100644 --- a/packages/engine/src/runtimes/child-process-worker.ts +++ b/packages/engine/src/runtimes/child-process-worker.ts @@ -31,11 +31,11 @@ import { ProjectEngine } from "../project-engine.js"; // Only run if we're in a forked child process if (!process.send) { - console.error("This module must be run as a forked child process"); + runtimeLog.error("This module must be run as a forked child process"); process.exit(1); } -runtimeLog.log("Child process worker starting..."); +runtimeLog.debug("Child process worker starting..."); // Create IPC worker const ipcWorker = new IpcWorker(); diff --git a/packages/engine/src/self-healing.ts b/packages/engine/src/self-healing.ts index dd64ed77cd..1d30f4dd5e 100644 --- a/packages/engine/src/self-healing.ts +++ b/packages/engine/src/self-healing.ts @@ -2704,7 +2704,7 @@ export class SelfHealingManager { fn: async () => { const cleaned = await this.cleanupStaleTempMergeWorktrees(); if (cleaned > 0) { - log.log(`Cleaned ${cleaned} stale AI merge temp worktree(s)`); + log.debug(`Cleaned ${cleaned} stale AI merge temp worktree(s)`); } return cleaned; }, @@ -2756,7 +2756,7 @@ export class SelfHealingManager { } const { sessionsDeleted, roomsDeleted } = await this.options.chatStore.cleanupOldChats(days * 86_400_000); if (sessionsDeleted > 0 || roomsDeleted > 0) { - log.log(`Maintenance batch 1 step "cleanup-old-chats" succeeded — sessions=${sessionsDeleted} rooms=${roomsDeleted}`); + log.debug(`Maintenance batch 1 step "cleanup-old-chats" removed stale data — sessions=${sessionsDeleted} rooms=${roomsDeleted}`); } else { log.debug(`Maintenance batch 1 step "cleanup-old-chats" succeeded — sessions=${sessionsDeleted} rooms=${roomsDeleted}`); } @@ -2776,7 +2776,7 @@ export class SelfHealingManager { } const { messagesDeleted } = await this.options.messageStore.cleanupOldMessages(value * 86_400_000); if (messagesDeleted > 0) { - log.log(`Maintenance batch 1 step "cleanup-old-mail" succeeded — messagesDeleted=${messagesDeleted}`); + log.debug(`Maintenance batch 1 step "cleanup-old-mail" removed stale data — messagesDeleted=${messagesDeleted}`); } else { log.debug(`Maintenance batch 1 step "cleanup-old-mail" succeeded — messagesDeleted=${messagesDeleted}`); } @@ -3062,7 +3062,7 @@ export class SelfHealingManager { if (stale.length === 0) return 0; - log.log(`Auto-archiving ${stale.length} done task(s) older than ${archiveAfterMs}ms`); + log.debug(`Auto-archiving ${stale.length} done task(s) older than ${archiveAfterMs}ms`); let archived = 0; const thresholdDays = Math.floor(archiveAfterMs / 86_400_000); @@ -3073,14 +3073,14 @@ export class SelfHealingManager { const ts = task.columnMovedAt || task.updatedAt; const movedAt = ts ? Date.parse(ts) : NaN; const ageDays = Number.isFinite(movedAt) ? Math.floor((now - movedAt) / 86_400_000) : 0; - log.log(`auto-archive: archived ${task.id} (age ${ageDays}d, threshold ${thresholdDays}d)`); + log.debug(`auto-archive: archived ${task.id} (age ${ageDays}d, threshold ${thresholdDays}d)`); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); log.error(`Failed to auto-archive ${task.id}: ${errorMessage}`); } } if (archived > 0) { - log.log(`Auto-archived ${archived} stale done task(s)`); + log.debug(`Auto-archived ${archived} stale done task(s)`); } return archived; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); diff --git a/packages/engine/src/worktree-hooks.ts b/packages/engine/src/worktree-hooks.ts index 149bf12fa5..c0b5601f28 100644 --- a/packages/engine/src/worktree-hooks.ts +++ b/packages/engine/src/worktree-hooks.ts @@ -2,7 +2,9 @@ import { exec } from "node:child_process"; import * as fs from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { promisify } from "node:util"; +import { createLogger } from "./logger.js"; +const log = createLogger("worktree-hooks"); const execAsync = promisify(exec); export const DEFAULT_ALLOWED_BRANCH_PATTERNS = ["^fusion/step-\\d+-[a-z0-9-]+$"] as const; @@ -272,9 +274,7 @@ async function installCommitMsgHook(input: { const hookPath = await resolveGitPath(input.worktreePath, "hooks/commit-msg"); const existing = await fs.readFile(hookPath, "utf-8").catch(() => null); if (existing && !existing.includes(COMMIT_MSG_HOOK_MARKER)) { - console.warn( - `[worktree-hooks] commit-msg hook already exists at ${hookPath}; skipping Fusion trailer hook install for ${input.taskId}` - ); + log.warn(`commit-msg hook already exists at ${hookPath}; skipping Fusion trailer hook install for ${input.taskId}`); return; } @@ -295,9 +295,7 @@ async function installPrepareCommitMsgEmptyGuard(input: { const hookPath = await resolveGitPath(input.worktreePath, "hooks/prepare-commit-msg"); const existing = await fs.readFile(hookPath, "utf-8").catch(() => null); if (existing && !existing.includes(PREPARE_COMMIT_MSG_HOOK_MARKER)) { - console.warn( - `[worktree-hooks] prepare-commit-msg hook already exists at ${hookPath}; skipping Fusion empty-commit guard install for ${input.taskId}` - ); + log.warn(`prepare-commit-msg hook already exists at ${hookPath}; skipping Fusion empty-commit guard install for ${input.taskId}`); return; }