import { existsSync, readFileSync, unlinkSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

import type { ExtensionContext } from "@singularity-forge/pi-coding-agent";

import { logWarning } from "../workflow-logger.js";
import { debugTime } from "../debug-logger.js";
import { loadPrompt, getTemplatesDir } from "../prompt-loader.js";
import { readForensicsMarker } from "../forensics.js";
import { resolveAllSkillReferences, renderPreferencesForSystemPrompt, loadEffectiveSFPreferences } from "../preferences.js";
import { resolveModelWithFallbacksForUnit } from "../preferences-models.js";
import { resolveSkillReference } from "../preferences-skills.js";
import { resolveSfRootFile, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, relSliceFile, relSlicePath, relTaskFile } from "../paths.js";
import { ensureCodebaseMapFresh, readCodebaseMap } from "../codebase-generator.js";
import { buildCodeIntelligenceContextBlock } from "../code-intelligence.js";
import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "../skill-discovery.js";
import { getActiveAutoWorktreeContext } from "../auto-worktree.js";
import { getActiveWorktreeName, getWorktreeOriginalCwd } from "../worktree-command.js";
import { deriveState } from "../state.js";
import { formatOverridesSection, formatShortcut, loadActiveOverrides, loadFile, parseContinue, parseSummary } from "../files.js";
import { toPosixPath } from "../../shared/mod.js";
import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../../cmux/index.js";
import { autoEnableCmuxPreferences } from "../commands-cmux.js";

const sfHome = process.env.SF_HOME || join(homedir(), ".sf");

/**
 * Bundled skill triggers — resolved dynamically at runtime instead of
 * hardcoding absolute paths in the system prompt template. Only skills
 * that actually exist on disk are included in the table. (#3575)
 */
const BUNDLED_SKILL_TRIGGERS: Array<{ trigger: string; skill: string }> = [
  { trigger: "Frontend UI - web components, pages, landing pages, dashboards, React/HTML/CSS, styling", skill: "frontend-design" },
  { trigger: "macOS or iOS apps - SwiftUI, Xcode, App Store", skill: "swiftui" },
  { trigger: "Debugging - complex bugs, failing tests, root-cause investigation after standard approaches fail", skill: "debug-like-expert" },
];

function buildBundledSkillsTable(): string {
  const cwd = process.cwd();
  const rows: string[] = [];
  for (const { trigger, skill } of BUNDLED_SKILL_TRIGGERS) {
    const resolution = resolveSkillReference(skill, cwd);
    if (resolution.method === "unresolved") continue; // skill not installed — omit from prompt
    rows.push(`| ${trigger} | \`${resolution.resolvedPath}\` |`);
  }
  if (rows.length === 0) {
    return "*No bundled skills found. Install skills to `~/.agents/skills/` or `~/.claude/skills/`.*";
  }
  return `| Trigger | Skill to load |\n|---|---|\n${rows.join("\n")}`;
}

function warnDeprecatedAgentInstructions(): void {
  const paths = [
    join(sfHome, "agent-instructions.md"),
    join(process.cwd(), ".sf", "agent-instructions.md"),
  ];
  for (const path of paths) {
    if (existsSync(path)) {
      console.warn(
        `[SF] DEPRECATED: ${path} is no longer loaded. ` +
        `Migrate your instructions to AGENTS.md (or CLAUDE.md) in the same directory. ` +
        `See https://github.com/sf-build/SF/issues/1492`,
      );
    }
  }
}

export async function buildBeforeAgentStartResult(
  event: { prompt: string; systemPrompt: string },
  ctx: ExtensionContext,
): Promise<{ systemPrompt: string; message?: { customType: string; content: string; display: false } } | undefined> {
  if (!existsSync(join(process.cwd(), ".sf"))) return undefined;

  const stopContextTimer = debugTime("context-inject");
  const systemContent = loadPrompt("system", {
    bundledSkillsTable: buildBundledSkillsTable(),
    templatesDir: getTemplatesDir(),
    shortcutDashboard: formatShortcut("Ctrl+Alt+G"),
    shortcutShell: formatShortcut("Ctrl+Alt+B"),
  });
  let loadedPreferences = loadEffectiveSFPreferences();
  if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
    markCmuxPromptShown();
    if (autoEnableCmuxPreferences()) {
      loadedPreferences = loadEffectiveSFPreferences();
      ctx.ui.notify(
        "cmux detected — auto-enabled. Run /sf cmux off to disable.",
        "info",
      );
    }
  }

  let preferenceBlock = "";
  if (loadedPreferences) {
    const cwd = process.cwd();
    const report = resolveAllSkillReferences(loadedPreferences.preferences, cwd);
    preferenceBlock = `\n\n${renderPreferencesForSystemPrompt(loadedPreferences.preferences, report.resolutions)}`;
    if (report.warnings.length > 0) {
      ctx.ui.notify(
        `SF skill preferences: ${report.warnings.length} unresolved skill${report.warnings.length === 1 ? "" : "s"}: ${report.warnings.join(", ")}`,
        "warning",
      );
    }
  }

  const { block: knowledgeBlock, globalSizeKb } = loadKnowledgeBlock(sfHome, process.cwd());
  if (globalSizeKb > 4) {
    ctx.ui.notify(
      `SF: ~/.sf/agent/KNOWLEDGE.md is ${globalSizeKb.toFixed(1)}KB — consider trimming to keep system prompt lean.`,
      "warning",
    );
  }

  let memoryBlock = "";
  try {
    const { formatMemoriesForPrompt, getActiveMemoriesRanked } = await import("../memory-store.js");
    const memories = getActiveMemoriesRanked(30);
    if (memories.length > 0) {
      const formatted = formatMemoriesForPrompt(memories, 2000);
      if (formatted) {
        memoryBlock = `\n\n${formatted}`;
      }
    }
  } catch (e) {
    logWarning("bootstrap", `memory block fetch failed: ${(e as Error).message}`);
  }

  let newSkillsBlock = "";
  if (hasSkillSnapshot()) {
    const newSkills = detectNewSkills();
    if (newSkills.length > 0) {
      newSkillsBlock = formatSkillsXml(newSkills);
    }
  }

  let codebaseBlock = "";
  let codeIntelligenceBlock = "";
  try {
    const codebaseOptions = loadedPreferences?.preferences?.codebase
      ? {
          excludePatterns: loadedPreferences.preferences.codebase.exclude_patterns,
          maxFiles: loadedPreferences.preferences.codebase.max_files,
          collapseThreshold: loadedPreferences.preferences.codebase.collapse_threshold,
        }
      : undefined;
    ensureCodebaseMapFresh(process.cwd(), codebaseOptions);
  } catch (e) {
    logWarning("bootstrap", `CODEBASE refresh failed: ${(e as Error).message}`);
  }

  try {
    codeIntelligenceBlock = buildCodeIntelligenceContextBlock(
      process.cwd(),
      loadedPreferences?.preferences?.codebase,
    );
  } catch (e) {
    logWarning("bootstrap", `code intelligence block failed: ${(e as Error).message}`);
  }

  const codebasePath = resolveSfRootFile(process.cwd(), "CODEBASE");
  const rawCodebase = readCodebaseMap(process.cwd());
  if (existsSync(codebasePath) && rawCodebase) {
    try {
      const rawContent = rawCodebase.trim();
      if (rawContent) {
        // Cap injection size to ~2 000 tokens to avoid bloating every request.
        // Full map is always available at .sf/CODEBASE.md.
        const MAX_CODEBASE_CHARS = 8_000;
        const generatedMatch = rawContent.match(/Generated: (\S+)/);
        const generatedAt = generatedMatch?.[1] ?? "unknown";
        const content = rawContent.length > MAX_CODEBASE_CHARS
          ? rawContent.slice(0, MAX_CODEBASE_CHARS) + "\n\n*(truncated — see .sf/CODEBASE.md for full map)*"
          : rawContent;
        codebaseBlock = `\n\n[PROJECT CODEBASE — File structure and descriptions (generated ${generatedAt}, auto-refreshed when SF detects tracked file changes; use /sf codebase stats for status)]\n\n${content}`;
      }
    } catch (e) {
      logWarning("bootstrap", `CODEBASE file read failed: ${(e as Error).message}`);
    }
  }

  warnDeprecatedAgentInstructions();

  const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());

  // Re-inject forensics context on follow-up turns (#2941)
  const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd(), event.prompt) : null;

  const worktreeBlock = buildWorktreeContextBlock();

  const subagentModelConfig = resolveModelWithFallbacksForUnit("subagent");
  const subagentModelBlock = subagentModelConfig
    ? `\n\n## Subagent Model\n\nWhen spawning subagents via the \`subagent\` tool, always pass \`model: "${subagentModelConfig.primary}"\` in the tool call parameters. Never omit this — always specify it explicitly.`
    : "";

  const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — SF]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${codeIntelligenceBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}${subagentModelBlock}`;

  stopContextTimer({
    systemPromptSize: fullSystem.length,
    injectionSize: injection?.length ?? forensicsInjection?.length ?? 0,
    hasPreferences: preferenceBlock.length > 0,
    hasNewSkills: newSkillsBlock.length > 0,
  });

  // Determine which context message to inject (guided execute takes priority)
  const contextMessage = injection
    ? { customType: "sf-guided-context", content: injection, display: false as const }
    : forensicsInjection
      ? { customType: "sf-forensics", content: forensicsInjection, display: false as const }
      : null;

  return {
    systemPrompt: fullSystem,
    ...(contextMessage ? { message: contextMessage } : {}),
  };
}

export function loadKnowledgeBlock(sfHomeDir: string, cwd: string): { block: string; globalSizeKb: number } {
  // 1. Global knowledge (~/.sf/agent/KNOWLEDGE.md) — cross-project, user-maintained
  let globalKnowledge = "";
  let globalSizeKb = 0;
  const globalKnowledgePath = join(sfHomeDir, "agent", "KNOWLEDGE.md");
  if (existsSync(globalKnowledgePath)) {
    try {
      const content = readFileSync(globalKnowledgePath, "utf-8").trim();
      if (content) {
        globalSizeKb = Buffer.byteLength(content, "utf-8") / 1024;
        globalKnowledge = content;
      }
    } catch (e) {
      logWarning("bootstrap", `global knowledge file read failed: ${(e as Error).message}`);
    }
  }

  // 2. Project knowledge (.sf/KNOWLEDGE.md) — project-specific
  let projectKnowledge = "";
  const knowledgePath = resolveSfRootFile(cwd, "KNOWLEDGE");
  if (existsSync(knowledgePath)) {
    try {
      const content = readFileSync(knowledgePath, "utf-8").trim();
      if (content) projectKnowledge = content;
    } catch (e) {
      logWarning("bootstrap", `project knowledge file read failed: ${(e as Error).message}`);
    }
  }

  if (!globalKnowledge && !projectKnowledge) {
    return { block: "", globalSizeKb: 0 };
  }

  const parts: string[] = [];
  if (globalKnowledge) parts.push(`## Global Knowledge\n\n${globalKnowledge}`);
  if (projectKnowledge) parts.push(`## Project Knowledge\n\n${projectKnowledge}`);
  return {
    block: `\n\n[KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${parts.join("\n\n")}`,
    globalSizeKb,
  };
}

