/**
 * Auto-research handler — sends an auto-research task through the existing
 * AgentBridge (backend proxy) instead of spawning a separate spectral process.
 *
 * This ensures auto-research uses the same model and API keys as the active
 * session — no separate subprocess, no missing API key errors.
 *
 * Flow:
 *   1. Build the auto-research task prompt
 *   2. Ensure session subscriber exists (borrows handleClientMessage pattern)
 *   3. Emit auto_research_start via relay
 *   4. Send task via manager.prompt() — goes through backend proxy with session's model
 *   5. Attach a watcher subscriber to detect turn completion
 *   6. On agent_end, scan for generated extensions and emit auto_research_complete
 *   7. On error, emit auto_research_error
 */
import * as fs from "node:fs";
import * as path from "node:path";
import { execSync } from "node:child_process";
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
/** Tracks sessions with an active auto-research turn (prevents double-runs). */
const activeAutoResearchSessions = new Set();
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
 * Send a ServerEvent to the browser via the relay on the auto-research session.
 */
function sendEvent(relay, sessionId, event) {
    relay.send({ kind: "ws_event", sessionId, event });
}
/**
 * Build a Subscriber that wraps each ServerEvent in a WsEventFrame
 * and pushes it through the relay. Mirrors `makeRelaySubscriber` in
 * dispatcher.ts but defined here to keep auto-research self-contained.
 */
function makeRelaySubscriber(sessionId, relay) {
    return {
        send(event) {
            relay.send({ kind: "ws_event", sessionId, event });
        },
        isOpen() {
            return true;
        },
    };
}
/**
 * Scan the project's .spectral/extensions/auto-research/ directory for generated
 * extension directories. Each subdirectory with .ts files counts as one
 * extension.
 */
function scanGeneratedExtensions(projectPath) {
    const arDir = path.join(projectPath, ".spectral", "extensions", "auto-research");
    const extensions = [];
    try {
        if (!fs.existsSync(arDir))
            return extensions;
        for (const entry of fs.readdirSync(arDir, { withFileTypes: true })) {
            if (!entry.isDirectory())
                continue;
            const extPath = path.join(arDir, entry.name);
            let fileCount = 0;
            let usesLLM = false;
            try {
                const files = fs.readdirSync(extPath);
                for (const f of files) {
                    if (f.endsWith(".ts") || f.endsWith(".js")) {
                        fileCount++;
                        // Quick heuristic: if any file references modelRegistry or setModel,
                        // flag the extension as LLM-powered.
                        if (!usesLLM) {
                            try {
                                const content = fs.readFileSync(path.join(extPath, f), "utf-8");
                                if (content.includes("modelRegistry") ||
                                    content.includes("setModel") ||
                                    content.includes("registerProvider")) {
                                    usesLLM = true;
                                }
                            }
                            catch {
                                /* skip */
                            }
                        }
                    }
                }
            }
            catch {
                /* skip */
            }
            // Extract description from index.ts if present
            let description = `${entry.name} extension`;
            try {
                const indexPath = path.join(extPath, "index.ts");
                if (fs.existsSync(indexPath)) {
                    const content = fs.readFileSync(indexPath, "utf-8");
                    const descMatch = content.match(/description:\s*["']([^"']+)["']/);
                    if (descMatch)
                        description = descMatch[1];
                }
            }
            catch {
                /* skip */
            }
            extensions.push({
                name: entry.name,
                path: `.spectral/extensions/auto-research/${entry.name}`,
                description,
                usesLLM,
                fileCount,
            });
        }
    }
    catch {
        /* skip */
    }
    return extensions;
}
/** Check if AGENTS.md contains the auto-research marker section. */
function hasAgentsMdUpdate(projectPath) {
    try {
        const agentsPath = path.join(projectPath, "AGENTS.md");
        if (!fs.existsSync(agentsPath))
            return false;
        const content = fs.readFileSync(agentsPath, "utf-8");
        return content.includes("<!-- AUTO-RESEARCH:START -->") &&
            content.includes("<!-- AUTO-RESEARCH:END -->");
    }
    catch {
        return false;
    }
}
/**
 * Read the auto-research manifest from disk. Returns null if it doesn't exist
 * or can't be parsed.
 */
