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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-26 12:01:19 -07:00
parent 0d40bc41d1
commit cca13737b6
67 changed files with 632 additions and 250 deletions

View File

@@ -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.

View File

@@ -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 <id> routed to node=local (source=local)` — routing to a **remote** node stays at info; only the local default is demoted.
- `Hold release for <id> deferred — no reservable slot for <column>` — 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]`)

View File

@@ -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)\(/);
}
});
});

View File

@@ -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``;

View File

@@ -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);

View File

@@ -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<MissionStoreEvents> {
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);

View File

@@ -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}`);
}
};

View File

@@ -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<CentralCoreEvents> {
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<CentralCoreEvents> {
}
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<CentralCoreEvents> {
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<CentralCoreEvents> {
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;
}

View File

@@ -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);

View File

@@ -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";

View File

@@ -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;
}
}

View File

@@ -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<string> {
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

View File

@@ -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)}`,
);
}

View File

@@ -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);
}
}

View File

@@ -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<MissionStoreEvents> {
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);
}
}

View File

@@ -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<NodeDiscoveryEvents> {
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 {

View File

@@ -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<string, unknown>);
} 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}`,
);
}

View File

@@ -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);
}
}

View File

@@ -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,
);

View File

@@ -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}`);
}
};

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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<void> {
} 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<void> {
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<void> {
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:

View File

@@ -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,

View File

@@ -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";
}

View File

@@ -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;
}
}

View File

@@ -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,}[^"']*["']/);
}
});
});

View File

@@ -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`);
}
}

View File

@@ -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 {

View File

@@ -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);
},
};

View File

@@ -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<DevServerManagerEvents> {
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<DevServerManagerEvents> {
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<DevServerManagerEvents> {
child.kill("SIGKILL");
}
} catch (error) {
console.warn("[devserver] failed to send SIGKILL", { error });
severityAuditLog.warn("[devserver] failed to send SIGKILL", { error });
}
}
}

View File

@@ -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)}`,
);
}

View File

@@ -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;

View File

@@ -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)}`,
);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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)}`,
);
}

View File

@@ -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;
}
}

View File

@@ -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<RegistryManifestShape> {
// 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);
}
}

View File

@@ -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[] = [];

View File

@@ -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);
}
}

View File

@@ -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(() => {

View File

@@ -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),
);

View File

@@ -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;

View File

@@ -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<ApiRoutesContex
try {
imageBodies.push(...(await client.listNotes(noteResource, noteProject, args.item.iid)));
} catch (error) {
console.warn(
severityAuditLog.warn(
`[fusion:gitlab-import] Could not fetch notes for ${args.resourceType} #${args.item.iid}; importing description images only: ${error instanceof Error ? error.message : String(error)}`,
);
}
@@ -151,7 +154,7 @@ async function importItem(ctx: ApiRoutesContext, req: Parameters<ApiRoutesContex
} catch (error) {
// FNXC:IssueImportAttachments 2026-07-15-14:10: The task and files are
// already durable; an audit-write failure must not make import retry collide.
console.warn(`[fusion:gitlab-import] Could not log image attachments for ${task.id}: ${error instanceof Error ? error.message : String(error)}`);
severityAuditLog.warn(`[fusion:gitlab-import] Could not log image attachments for ${task.id}: ${error instanceof Error ? error.message : String(error)}`);
}
}

View File

