/**
 * NERF Forensics — Post-mortem investigation of auto-mode failures
 *
 * Programmatically scans activity logs, metrics, crash locks, and doctor
 * diagnostics for anomalies, then hands a structured report to the LLM
 * for interactive investigation.
 *
 * Entry point: handleForensics() called from commands.ts
 */
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { join, dirname, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
import { extractTrace } from "./session-forensics.js";
import { nativeParseJsonlTail } from "./native-parser-bridge.js";
import { MAX_JSONL_BYTES, parseJSONL } from "./jsonl-utils.js";
import { loadLedgerFromDisk, getAverageCostPerUnitType, getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js";
import { readCrashLock, isLockProcessAlive, formatCrashInfo } from "./crash-recovery.js";
import { runNerfDoctor, formatDoctorIssuesForPrompt } from "./doctor.js";
import { verifyExpectedArtifact } from "./auto-recovery.js";
import { deriveState } from "./state.js";
import { isAutoActive } from "./auto.js";
import { loadPrompt } from "./prompt-loader.js";
import { nerfRoot } from "./paths.js";
import { formatDuration } from "../shared/format-utils.js";
import { getAutoWorktreePath } from "./auto-worktree.js";
import { loadEffectiveNerfPreferences, loadGlobalNerfPreferences, getGlobalNerfPreferencesPath } from "./preferences.js";
import { showNextAction } from "../shared/tui.js";
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
// ─── Duplicate Detection ──────────────────────────────────────────────────────
const DEDUP_PROMPT_SECTION = `
## Duplicate Detection (REQUIRED before issue creation)

Before offering to create a GitHub issue, you MUST search for existing issues and PRs that may already address this bug. This step uses the user's AI tokens for analysis.

### Search Steps

1. **Search closed issues** for similar keywords from your diagnosis:
   \`\`\`
   gh issue list --repo nerf-build/nerf-2 --state closed --search "<keywords from root cause>" --limit 20
   \`\`\`

2. **Search open PRs** that might contain the fix:
   \`\`\`
   gh pr list --repo nerf-build/nerf-2 --state open --search "<keywords>" --limit 10
   \`\`\`

3. **Search merged PRs** that may have already fixed this:
   \`\`\`
   gh pr list --repo nerf-build/nerf-2 --state merged --search "<keywords>" --limit 10
   \`\`\`

### Analysis

For each result, compare it against your root-cause diagnosis:
- Does the issue describe the same code path or file?
- Does the PR modify the same file:line you identified?
- Is the symptom description semantically similar even if keywords differ?

### Present Findings

If you find potential matches, present them to the user:

1. **"Already fixed by PR #X — skip issue creation"** — when a merged PR or closed issue clearly addresses the same root cause. Explain why you believe it matches.
2. **"Add my findings to existing issue #Y"** — when an open issue exists for the same bug. Use \`gh issue comment #Y --repo nerf-build/nerf-2\` to add forensic evidence.
3. **"Create new issue anyway"** — when existing results do not cover this specific failure.

Only proceed to issue creation if no matches were found OR the user explicitly chooses "Create new issue anyway".
`;
async function writeForensicsDedupPref(ctx, enabled) {
    const prefsPath = getGlobalNerfPreferencesPath();
    await ensurePreferencesFile(prefsPath, ctx, "global");
    const existing = loadGlobalNerfPreferences();
    const prefs = existing?.preferences ? { ...existing.preferences } : {};
    prefs.version = prefs.version || 1;
    prefs.forensics_dedup = enabled;
    const frontmatter = serializePreferencesToFrontmatter(prefs);
    const raw = existsSync(prefsPath) ? readFileSync(prefsPath, "utf-8") : "";
    let body = "\n# NERF Skill Preferences\n\nSee `~/.nerf/agent/extensions/nerf-engine/docs/preferences-reference.md` for full field documentation and examples.\n";
    const start = raw.startsWith("---\n") ? 4 : raw.startsWith("---\r\n") ? 5 : -1;
    if (start !== -1) {
        const closingIdx = raw.indexOf("\n---", start);
        if (closingIdx !== -1) {
            const after = raw.slice(closingIdx + 4);
            if (after.trim())
                body = after;
        }
    }
    writeFileSync(prefsPath, `---\n${frontmatter}---${body}`, "utf-8");
}
// ─── Entry Point ──────────────────────────────────────────────────────────────
export async function handleForensics(args, ctx, pi) {
    if (isAutoActive()) {
        ctx.ui.notify("Cannot run forensics while auto-mode is active. Stop auto-mode first.", "error");
        return;
    }
    const basePath = process.cwd();
    const root = nerfRoot(basePath);
    if (!existsSync(root)) {
        ctx.ui.notify("No NERF state found. Run /nerf auto first.", "warning");
        return;
    }
    let problemDescription = args.trim();
    if (!problemDescription) {
        problemDescription = await ctx.ui.input("Describe what went wrong:", "e.g. auto-mode got stuck on task T03") ?? "";
    }
    if (!problemDescription?.trim()) {
        ctx.ui.notify("Problem description required for forensic analysis.", "warning");
        return;
    }
    // ─── Duplicate detection opt-in ─────────────────────────────────────────────
    const effectivePrefs = loadEffectiveNerfPreferences()?.preferences;
    let dedupEnabled = effectivePrefs?.forensics_dedup === true;
    if (effectivePrefs?.forensics_dedup === undefined) {
        const choice = await showNextAction(ctx, {
            title: "Duplicate detection available",
            summary: ["Before filing a GitHub issue, forensics can search existing issues and PRs to avoid duplicates.", "This uses additional AI tokens for analysis."],
            actions: [
                { id: "enable", label: "Enable duplicate detection", description: "Search issues/PRs before filing (recommended)", recommended: true },
                { id: "skip", label: "Skip for now", description: "File without checking for duplicates" },
            ],
            notYetMessage: "You can enable this later via preferences (forensics_dedup: true).",
        });
        if (choice === "enable") {
            await writeForensicsDedupPref(ctx, true);
            dedupEnabled = true;
        }
    }
    const dedupSection = dedupEnabled ? DEDUP_PROMPT_SECTION : "";
    ctx.ui.notify("Building forensic report...", "info");
    const report = await buildForensicReport(basePath);
    const savedPath = saveForensicReport(basePath, report, problemDescription);
    // Derive NERF source dir for prompt — fall back to ~/.nerf/agent/extensions/nerf-engine/
    // when import.meta.url resolves to the npm-global install path (Windows).
    let nerfSourceDir = dirname(fileURLToPath(import.meta.url));
    if (!existsSync(join(nerfSourceDir, "prompts"))) {
        const nerfHome = process.env.NERF_HOME || join(homedir(), ".nerf");
        const fallback = join(nerfHome, "agent", "extensions", "nerf");
        if (existsSync(join(fallback, "prompts")))
            nerfSourceDir = fallback;
    }
    const forensicData = formatReportForPrompt(report);
    const content = loadPrompt("forensics", {
        problemDescription,
        forensicData,
        nerfSourceDir,
        dedupSection,
    });
    ctx.ui.notify(`Forensic report saved: ${relative(basePath, savedPath)}`, "info");
    pi.sendMessage({ customType: "nerf-forensics", content, display: false }, { triggerTurn: true });
}
// ─── Report Builder ───────────────────────────────────────────────────────────
export async function buildForensicReport(basePath) {
    const anomalies = [];
    // 1. Derive current state
    let activeMilestone = null;
    let activeSlice = null;
    try {
        const state = await deriveState(basePath);
        activeMilestone = state.activeMilestone?.id ?? null;
        activeSlice = state.activeSlice?.id ?? null;
    }
    catch { /* state derivation failure is non-fatal */ }
    // 1b. Check for active auto-worktree
    const activeWorktree = activeMilestone ? getAutoWorktreePath(basePath, activeMilestone) : null;
    // 2. Scan activity logs (last 5) — worktree-aware
    const unitTraces = scanActivityLogs(basePath, activeMilestone);
    // 3. Load metrics
    const metrics = loadLedgerFromDisk(basePath);
    // 4. Load completed keys
    const completedKeys = loadCompletedKeys(basePath);
    // 5. Check crash lock
    const crashLock = readCrashLock(basePath);
    // 6. Run doctor
    let doctorIssues = [];
    try {
        const report = await runNerfDoctor(basePath, { scope: undefined });
        doctorIssues = report.issues;
    }
    catch { /* doctor failure is non-fatal */ }
    // 7. Build recent units from metrics
    const recentUnits = [];
    if (metrics?.units) {
        const sorted = [...metrics.units].sort((a, b) => b.finishedAt - a.finishedAt).slice(0, 10);
        for (const u of sorted) {
            recentUnits.push({
                type: u.type,
                id: u.id,
                cost: u.cost,
                duration: u.finishedAt - u.startedAt,
                model: u.model,
                finishedAt: u.finishedAt,
            });
        }
    }
    // 8. NERF version — use NERF_VERSION env var set by the loader at startup.
    // Extensions run from ~/.nerf/agent/extensions/nerf-engine/ at runtime, so path-traversal
    // from import.meta.url would resolve to ~/package.json (wrong on every system).
    const nerfVersion = process.env.NERF_VERSION || "unknown";
    // 9. Scan journal for flow timeline and structured events
    const journalSummary = scanJournalForForensics(basePath);
    // 10. Gather activity log directory metadata
    const activityLogMeta = gatherActivityLogMeta(basePath, activeMilestone);
    // 11. Run anomaly detectors
    if (metrics?.units)
        detectStuckLoops(metrics.units, anomalies);
    if (metrics?.units)
        detectCostSpikes(metrics.units, anomalies);
    detectTimeouts(unitTraces, anomalies);
    detectMissingArtifacts(completedKeys, basePath, activeMilestone, anomalies);
    detectCrash(crashLock, anomalies);
    detectDoctorIssues(doctorIssues, anomalies);
    detectErrorTraces(unitTraces, anomalies);
    detectJournalAnomalies(journalSummary, anomalies);
    return {
        nerfVersion,
        timestamp: new Date().toISOString(),
        basePath,
        activeMilestone,
        activeSlice,
        activeWorktree: activeWorktree ? relative(basePath, activeWorktree) : null,
        unitTraces,
        metrics,
        completedKeys,
        crashLock,
        doctorIssues,
        anomalies,
        recentUnits,
        journalSummary,
        activityLogMeta,
    };
}
// ─── Activity Log Scanner ─────────────────────────────────────────────────────
const ACTIVITY_FILENAME_RE = /^(\d+)-(.+?)-(.+)\.jsonl$/;
/** Threshold below which iteration cadence is considered rapid (thrashing). */
const RAPID_ITERATION_THRESHOLD_MS = 5000;
function scanActivityLogs(basePath, activeMilestone) {
    const activityDirs = resolveActivityDirs(basePath, activeMilestone);
    const allTraces = [];
    for (const activityDir of activityDirs) {
        if (!existsSync(activityDir))
            continue;
        const files = readdirSync(activityDir).filter(f => f.endsWith(".jsonl")).sort();
        const lastFiles = files.slice(-5);
        for (const file of lastFiles) {
            const match = ACTIVITY_FILENAME_RE.exec(file);
            if (!match)
                continue;
            const seq = parseInt(match[1], 10);
            const unitType = match[2];
            const unitId = match[3];
            const filePath = join(activityDir, file);
            let entries = [];
            const nativeResult = nativeParseJsonlTail(filePath, MAX_JSONL_BYTES);
            if (nativeResult) {
                entries = nativeResult.entries;
            }
            else {
                try {
                    const raw = readFileSync(filePath, "utf-8");
                    entries = parseJSONL(raw);
                }
                catch {
                    continue;
                }
            }
            const trace = extractTrace(entries);
            const stat = statSync(filePath, { throwIfNoEntry: false });
            allTraces.push({
                file: activityDirs.length > 1 ? `[${relative(basePath, activityDir)}] ${file}` : file,
                unitType,
                unitId,
                seq,
                trace,
                mtime: stat?.mtimeMs ?? 0,
            });
        }
    }
    // Sort by mtime descending so the most recent traces (regardless of source) come first
    return allTraces.sort((a, b) => b.mtime - a.mtime).slice(0, 5);
}
/**
 * Resolve activity directories to scan for forensics.
 * If an active auto-worktree exists for the milestone, its activity dir
 * is included first (preferred) so stale root logs don't mask worktree progress.
 */
function resolveActivityDirs(basePath, activeMilestone) {
    const dirs = [];
    // Check for active auto-worktree activity logs
    if (activeMilestone) {
        const wtPath = getAutoWorktreePath(basePath, activeMilestone);
        if (wtPath) {
            const wtActivityDir = join(nerfRoot(wtPath), "activity");
            if (existsSync(wtActivityDir)) {
                dirs.push(wtActivityDir);
            }
        }
    }
    // Always include root activity logs
    const rootActivityDir = join(nerfRoot(basePath), "activity");
    dirs.push(rootActivityDir);
    return dirs;
}
// ─── Journal Scanner ──────────────────────────────────────────────────────────
/**
 * Max recent journal files to fully parse for event counts and recent events.
 * Older files are line-counted only to avoid loading huge amounts of data.
 */
const MAX_JOURNAL_RECENT_FILES = 3;
/** Max recent events to extract for the forensic report timeline. */
const MAX_JOURNAL_RECENT_EVENTS = 20;
/**
 * Intelligently scan journal files for forensic summary.
 *
 * Journal files can be huge (thousands of JSONL entries over weeks of auto-mode).
 * Instead of loading all entries into memory:
 * - Only fully parse the most recent N daily files (event counts, flow tracking)
 * - Line-count older files for approximate totals (no JSON parsing)
 * - Extract only the last 20 events for the timeline
 */
function scanJournalForForensics(basePath) {
    try {
        const journalDir = join(nerfRoot(basePath), "journal");
        if (!existsSync(journalDir))
            return null;
        const files = readdirSync(journalDir).filter(f => f.endsWith(".jsonl")).sort();
        if (files.length === 0)
            return null;
        // Split into recent (fully parsed) and older (line-counted only)
        const recentFiles = files.slice(-MAX_JOURNAL_RECENT_FILES);
        const olderFiles = files.slice(0, -MAX_JOURNAL_RECENT_FILES);
        // Line-count older files without parsing — avoids loading megabytes of JSON
        let olderEntryCount = 0;
        let oldestEntry = null;
        for (const file of olderFiles) {
            try {
                const raw = readFileSync(join(journalDir, file), "utf-8");
                const lines = raw.split("\n");
                for (const line of lines) {
                    if (!line.trim())
                        continue;
                    olderEntryCount++;
                    // Extract only the timestamp from the first non-empty line of the oldest file
                    if (!oldestEntry) {
                        try {
                            const parsed = JSON.parse(line);
                            if (parsed.ts)
                                oldestEntry = parsed.ts;
                        }
                        catch { /* skip malformed */ }
                    }
                }
            }
            catch { /* skip unreadable files */ }
        }
        // Fully parse recent files for event counts and timeline
        const eventCounts = {};
        const flowIds = new Set();
        const recentParsedEntries = [];
        let recentEntryCount = 0;
        for (const file of recentFiles) {
            try {
                const raw = readFileSync(join(journalDir, file), "utf-8");
                for (const line of raw.split("\n")) {
                    if (!line.trim())
                        continue;
                    try {
                        const entry = JSON.parse(line);
                        recentEntryCount++;
                        eventCounts[entry.eventType] = (eventCounts[entry.eventType] ?? 0) + 1;
                        flowIds.add(entry.flowId);
                        if (!oldestEntry)
                            oldestEntry = entry.ts;
                        // Keep a rolling window of last N events — avoids accumulating unbounded arrays
                        recentParsedEntries.push({
                            ts: entry.ts,
                            flowId: entry.flowId,
                            eventType: entry.eventType,
                            rule: entry.rule,
                            unitId: entry.data?.unitId,
                        });
                        if (recentParsedEntries.length > MAX_JOURNAL_RECENT_EVENTS) {
                            recentParsedEntries.shift();
                        }
                    }
                    catch { /* skip malformed lines */ }
                }
            }
            catch { /* skip unreadable files */ }
        }
        const totalEntries = olderEntryCount + recentEntryCount;
        if (totalEntries === 0)
            return null;
        const newestEntry = recentParsedEntries.length > 0
            ? recentParsedEntries[recentParsedEntries.length - 1].ts
            : null;
        return {
            totalEntries,
            flowCount: flowIds.size,
            eventCounts,
            recentEvents: recentParsedEntries,
            oldestEntry,
            newestEntry,
            fileCount: files.length,
        };
    }
    catch {
        return null;
    }
}
// ─── Activity Log Metadata ────────────────────────────────────────────────────
function gatherActivityLogMeta(basePath, activeMilestone) {
    try {
        const activityDirs = resolveActivityDirs(basePath, activeMilestone);
        let fileCount = 0;
        let totalSizeBytes = 0;
        let oldestFile = null;
        let newestFile = null;
        let oldestMtime = Infinity;
        let newestMtime = 0;
        for (const activityDir of activityDirs) {
            if (!existsSync(activityDir))
                continue;
            const files = readdirSync(activityDir).filter(f => f.endsWith(".jsonl"));
            for (const file of files) {
                const filePath = join(activityDir, file);
                const stat = statSync(filePath, { throwIfNoEntry: false });
                if (!stat)
                    continue;
                fileCount++;
                totalSizeBytes += stat.size;
                if (stat.mtimeMs < oldestMtime) {
                    oldestMtime = stat.mtimeMs;
                    oldestFile = file;
                }
                if (stat.mtimeMs > newestMtime) {
                    newestMtime = stat.mtimeMs;
                    newestFile = file;
                }
            }
        }
        if (fileCount === 0)
            return null;
        return { fileCount, totalSizeBytes, oldestFile, newestFile };
    }
    catch {
        return null;
    }
}
// ─── Completed Keys Loader ────────────────────────────────────────────────────
function loadCompletedKeys(basePath) {
    const file = join(nerfRoot(basePath), "completed-units.json");
    try {
        if (existsSync(file)) {
            return JSON.parse(readFileSync(file, "utf-8"));
        }
    }
    catch { /* non-fatal */ }
    return [];
}
// ─── Anomaly Detectors ───────────────────────────────────────────────────────
function detectStuckLoops(units, anomalies) {
    const counts = new Map();
    for (const u of units) {
        const key = `${u.type}/${u.id}`;
        counts.set(key, (counts.get(key) ?? 0) + 1);
    }
    for (const [key, count] of counts) {
        if (count > 1) {
            const [unitType, ...idParts] = key.split("/");
            anomalies.push({
                type: "stuck-loop",
                severity: count >= 3 ? "error" : "warning",
                unitType,
                unitId: idParts.join("/"),
                summary: `Unit ${key} was dispatched ${count} times`,
                details: `Repeated dispatch suggests the unit completed but its artifacts weren't verified, or the state machine kept returning it.`,
            });
        }
    }
}
function detectCostSpikes(units, anomalies) {
    const avgMap = getAverageCostPerUnitType(units);
    for (const u of units) {
        const avg = avgMap.get(u.type);
        if (avg && avg > 0 && u.cost > avg * 3) {
            anomalies.push({
                type: "cost-spike",
                severity: "warning",
                unitType: u.type,
                unitId: u.id,
                summary: `${formatCost(u.cost)} vs ${formatCost(avg)} average for ${u.type}`,
                details: `Unit ${u.type}/${u.id} cost ${(u.cost / avg).toFixed(1)}x the average. May indicate excessive retries or large context.`,
            });
        }
    }
}
function detectTimeouts(traces, anomalies) {
    for (const ut of traces) {
        // Check for timeout-recovery custom messages in tool calls
        const hasTimeout = ut.trace.toolCalls.some(tc => tc.name === "sendmessage" &&
            JSON.stringify(tc.input).includes("nerf-auto-timeout-recovery"));
        // Check for timeout keywords in last reasoning
        const reasoningTimeout = ut.trace.lastReasoning &&
            /(?:idle.?timeout|hard.?timeout|timeout.?recovery)/i.test(ut.trace.lastReasoning);
        if (hasTimeout || reasoningTimeout) {
            anomalies.push({
                type: "timeout",
                severity: "warning",
                unitType: ut.unitType,
                unitId: ut.unitId,
                summary: `Timeout detected in ${ut.unitType}/${ut.unitId}`,
                details: `Activity log ${ut.file} contains timeout recovery patterns. The unit may have stalled.`,
            });
        }
    }
}
function detectMissingArtifacts(completedKeys, basePath, activeMilestone, anomalies) {
    // Also check the worktree path for artifacts — they may exist there but not at root
    const wtBasePath = activeMilestone ? getAutoWorktreePath(basePath, activeMilestone) : null;
    for (const key of completedKeys) {
        const slashIdx = key.indexOf("/");
        if (slashIdx === -1)
            continue;
        const unitType = key.slice(0, slashIdx);
        const unitId = key.slice(slashIdx + 1);
        const rootHasArtifact = verifyExpectedArtifact(unitType, unitId, basePath);
        const wtHasArtifact = wtBasePath ? verifyExpectedArtifact(unitType, unitId, wtBasePath) : false;
        if (!rootHasArtifact && !wtHasArtifact) {
            anomalies.push({
                type: "missing-artifact",
                severity: "error",
                unitType,
                unitId,
                summary: `Completed key ${key} but artifact missing or invalid`,
                details: `The unit is recorded as completed but verifyExpectedArtifact() returns false at both project root and worktree. The completion state is stale.`,
            });
        }
    }
}
function detectCrash(crashLock, anomalies) {
    if (!crashLock)
        return;
    if (isLockProcessAlive(crashLock))
        return; // Process still running, not a crash
    anomalies.push({
        type: "crash",
        severity: "error",
        unitType: crashLock.unitType,
        unitId: crashLock.unitId,
        summary: `Stale crash lock: PID ${crashLock.pid} is dead`,
        details: formatCrashInfo(crashLock),
    });
}
function detectDoctorIssues(issues, anomalies) {
    for (const issue of issues) {
        if (issue.severity === "error") {
            anomalies.push({
                type: "doctor-issue",
                severity: "error",
                summary: `Doctor: ${issue.message}`,
                details: `Code: ${issue.code}, Scope: ${issue.scope}, Unit: ${issue.unitId}${issue.file ? `, File: ${issue.file}` : ""}`,
            });
        }
    }
}
function detectErrorTraces(traces, anomalies) {
    for (const ut of traces) {
        if (ut.trace.errors.length > 0) {
            anomalies.push({
                type: "error-trace",
                severity: "warning",
                unitType: ut.unitType,
                unitId: ut.unitId,
                summary: `${ut.trace.errors.length} error(s) in ${ut.unitType}/${ut.unitId}`,
                details: ut.trace.errors.slice(0, 3).join("\n"),
            });
        }
    }
}
function detectJournalAnomalies(journal, anomalies) {
    if (!journal)
        return;
    // Detect stuck-detected events from the journal
    const stuckCount = journal.eventCounts["stuck-detected"] ?? 0;
    if (stuckCount > 0) {
        anomalies.push({
            type: "journal-stuck",
            severity: stuckCount >= 3 ? "error" : "warning",
            summary: `Journal recorded ${stuckCount} stuck-detected event(s)`,
            details: `The auto-mode loop detected it was stuck ${stuckCount} time(s). Check journal events for flow IDs and causal chains to trace the root cause.`,
        });
    }
    // Detect guard-block events (dispatch was blocked by a guard)
    const guardCount = journal.eventCounts["guard-block"] ?? 0;
    if (guardCount > 0) {
        anomalies.push({
            type: "journal-guard-block",
            severity: guardCount >= 5 ? "warning" : "info",
            summary: `Journal recorded ${guardCount} guard-block event(s)`,
            details: `Dispatch was blocked by a guard condition ${guardCount} time(s). This may indicate a persistent blocking condition preventing progress.`,
        });
    }
    // Detect rapid iterations (many flows in short time = likely thrashing)
    if (journal.flowCount > 0 && journal.oldestEntry && journal.newestEntry) {
        const oldest = new Date(journal.oldestEntry).getTime();
        const newest = new Date(journal.newestEntry).getTime();
        const spanMs = newest - oldest;
        if (spanMs > 0 && journal.flowCount > 10) {
            const avgMs = spanMs / journal.flowCount;
            if (avgMs < RAPID_ITERATION_THRESHOLD_MS) {
                anomalies.push({
                    type: "journal-rapid-iterations",
                    severity: "warning",
                    summary: `${journal.flowCount} iterations in ${formatDuration(spanMs)} (avg ${formatDuration(avgMs)}/iteration)`,
                    details: `Unusually rapid iteration cadence suggests the loop may be thrashing without making progress. Review recent journal events for dispatch-stop or terminal events.`,
                });
            }
        }
    }
    // Detect worktree failures from journal events
    const wtCreateFailed = journal.eventCounts["worktree-create-failed"] ?? 0;
    const wtMergeFailed = journal.eventCounts["worktree-merge-failed"] ?? 0;
    const wtFailures = wtCreateFailed + wtMergeFailed;
    if (wtFailures > 0) {
        const parts = [];
        if (wtCreateFailed > 0)
            parts.push(`${wtCreateFailed} create failure(s)`);
        if (wtMergeFailed > 0)
            parts.push(`${wtMergeFailed} merge failure(s)`);
        anomalies.push({
            type: "journal-worktree-failure",
            severity: "warning",
            summary: `Worktree failures: ${parts.join(", ")}`,
            details: `Journal recorded worktree operation failures. These may indicate git state corruption or conflicting branches.`,
        });
    }
}
// ─── Report Persistence ───────────────────────────────────────────────────────
function saveForensicReport(basePath, report, problemDescription) {
    const dir = join(nerfRoot(basePath), "forensics");
    mkdirSync(dir, { recursive: true });
    const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
    const filePath = join(dir, `report-${ts}.md`);
    const redact = (s) => redactForGitHub(s, basePath);
    const sections = [
        `# NERF Forensic Report`,
        ``,
        `**Generated:** ${report.timestamp}`,
        `**NERF Version:** ${report.nerfVersion}`,
        `**Active Milestone:** ${report.activeMilestone ?? "none"}`,
        `**Active Slice:** ${report.activeSlice ?? "none"}`,
        `**Active Worktree:** ${report.activeWorktree ?? "none"}`,
        ``,
        `## Problem Description`,
        ``,
        problemDescription,
        ``,
    ];
    // Anomalies
    if (report.anomalies.length > 0) {
        sections.push(`## Anomalies Detected (${report.anomalies.length})`, ``);
        for (const a of report.anomalies) {
            sections.push(`### [${a.severity.toUpperCase()}] ${a.type}: ${a.summary}`);
            if (a.unitType)
                sections.push(`- Unit: ${a.unitType}/${a.unitId ?? ""}`);
            sections.push(`- ${redact(a.details)}`, ``);
        }
    }
    else {
        sections.push(`## Anomalies`, ``, `No anomalies detected.`, ``);
    }
    // Recent units
    if (report.recentUnits.length > 0) {
        sections.push(`## Recent Units`, ``);
        sections.push(`| Type | ID | Cost | Duration | Model |`);
        sections.push(`|------|-----|------|----------|-------|`);
        for (const u of report.recentUnits) {
            sections.push(`| ${u.type} | ${u.id} | ${formatCost(u.cost)} | ${formatDuration(u.duration)} | ${u.model} |`);
        }
        sections.push(``);
    }
    // Unit traces
    if (report.unitTraces.length > 0) {
        sections.push(`## Activity Log Traces (last ${report.unitTraces.length})`, ``);
        for (const ut of report.unitTraces) {
            sections.push(`### ${ut.unitType}/${ut.unitId} (seq ${ut.seq})`);
            sections.push(`- Tool calls: ${ut.trace.toolCallCount}`);
            sections.push(`- Files written: ${ut.trace.filesWritten.length}`);
            sections.push(`- Errors: ${ut.trace.errors.length}`);
            if (ut.trace.lastReasoning) {
                sections.push(`- Last reasoning: ${redact(ut.trace.lastReasoning.slice(0, 200))}`);
            }
            sections.push(``);
        }
    }
    // Doctor issues
    if (report.doctorIssues.length > 0) {
        sections.push(`## Doctor Issues`, ``);
        sections.push(formatDoctorIssuesForPrompt(report.doctorIssues), ``);
    }
    // Crash lock
    if (report.crashLock) {
        sections.push(`## Crash Lock`, ``);
        sections.push(redact(formatCrashInfo(report.crashLock)), ``);
    }
    // Activity log metadata
    if (report.activityLogMeta) {
        const meta = report.activityLogMeta;
        sections.push(`## Activity Log Metadata`, ``);
        sections.push(`- Files: ${meta.fileCount}`);
        sections.push(`- Total size: ${(meta.totalSizeBytes / 1024).toFixed(1)} KB`);
        if (meta.oldestFile)
            sections.push(`- Oldest: ${meta.oldestFile}`);
        if (meta.newestFile)
            sections.push(`- Newest: ${meta.newestFile}`);
        sections.push(``);
    }
    // Journal summary
    if (report.journalSummary) {
        const js = report.journalSummary;
        sections.push(`## Journal Summary`, ``);
        sections.push(`- Total entries: ${js.totalEntries}`);
        sections.push(`- Distinct flows (iterations): ${js.flowCount}`);
        sections.push(`- Daily files: ${js.fileCount}`);
        if (js.oldestEntry)
            sections.push(`- Date range: ${js.oldestEntry} — ${js.newestEntry}`);
        sections.push(``);
        sections.push(`### Event Type Distribution`, ``);
        sections.push(`| Event Type | Count |`);
        sections.push(`|------------|-------|`);
        for (const [evType, count] of Object.entries(js.eventCounts).sort((a, b) => b[1] - a[1])) {
            sections.push(`| ${evType} | ${count} |`);
        }
        sections.push(``);
        if (js.recentEvents.length > 0) {
            sections.push(`### Recent Journal Events (last ${js.recentEvents.length})`, ``);
            for (const ev of js.recentEvents) {
                const parts = [`${ev.ts} [${ev.eventType}] flow=${ev.flowId.slice(0, 8)}`];
                if (ev.rule)
                    parts.push(`rule=${ev.rule}`);
                if (ev.unitId)
                    parts.push(`unit=${ev.unitId}`);
                sections.push(`- ${parts.join(" ")}`);
            }
            sections.push(``);
        }
    }
    writeFileSync(filePath, sections.join("\n"), "utf-8");
    return filePath;
}
// ─── Prompt Formatter ─────────────────────────────────────────────────────────
function formatReportForPrompt(report) {
    const MAX_BYTES = 30 * 1024;
    const sections = [];
    // Anomalies (most important, first)
    sections.push(`### Anomalies (${report.anomalies.length})`);
    if (report.anomalies.length === 0) {
        sections.push("No anomalies detected.");
    }
    else {
        for (const a of report.anomalies) {
            sections.push(`- **[${a.severity.toUpperCase()}] ${a.type}**: ${a.summary}`);
            if (a.details)
                sections.push(`  ${a.details.slice(0, 300)}`);
        }
    }
    sections.push("");
    // Recent unit history
    if (report.recentUnits.length > 0) {
        sections.push(`### Recent Units (last ${report.recentUnits.length})`);
        sections.push("| Type | ID | Cost | Duration | Model |");
        sections.push("|------|-----|------|----------|-------|");
        for (const u of report.recentUnits) {
            sections.push(`| ${u.type} | ${u.id} | ${formatCost(u.cost)} | ${formatDuration(u.duration)} | ${u.model} |`);
        }
        sections.push("");
    }
    // Trace summaries (last 3)
    const recentTraces = report.unitTraces.slice(0, 3);
    if (recentTraces.length > 0) {
        sections.push(`### Activity Log Traces (last ${recentTraces.length})`);
        for (const ut of recentTraces) {
            sections.push(`**${ut.unitType}/${ut.unitId}** (seq ${ut.seq})`);
            sections.push(`- Tool calls: ${ut.trace.toolCallCount}, Errors: ${ut.trace.errors.length}`);
            if (ut.trace.filesWritten.length > 0) {
                sections.push(`- Files written: ${ut.trace.filesWritten.slice(0, 5).join(", ")}`);
            }
            if (ut.trace.errors.length > 0) {
                sections.push(`- Errors: ${ut.trace.errors.slice(0, 2).map(e => e.slice(0, 200)).join("; ")}`);
            }
            if (ut.trace.lastReasoning) {
                sections.push(`- Last reasoning: "${ut.trace.lastReasoning.slice(0, 300)}"`);
            }
            sections.push("");
        }
    }
    // Doctor issues (error severity only)
    const errorIssues = report.doctorIssues.filter(i => i.severity === "error");
    if (errorIssues.length > 0) {
        sections.push(`### Doctor Issues (${errorIssues.length} errors)`);
        sections.push(formatDoctorIssuesForPrompt(errorIssues));
        sections.push("");
    }
    // Crash lock
    if (report.crashLock) {
        sections.push("### Crash Lock");
        sections.push(formatCrashInfo(report.crashLock));
        const alive = isLockProcessAlive(report.crashLock);
        sections.push(`Process alive: ${alive}`);
        sections.push("");
    }
    // Metrics summary
    if (report.metrics?.units) {
        const totals = getProjectTotals(report.metrics.units);
        sections.push("### Metrics Summary");
        sections.push(`- Total units: ${totals.units}`);
        sections.push(`- Total cost: ${formatCost(totals.cost)}`);
        sections.push(`- Total tokens: ${formatTokenCount(totals.tokens.total)}`);
        sections.push(`- Total duration: ${formatDuration(totals.duration)}`);
        sections.push("");
    }
    // Activity log metadata
    if (report.activityLogMeta) {
        const meta = report.activityLogMeta;
        sections.push("### Activity Log Overview");
        sections.push(`- Files: ${meta.fileCount}, Total size: ${(meta.totalSizeBytes / 1024).toFixed(1)} KB`);
        if (meta.oldestFile)
            sections.push(`- Oldest: ${meta.oldestFile}`);
        if (meta.newestFile)
            sections.push(`- Newest: ${meta.newestFile}`);
        sections.push("");
    }
    // Journal summary — structured event timeline
    if (report.journalSummary) {
        const js = report.journalSummary;
        sections.push("### Journal Summary (Iteration Event Log)");
        sections.push(`- Total entries: ${js.totalEntries}, Distinct flows: ${js.flowCount}, Daily files: ${js.fileCount}`);
        if (js.oldestEntry)
            sections.push(`- Date range: ${js.oldestEntry} — ${js.newestEntry}`);
        // Event type distribution (compact)
        const eventPairs = Object.entries(js.eventCounts).sort((a, b) => b[1] - a[1]);
        sections.push(`- Events: ${eventPairs.map(([t, c]) => `${t}(${c})`).join(", ")}`);
        // Recent events timeline (for tracing what just happened)
        if (js.recentEvents.length > 0) {
            sections.push("");
            sections.push(`**Recent Journal Events (last ${js.recentEvents.length}):**`);
            for (const ev of js.recentEvents) {
                const parts = [`${ev.ts} [${ev.eventType}] flow=${ev.flowId.slice(0, 8)}`];
                if (ev.rule)
                    parts.push(`rule=${ev.rule}`);
                if (ev.unitId)
                    parts.push(`unit=${ev.unitId}`);
                sections.push(`- ${parts.join(" ")}`);
            }
        }
        sections.push("");
    }
    // Completed keys count
    sections.push(`### Completed Keys: ${report.completedKeys.length}`);
    sections.push(`### NERF Version: ${report.nerfVersion}`);
    sections.push(`### Active Milestone: ${report.activeMilestone ?? "none"}`);
    sections.push(`### Active Slice: ${report.activeSlice ?? "none"}`);
    if (report.activeWorktree) {
        sections.push(`### Active Worktree: ${report.activeWorktree}`);
        sections.push(`Note: Activity logs were scanned from both the worktree and the project root. Worktree logs take priority.`);
    }
    let result = sections.join("\n");
    if (result.length > MAX_BYTES) {
        result = result.slice(0, MAX_BYTES) + "\n\n[... truncated at 30KB ...]";
    }
    return result;
}
// ─── Redaction ────────────────────────────────────────────────────────────────
function redactForGitHub(text, basePath) {
    let result = text;
    // Replace absolute paths
    result = result.replaceAll(basePath, ".");
    const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
    if (home)
        result = result.replaceAll(home, "~");
    // Strip API key patterns
    result = result.replace(/sk-[a-zA-Z0-9]{20,}/g, "sk-***");
    result = result.replace(/Bearer\s+\S+/g, "Bearer ***");
    // Strip env var assignments
    result = result.replace(/[A-Z_]{2,}=\S+/g, (match) => {
        const eq = match.indexOf("=");
        return match.slice(0, eq + 1) + "***";
    });
    // Truncate long lines
    result = result.split("\n").map(line => line.length > 500 ? line.slice(0, 497) + "..." : line).join("\n");
    return result;
}
