// @ts-nocheck
/**
 * Codebase Index Extension
 *
 * Auto-indexes the current git repo on session start and exposes a
 * `search_codebase` tool for semantic code search via Memgraph + MAGE embeddings.
 *
 * - On session_start: checks manifest freshness and spawns index-codebase-fast.js if needed
 * - Tool: search_codebase — vector search over indexed code chunks
 * - Command: /reindex — force full re-index of current repo
 */

import type { ExtensionAPI } from "@helios-agent/pi-coding-agent";
// startup-lifecycle waitForPhase used to ensure Memgraph is ready before indexing
import { waitForPhase, StartupPhase, getStartupState } from '../lib/startup-lifecycle.ts';
import { Type } from "@sinclair/typebox";
import * as path from "node:path";
import { homedir as _homedir } from "node:os";
import { createRequire } from 'module';
const { heliosPath, HOME } = require('./lib/helios-root');
const _requireCI = createRequire(import.meta.url);
import * as fs from "node:fs";
import * as crypto from "node:crypto";
import { execSync } from "node:child_process";

import {
  graphRead,
  graphReadFresh,
  isGraphAvailable,
} from "../lib/unified-graph.ts";
import {
  computeManifest,
  loadPreviousManifest,
  diffManifest,
  saveManifest,
} from "../lib/manifest-diff.ts";
import { FileWatcher } from './lib/file-watcher.ts';
import { incrementalIndex, gitDiffIndex } from './lib/incremental-indexer.ts';
import { orchestrateSessionIndex } from '../lib/index-orchestrator.ts';
import { requestIndex, readConsent, writeConsent } from '../lib/index-gateway.ts';

import { resolve as di } from '../lib/container.ts';

// ---------------------------------------------------------------------------
// Coordination Flag (read-only here — gateway owns writes)
// ---------------------------------------------------------------------------

const indexingRepos = ((globalThis as any).__helios_indexing_repos ??= new Set<string>());

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const resolveGraphScript: (name: string) => string = _requireCI('../lib/graph/resolve');
const MANIFEST_DIR = path.join(HOME, '.familiar', 'index-manifests');
const STALE_MS = 24 * 60 * 60 * 1000; // 24 hours
const EMBEDDING_DIMS = 768; // Unified dimension — all Memgraph vector indexes use 768d

// ---------------------------------------------------------------------------
// Graph access — via shared unified-graph (circuit breaker + cache + pool)
// ---------------------------------------------------------------------------