@@ -1,3 +1,6 @@
import { createLogger } from "@fusion/core";
const severityAuditLog = createLogger("dashboard-register-session-diff-routes");
import { access } from "node:fs/promises";
import { join } from "node:path";
import type { Request, Router } from "express";
@@ -829,7 +832,7 @@ async function restrictRebaseRangeFiles(
const ownSet = new Set(attribution.files);
return rebaseRangeFiles.filter((file) => 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 {

View File

@@ -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";
}

View File

@@ -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 {

View File

@@ -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<SignalIngest
await resolveIncident(layer, signal.groupingKey, at);
}
} catch (err) {
console.error("[signal-incident-bridge] Failed to record connector signal", err);
severityAuditLog.error("[signal-incident-bridge] Failed to record connector signal", err);
}
return { status: 201, taskId: task.id };

View File

@@ -1,3 +1,6 @@
import { createLogger } from "@fusion/core";
const severityAuditLog = createLogger("dashboard-register-task-workflow-routes");
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { join } from "node:path";
@@ -422,7 +425,7 @@ function extractAutoSyncOutcome(event: RunAuditEvent): AutoSyncOutcome | null {
function extractMergeAdvanceEvent(event: RunAuditEvent): Omit<MergeAdvanceEvent, "userCheckout" | "autoSync"> | 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<MergeAdvanceEvent,
succeeded?: unknown;
};
if (typeof candidate.integrationBranch !== "string" || candidate.integrationBranch.length === 0 || typeof candidate.toSha !== "string" || candidate.toSha.length === 0) {
console.warn(`[merge-advance-events] dropping run-audit event ${event.id}: missing integrationBranch or toSha`);
severityAuditLog.warn(`[merge-advance-events] dropping run-audit event ${event.id}: missing integrationBranch or toSha`);
return null;
}
if (typeof event.taskId !== "string" || event.taskId.length === 0) {
console.warn(`[merge-advance-events] dropping run-audit event ${event.id}: missing taskId`);
severityAuditLog.warn(`[merge-advance-events] dropping run-audit event ${event.id}: missing taskId`);
return null;
}
return {
@@ -1956,7 +1959,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
if (exists) {
aiUndoWorkflowId = configuredAiUndoWorkflowId;
} else {
console.warn(
severityAuditLog.warn(
`[task-revert] aiUndoTaskWorkflowId "${configuredAiUndoWorkflowId}" does not resolve to a known workflow; AI-undo task will inherit the project default workflow instead`,
);
}

View File

@@ -1,3 +1,5 @@
import { createLogger } from "@fusion/core";
export type RuntimeLogLevel = "info" | "warn" | "error";
export interface RuntimeLogContext {
@@ -24,18 +26,17 @@ function defaultRuntimeLogSink(
message: string,
context?: RuntimeLogContext,
): void {
const line = `[${scope}] ${message}`;
const args = context === undefined ? [line] : [line, context];
const log = createLogger(scope);
try {
switch (level) {
case "info":
console.log(...args);
log.log(message, context);
break;
case "warn":
console.warn(...args);
log.warn(message, context);
break;
case "error":
console.error(...args);
log.error(message, context);
break;
}
} catch {

View File

@@ -1,3 +1,6 @@
import { createLogger } from "@fusion/core";
const severityAuditLog = createLogger("dashboard-server");
import express, { type Router } from "express";
import { randomUUID } from "node:crypto";
import { join, dirname } from "node:path";
@@ -835,7 +838,7 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
// Some unit tests mock @fusion/core with narrow export surfaces. Keep
// server bootstrap resilient when hook registration is unavailable.
const message = error instanceof Error ? error.message : String(error);
console.warn(`[github-tracking-hook] registration skipped: ${message}`);
severityAuditLog.warn(`[github-tracking-hook] registration skipped: ${message}`);
}
const cliPackageVersion = getCliPackageVersion(import.meta.url);
// ── Derive defaults from engine when provided (explicit options override) ──
@@ -1078,7 +1081,7 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
res.setHeader("Cache-Control", "no-store, max-age=0");
res.status(200).send(html);
} catch (err) {
console.error("[dashboard] serveIndexHtml failed:", err);
severityAuditLog.error("[dashboard] serveIndexHtml failed:", err);
// Drop the cached HTML so the next request retries from disk rather
// than re-throwing the same failure until the server restarts.
cachedIndexHtml = null;

View File

@@ -1,4 +1,5 @@
import type { Request, Response } from "express";
import { createLogger } from "@fusion/core";
import type {
TaskStore,
MissionStore,
@@ -46,9 +47,10 @@ function isSseDebugEnabled(): boolean {
.includes("sse");
}
const sseLog = createLogger("sse");
function sseDebug(message: string): void {
if (!isSseDebugEnabled()) return;
console.log(message);
sseLog.debug(message);
}
const SSE_CLIENT_ID_MAX_LENGTH = 128;
@@ -563,8 +565,8 @@ export function createSSE(
const result = safeWrite(res, data);
if (result === "ok") return;
if (result === "backpressure") {
console.warn(
`[sse] connection ${connectionId} backpressure exceeded ` +
sseLog.warn(
`connection ${connectionId} backpressure exceeded ` +
`(buffered=${res.writableLength}B, threshold=${SSE_MAX_BUFFERED_BYTES}B); closing`,
);
closeConnection("backpressure");

View File

@@ -16,9 +16,11 @@ import { stat } from "node:fs/promises";
// fallback, and permission repair) lives in @fusion/engine so PTY owners share
// one implementation. See packages/engine/src/pty-native.ts.
import { loadPtyModule } from "@fusion/engine";
import { createLogger } from "@fusion/core";
import { isAuthorizedProjectOrRegisteredWorktreePath, isPathWithin } from "./git-worktree-safety.js";
// Maximum scrollback buffer size (characters)
const terminalLog = createLogger("dashboard-terminal");
const MAX_SCROLLBACK_SIZE = 50000; // ~50KB per terminal
// Session limit constants
@@ -311,7 +313,7 @@ export class TerminalService extends EventEmitter {
// Reject paths with null bytes (could bypass path checks)
if (cwd.includes("\0")) {
console.warn(`Rejecting path with null byte: ${cwd.replace(/\0/g, "\\0")}`);
terminalLog.warn(`Rejecting path with null byte: ${cwd.replace(/\0/g, "\\0")}`);
throw new TerminalCwdError("Terminal working directory is not an authorized project or task worktree.");
}
@@ -322,7 +324,7 @@ export class TerminalService extends EventEmitter {
cwd = path.resolve(this.projectRoot, cwd);
if (!isAbsoluteRequest && !isPathWithin(this.projectRoot, cwd)) {
console.warn(`Terminal relative working directory escape blocked: ${requestedCwd}`);
terminalLog.warn(`Terminal relative working directory escape blocked: ${requestedCwd}`);
throw new TerminalCwdError("Terminal working directory is not an authorized project or task worktree.");
}
@@ -332,7 +334,7 @@ export class TerminalService extends EventEmitter {
this.registeredWorktreeCache,
);
if (!authorized) {
console.warn(`Terminal working directory outside project worktrees blocked: ${requestedCwd}`);
terminalLog.warn(`Terminal working directory outside project worktrees blocked: ${requestedCwd}`);
throw new TerminalCwdError("Terminal working directory is not an authorized project or task worktree.");
}
@@ -342,9 +344,9 @@ export class TerminalService extends EventEmitter {
if (cwdStat.isDirectory()) {
return cwd;
}
console.warn(`Working directory is not a directory: ${cwd}`);
terminalLog.debug(`Working directory is not a directory: ${cwd}`);
} catch {
console.warn(`Working directory does not exist: ${cwd}`);
terminalLog.debug(`Working directory does not exist: ${cwd}`);
}
throw new TerminalCwdError("Terminal working directory is not a readable directory.");
@@ -484,7 +486,7 @@ export class TerminalService extends EventEmitter {
// Check session limit
if (this.sessions.size >= 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;

View File

@@ -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<string>, key: string, message: string): void {
return;
}
set.add(key);
console.warn(message);
severityAuditLog.warn(message);
}
export function loadViewChunkManifest(clientDir: string): Record<TaskViewId, ViewChunkManifestEntry> {

View File

@@ -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" },
];

View File

@@ -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`\)/);

View File

@@ -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", () => {

View File

@@ -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);
}
}

View File

@@ -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 ────────────────────────────────────

View File

@@ -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<typeof import("node-pty")> {
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);
}
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;
}