function buildWorktreeContextBlock(): string {
  const worktreeName = getActiveWorktreeName();
  const worktreeMainCwd = getWorktreeOriginalCwd();
  const autoWorktree = getActiveAutoWorktreeContext();

  if (worktreeName && worktreeMainCwd) {
    return [
      "",
      "",
      "[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
      `IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
      `The actual current working directory is: ${toPosixPath(process.cwd())}`,
      "",
      `You are working inside a SF worktree.`,
      `- Worktree name: ${worktreeName}`,
      `- Worktree path (this is the real cwd): ${toPosixPath(process.cwd())}`,
      `- Main project: ${toPosixPath(worktreeMainCwd)}`,
      `- Branch: worktree/${worktreeName}`,
      "",
      "All file operations, bash commands, and SF state resolve against the worktree path above.",
      "Use /worktree merge to merge changes back. Use /worktree return to switch back to the main tree.",
    ].join("\n");
  }

  if (autoWorktree) {
    return [
      "",
      "",
      "[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
      `IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
      `The actual current working directory is: ${toPosixPath(process.cwd())}`,
      "",
      "You are working inside a SF auto-worktree.",
      `- Milestone worktree: ${autoWorktree.worktreeName}`,
      `- Worktree path (this is the real cwd): ${toPosixPath(process.cwd())}`,
      `- Main project: ${toPosixPath(autoWorktree.originalBase)}`,
      `- Branch: ${autoWorktree.branch}`,
      "",
      "All file operations, bash commands, and SF state resolve against the worktree path above.",
      "Write every .sf artifact in the worktree path above, never in the main project tree.",
    ].join("\n");
  }

  return "";
}