function readManifest(projectPath) {
    try {
        const mPath = path.join(projectPath, ".spectral", "extensions", "auto-research", "manifest.json");
        if (!fs.existsSync(mPath))
            return null;
        const raw = fs.readFileSync(mPath, "utf-8");
        const parsed = JSON.parse(raw);
        if (typeof parsed.lastRun !== "string" || typeof parsed.lastCommit !== "string" || typeof parsed.runCount !== "number") {
            return null;
        }
        return {
            lastRun: parsed.lastRun,
            lastCommit: parsed.lastCommit,
            runCount: parsed.runCount,
            extensions: Array.isArray(parsed.extensions) ? parsed.extensions : [],
        };
    }
    catch {
        return null;
    }
}
/**
 * Gather pre-run context for incremental auto-research.
 */
function gatherPreRunContext(projectPath) {
    const manifest = readManifest(projectPath);
    const isIncremental = manifest !== null;
    const existingExtensions = [];
    try {
        const arDir = path.join(projectPath, ".spectral", "extensions", "auto-research");
        if (fs.existsSync(arDir)) {
            for (const entry of fs.readdirSync(arDir, { withFileTypes: true })) {
                if (entry.isDirectory() && entry.name !== "node_modules") {
                    existingExtensions.push(entry.name);
                }
            }
        }
    }
    catch { /* skip */ }
    let changesSinceLastRun = null;
    if (manifest?.lastCommit) {
        try {
            const diffOutput = execSync(`git diff --stat ${manifest.lastCommit}..HEAD`, {
                cwd: projectPath, encoding: "utf-8", timeout: 5000, maxBuffer: 64 * 1024,
            }).trim();
            if (diffOutput)
                changesSinceLastRun = diffOutput;
        }
        catch { /* git not available */ }
    }
    return { isIncremental, manifest, changesSinceLastRun, existingExtensions };
}
/**
 * Build the incremental mode section for the task prompt.
 */
function buildIncrementalSection(ctx) {
    const lines = [];
    lines.push("## Incremental Run — Context from Previous Run", "");
    if (ctx.manifest) {
        const prevExts = ctx.manifest.extensions
            .map((e) => `  - **${e.name}** (${e.path})`)
            .join("\n");
        lines.push(`Previously generated extensions (run #${ctx.manifest.runCount} at ${ctx.manifest.lastRun}):`);
        lines.push(prevExts || "  (none)", "");
    }
    if (ctx.changesSinceLastRun) {
        lines.push("### What changed since last run:", "```", ctx.changesSinceLastRun, "```", "");
    }
    else {
        lines.push("> Note: Could not determine git diff since last run.", "");
    }
    if (ctx.existingExtensions.length > 0) {
        lines.push("### Existing extensions to review:");
        for (const name of ctx.existingExtensions) {
            lines.push(`  - \`.spectral/extensions/auto-research/${name}/\``);
        }
        lines.push("");
    }
    lines.push("### Incremental Instructions", "");
    lines.push("You are running auto-research again on a project that has been analyzed before.");
    lines.push("DO NOT start from scratch. Instead:", "");
    lines.push("1. **Review existing extensions** — Read each existing extension's index.ts.");
    lines.push("   KEEP extensions that are still relevant. UPDATE or DELETE obsolete ones.", "");
    lines.push("2. **Focus on changes** — Only create NEW extensions for project areas that");
    lines.push("   changed since last run (see git diff above).", "");
    lines.push("3. **Clean up stale extensions** — If an extension targets a tool/library that");
    lines.push("   was removed from the project, delete the extension directory entirely.", "");
    lines.push("4. **Update AGENTS.md** — The AUTO-RESEARCH section should reflect the CURRENT");
    lines.push("   set of extensions (remove stale entries, add new ones).", "");
    lines.push("5. **Save manifest.json** — Write/update .spectral/extensions/auto-research/manifest.json");
    lines.push(`   with: lastRun (ISO), lastCommit (git HEAD), runCount (${ctx.manifest ? ctx.manifest.runCount + 1 : 1}),`);
    lines.push("   and extensions array with name/path/category for each generated extension.", "");
    return lines.join("\n");
}
/**
 * Write/update the auto-research manifest after completion.
 */