/** Cached graph read — for metadata queries like chunk counts */
async function graphQuery(cypher: string, params?: Record<string, unknown>): Promise<any[]> {
	return graphRead(cypher, params);
}
/** Fresh (uncached) graph read — for vector search queries where results must be current */
async function graphQueryFresh(cypher: string, params?: Record<string, unknown>): Promise<any[]> {
	return graphReadFresh(cypher, params);
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function getManifestPath(repoPath: string): string {
	const hash = crypto.createHash("sha256").update(repoPath).digest("hex").slice(0, 16);
	return path.join(MANIFEST_DIR, `${hash}.json`);
}

const LOCK_EXPIRY_MS = 30 * 60 * 1000; // 30 minutes — must match index-gateway.ts SINGLE_LOCK_EXPIRY_MS

// Per-repo lock path — shared formula with index-gateway.ts (uses os.tmpdir() for cross-platform)
const _os = require('os');
function getLockPath(repoPath: string): string {
	const hash = crypto.createHash("sha256").update(repoPath).digest("hex").slice(0, 16);
	return require('path').join(_os.tmpdir(), `codebase-index-${hash}.lock`);
}

async function needsIndexing(repoPath: string): Promise<boolean> {
	// If a fresh lock exists, indexing is already in progress — skip
	const lockPath = getLockPath(repoPath);
	if (fs.existsSync(lockPath)) {
		try {
			const lockAge = Date.now() - parseInt(fs.readFileSync(lockPath, "utf-8"), 10);
			if (lockAge < LOCK_EXPIRY_MS) return false;
  } catch (e) { process.stderr.write(`[codebase-index.ts] operation failed: ${String(e)}\n`); }
	}

	const manifestPath = getManifestPath(repoPath);

	// No manifest at all → must index
	if (!fs.existsSync(manifestPath)) return true;

	try {
		// --- Manifest-diff change detection (Merkle-tree-inspired) ---
		// Even if the manifest is old, skip re-indexing when content hasn't changed.
		const previous = loadPreviousManifest(repoPath);
		if (previous) {
			const current = computeManifest(repoPath);
			const delta = diffManifest(current, previous);
			const hasChanges =
				delta.added.length > 0 ||
				delta.modified.length > 0 ||
				delta.deleted.length > 0;

			if (!hasChanges) {
				// Content is identical — bump lastRun so we don't re-check for another STALE_MS
				saveManifest(current);
				if (process.env.DEBUG) {
					process.stderr.write(`[codebase-index] skip: 0 file changes in ${repoPath}\n`);
				}
				return false;
			}
			// Content changed → fall through to index
			if (process.env.DEBUG) {
				process.stderr.write(
					`[codebase-index] changes detected in ${repoPath}: ` +
						`+${delta.added.length} ~${delta.modified.length} -${delta.deleted.length}\n`
				);
			}
			return true;
		}

		// --- Fallback: manifest age check (original behaviour) ---
		const stat = fs.statSync(manifestPath);
		const ageMs = Date.now() - stat.mtimeMs;
		if (ageMs > STALE_MS) return true;
		return (await getIndexedChunkCount(repoPath)) === 0;
 } catch (e) {
   process.stderr.write(`[codebase-index.ts] operation failed: ${String(e)}\n`);
   return true;
	}
}

function findGitRoot(startDir: string, maxLevels: number = 4): string | null {
	let current = startDir;
	for (let i = 0; i <= maxLevels; i++) {
		if (fs.existsSync(path.join(current, ".git"))) return current;
		const parent = path.dirname(current);
		if (parent === current) break;
		current = parent;
	}
	return null;
}

function escapeCypherLiteral(value: string): string {
	return value
		.replace(/\\/g, "\\\\")
		.replace(/\r/g, "\\r")
		.replace(/\n/g, "\\n")
		.replace(/"/g, '\\"');
}

async function getIndexedChunkCount(repoPath: string): Promise<number> {
	try {
		const cypher = `MATCH (n:CodeChunk) WHERE n.filePath STARTS WITH "${escapeCypherLiteral(repoPath)}" RETURN count(n) AS count`;
		const records = await graphQuery(cypher);
		if (records.length > 0) {
			const val = records[0]["count"];
			const num = parseInt(String(val), 10);
			if (!isNaN(num)) return num;
		}
	} catch (e) { process.stderr.write(`[extensions] fail-open: non-critical: ${String(e)}\n`); }
	return 0;
}

function extractSearchTerms(query: string): string[] {
	const stop = new Set(["the","and","for","with","from","into","that","this","what","when","where","which","how","why","are","was","were","have","has","had","use","using","find","look","codebase","code","search"]);
	return Array.from(new Set(query.toLowerCase().split(/[^a-z0-9_./-]+/).map((s) => s.trim()).filter((s) => s.length >= 3 && !stop.has(s)))).slice(0, 6);
}

// ── Embedding (delegated to lib/embedding-client.ts) ─────────────────────────
// All resilience logic (circuit breaker, cache, bulkhead, multi-provider fallback)
// lives in the reusable embedding-client module. See lib/embedding-client.ts.
import { getEmbeddingClient } from '../lib/embedding-client.ts';

function isValidEmbedding(value: number[] | null | undefined): boolean {
	return Array.isArray(value) && value.length === EMBEDDING_DIMS;
}

async function fetchEmbedding(query: string): Promise<number[] | null> {
	return getEmbeddingClient().embed(query);
}

function runLexicalFallback(query: string, limit: number, project?: string): { text: string; count: number } {
	const searchRoot = project || findGitRoot(process.cwd()) || (require('path').join(require('os').homedir(), 'helios-agent'));
	const terms = extractSearchTerms(query);
	if (!terms.length) {
		return { text: "⚡ Lexical fallback (semantic search unavailable) — could not derive useful search terms from the query.", count: 0 };
	}
	const pattern = terms.join("|");
	const globs = ["*.ts","*.tsx","*.js","*.jsx","*.py","*.go","*.rs","*.java","*.json","*.md","*.yaml","*.yml"].map((g) => `-g '${g}'`).join(" ");
	try {
		const cmd = `cd '${searchRoot.replace(/'/g, "'\''")}' && rg -n -S -m 1 -e '${pattern.replace(/'/g, "'\''")}' ${globs} . | head -n ${Math.max(1, Math.min(limit, 50))}`;
		const raw = execSync(cmd, { encoding: "utf-8", timeout: 15000, shell: "/bin/bash" }).trim();
		if (!raw) return { text: `⚡ Lexical fallback (semantic search unavailable) — no matches found for: ${query}`, count: 0 };
		const lines = raw.split("\n").filter(Boolean);
		let output = `⚡ Lexical fallback (semantic search unavailable) — ${lines.length} match${lines.length === 1 ? "" : "es"} for: ${query}\n\n`;
		for (const line of lines) {
			const match = line.match(/^(.+?):(\d+):(.*)$/);
			if (!match) continue;
			const [, file, ln, snippet] = match;
			output += `📁 ${file}:${ln}\n`;
			output += "```\n" + snippet.trim().slice(0, 500) + "\n```\n\n";
		}
		return { text: output, count: lines.length };
	} catch (err: any) {
		return { text: `⚡ Lexical fallback (semantic search unavailable) — search failed: ${err?.message || err}`, count: 0 };
	}
}

function discoverAndIndexStaleRepos(): void {
  const gitBaseDir = heliosPath('git');
  if (!fs.existsSync(gitBaseDir)) return;

  const repos: string[] = [];
  function walkForGitRepos(dir: string, depth: number = 0): void {
    if (depth > 4) return;
    try {
      const entries = fs.readdirSync(dir, { withFileTypes: true });
      if (entries.some(e => e.name === ".git")) { repos.push(dir); return; }
      for (const entry of entries) {
        if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
          walkForGitRepos(path.join(dir, entry.name), depth + 1);
        }
      }
    } catch (e) { process.stderr.write(`[extensions] fail-open: non-critical: ${String(e)}\n`); }
  }
  walkForGitRepos(gitBaseDir);

  for (const repo of repos) {
    if (needsIndexingSync(repo)) {
      // Delegate to gateway — single lock, correct flags, full post-chain
      // Use 'discovery' trigger (not 'orchestrator') — this path walks arbitrary repos
      // from heliosPath('git') and must respect per-repo user consent.
      requestIndex(repo, { trigger: 'discovery', mode: 'incremental' });
    }
  }
}

function needsIndexingSync(repoPath: string): boolean {
  const manifestPath = getManifestPath(repoPath);
  try {
    const stat = fs.statSync(manifestPath);
    return Date.now() - stat.mtimeMs > STALE_MS;
  } catch (e) {
    process.stderr.write(`[codebase-index.ts] operation failed: ${String(e)}\n`);
    return true;
  }
}

// ---------------------------------------------------------------------------
// Extension
// ---------------------------------------------------------------------------

export default function codebaseIndexExtension(pi: ExtensionAPI) {
	// Store pi instance globally so _deferredGraphInit can call pi.followUp
	(globalThis as any).__helios_pi_instance = pi;
	// ── HELIOS_SKIP_INFRA / HELIOS_SKIP_INDEX: skip codebase indexer ──
	// HELIOS_SKIP_INFRA=1: full headless/judge mode (set by helios-rpc.js --headless).
	// HELIOS_SKIP_INDEX=1: narrow flag — disable indexer only, keep Memgraph/HEMA/tools running.
	//   Use this during evals so the background indexer doesn't keep the tee pipe open.
	if (process.env.HELIOS_SKIP_INFRA === '1' || process.env.HELIOS_SKIP_INDEX === '1') {
		const _skipFlag = process.env.HELIOS_SKIP_INDEX === '1' ? 'HELIOS_SKIP_INDEX' : 'HELIOS_SKIP_INFRA';
		console.warn(`[WARN] [codebase-index] ${_skipFlag}=1 — skipping codebase indexer`);
		return;
	}

	// ── Performance tier gate ─────────────────────────────────────────────
	// Lite tier: skip entirely — no MAGE, no indexer, no search_codebase tool.
	// Users fall back to grep/find for code search.
	const _perfFeatures = di('helios_features');
	if (_perfFeatures && !_perfFeatures.codebaseIndex && !_perfFeatures.codebaseIndexLazy) {
		if (process.env.DEBUG) process.stderr.write('[codebase-index] DISABLED by performance tier (lite)\n');
		return; // Don't register tool, don't index, don't start MAGE embeddings. Saves ~500MB.
	}

	// When Code Atlas is available, skip legacy indexer (atlas handles indexing)
	// Legacy search_codebase tool still registers for backward compat
	const _atlasAvailable = fs.existsSync(heliosPath('git', 'github.com', 'helios-agi', 'code-atlas', 'src', 'code_atlas'));
	const _skipLegacyIndexer = _atlasAvailable && process.env.HELIOS_FORCE_LEGACY_INDEXER !== '1';

	// Session cwd — captured at session_start; used in tool execute handlers
	let _sessionCwd: string = process.cwd();

	// ── Incremental file watcher (Phase 1: auto-knowledge-graph) ──────────
	let fileWatcher: FileWatcher | null = null;

	// ── Lazy migration: defer graph-heavy init to agent_start ────────────
	let _graphInitDone = false;
	let _sessionCtx: any = null;

	async function _deferredGraphInit() {
		if (_graphInitDone) return;
		_graphInitDone = true;

		await waitForPhase(StartupPhase.INFRASTRUCTURE);
		const { degraded } = getStartupState();
		if (degraded) {
			process.stderr.write('[codebase-index] Skipping graph init — startup degraded\n');
			return;
		}

		const ctx = _sessionCtx;
		const cwd = _sessionCwd;

		// Cross-repo discovery: always fire regardless of cwd being a git repo
		// Standard tier (lazy): defer auto-index to first search_codebase call
		if (_perfFeatures?.codebaseIndexLazy) {
			process.stderr.write('[codebase-index] Deferring auto-index to first search_codebase call (standard tier)\n');
		} else {
		// unref: background indexing is opportunistic — don't hold the process alive
		// for it. If pi -p finishes before 30s, the index fires only if still running.
		// For interactive sessions, other ref'd handles (stdin, event loop anchor) keep
		// the process alive so this fires as expected.
		const _indexTimer = setTimeout(async () => {
  try {
    const report = await orchestrateSessionIndex(_sessionCwd);
    if (report && report.projectsIndexed > 0) {
      ctx?.ui?.notify(`Indexed ${report.projectsIndexed} projects (${report.projectsSkipped} skipped)`, 'info');
    }
  } catch (err: any) {
    if (process.env.DEBUG) process.stderr.write(`[codebase-index] orchestrator failed: ${err.message}\n`);
    discoverAndIndexStaleRepos();
  }
}, 30_000);
		if (typeof (_indexTimer as any)?.unref === 'function') (_indexTimer as any).unref();
		} // end else (non-lazy)

		const gitRoot = findGitRoot(cwd);
		if (!gitRoot) {
			ctx?.ui?.notify("Codebase indexing skipped: no git repo found", "info");
			return;
		}

		// Non-blocking: don't delay subprocess startup for indexing
		if (!_skipLegacyIndexer) {
		setTimeout(async () => {
			if (!(await needsIndexing(gitRoot))) {
				if (process.env.DEBUG) process.stderr.write(`[codebase-index] skip: index healthy for ${gitRoot}\n`);
				return;
			}

			// ── Consent gate — handled by gateway ────────────────────────────
			// readConsent/writeConsent live in index-gateway.ts. The consent prompt
			// (followUp) is surfaced here because only codebase-index has access to
			// the pi instance. Gateway skips silently when consent === 'ask'.
			const consent = readConsent(gitRoot);
			if (consent === 'no') {
				if (process.env.DEBUG) process.stderr.write(`[codebase-index] skip: user declined indexing for ${gitRoot}\n`);
				return;
			}
			if (consent === 'ask') {
				writeConsent(gitRoot, 'no'); // default to no until user says yes
				const repoName = path.basename(gitRoot);
				const msg =
					`📦 Helios detected a codebase at \`${repoName}\` that hasn't been indexed yet.\n\n` +
					`Indexing enables semantic code search (\`search_codebase\`) and the Coding Matrix — ` +
					`it runs in the background and takes 1–5 minutes.\n\n` +
					`**Would you like to index this codebase?**\n` +
					`- Reply \`yes\` or run \`/reindex\` to index now\n` +
					`- Reply \`no\` to skip (you can always run \`/reindex\` later)\n\n` +
					`_(This question won't appear again for this repo once you answer.)_`;
				try {
					const pi_instance = (globalThis as any).__helios_pi_instance;
					if (pi_instance && typeof pi_instance.followUp === 'function') {
						pi_instance.followUp(msg);
					} else {
						ctx?.ui?.notify(`Codebase ${repoName} not indexed — run /reindex to enable semantic search`, 'info');
					}
				} catch {
					ctx?.ui?.notify(`Codebase ${repoName} not indexed — run /reindex to enable semantic search`, 'info');
				}
				return;
			}

			// consent === 'yes' — delegate to gateway (handles locks, chain, pressure)
			const spawned = requestIndex(gitRoot, { trigger: 'session', mode: 'incremental' });
			if (spawned) ctx?.ui?.notify("Codebase indexing started (incremental).", "info");
		}, 0);
		} else {
			process.stderr.write('[codebase-index] Legacy indexer skipped — Code Atlas handles indexing\n');
			// Embed backfill still needed — atlas indexes structure without embeddings.
			// Gateway post-chain handles this; fire a session-trigger with consent bypass.
			setTimeout(() => requestIndex(gitRoot, { trigger: 'orchestrator', mode: 'incremental', extraFlags: ['--no-ast'] }), 2 * 60 * 1000);
		}

		// Backfill IN_PROJECT edges if missing (fast — batched Cypher)
		setTimeout(async () => {
			try {
				const mg = require('../lib/safe-memgraph.js');
				let linked = 500;
				while (linked >= 500) {
					const r = await mg.safeWrite(
						`MATCH (cf:CodeFile) WHERE cf.repoRoot IS NOT NULL
						 MATCH (p:Project {path: cf.repoRoot})
						 WHERE NOT (cf)-[:IN_PROJECT]->(p)
						 WITH cf, p LIMIT toInteger(500)
						 MERGE (cf)-[:IN_PROJECT]->(p)
						 RETURN count(*) as linked`,
						{}
					);
					linked = r?.[0]?.linked ?? 0;
				}
			} catch (e) { process.stderr.write('[codebase-index] IN_PROJECT backfill: ' + (e as any)?.message + '\n'); }
		}, 30_000); // 30s after session_start, gives indexer a head start
		// Backfill CodeSymbol.kind + create text search indexes (120s after start)
		setTimeout(async () => {
			try {
				const mg = require('../lib/safe-memgraph.js');
				// Backfill CodeSymbol.kind from name heuristics
				await mg.safeWrite(`
					MATCH (s:CodeSymbol) WHERE s.kind IS NULL AND s.name IS NOT NULL
					SET s.kind = CASE
						WHEN s.name =~ '^[A-Z][a-z].*' AND s.name =~ '.*Interface$' THEN 'interface'
						WHEN s.name =~ '^[A-Z][a-z].*' AND s.name =~ '.*Type$' THEN 'type'
						WHEN s.name =~ '^[A-Z][a-z].*' THEN 'class'
						WHEN s.name =~ '^use[A-Z].*' THEN 'function'
						WHEN s.name =~ '^[a-z].*' THEN 'function'
						ELSE 'unknown'
					END
				`, {});
				process.stderr.write('[codebase-index] CodeSymbol.kind backfill complete\n');
			} catch (e: any) { process.stderr.write('[codebase-index] kind backfill: ' + (e as any)?.message + '\n'); }

			// Text search indexes are created at container startup via init.cypherl
			// No runtime DDL — prevents deadlocking Memgraph with concurrent CREATE INDEX
			process.stderr.write('[codebase-index] text search indexes handled by init.cypherl\n');
		}, 300_000);

		// Verify indexer produced IN_PROJECT edges
		setTimeout(async () => {
			try {
				const mg = require('../lib/safe-memgraph.js');
				const r = await mg.safeRead('MATCH ()-[:IN_PROJECT]->() RETURN count(*) as c');
				const count = r?.[0]?.c ?? 0;
				if (count === 0) {
					process.stderr.write('[codebase-index] WARNING: 0 IN_PROJECT edges after indexer — matrix queries will return empty\n');
				} else {
					process.stderr.write(`[codebase-index] IN_PROJECT health: ${count} edges ✓\n`);
				}
			} catch (e: any) {
				process.stderr.write('[codebase-index] IN_PROJECT health check failed: ' + (e?.message ?? e) + '\n');
			}
		}, 90_000);

		// Initialize file watcher for real-time incremental indexing
		// Skip when atlas is active — atlas daemon has its own file watcher
		if (!_skipLegacyIndexer) {
		try {
			fileWatcher = new FileWatcher({ watchPaths: [`${gitRoot}/**/*.{ts,tsx,js,jsx}`] });
			fileWatcher.onBatch(async (events) => {
				const files = events.map(e => e.filePath);
				const result = await incrementalIndex(files);
				if (result.filesProcessed > 0) {
					ctx?.ui?.setStatus('index', `index: ${result.filesProcessed} files updated`);
				}
			});
			fileWatcher.start();
		} catch (err: any) {
			if (process.env.DEBUG) process.stderr.write(`[codebase-index] file watcher failed: ${err.message}\n`);
		}
		} // end if (!_skipLegacyIndexer) for file watcher
	}

	// -----------------------------------------------------------------------
	// Part 1: Auto-index on session start (lightweight state only)
	// -----------------------------------------------------------------------

	pi.on("session_start", async (_event, ctx) => {
		const cwd = ctx.cwd ?? process.cwd();
		_sessionCwd = cwd;
		_sessionCtx = ctx;
		_graphInitDone = false;

		// Run ensure-indexes AFTER the broker is ready (DATA_LAYER phase).
		// Non-blocking spawn: ensure-indexes now creates vector indexes which can take
		// 20-30s under load. execFileSync would block the event loop for that entire
		// duration, stalling ALL 100 agents. Spawn + unref allows queries to flow.
		// Guard: skip when HELIOS_SKIP_INDEX=1 (eval containers, CI) — previously missing.
		if (process.env.HELIOS_SKIP_INFRA === '1' || process.env.HELIOS_SKIP_INDEX === '1') return;
		try {
			const { waitForPhase, StartupPhase: SP } = require('../lib/startup-lifecycle.ts');
			waitForPhase(SP.DATA_LAYER).then(() => {
				try {
					const { spawnFireAndForget } = require('../lib/spawn-with-timeout.cjs.js');
					const idxScript = resolveGraphScript('ensure-indexes.js');
					if (require('fs').existsSync(idxScript)) {
						spawnFireAndForget('node', [idxScript, '--force'], { // P1.6: --force required or ensure-indexes self-disables (OC-3)
							timeoutMs: 300_000,
						env: {
							...process.env,
							MEMGRAPH_USER: 'memgraph',
							MEMGRAPH_PASS: 'memgraph',
							// Use getBrokerIpcPath() for cross-platform IPC path (Named Pipe on Windows).
							// Pass heliosDataPath('') as second arg for correct multi-company pipe naming.
							HELIOS_BROKER_SOCK: process.env.HELIOS_BROKER_SOCK || (() => {
								try {
									const { getBrokerIpcPath } = _requireCI('../lib/broker/ipc-path');
									const { HELIOS_DATA: _ciHeliosData } = _requireCI('../lib/helios-root');
									return getBrokerIpcPath(heliosPath(''), _ciHeliosData);
								} catch { return heliosPath('run', 'memgraph-broker.sock'); }
							})(),
							HELIOS_BROKER_MANAGED: '1',
						},
						});
					}
				} catch (e: any) { process.stderr.write('[codebase-index] ensure-indexes: ' + (e?.message || '') + '\n'); }
			}).catch((err) => { console.error('[codebase-index] deferred init failed:', err?.message || err) });
		} catch {
			setTimeout(() => {
				try {
					const { spawnFireAndForget } = require('../lib/spawn-with-timeout.cjs.js');
					const idxScript = resolveGraphScript('ensure-indexes.js');
					if (require('fs').existsSync(idxScript)) {
						spawnFireAndForget('node', [idxScript, '--force'], { timeoutMs: 300_000, env: { ...process.env, MEMGRAPH_USER: 'memgraph', MEMGRAPH_PASS: 'memgraph' } }); // P1.6
					}
				} catch (e: any) { process.stderr.write('[codebase-index] ensure-indexes: ' + (e?.message || '') + '\n'); }
			}, 5_000);
		}
	});

	// Defer graph-heavy init (indexing, MAGE embeddings, file watcher) to agent_start
	pi.on("agent_start", () => {
		if (process.env.HELIOS_SKIP_INFRA === '1' || process.env.HELIOS_SKIP_INDEX === '1') return;
		_deferredGraphInit().catch(e => process.stderr.write(`[codebase-index] graph init failed (non-blocking): ${e?.message}\n`));
	});

	// -----------------------------------------------------------------------
	// Part 2: search_codebase tool
	// -----------------------------------------------------------------------

	pi.registerTool({
		name: "search_codebase",
		label: "Search Codebase",
		description:
			"Search the indexed codebase using natural language. Returns relevant code chunks with file paths, function names, and similarity scores. Use this before implementing features, debugging, or reviewing code to understand the codebase.",
		parameters: Type.Object({
			query: Type.String({
				description:
					"Natural language description of what you're looking for (e.g., 'authentication middleware', 'database connection setup', 'error handling patterns')",
			}),
			limit: Type.Optional(
				Type.Number({
					description: "Max results (default 10)",
					minimum: 1,
					maximum: 50,
				})
			),
			type: Type.Optional(
				Type.String({
					description:
						"Filter by chunk type: 'function', 'class', 'method', 'config', 'all' (default: 'all')",
				})
			),
			project: Type.Optional(
				Type.String({
					description: "Filter to a specific project path",
				})
			),
		}),

		async execute(toolCallId, params, signal, _onUpdate?, ctx?) {
			const { query, limit = 10, type, project } = params;

			// Overall operation timeout — 45s max for entire search (2026: AbortController for lifecycle)
			const OVERALL_TIMEOUT_MS = 45_000;
			const opController = new AbortController();
			const opTimer = setTimeout(() => opController.abort(), OVERALL_TIMEOUT_MS);
			const effectiveSignal = signal
				? AbortSignal.any([signal, opController.signal])
				: opController.signal;

			try {

			const fallback = () => {
				const fallbackResult = runLexicalFallback(query, limit, project);
				return {
					content: [{ type: "text" as const, text: fallbackResult.text }],
					details: { mode: "lexical-fallback", resultCount: fallbackResult.count },
				};
			};

			// -- Step 1: Generate embedding via MAGE -----------------------
			let embedding: number[] | null = null;
			try {
				embedding = await fetchEmbedding(query);
				if (!isValidEmbedding(embedding)) return fallback();
			} catch (err: any) {
				if (err?.name === "AbortError") throw err;
				return fallback();
			}

			// -- Step 2: Safe vector search (population-aware, cascade-proof) -----
			// safeVectorSearch checks index population before querying — prevents the
			// connection-hold cascade that generated 17K+ errors on empty indexes.
			const { safeVectorSearch } = await import('../lib/vector-search-safe.ts');
			const rawVsResults = await safeVectorSearch('code_chunk_emb', limit * 2, embedding);

			// Apply type/project filters in JS (safeVectorSearch returns {node,similarity}[])
			const filteredVsResults = (rawVsResults || []).filter((r: any) => {
				const props = r.node?.properties || {};
				if (type && type !== 'all' && props.type !== type) return false;
				if (project && !String(props.filePath || '').startsWith(project)) return false;
				return true;
			}).slice(0, limit);

			// -- Step 3: Map {node,similarity} → flat record shape expected downstream ---
			let records: any[] = filteredVsResults.map((r: any) => {
				const props = r.node?.properties || {};
				return {
					file: String(props.filePath ?? ''),
					name: String(props.name ?? ''),
					type: String(props.type ?? ''),
					line: props.startLine,
					code: String(props.content ?? ''),
					similarity: r.similarity,
				};
			}).sort((a: any, b: any) => (b.similarity || 0) - (a.similarity || 0));

			// Secondary: search CodeSymbol by name (BM25 text search)
			if (records.length < 5) {
				try {
					const mg = require('../lib/safe-memgraph.js');
					const repoPath = project || '';
					const symbolResults = await mg.safeRead(
						`CALL text_search.search_all('code_symbol_text', $query, {limit: 5})
						 YIELD node, score
						 WHERE node.filePath STARTS WITH $repoPath
						 RETURN node.name AS name, node.kind AS kind, node.filePath AS filePath,
						        node.startLine AS lineStart, score
						 ORDER BY score DESC`,
						{ query, repoPath }
					);
					if (symbolResults && symbolResults.length > 0) {
						for (const sr of symbolResults) {
							// Avoid duplicates
							const already = records.some((r: any) => r.file === sr.filePath && r.name === sr.name);
							if (!already) {
								records.push({
									file: sr.filePath,
									name: sr.name,
									type: sr.kind || 'symbol',
									line: String(sr.lineStart ?? ''),
									code: '',
									similarity: String(sr.score || 0),
									source: 'text_search',
								});
							}
						}
					}
				} catch { /* fail-open: text index may not exist yet */ }
			}

			if (!records.length) {
				// Detect bolt failure (driver nulled by runCypher on error) vs genuinely no results
				if (!isGraphAvailable()) {
					process.stderr.write(`[codebase-index] Bolt connection lost or circuit open, degrading to lexical\n`);
					return fallback();
				}
				return {
					content: [
						{
							type: "text" as const,
							text: "□ Unindexed or no results — the codebase may not be indexed yet. Run `/reindex` to index it first, or results may be below the similarity threshold.",
						},
					],
					details: { mode: "unindexed", resultCount: 0 },
				};
			}

			// -- Step 4: Parse Bolt records into rows ------------------------
			const rows = records.map((rec: Record<string, any>) => ({
					file: String(rec.file ?? ""),
					name: String(rec.name ?? ""),
					type: String(rec.type ?? ""),
					line: String(rec.line ?? ""),
					code: String(rec.code ?? ""),
					similarity: String(rec.similarity ?? "0"),
				})).filter((r) => r.file || r.name); // drop empty/malformed rows

			if (!rows.length) {
				return {
					content: [
						{
							type: "text" as const,
							text: "□ Unindexed or no results — the codebase may not be indexed yet. Run `/reindex` to index it first, or results may be below the similarity threshold.",
						},
					],
					details: { mode: "unindexed", resultCount: 0 },
				};
			}

			// -- Step 5: Format output ---------------------------------------
			let output = `🔍 Semantic search — ${rows.length} matching code chunk${rows.length === 1 ? "" : "s"}:\n\n`;

			for (const row of rows) {
				const simNum = parseFloat(row.similarity);
				const sim = isNaN(simNum) ? "?" : simNum.toFixed(3);
				output += `### ${row.name || "(unnamed)"} (${row.type || "unknown"}) — similarity: ${sim}\n`;
				output += `📁 ${row.file}:${row.line}\n`;
				const snippet = (row.code || "").slice(0, 500);
				output += "```\n" + snippet + (row.code.length > 500 ? "\n…(truncated)" : "") + "\n```\n\n";
			}

			return {
				content: [{ type: "text" as const, text: output }],
				details: { mode: "semantic", resultCount: rows.length },
			};
			} finally {
				clearTimeout(opTimer);
			}
		},
	});

	// -----------------------------------------------------------------------
	// Part 2b: query_code_matrix tool
	// -----------------------------------------------------------------------

	pi.registerTool({
		name: "query_code_matrix",
		label: "Query Code Matrix",
		description:
			"Query the structured coding matrix for a project from the knowledge graph. Returns dependency graph, code inventory, API endpoints, integration points, gap analysis, and task priority rankings. Use as PRIMARY tool for understanding codebase structure before grep/find/read.",
		parameters: Type.Object({
			project: Type.Optional(
				Type.String({
					description: "Project name or repo path. If omitted, uses current working directory.",
				})
			),
			matrix: Type.Optional(
				Type.String({
					description:
						"Which matrix: 'dependency' | 'codeInventory' | 'api' | 'integration' | 'gapAnalysis' | 'taskPriority' | 'all' (default: 'all')",
				})
			),
			limit: Type.Optional(
				Type.Number({
					description: "Max results per matrix section (default: 50)",
					minimum: 1,
					maximum: 200,
				})
			),
		}),

		async execute(toolCallId, params, signal, _onUpdate?, ctx?) {
			const { project, matrix = "all", limit = 50 } = params;
			// Resolve to git root so we match the correct Project node, not a parent dir
			const rawCwd = project || ctx?.cwd || _sessionCwd;
			const repoPath = findGitRoot(rawCwd) || rawCwd;
			// Detect current branch for branch-specific matrix filtering
			let currentBranch: string | null = null;
			try {
				const branchOut = execSync("git rev-parse --abbrev-ref HEAD", {
					cwd: repoPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
				}).trim();
				if (branchOut && branchOut !== "HEAD") currentBranch = branchOut;
			} catch (e) { process.stderr.write(`[extensions] not a git repo or git unavailable — branch stays null: ${String(e)}\n`); }

			const matrixGenPath = resolveGraphScript("matrix-generator.js");
			if (!fs.existsSync(matrixGenPath)) {
				return {
					content: [{ type: "text" as const, text: "Matrix generator not found at " + matrixGenPath }],
					details: { mode: "error" },
				};
			}

			let result: any;
			try {
				// Use local variables for Memgraph credentials — avoid polluting process.env
				// generateCodingMatrix reads from process.env, so set before call and restore after
				const prevUser = process.env.MEMGRAPH_USER;
				const prevPass = process.env.MEMGRAPH_PASS;
				process.env.MEMGRAPH_USER = process.env.MEMGRAPH_USER || "memgraph";
				process.env.MEMGRAPH_PASS = process.env.MEMGRAPH_PASS || "memgraph";
			const matrixMod = await import(matrixGenPath);
			const { generateCodingMatrix } = matrixMod;
				try {
					// Timeout guard: adaptive based on scope (single matrix = fast, all = slower)
					const MATRIX_TIMEOUT_MS = matrix === 'all' ? 30_000 : 15_000;
					result = await Promise.race([
						generateCodingMatrix(repoPath, currentBranch, matrix),
						new Promise((_, reject) => setTimeout(() => reject(new Error(
							"Matrix generation timed out after " + (MATRIX_TIMEOUT_MS / 1000) + "s. " +
							"Try a specific matrix (e.g. matrix: 'integration') instead of 'all'."
						)), MATRIX_TIMEOUT_MS)),
					]);
					// Null guard: generator may return null/undefined on empty projects
					if (!result || typeof result !== 'object') {
						throw new Error('generateCodingMatrix returned empty result for ' + repoPath);
					}
					// Size guard: cap items to prevent 1.4MB payloads crashing sessions
					const MAX_ITEMS = Math.min(limit, 30);
					if (result.codeInventory?.length > MAX_ITEMS) {
						const total = result.codeInventory.length;
						result.codeInventory = result.codeInventory.slice(0, MAX_ITEMS);
						result._codeInventoryTruncated = "Showing " + MAX_ITEMS + " of " + total + " files";
					}
					if (result.dependency?.internal?.length > MAX_ITEMS) {
						const total = result.dependency.internal.length;
						result.dependency.internal = result.dependency.internal.slice(0, MAX_ITEMS);
						result._depInternalTruncated = "Showing " + MAX_ITEMS + " of " + total + " imports";
					}
					if (result.dependency?.libraries?.length > MAX_ITEMS) {
						const total = result.dependency.libraries.length;
						result.dependency.libraries = result.dependency.libraries.slice(0, MAX_ITEMS);
						result._depLibrariesTruncated = "Showing " + MAX_ITEMS + " of " + total + " libraries";
					}
					if (result.taskPriority?.length > MAX_ITEMS) {
						const total = result.taskPriority.length;
						result.taskPriority = result.taskPriority.slice(0, MAX_ITEMS);
						result._taskPriorityTruncated = "Showing " + MAX_ITEMS + " of " + total + " files";
					}
				} finally {
					if (prevUser === undefined) delete process.env.MEMGRAPH_USER;
					else process.env.MEMGRAPH_USER = prevUser;
					if (prevPass === undefined) delete process.env.MEMGRAPH_PASS;
					else process.env.MEMGRAPH_PASS = prevPass;
				}
			} catch (err: any) {
				return {
					content: [
						{
							type: "text" as const,
							text: `Matrix generation failed: ${err.message}\n\nIs Memgraph running on bolt://localhost:7687?`,
						},
					],
					details: { mode: "error" },
				};
			}

			const validMatrices = ["dependency", "codeInventory", "api", "integration", "gapAnalysis", "taskPriority"];
			const matrices =
				matrix === "all" ? result : validMatrices.includes(matrix) ? { [matrix]: result[matrix] } : null;

			if (!matrices) {
				return {
					content: [
						{
							type: "text" as const,
							text: `Unknown matrix: "${matrix}". Valid: ${validMatrices.join(", ")}, all`,
						},
					],
					details: { mode: "error" },
				};
			}

			const projectName = repoPath.split("/").pop() || repoPath;
			let output = `## Coding Matrix — ${projectName}\n\n`;

			if (matrices.dependency) {
				const dep = matrices.dependency;
				output += `### 1. Dependency Matrix\n`;
				const depIntNote = result._depInternalTruncated ? ` *(${result._depInternalTruncated})*` : "";
				const depLibNote = result._depLibrariesTruncated ? ` *(${result._depLibrariesTruncated})*` : "";
				output += `**Internal imports:** ${dep.internal?.length || 0}${depIntNote} | **Libraries:** ${dep.libraries?.length || 0}${depLibNote}

`;
				if (dep.internal?.length) {
					const shown = dep.internal.slice(0, limit);
					output += `| Source | Target | Type |\n|--------|--------|------|\n`;
					for (const r of shown) {
						output += `| ${r.source || r.from || ""} | ${r.target || r.to || ""} | ${r.type || "import"} |\n`;
					}
					if (dep.internal.length > limit) output += `\n*...and ${dep.internal.length - limit} more*\n`;
				}
				output += "\n";
			}

			if (matrices.codeInventory) {
				const inv = matrices.codeInventory;
				const truncNote = result._codeInventoryTruncated ? ` *(${result._codeInventoryTruncated})*` : "";
				output += `### 2. Code Inventory (${inv.length} files)${truncNote}

`;
				const shown = inv.slice(0, limit);
				output += `| File | Language | Symbols | Tests |\n|------|----------|---------|-------|\n`;
				for (const r of shown) {
					const symCount = Array.isArray(r.symbols) ? r.symbols.length : 0;
					output += `| ${r.relPath || ""} | ${r.language || "?"} | ${symCount} | ${r.hasTests ? "✅" : "❌"} |\n`;
				}
				output += "\n";
			}

			if (matrices.api) {
				const api = matrices.api;
				output += `### 3. API Endpoints (${api.length})\n\n`;
				if (api.length) {
					output += `| Method | Path | Handler | Framework | Auth |\n|--------|------|---------|-----------|------|\n`;
					for (const r of api.slice(0, limit)) {
						output += `| ${r.method || "?"} | ${r.path || "?"} | ${r.handler || "?"} | ${r.framework || "?"} | ${r.auth ? "🔒" : "🔓"} |\n`;
					}
				} else {
					output += `No API endpoints detected (run deep-index.sh on a project with Express/Next routes)\n`;
				}
				output += "\n";
			}

			if (matrices.integration) {
				const intg = matrices.integration;
				const all = intg.all || [];
				const byType = intg.byType || {};
				output += `### 4. Integration Points (${all.length} external calls)\n`;
				if (Object.keys(byType).length) {
					output += `Types: ${Object.entries(byType)
						.map(([t, arr]: [string, any]) => `${t}(${Array.isArray(arr) ? arr.length : 0})`)
						.join(", ")}\n\n`;
				}
				if (all.length) {
					const shown = all.slice(0, limit);
					output += `| File | Type | Target | Method |\n|------|------|--------|--------|\n`;
					for (const r of shown) {
						output += `| ${r.sourceFile || r.file || ""} | ${r.type || "?"} | ${r.target || r.url || "?"} | ${r.method || ""} |\n`;
					}
				}
				output += "\n";
			}

			if (matrices.gapAnalysis) {
				const gap = matrices.gapAnalysis;
				output += `### 5. Gap Analysis\n`;
				output += `- **Hotspots:** ${gap.hotspots?.length || 0}\n`;
				output += `- **Temporal coupling:** ${gap.temporalCoupling?.length || 0}\n`;
				output += `- **High complexity:** ${gap.highComplexityFunctions?.length || 0}\n`;
				output += `- **Env vars:** ${gap.envVars?.length || 0}\n\n`;
				if (gap.hotspots?.length) {
					output += `**Top hotspots:**\n`;
					for (const h of gap.hotspots.slice(0, Math.min(limit, 10))) {
						output += `- ${h.relPath || h.file || "?"} (score: ${h.score || h.hotspotScore || "?"})\n`;
					}
					output += "\n";
				}
				if (gap.envVars?.length) {
					output += `**Env vars:** ${gap.envVars
						.slice(0, 20)
						.map((e: any) => e.name || e)
						.join(", ")}\n\n`;
				}
			}

			if (matrices.taskPriority) {
				const tp = matrices.taskPriority;
				output += `### 6. Task Priority (${tp.length} files ranked)\n\n`;
				const topRisk = tp.filter((r: any) => (r.riskScore || 0) > 0).slice(0, Math.min(limit, 15));
				if (topRisk.length) {
					output += `| File | Risk | Hotspot | Churn | Coupling |\n|------|------|---------|-------|----------|\n`;
					for (const r of topRisk) {
						output += `| ${r.relPath || ""} | ${r.riskScore || 0} | ${r.hotspotScore || 0} | ${r.churn90d || 0} | ${r.couplingCount || 0} |\n`;
					}
				} else {
					output += `All files at risk 0 — run git-analyze.js to populate churn/coupling data\n`;
				}
				output += "\n";
			}

			return {
				content: [{ type: "text" as const, text: output }],
				details: { mode: "matrix", matricesQueried: Object.keys(matrices) },
			};
		},
	});

	// -----------------------------------------------------------------------
	// Part 2c: query_code_symbols tool
	// -----------------------------------------------------------------------
	// Queries :CodeSymbol nodes written by extensions/lib/tree-sitter-extractor.ts.
	// Schema: name, kind, filePath, startLine, endLine, isExported, signature,
	//         access, isAsync, cyclomaticComplexity. Edges: CALLS.

	pi.registerTool({
		name: "query_code_symbols",
		description: "Search code symbols (functions, classes, types) by name, kind, or characteristics. Returns AST-level data with call graphs.",
		parameters: {
			type: "object",
			properties: {
				query: { type: "string", description: "Symbol name or partial name to search for" },
				kind: { type: "string", enum: ["function", "method", "class", "interface", "type", "const", "enum"], description: "Filter by symbol kind" },
				visibility: { type: "string", enum: ["public", "private", "protected"], description: "Filter by access level (stored as n.access)" },
				project: { type: "string", description: "Absolute path prefix to scope results to a project" },
				limit: { type: "number", description: "Max results (default: 10)" },
			},
			required: ["query"],
		},
		async execute(_toolCallId: string, params: any, _signal: any, _onUpdate: any, _ctx: any) {
			try {
				const mg = require('../lib/safe-memgraph.js');
				const query: string = params.query || '';
				// Item 9 guard: reject empty / trivially short queries to prevent full-table dumps
				if (!query || query.trim().length < 2) {
					return { content: [{ type: 'text', text: 'query must be at least 2 characters.' }] };
				}
				const limit: number = Math.min(params.limit || 10, 50);

				// Build WHERE clause. All values bound as parameters — no string injection.
				// kind "type_alias" maps to "type" (tree-sitter output uses "type").
				const whereParts: string[] = [];
				if (query) whereParts.push('toLower(n.name) CONTAINS toLower($query)');
				const kindVal = params.kind === 'type_alias' ? 'type' : (params.kind || '');
				if (kindVal) whereParts.push('n.kind = $kind');
				if (params.visibility) whereParts.push('n.access = $visibility');
				if (params.project) whereParts.push('n.filePath STARTS WITH $project');

				const whereClause = whereParts.length > 0 ? ' WHERE ' + whereParts.join(' AND ') : '';

				// Memgraph 3.x requires WITH...LIMIT before OPTIONAL MATCH.
				// Pattern: MATCH...WHERE  WITH n ORDER BY ... LIMIT  OPTIONAL MATCH...
				const cypher =
					'MATCH (n:CodeSymbol)' + whereClause + ' ' +
					'WITH n ORDER BY n.name LIMIT toInteger($limit) ' +
					'OPTIONAL MATCH (n)-[:CALLS]->(callee:CodeSymbol) ' +
					'OPTIONAL MATCH (caller2:CodeSymbol)-[:CALLS]->(n) ' +
					'RETURN n.name AS name, n.kind AS kind, n.filePath AS file, ' +
					'n.startLine AS line, n.endLine AS lineEnd, ' +
					'n.signature AS signature, n.isExported AS isExported, ' +
					'n.access AS visibility, n.isAsync AS isAsync, ' +
					'n.cyclomaticComplexity AS complexity, ' +
					'collect(DISTINCT callee.name)[..5] AS callees, ' +
					'collect(DISTINCT caller2.name)[..5] AS callers';

				const queryParams: Record<string, any> = { limit };
				if (query) queryParams.query = query;
				if (kindVal) queryParams.kind = kindVal;
				if (params.visibility) queryParams.visibility = params.visibility;
				if (params.project) queryParams.project = params.project;

				const rows = await mg.safeRead(cypher, queryParams);
				if (!rows || rows.length === 0) {
					return { content: [{ type: 'text', text: 'No symbols found. The codebase may not be indexed — run /reindex to index it.' }] };
				}
				const text = rows.map((r: any) => {
					const shortFile = r.file ? r.file.split('/').slice(-2).join('/') : '';
					const loc = shortFile ? ` (${shortFile}:${r.line || '?'})` : '';
					const flags = [r.isAsync ? 'async' : '', r.isExported ? 'exported' : ''].filter(Boolean).join(', ');
					return `${r.kind || '?'} ${r.name}${loc}${flags ? ' [' + flags + ']' : ''}`;
				}).join('\n');
				return {
					content: [{ type: 'text', text: `Found ${rows.length} symbol${rows.length === 1 ? '' : 's'}:\n\n${text}` }],
					details: { symbols: rows, count: rows.length },
				};
			} catch (err) {
				return { content: [{ type: 'text', text: `Symbol query failed: ${String(err)}` }] };
			}
		},
	});

	// Part 3: /reindex command
	// -----------------------------------------------------------------------

	pi.registerCommand("reindex", {
		description: "Re-index the current codebase (force full re-index)",
		handler: async (_args, ctx) => {
			const cwd = ctx.cwd ?? process.cwd();

			const gitRoot = findGitRoot(cwd);
			if (!gitRoot) {
				ctx.ui.notify("Not a git repo — nothing to index", "warning");
				return;
			}

			// User explicitly ran /reindex — gateway writes 'yes' consent
			ctx.ui.notify("Re-indexing codebase (full)...", "info");
			requestIndex(gitRoot, { trigger: 'reindex-cmd', mode: 'full' });
		},
	});

	// -----------------------------------------------------------------------
	// Part 4: session_shutdown — build temporal session chains
	// -----------------------------------------------------------------------

	pi.on("session_shutdown", async () => {
		// Build temporal session chains (FOLLOWED_BY edges)
		const chainScript = resolveGraphScript("add-temporal-chains.js");
		if (fs.existsSync(chainScript)) {
			try {
				const child = spawn("node", [chainScript, "--quiet"], {
					detached: true,
     windowsHide: true,  // Prevent Windows console popup
					stdio: "ignore",
					env: { ...process.env, MEMGRAPH_USER: process.env.MEMGRAPH_USER || "memgraph", MEMGRAPH_PASS: process.env.MEMGRAPH_PASS || "memgraph" },
				});
				child.unref();
			} catch (e) { process.stderr.write(`[extensions] fail-open: non-critical: ${String(e)}\n`); }
		}
	});

	// -----------------------------------------------------------------------
	// Part 5: session_end — stop file watcher
	// -----------------------------------------------------------------------

	pi.on("session_end", async () => {
		if (fileWatcher) { fileWatcher.stop(); fileWatcher = null; }
	});

	// -----------------------------------------------------------------------
	// Part 6b: user consent replies — handle yes/no to indexing prompt
	// -----------------------------------------------------------------------
	// When the consent prompt fires (consent === 'ask'), we write 'no' optimistically
	// and surface a followUp. If the user replies 'yes' / 'no' as their first message,
	// capture it here and act accordingly.

	pi.on("turn_start", async (event: any) => {
		const text = (event?.message ?? event?.userMessage ?? '').trim().toLowerCase();
		if (!text) return;

		const gitRoot = findGitRoot(_sessionCwd);
		if (!gitRoot) return;

		// Only intercept bare yes/no replies — don't hijack normal messages
		const isYes = text === 'yes' || text === 'yes.' || text === 'y' || text === 'yes, index' || text === 'index';
		const isNo  = text === 'no'  || text === 'no.'  || text === 'n' || text === 'no, skip'  || text === 'skip';

		if (!isYes && !isNo) return;

		// Only intercept if we previously asked (consent file exists and is 'no' with very recent ts)
		const consentPath = getConsentPath(gitRoot);
		try {
			const data = JSON.parse(fs.readFileSync(consentPath, 'utf-8'));
			const age = Date.now() - (data.ts ?? 0);
			// Only act on responses within 10 minutes of the consent prompt
			if (data.consent !== 'no' || age > 10 * 60 * 1000) return;
		} catch {
			return;
		}

		if (isYes) {
			writeConsent(gitRoot, 'yes');
			_sessionCtx?.ui?.notify('Codebase indexing started (incremental).', 'info');
			requestIndex(gitRoot, { trigger: 'reindex-cmd', mode: 'incremental' });
		} else {
			writeConsent(gitRoot, 'no');
			_sessionCtx?.ui?.notify('Indexing skipped. Run /reindex anytime to index this codebase.', 'info');
		}
	});

	// -----------------------------------------------------------------------
	// Part 6: tool_result — re-index on write/edit
	// -----------------------------------------------------------------------

	pi.on("tool_result", async (event: any) => {
		if (event.toolName === "write" || event.toolName === "edit") {
			const filePath = event.input?.path;
			if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
				try {
					const result = await incrementalIndex([filePath]);
					if (result.filesProcessed > 0) {
						// Status update is best-effort — ctx may not be available in tool_result
					}
				} catch (e) { process.stderr.write(`[extensions] non-critical: ${String(e)}\n`); }
			}
		}
	});
}