/**
 * Low-entropy resume intent patterns — short phrases a user types to
 * continue work after a pause, rate limit, or context reset (#3615).
 * Tested against the trimmed, lowercased prompt with trailing punctuation stripped.
 */
const RESUME_INTENT_PATTERNS = /^(continue|resume|ok|go|go ahead|proceed|keep going|carry on|next|yes|yeah|yep|sure|do it|let's go|pick up where you left off)$/;

async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise<string | null> {
  const ensureStateDbOpen = async () => {
    const { ensureDbOpen } = await import("./dynamic-tools.js");
    await ensureDbOpen();
  };

  const executeMatch = prompt.match(/Execute the next task:\s+(T\d+)\s+\("([^"]+)"\)\s+in slice\s+(S\d+)\s+of milestone\s+(M\d+(?:-[a-z0-9]{6})?)/i);
  if (executeMatch) {
    const [, taskId, taskTitle, sliceId, milestoneId] = executeMatch;
    return buildTaskExecutionContextInjection(basePath, milestoneId, sliceId, taskId, taskTitle);
  }

  const resumeMatch = prompt.match(/Resume interrupted work\.[\s\S]*?slice\s+(S\d+)\s+of milestone\s+(M\d+(?:-[a-z0-9]{6})?)/i);
  if (resumeMatch) {
    const [, sliceId, milestoneId] = resumeMatch;
    await ensureStateDbOpen();
    const state = await deriveState(basePath);
    if (state.activeMilestone?.id === milestoneId && state.activeSlice?.id === sliceId && state.activeTask) {
      return buildTaskExecutionContextInjection(basePath, milestoneId, sliceId, state.activeTask.id, state.activeTask.title);
    }
  }

  // Fallback: low-entropy resume prompt (e.g., "continue", "ok", "go ahead")
  // during an active executing task — inject task context so the agent
  // doesn't rebuild from scratch (#3615).
  // Intent-gated: only fire for short, resume-like prompts to avoid hijacking
  // control/help/diagnostic prompts with unrelated execution context.
  // Phase-gated: only fire during "executing" to avoid misrouting during
  // replanning, gate evaluation, or other non-execution phases.
  const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, "");
  if (RESUME_INTENT_PATTERNS.test(trimmed)) {
    await ensureStateDbOpen();
    const state = await deriveState(basePath);
    if (state.phase === "executing" && state.activeTask && state.activeMilestone && state.activeSlice) {
      return buildTaskExecutionContextInjection(
        basePath,
        state.activeMilestone.id,
        state.activeSlice.id,
        state.activeTask.id,
        state.activeTask.title,
      );
    }
  }

  return null;
}

