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:
7
.changeset/fn-8603-log-severity.md
Normal file
7
.changeset/fn-8603-log-severity.md
Normal 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.
|
||||
@@ -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]`)
|
||||
|
||||
|
||||
@@ -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)\(/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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``;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,}[^"']*["']/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
83
packages/engine/src/__tests__/log-severity-manifest.ts
Normal file
83
packages/engine/src/__tests__/log-severity-manifest.ts
Normal 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" },
|
||||
];
|
||||
@@ -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`\)/);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ────────────────────────────────────
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user