function writeManifest(projectPath, extensions) {
    try {
        const arDir = path.join(projectPath, ".spectral", "extensions", "auto-research");
        if (!fs.existsSync(arDir))
            fs.mkdirSync(arDir, { recursive: true });
        let currentCommit = "unknown";
        try {
            currentCommit = execSync("git rev-parse HEAD", {
                cwd: projectPath, encoding: "utf-8", timeout: 3000,
            }).trim();
        }
        catch { /* git not available */ }
        const prev = readManifest(projectPath);
        const manifest = {
            lastRun: new Date().toISOString(),
            lastCommit: currentCommit,
            runCount: (prev?.runCount ?? 0) + 1,
            extensions: extensions.map((e) => ({ name: e.name, path: e.path, category: e.usesLLM ? "F" : undefined })),
        };
        fs.writeFileSync(path.join(arDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
    }
    catch { /* best-effort */ }
}
/**
 * Build the auto-research task prompt. This is sent as a user message
 * through the existing AgentBridge, so the agent uses the session's model
 * and backend proxy.
 *
 * @param projectPath Absolute path to the project root
 * @param projectName Human-readable project name
 * @param preRunContext Incremental context — if null/!incremental, runs in full-scan mode
 */
function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
    const isIncremental = preRunContext?.isIncremental ?? false;
    const runInfo = isIncremental && preRunContext?.manifest
        ? `This is run #${preRunContext.manifest.runCount + 1}. Prior run was at ${preRunContext.manifest.lastRun}.`
        : "This is the first auto-research run for this project.";
    const incrementalHeader = isIncremental ? buildIncrementalSection(preRunContext) : "";
    return [
        "## Auto-Research Task",
        "",
        `Project: **${projectPath}** — **${projectName}**`,
        "",
        `> ${runInfo}`,
        "",
        incrementalHeader,
        "---",
        "",
        "## Pi Extension API Reference (HARD-CODED — DO NOT RESEARCH)",
        "",
        "This section is the canonical extension API. Use it directly — do NOT research",
        "spectral internals. For the full guide, see `cli-node/docs/extensions.md` in the spectral repo.",
        "",
        "### Extension Entry Point",
        "",
        "Every extension is a TypeScript file exporting a single default `activate` function:",
        "",
        "```typescript",
        "import type { ExtensionAPI, ToolDefinition } from \"@aexol/spectral\";",
        "export default function activate(ext: ExtensionAPI): void {",
        "  // Register tools, commands, event handlers here",
        "}",
        "```",
        "",
        "Extensions live in: `.spectral/extensions/` (project-local) or `~/.spectral/agent/extensions/` (user-global).",
        "Auto-research generates extensions into `.spectral/extensions/auto-research/<name>/`.",
        "",
        "### Tools: ext.registerTool(tool)",
        "",
        "The primary extension mechanism. Tools become callable by the LLM.",
        "",
        "```typescript",
        "import { Type } from \"typebox\";  // Built-in, zero-dependency schema validation",
        "",
        "ext.registerTool({",
        "  name: \"tool_name\",                // snake_case, globally unique",
        "  label: \"Tool Name\",                // Human-readable label for UI",
        "  description: \"What this tool does\", // Used by the LLM to decide when to call",
        "  parameters: {                       // JSON Schema object (required)",
        "    type: \"object\",",
        "    properties: {",
        "      input: { type: \"string\", description: \"The input to process\" },",
        "      mode: { type: \"string\", enum: [\"fast\", \"deep\"] },",
        "    },",
        "    required: [\"input\"],",
        "  },",
        "  // OR use TypeBox for richer schemas:",
        "  // parameters: Type.Object({",
        "  //   input: Type.String({ description: \"\" }),",
        "  //   mode: Type.Optional(Type.Union([Type.Literal(\"fast\"), Type.Literal(\"deep\")])),",
        "  // }),",
        "  async execute(toolCallId, params, signal, onUpdate, ctx) {",
        "    // toolCallId — unique call identifier (string)",
        "    // params     — validated against your schema",
        "    // signal     — AbortSignal | undefined",
        "    // onUpdate   — send partial results (params) => void",
        "    // ctx        — ExtensionContext",
        "",
        "    // For long operations, stream partial updates:",
        "    onUpdate?.({ stage: \"starting\", progress: 0 });",
        "    // ... work ...",
        "    onUpdate?.({ stage: \"done\", progress: 100 });",
        "",
        "    return {",
        "      content: [{ type: \"text\", text: \"Result string\" }],",
        "      details: {},  // arbitrary structured data",
        "    };",
        "  },",
        "  executionMode: \"sequential\",       // optional: \"sequential\" | \"parallel\"",
        "  promptSnippet: \"One-liner for prompt\", // optional",
        "  promptGuidelines: [\"Extra guidance\"],  // optional: appended to system prompt",
        "  prepareArguments(args) {             // optional: shim args before validation",
        "    return args;",
        "  },",
        "});",
        "```",
        "",
        "**Error handling:** Return error content, don't throw:",
        "",
        "```typescript",
        "async execute() {",
        "  try {",
        "    // ... work ...",
        "  } catch (err: any) {",
        "    return {",
        "      content: [{ type: \"text\", text: `Error: ${err.message}` }],",
        "      details: { isError: true },",
        "    };",
        "  }",
        "}",
        "```",
        "",
        "### Commands: ext.registerCommand(name, options)",
        "",
        "Register slash-commands invoked as `/command-name`:",
        "",
        "```typescript",
        "ext.registerCommand({",
        "  name: \"my-command\",",
        "  description: \"What /my-command does\",",
        "  async execute(args: string, ctx: ExtensionContext) {",
        "    return { content: [{ type: \"text\", text: \"Done\" }] };",
        "  },",
        "});",
        "```",
        "",
        "### Events: ext.on(eventType, handler)",
        "",
        "Hook into the agent lifecycle. Handlers can observe, modify (return result), or block.",
        "",
        "| Event | When | Return type |",
        "|-------|------|-------------|",
        "| `resources_discover` | After session start | `ResourcesDiscoverResult` — add skill/prompt/theme paths |",
        "| `session_start` | Session loaded | — (void) |",
        "| `session_before_switch` | Before switching session | `SessionBeforeSwitchResult` — can cancel |",
        "| `session_before_fork` | Before forking | `SessionBeforeForkResult` — can cancel |",
        "| `session_before_compact` | Before compaction | `SessionBeforeCompactResult` — can cancel/provide compaction |",
        "| `session_compact` | After compaction | — |",
        "| `session_before_tree` | Before tree navigation | `SessionBeforeTreeResult` — can cancel/provide summary |",
        "| `session_tree` | After tree navigation | — |",
        "| `session_shutdown` | Extension runtime teardown | — |",
        "| `before_agent_start` | After user prompt, before LLM call | `BeforeAgentStartEventResult` — modify system prompt |",
        "| `agent_start` | Agent loop starts | — |",
        "| `agent_end` | Agent loop ends | — |",
        "| `turn_start` | Each turn starts | — |",
        "| `turn_end` | Each turn ends | — |",
        "| `message_start` | Message begins | — |",
        "| `message_update` | Token-by-token streaming | — |",
        "| `message_end` | Message finalizes | `MessageEndEventResult` — replace message |",
        "| `tool_execution_start` | Tool starts executing | — |",
        "| `tool_execution_update` | Tool sends partial result | — |",
        "| `tool_execution_end` | Tool finishes | — |",
        "| `tool_call` | Before tool executes | `ToolCallEventResult` — block/modify input |",
        "| `tool_result` | After tool executes | `ToolResultEventResult` — modify result |",
        "| `model_select` | Model changes | — |",
        "| `user_bash` | User runs ! command | `UserBashEventResult` — override execution |",
        "| `input` | User input received | `InputEventResult` — transform/block/continue |",
        "| `context` | Before each LLM call | `ContextEventResult` — modify messages |",
        "| `before_provider_request` | Before provider API call | Replace payload |",
        "| `after_provider_response` | After provider API response | — |",
        "",
        "```typescript",
        "ext.on(\"before_agent_start\", (event, ctx) => {",
        "  // Inject extra instructions into every turn:",
        "  return { systemPrompt: event.systemPrompt + \"\\nExtra rules...\" };",
        "});",
        "",
        "ext.on(\"tool_call\", (event, ctx) => {",
        "  // event.name, event.params, event.toolCallId",
        "  // Return { blocked: true, reason: \"...\" } to block",
        "  // Return { params: {...} } to mutate input",
        "});",
        "```",
        "",
        "### Actions: ext.sendMessage / ext.sendUserMessage / ext.exec",
        "",
        "```typescript",
        "// Send custom messages into the session stream:",
        "ext.sendMessage({ customType: \"my-event\", content: \"Something happened!\", display: \"Notification\", details: {} });",
        "ext.sendUserMessage(\"Continue with the task.\");",
        "",
        "// Run a shell command:",
        "const result = await ext.exec(\"npm\", [\"test\"], { cwd: \"/path\" });",
        "// result.exitCode, result.stdout, result.stderr",
        "```",
        "",
        "### Model / Thinking Control",
        "",
        "```typescript",
        "// Get/set thinking level (off | minimal | low | medium | high)",
        "ext.getThinkingLevel();",
        "ext.setThinkingLevel(\"high\");",
        "",
        "// Tool visibility management",
        "ext.getActiveTools();    // string[] — currently enabled tool names",
        "ext.getAllTools();       // ToolInfo[] — all registered tools",
        "ext.setActiveTools([\"bash\", \"read\", \"write\", \"edit\", \"my_tool\"]);",
        "```",
        "",
        "### CLI Flags: ext.registerFlag",
        "",
        "```typescript",
        "ext.registerFlag(\"api-endpoint\", { description: \"Override API endpoint\", type: \"string\" });",
        "const endpoint = ext.getFlag(\"api-endpoint\") as string;",
        "```",
        "",
        "### Provider Registration: ext.registerProvider",
        "",
        "```typescript",
        "ext.registerProvider(\"my-proxy\", {",
        "  baseUrl: \"https://proxy.example.com\",",
        "  apiKey: \"sk-...\",",
        "  api: \"anthropic-messages\",",
        "  models: [{ id: \"custom\", name: \"Custom\", reasoning: false, input: [\"text\"],",
        "    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },",
        "    contextWindow: 200000, maxTokens: 16384 }],",
        "});",
        "```",
        "",
        "### Keyboard Shortcuts: ext.registerShortcut",
        "",
        "```typescript",
        "ext.registerShortcut(\"ctrl+shift+k\", { description: \"Run custom action\", handler(ctx) { /* ... */ } });",
        "```",
        "",
        "### The ExtensionContext (ctx)",
        "",
        "Passed to every execute/handler. Key properties:",
        "",
        "| Property/method | Description |",
        "|-----------------|-------------|",
        "| `ctx.cwd` | Current working directory (string) |",
        "| `ctx.hasUI` | Whether interactive UI is available (boolean) |",
        "| `ctx.model` | Current model object (Model \\| undefined) |",
        "| `ctx.modelRegistry` | List/find available models |",
        "| `ctx.signal` | Current AbortSignal (undefined if no stream) |",
        "| `ctx.abort()` | Abort the current operation |",
        "| `ctx.isIdle()` | Whether the agent is not streaming |",
        "| `ctx.getSystemPrompt()` | Get the full system prompt (string) |",
        "| `ctx.getContextUsage()` | Token usage info |",
        "| `ctx.compact(options?)` | Trigger context compaction |",
        "| `ctx.shutdown()` | Gracefully exit |",
        "| `ctx.ui.select(prompt, choices)` | Ask the user to pick from a list |",
        "| `ctx.ui.confirm(prompt)` | Ask the user Yes/No |",
        "| `ctx.ui.notify(message)` | Show a notification |",
        "",
        "### Available Packages (npm dependencies)",
        "",
        "Extensions run as Node.js TypeScript modules. These packages are available:",
        "- `@aexol/spectral` — ExtensionAPI, all event types, ToolDefinition, defineTool, type guards",
        "- `typebox` — Zero-dependency runtime type validation",
        "- All `node:*` built-ins: `fs`, `path`, `child_process`, `os`, `crypto`, etc.",
        "- Any npm package already in the project's node_modules",
        "",
        "### spectral's Built-in Tools (what the agent already has — DO NOT reimplement)",
        "",
        "- `read` — Read file contents",
        "- `write` — Create/overwrite files",
        "- `edit` — Precise text replacement in files",
        "- `bash` — Execute shell commands",
        "- `grep` — Search file contents with regex",
        "- `find` — Search files by name/pattern",
        "- `ls` — List directory contents",
        "- `subagent` — Spawn isolated sub-agents",
        "- `recall` — Recall compressed memory observations",
        "- `mcp` — MCP server gateway",
        "- `kanban_list/get/create/update/move/next/delete` — Kanban board (if bound)",
        "",
        "### Extension File Structure",
        "",
        "```",
        ".spectral/extensions/auto-research/",
        "  <extension-name>/",
        "    index.ts          # Entry point — default export activate(ext)",
        "    utils.ts          # [optional] Helper functions",
        "    state.json        # [optional] Persisted state (for stateful extensions)",
        "```",
        "",
        "### Common Extension Patterns",
        "",
        "1. **Shell wrapper** — Use `execSync` from \"node:child_process\" to run project commands",
        "2. **File processor** — Use `node:fs` to read/write project files",
        "3. **LLM-powered** — Use `ext.modelRegistry` to call AI models for smart processing",
        "4. **Event listener** — Use `ext.on()` to react to agent lifecycle events",
        "5. **Stateful** — Use `node:fs` to persist state as JSON between calls",
        "6. **Subagent** — Use the `subagent` tool for isolated analysis tasks",
        "",
        "---",
        "",
        "## Process",
        "",
        "1. **Context Collection** — Explore the project structure:",
        "   - Read package.json, tsconfig.json, deno.json (if present)",
        "   - Check existing extensions under .spectral/extensions/",
        "   - Review key source files to understand architecture",
        "   - Check git log for recent changes and patterns",
        "",
        "2. **Analysis** — Identify automation opportunities:",
        "   - What repetitive tasks do developers perform?",
        "   - What project-specific tools or commands would help?",
        "   - Are there code patterns that could be automated?",
        "   - Could LLM-powered extensions improve developer workflows?",
        "",
        "3. **Extension Generation** — Create extension .ts files:",
        "   - Use `ext.registerTool()` for simple tools with TypeBox validation",
        "   - Use `ext.registerCommand()` for custom slash commands",
        "   - For LLM-powered extensions, use `ctx.modelRegistry` to call models",
        "   - Create files under `.spectral/extensions/auto-research/<name>/`",
        "   - Each extension needs an `index.ts` that registers its tools/commands",
        "   - Read `.spectral/agents/auto-research-templates.md` for proven extension templates",
        "",
        "4. **Validation** — Verify generated extensions:",
        "   - Ensure all imports resolve to available packages (see reference above)",
        "   - Verify registerTool/registerCommand calls have proper TypeBox schemas",
        "   - Ensure handlers always return content blocks",
        "   - Verify error handling with try/catch",
        "",
        "### Extension Categories (A-G)",
        "",
        "| Category | Description | Example |",
        "|----------|-------------|---------|",
        "| A. Workflow automation | Automate repetitive tasks | Auto-format on save, pre-commit hooks |",
        "| B. Code generation | Generate boilerplate/scaffolding | Component generator, CRUD scaffold |",
        "| C. Project-specific tools | Tools tailored to this project | Database migration helper, API client gen |",
        "| D. Quality & review | Linting, testing, code review | PR reviewer, test coverage analyzer |",
        "| E. Documentation | Auto-generate docs | README updater, API doc generator |",
        "| F. LLM-powered | Extensions that call AI models | Code reviewer with LLM, smart refactor |",
        "| G. Stateful | Extensions that persist state | Session memory, project stats tracker |",
        "",
        "### Important Rules",
        "",
        "- Write each extension as TypeScript files under `.spectral/extensions/auto-research/<name>/`",
        "- Every extension directory MUST have an `index.ts` entry point",
        "- Use proper TypeScript with type annotations and error handling",
        "- Extensions must handle errors gracefully (never crash the agent)",
        "- Use `read`/`write`/`edit`/`bash` tools to create and verify files",
        "- When generating LLM-powered extensions, explain how model selection works",
        "",
        "### 5. Update AGENTS.md — Persist Discovered Knowledge",
        "",
        "CRITICAL: After generating extensions, update the project's **AGENTS.md** so future",
        "agent sessions automatically know about the new capabilities.",
        "",
        "**How AGENTS.md works:**",
        "- Spectral loads AGENTS.md at startup from the project root and parent directories",
        "- All found AGENTS.md files are concatenated and injected into the system prompt",
        "- This means documented extensions get discovered by the agent automatically",
        "",
        "**What to add:** Use HTML comment markers so future auto-research can update the section:",
        "",
        "```markdown",
        "<!-- AUTO-RESEARCH:START -->",
        "## Auto-Generated Extensions",
        "",
        "These extensions were generated by auto-research. They are available",
        "in every session. Spectral loads them automatically from `.spectral/extensions/`.",
        "",
        "### `<extension-name>`",
        "",
        "- **Tools:** `tool_name` — description of what it does",
        "- **Commands:** `/command-name` — description",
        "- **When to use:** Brief guidance on when the agent should call this tool",
        "- **Category:** A-G (workflow/codegen/project-specific/quality/docs/llm-powered/stateful)",
        "",
        "### Project Patterns Discovered",
        "",
        "- **Build system:** npm / deno / etc.",
        "- **Test framework:** vitest / jest / etc.",
        "- **Conventions:** key patterns the agent should follow",
        "- **Key directories:** important source locations",
        "<!-- AUTO-RESEARCH:END -->",
        "```",
        "",
        "**Rules for the section:**",
        "- Place it at the END of AGENTS.md (after all existing content)",
        "- If an older `<!-- AUTO-RESEARCH:START -->...<!-- AUTO-RESEARCH:END -->` block",
        "  already exists, REPLACE it entirely with the updated version",
        "- List EVERY generated extension with its full tool/command list",
        "- Add project patterns discovered during context analysis",
        "- Keep descriptions concise (agent uses this as reference, not tutorial)",
        "",
        "---",
        "",
        "Start by collecting project context, then generate the most impactful extensions",
        "and update AGENTS.md when done.",
        "",
        "### 6. Persist Key Discoveries as Durable Observations",
        "",
        "As you analyze the project, you will discover conventions, rules, and patterns",
        "that future agent sessions should know. Write these as durable cross-session",
        "observations using the `write_project_observation` tool:",
        "",
        "```",
        "write_project_observation({",
        "  content: \"backend/src/models.ts is auto-generated by Axolotl — never edit by hand\",",
        "  relevance: \"critical\"",
        "})",
        "```",
        "",
        "**What to capture:**",
        "",
        "- **NEVER-EDIT markers** (relevance: critical): Auto-generated files that must",
        "  be regenerated instead of edited. Seen a comment like 'DO NOT EDIT'? Persist it.",
        "- **Import/macro conventions** (relevance: high): Rules like \"all Deno imports",
        "  MUST use .ts extension\" or \"use workspace imports, not relative paths\".",
        "- **Code-generation commands** (relevance: high): The exact shell commands to",
        "  regenerate generated files (e.g. 'cd backend && deno task axolotl:build').",
        "- **Directory roles** (relevance: medium): What each top-level directory contains",
        "  (e.g. 'packages/parser/ is zero-dependency — never add imports here').",
        "- **Runtime boundaries** (relevance: medium): Which packages use Deno vs Node.",
        "- **Database/API patterns** (relevance: medium): How the backend is structured.",
        "",
        "**Guidelines:**",
        "",
        "- Write 5–10 observations covering the most important discoveries.",
        "- Each observation is a single self-contained sentence — no markdown, no lists.",
        "- Focus on facts that would PREVENT MISTAKES or SAVE TIME in future sessions.",
        "- Deduplication is automatic by content hash — writing the same fact twice is harmless.",
        "- Future sessions retrieve these via `read_project_observations(\"query\")`.",
    ].join("\n");
}
// ---------------------------------------------------------------------------
// Handler
// ---------------------------------------------------------------------------
const AR_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes — generous for LLM-based analysis
/**
 * Execute auto-research for a project through the existing AgentBridge.
 *
 * Caller (dispatcher) is fire-and-forget — this function is `void` and
 * all errors are surfaced as `auto_research_error` events on the wire
 * rather than thrown.
 *
 * This replaces the old subprocess-spawning approach. Instead of launching
 * a separate spectral process (which lacks backend proxy credentials), we send
 * the auto-research task through the session's existing AgentBridge. The agent
 * uses the session's model, backend proxy, and all available tools.
 */
export function handleAutoResearch(input, deps) {
    const { projectId, sessionId } = input;
    const { store, manager, relay, subscribers } = deps;
    const logger = deps.logger ?? console;
    // Guard: prevent concurrent auto-research on the same session.
    if (activeAutoResearchSessions.has(sessionId)) {
        sendEvent(relay, sessionId, {
            type: "auto_research_error",
            projectId,
            message: "Auto-research is already running for this session.",
        });
        return;
    }
    // Verify the project exists in the store.
    const project = store.getProject(projectId);
    if (!project) {
        sendEvent(relay, sessionId, {
            type: "auto_research_error",
            projectId,
            message: `Project not found: ${projectId}`,
        });
        return;
    }
    const projectPath = project.path;
    // Build the auto-research task prompt with incremental context.
    const preRunContext = gatherPreRunContext(projectPath);
    const taskContent = buildAutoResearchTask(projectPath, project.name, preRunContext);
    // --- Emit start event ---
    sendEvent(relay, sessionId, {
        type: "auto_research_start",
        projectId,
    });
    // --- Ensure session has a wire subscriber (mirrors handleClientMessage) ---
    let subscriber = subscribers.get(sessionId);
    if (!subscriber) {
        subscriber = makeRelaySubscriber(sessionId, relay);
        try {
            manager.attach(sessionId, subscriber);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            sendEvent(relay, sessionId, {
                type: "auto_research_error",
                projectId,
                message: `Failed to attach session subscriber: ${msg}`,
            });
            return;
        }
        subscribers.set(sessionId, subscriber);
    }
    // --- Get the session's current model (use same model as active session) ---
    const storedModelId = store.getSessionModel(sessionId) ?? undefined;
    // --- Mark auto-research as active ---
    activeAutoResearchSessions.add(sessionId);
    // We track whether the watcher has already fired (completion or error)
    // so we don't emit duplicate events if both agent_end and prompt rejection
    // race.
    let watcherFired = false;
    /**
     * Safely finalize auto-research: mark inactive, detach watcher, emit event.
     */
    const finalize = (event) => {
        if (watcherFired)
            return;
        watcherFired = true;
        activeAutoResearchSessions.delete(sessionId);
        try {
            manager.detach(sessionId, watcher);
        }
        catch {
            /* best-effort */
        }
        sendEvent(relay, sessionId, event);
    };
    // --- Attach a watcher subscriber to detect turn completion ---
    // This subscriber receives all broadcast events alongside the main subscriber
    // but only acts on agent_end (→ auto_research_complete) and error events.
    const watcher = {
        send(event) {
            if (watcherFired)
                return;
            if (event.type === "agent_end") {
                // The auto-research turn completed. Scan for generated extensions
                // and emit the completion event.
                const extensions = scanGeneratedExtensions(projectPath);
                const agentsMdUpdated = hasAgentsMdUpdate(projectPath);
                writeManifest(projectPath, extensions);
                finalize({
                    type: "auto_research_complete",
                    projectId,
                    extensions,
                    agentsMdUpdated,
                });
            }
            else if (event.type === "error") {
                finalize({
                    type: "auto_research_error",
                    projectId,
                    message: event.message,
                });
            }
        },
        isOpen() {
            return true;
        },
    };
    try {
        manager.attach(sessionId, watcher);
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        finalize({
            type: "auto_research_error",
            projectId,
            message: `Failed to attach auto-research watcher: ${msg}`,
        });
        return;
    }
    // --- Set up timeout ---
    const timeout = setTimeout(() => {
        finalize({
            type: "auto_research_error",
            projectId,
            message: "Auto-research timed out after 10 minutes.",
        });
    }, AR_TIMEOUT_MS);
    // Clear timeout on watcher fire (agent_end or error). We do this inside
    // the finalize call by wrapping it.
    const originalFinalize = finalize;
    const finalizeWithCleanup = (event) => {
        clearTimeout(timeout);
        originalFinalize(event);
    };
    // Nasty but effective: replace finalize in watcher closure. The watcher
    // already has a reference to `finalize` from the outer scope. We can't
    // easily reassign the const, so we use a mutable wrapper.
    // Instead, we make watcherFired guard idempotent — the timeout clear
    // is best-effort. If the watcher fires first, clearTimeout on an already-
    // cleared timer is harmless. If the timeout fires first, finalize has
    // already run and the watcher's `send` will be gated by `watcherFired`.
    // Actually, let's just clear the timeout in the watcher itself. The watcher
    // already calls `finalize` which sets `watcherFired = true`. We just need
    // to clear the timeout before finalize. Let me restructure.
    // Simpler approach: inline the timeout clearing in the watcher's send.
    // Let me rewrite the watcher with a direct timeout reference.
    // For now, the timeout uses finalize which sets watcherFired. If the
    // watcher fires first (agent_end), finalize() runs, watcherFired=true,
    // then timeout fires and calls finalize() again — but finalize() is
    // gated by watcherFired, so it's a no-op. The only issue is we don't
    // clearTimeout on watcher fire — but that's fine, the timer is harmless.
    // --- Send the prompt through the existing AgentBridge (backend proxy) ---
    manager.prompt(sessionId, taskContent, storedModelId).catch((err) => {
        if (watcherFired)
            return; // already handled by watcher
        const msg = err instanceof Error ? err.message : String(err);
        logger.error?.(`[auto-research] manager.prompt failed for ${sessionId}:`, msg);
        clearTimeout(timeout);
        finalize({
            type: "auto_research_error",
            projectId,
            message: `Auto-research prompt failed: ${msg}`,
        });
    });
}