async function buildTaskExecutionContextInjection(
  basePath: string,
  milestoneId: string,
  sliceId: string,
  taskId: string,
  taskTitle: string,
): Promise<string> {
  const taskPlanPath = resolveTaskFile(basePath, milestoneId, sliceId, taskId, "PLAN");
  const taskPlanRelPath = relTaskFile(basePath, milestoneId, sliceId, taskId, "PLAN");
  const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
  const taskPlanInline = taskPlanContent
    ? ["## Inlined Task Plan (authoritative local execution contract)", `Source: \`${taskPlanRelPath}\``, "", taskPlanContent.trim()].join("\n")
    : ["## Inlined Task Plan (authoritative local execution contract)", `Task plan not found at dispatch time. Read \`${taskPlanRelPath}\` before executing.`].join("\n");

  const slicePlanPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN");
  const slicePlanRelPath = relSliceFile(basePath, milestoneId, sliceId, "PLAN");
  const slicePlanContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
  const slicePlanExcerpt = extractSliceExecutionExcerpt(slicePlanContent, slicePlanRelPath);
  const priorTaskLines = await buildCarryForwardLines(basePath, milestoneId, sliceId, taskId);
  const resumeSection = await buildResumeSection(basePath, milestoneId, sliceId);
  const activeOverrides = await loadActiveOverrides(basePath);
  const overridesSection = formatOverridesSection(activeOverrides);

  return [
    "[SF Guided Execute Context]",
    "Use this injected context as startup context for guided task execution. Treat the inlined task plan as the authoritative local execution contract. Use source artifacts to verify details and run checks.",
    overridesSection, "",
    "",
    resumeSection,
    "",
    "## Carry-Forward Context",
    ...priorTaskLines,
    "",
    taskPlanInline,
    "",
    slicePlanExcerpt,
    "",
    "## Backing Source Artifacts",
    `- Slice plan: \`${slicePlanRelPath}\``,
    `- Task plan source: \`${taskPlanRelPath}\``,
  ].join("\n");
}

async function buildCarryForwardLines(
  basePath: string,
  milestoneId: string,
  sliceId: string,
  taskId: string,
): Promise<string[]> {
  const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId);
  if (!tasksDir) return ["- No prior task summaries in this slice."];

  const currentNum = parseInt(taskId.replace(/^T/, ""), 10);
  const sliceRel = relSlicePath(basePath, milestoneId, sliceId);
  const summaryFiles = resolveTaskFiles(tasksDir, "SUMMARY")
    .filter((file) => parseInt(file.replace(/^T/, ""), 10) < currentNum)
    .sort();

  if (summaryFiles.length === 0) return ["- No prior task summaries in this slice."];

  return Promise.all(summaryFiles.map(async (file) => {
    const absPath = join(tasksDir, file);
    const content = await loadFile(absPath);
    const relPath = `${sliceRel}/tasks/${file}`;
    if (!content) return `- \`${relPath}\``;

    const summary = parseSummary(content);
    const provided = summary.frontmatter.provides.slice(0, 2).join("; ");
    const decisions = summary.frontmatter.key_decisions.slice(0, 2).join("; ");
    const patterns = summary.frontmatter.patterns_established.slice(0, 2).join("; ");
    const diagnostics = extractMarkdownSection(content, "Diagnostics");
    const parts = [summary.title || relPath];
    if (summary.oneLiner) parts.push(summary.oneLiner);
    if (provided) parts.push(`provides: ${provided}`);
    if (decisions) parts.push(`decisions: ${decisions}`);
    if (patterns) parts.push(`patterns: ${patterns}`);
    if (diagnostics) parts.push(`diagnostics: ${oneLine(diagnostics)}`);
    return `- \`${relPath}\` — ${parts.join(" | ")}`;
  }));
}

async function buildResumeSection(basePath: string, milestoneId: string, sliceId: string): Promise<string> {
  const continueFile = resolveSliceFile(basePath, milestoneId, sliceId, "CONTINUE");
  const legacyDir = resolveSlicePath(basePath, milestoneId, sliceId);
  const legacyPath = legacyDir ? join(legacyDir, "continue.md") : null;
  const continueContent = continueFile ? await loadFile(continueFile) : null;
  const legacyContent = !continueContent && legacyPath ? await loadFile(legacyPath) : null;
  const resolvedContent = continueContent ?? legacyContent;
  const resolvedRelPath = continueContent
    ? relSliceFile(basePath, milestoneId, sliceId, "CONTINUE")
    : (legacyPath ? `${relSlicePath(basePath, milestoneId, sliceId)}/continue.md` : null);

  if (!resolvedContent || !resolvedRelPath) {
    return ["## Resume State", "- No continue file present. Start from the top of the task plan."].join("\n");
  }

  const cont = parseContinue(resolvedContent);
  const lines = [
    "## Resume State",
    `Source: \`${resolvedRelPath}\``,
    `- Status: ${cont.frontmatter.status || "in_progress"}`,
  ];
  if (cont.frontmatter.step && cont.frontmatter.totalSteps) {
    lines.push(`- Progress: step ${cont.frontmatter.step} of ${cont.frontmatter.totalSteps}`);
  }
  if (cont.completedWork) lines.push(`- Completed: ${oneLine(cont.completedWork)}`);
  if (cont.remainingWork) lines.push(`- Remaining: ${oneLine(cont.remainingWork)}`);
  if (cont.decisions) lines.push(`- Decisions: ${oneLine(cont.decisions)}`);
  if (cont.nextAction) lines.push(`- Next action: ${oneLine(cont.nextAction)}`);
  return lines.join("\n");
}

function extractSliceExecutionExcerpt(content: string | null, relPath: string): string {
  if (!content) {
    return ["## Slice Plan Excerpt", `Slice plan not found at dispatch time. Read \`${relPath}\` before running slice-level verification.`].join("\n");
  }
  const lines = content.split("\n");
  const goalLine = lines.find((line) => line.startsWith("**Goal:**"))?.trim();
  const demoLine = lines.find((line) => line.startsWith("**Demo:**"))?.trim();
  const verification = extractMarkdownSection(content, "Verification");
  const observability = extractMarkdownSection(content, "Observability / Diagnostics");
  const parts = ["## Slice Plan Excerpt", `Source: \`${relPath}\``];
  if (goalLine) parts.push(goalLine);
  if (demoLine) parts.push(demoLine);
  if (verification) parts.push("", "### Slice Verification", verification.trim());
  if (observability) parts.push("", "### Slice Observability / Diagnostics", observability.trim());
  return parts.join("\n");
}

function extractMarkdownSection(content: string, heading: string): string | null {
  const match = new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m").exec(content);
  if (!match) return null;
  const start = match.index + match[0].length;
  const rest = content.slice(start);
  const nextHeading = rest.match(/^##\s+/m);
  const end = nextHeading?.index ?? rest.length;
  return rest.slice(0, end).trim();
}

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function oneLine(text: string): string {
  return text.replace(/\s+/g, " ").trim();
}

// ─── Forensics Context Re-injection (#2941) ──────────────────────────────────

/**
 * Check for an active forensics session and return the prompt content
 * so it can be re-injected on follow-up turns.
 */
export function buildForensicsContextInjection(basePath: string, prompt: string): string | null {
  const marker = readForensicsMarker(basePath);
  if (!marker) return null;

  // Expire markers older than 2 hours to avoid stale context
  const age = Date.now() - new Date(marker.createdAt).getTime();
  if (age > 2 * 60 * 60 * 1000) {
    clearForensicsMarker(basePath);
    return null;
  }

  const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, "");
  if (trimmed && !RESUME_INTENT_PATTERNS.test(trimmed)) {
    clearForensicsMarker(basePath);
    return null;
  }

  return marker.promptContent;
}

/**
 * Remove the active forensics marker file, e.g. when the investigation
 * is complete or the session expires.
 */
export function clearForensicsMarker(basePath: string): void {
  const markerPath = join(basePath, ".sf", "runtime", "active-forensics.json");
  if (existsSync(markerPath)) {
    try {
      unlinkSync(markerPath);
    } catch (e) {
      logWarning("bootstrap", `unlinkSync forensics marker failed: ${(e as Error).message}`);
    }
  }
}
