import 'node:module';
import { existsSync } from 'node:fs';
import { resolve, dirname, isAbsolute, relative } from 'node:path';
import { execFile } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import 'node:os';
import { o as resolveOrCwd, b as getLogger } from './paths-2M_UxD_H.js';
import { h as withWriterLease } from './dual-scope-db-ATrwq0LH.js';
import { h as hooks } from './registry2-BSyCTA3q.js';
import { g as getTaskAccessor } from './data-accessor-CVbz8X_7.js';
import { k as getConduitDbPath, m as openFreshConduitDb } from './sqlite-Dsbe0wec.js';
import './index-IVoIz4D5.js';
import { mkdir, appendFile } from 'node:fs/promises';
import './index-Ca4C2M45.js';
import { s as sessionListItemNext } from './mvi-helpers-BNsJ8ZpQ.js';
import './session-store-OwQWMSf_.js';
import { promisify } from 'node:util';
import './session-manifest-ZHrj54LR.js';
import './memory-schema-Bi7cChwo.js';

//#region ../core/dist/hooks/handlers/handler-helpers.js
/**
* Shared helpers for hook handler modules.
*
* Centralises the two patterns repeated across all brain-capture handlers:
*
* 1. `isMissingBrainSchemaError` — detects the "no such table: brain_*" error
*    thrown when brain.db has not been migrated yet, so handlers can swallow
*    it silently instead of crashing agent workflows.
*
* 2. `isAutoCaptureEnabled` — reads `brain.autoCapture` from the cascaded
*    project config and returns false when the config is unreadable.
*
* Import these instead of redefining them in each handler file.
*
* @task T166
* @epic T134
*/
/**
* Return true when the error is the "no such table: brain_*" SQLite error
* thrown before brain.db has been migrated.
*
* Hook handlers use this to swallow migration-lag errors without hiding
* genuine failures: if the error is NOT a missing-schema error it is
* re-thrown so it propagates normally.
*
* @param err - The caught error value (may be any type).
*/
function isMissingBrainSchemaError(err) {
	if (!(err instanceof Error)) return false;
	const message = String(err.message || "").toLowerCase();
	return message.includes("no such table") && message.includes("brain_");
}
/**
* Return true when the error is the "No CLEO project found" resolution error
* thrown by `resolveCleoDir` (CleoError `NEXUS_PROJECT_NOT_FOUND`) when no CLEO
* project is resolvable from the current context.
*
* Brain-capture hooks are best-effort: an observation with no project to write
* into is a no-op, not a crash. Handlers swallow this exactly like the
* missing-schema case, so a context with no resolvable project never fails the
* agent flow.
*
* This also closes a cross-file test-isolation leak (T11281): the
* `brain-tool-complete` PostToolUse capture could resolve with no anchor inside a
* shared vitest fork — most acutely in CI, where the gitignored
* `.cleo/project-info.json` is absent — and the uncaught throw collateral-failed
* sibling test files (saga-audit / invariant-audit). Swallowing it at the source
* fixes the leak without a global env / resolution-precedence change.
*
* @param err - The caught error value (may be any type).
*/
function isNoProjectError(err) {
	if (!(err instanceof Error)) return false;
	return String(err.message || "").includes("No CLEO project found");
}
/**
* Return true when `brain.autoCapture` is enabled for the given project.
*
* Resolution order (first truthy wins):
*   1. `brain.autoCapture` project config value (via `loadConfig` cascade)
*
* Defaults to `false` when the config is unreadable or the key is absent,
* so handlers are safely disabled in environments where brain.db is not set up.
*
* @param projectRoot - Absolute path to the project root directory.
*/
async function isAutoCaptureEnabled(projectRoot) {
	try {
		const { loadConfig } = await import('./config2-CdsX487B.js');
		return (await loadConfig(projectRoot)).brain?.autoCapture ?? false;
	} catch {
		return false;
	}
}

promisify(execFile);
/**
* Get handoff data for the most recent ended session.
* Filters by scope if provided.
*/
async function getLastHandoff(projectRoot, scope) {
	let endedSessions = (await (await getTaskAccessor(projectRoot)).loadSessions()).filter((s) => s.status === "ended" && s.endedAt);
	endedSessions.sort((a, b) => new Date(b.endedAt).getTime() - new Date(a.endedAt).getTime());
	for (const session of endedSessions) {
		if (typeof session.handoffJson === "string") try {
			const handoff = JSON.parse(session.handoffJson);
			return {
				sessionId: session.id,
				handoff
			};
		} catch {}
		if (typeof session.debriefJson === "string") try {
			const debrief = JSON.parse(session.debriefJson);
			if (debrief?.handoff) return {
				sessionId: session.id,
				handoff: debrief.handoff
			};
		} catch {}
	}
	return null;
}

//#region ../core/dist/conduit/local-transport.js
/**
* LocalTransport — In-process SQLite transport for fully offline agent messaging.
*
* Reads and writes messages directly to conduit.db via node:sqlite.
* No network calls. Works fully offline. Messages are stored in the
* project-local conduit.db (ADR-037), keeping agent messaging isolated
* from the global-identity signaldock.db.
*
* Priority: LocalTransport is preferred over HttpTransport when
* conduit.db is available (see factory.ts).
*
* @see docs/specs/SIGNALDOCK-UNIFIED-AGENT-REGISTRY.md Section 4.4
* @task T213
* @task T356
* @epic T310
*/
/**
* Parse an A2A topic name into its epic_id and optional wave_id components.
*
* Supported formats:
* - `"epic-T1149.wave-2"` → `{ epicId: "T1149", waveId: 2 }`
* - `"epic-T1149.coordination"` → `{ epicId: "T1149", waveId: undefined }`
* - `"T1149.wave-2"` → `{ epicId: "T1149", waveId: 2 }` (short form)
* - `"some-topic"` → `{ epicId: "some-topic", waveId: undefined }` (fallback)
*
* @param topicName - Raw topic name string.
* @returns Parsed epic ID and optional integer wave ID.
* @task T1252
*/
function parseTopicName(topicName) {
	const epicWaveMatch = topicName.match(/(?:epic-)?([A-Z]\d+)\.wave-(\d+)/);
	if (epicWaveMatch) return {
		epicId: epicWaveMatch[1] ?? topicName,
		waveId: parseInt(epicWaveMatch[2] ?? "0", 10)
	};
	const epicMatch = topicName.match(/(?:epic-)?([A-Z]\d+)\./);
	if (epicMatch) return {
		epicId: epicMatch[1] ?? topicName,
		waveId: void 0
	};
	return {
		epicId: topicName,
		waveId: void 0
	};
}
/** In-process SQLite transport for fully offline agent messaging. */
var LocalTransport = class {
	name = "local";
	state = null;
	/**
	* Connect to conduit.db for in-process messaging.
	*
	* Opens the database, sets WAL mode pragmas, and verifies
	* the messages table exists. Throws if conduit.db is missing
	* or uninitialized (run `cleo init` first).
	*
	* @task T356
	* @epic T310
	*/
	async connect(config) {
		const dbPath = getConduitDbPath(process.cwd());
		if (!existsSync(dbPath)) throw new Error(`LocalTransport: conduit.db not found at ${dbPath}. Run: cleo init`);
		const db = openFreshConduitDb(process.cwd());
		if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='conduit_messages'").get()) {
			db.close();
			throw new Error("LocalTransport: conduit.db exists but conduit_messages table missing — run cleo init or allow auto-migration (T358)");
		}
		this.state = {
			agentId: config.agentId,
			db,
			dbPath,
			subscribers: /* @__PURE__ */ new Set(),
			topicHandlers: /* @__PURE__ */ new Map(),
			pollTimer: null,
			topicPollTimer: null,
			topicLastSeen: /* @__PURE__ */ new Map()
		};
	}
	/** Close the database connection and stop any subscriber polling. */
	async disconnect() {
		if (!this.state) return;
		if (this.state.pollTimer) clearInterval(this.state.pollTimer);
		if (this.state.topicPollTimer) clearInterval(this.state.topicPollTimer);
		this.state.subscribers.clear();
		this.state.topicHandlers.clear();
		this.state.topicLastSeen.clear();
		this.state.db.close();
		this.state = null;
	}
	/**
	* Store a message in conduit.db.
	*
	* Inserts into the messages table with status 'pending'.
	* For conversation messages, also links via conversation_participants
	* if not already present.
	*/
	async push(to, content, options) {
		this.ensureConnected();
		const { db, agentId } = this.state;
		const messageId = randomUUID();
		const nowIso = (/* @__PURE__ */ new Date()).toISOString();
		await this.runWrite(() => {
			if (options?.conversationId) db.prepare(`INSERT INTO conduit_messages (id, conversation_id, from_agent_id, to_agent_id, content, content_type, status, created_at)
         VALUES (?, ?, ?, ?, ?, 'text', 'pending', ?)`).run(messageId, options.conversationId, agentId, to, content, nowIso);
			else {
				const convId = this.ensureDmConversation(agentId, to);
				db.prepare(`INSERT INTO conduit_messages (id, conversation_id, from_agent_id, to_agent_id, content, content_type, status, created_at)
         VALUES (?, ?, ?, ?, ?, 'text', 'pending', ?)`).run(messageId, convId, agentId, to, content, nowIso);
			}
		});
		this.notifySubscribers({
			id: messageId,
			from: agentId,
			content,
			threadId: options?.conversationId,
			timestamp: nowIso
		});
		return { messageId };
	}
	/**
	* Poll for messages addressed to this agent.
	*
	* Returns messages with status 'pending' where to_agent_id matches
	* the connected agent. Messages are returned oldest-first.
	*/
	async poll(options) {
		this.ensureConnected();
		const { db, agentId } = this.state;
		const limit = options?.limit ?? 50;
		let query;
		let params;
		if (options?.since) {
			query = `SELECT id, from_agent_id, content, conversation_id, created_at
               FROM conduit_messages
               WHERE to_agent_id = ? AND status = 'pending' AND created_at > ?
               ORDER BY created_at ASC
               LIMIT ?`;
			params = [
				agentId,
				options.since,
				limit
			];
		} else {
			query = `SELECT id, from_agent_id, content, conversation_id, created_at
               FROM conduit_messages
               WHERE to_agent_id = ? AND status = 'pending'
               ORDER BY created_at ASC
               LIMIT ?`;
			params = [agentId, limit];
		}
		return db.prepare(query).all(...params).map((r) => ({
			id: r.id,
			from: r.from_agent_id,
			content: r.content,
			threadId: r.conversation_id ?? void 0,
			timestamp: r.created_at
		}));
	}
	/**
	* Acknowledge messages by marking them as 'delivered'.
	*
	* Updates the status and delivered_at timestamp for each message ID.
	*/
	async ack(messageIds) {
		this.ensureConnected();
		if (messageIds.length === 0) return;
		const { db } = this.state;
		const nowIso = (/* @__PURE__ */ new Date()).toISOString();
		const placeholders = messageIds.map(() => "?").join(", ");
		await this.runWrite(() => {
			db.prepare(`UPDATE conduit_messages SET status = 'delivered', delivered_at = ? WHERE id IN (${placeholders})`).run(nowIso, ...messageIds);
		});
	}
	/**
	* Subscribe to real-time local messages.
	*
	* Since this is in-process, subscribers are notified synchronously
	* when push() is called. Additionally, a polling interval checks
	* for messages inserted by other processes (e.g., Rust CLI).
	*
	* @returns Unsubscribe function.
	*/
	subscribe(handler) {
		this.ensureConnected();
		this.state.subscribers.add(handler);
		if (!this.state.pollTimer && this.state.subscribers.size === 1) this.state.pollTimer = setInterval(() => {
			this.pollAndNotify();
		}, 1e3);
		return () => {
			this.state?.subscribers.delete(handler);
			if (this.state?.subscribers.size === 0 && this.state.pollTimer) {
				clearInterval(this.state.pollTimer);
				this.state.pollTimer = null;
			}
		};
	}
	/**
	* Subscribe this agent to a named topic in conduit.db.
	*
	* Creates the topic row if it does not exist (idempotent via ON CONFLICT DO NOTHING).
	* Inserts a `topic_subscriptions` row linking this agent to the topic.
	*
	* Topic name convention: `<epicId>.<waveId>` or `<epicId>.coordination`.
	*
	* @param topicName - Topic name, e.g. `"epic-T1149.wave-2"`.
	* @param _options  - Reserved for future filter support (not stored in DB yet).
	* @task T1252
	*/
	async subscribeTopic(topicName, _options) {
		this.ensureConnected();
		const { db, agentId } = this.state;
		const nowIso = (/* @__PURE__ */ new Date()).toISOString();
		const { epicId, waveId } = parseTopicName(topicName);
		const topicId = randomUUID();
		await this.runWrite(() => {
			db.prepare(`INSERT INTO conduit_topics (id, name, epic_id, wave_id, created_by, created_at)
       VALUES (?, ?, ?, ?, ?, ?)
       ON CONFLICT(name) DO NOTHING`).run(topicId, topicName, epicId, waveId ?? null, agentId, nowIso);
			const topic = db.prepare("SELECT id FROM conduit_topics WHERE name = ?").get(topicName);
			if (!topic) return;
			db.prepare(`INSERT INTO conduit_topic_subscriptions (topic_id, agent_id, subscribed_at)
       VALUES (?, ?, ?)
       ON CONFLICT(topic_id, agent_id) DO NOTHING`).run(topic.id, agentId, nowIso);
		});
	}
	/**
	* Publish a message to a named topic in conduit.db.
	*
	* The message is broadcast to all current subscribers via the in-process
	* handler map (`topicHandlers`). Cross-process subscribers receive it via
	* the periodic poll timer started by `onTopic()`.
	*
	* The topic must already exist (via a prior `subscribeTopic()` call), or
	* this method creates it automatically with a synthetic creator.
	*
	* @param topicName - Target topic name.
	* @param content   - Human-readable message content.
	* @param options   - Message kind and optional structured payload.
	* @returns Object containing the assigned `messageId`.
	* @task T1252
	*/
	async publishToTopic(topicName, content, options) {
		this.ensureConnected();
		const { db, agentId } = this.state;
		const nowIso = (/* @__PURE__ */ new Date()).toISOString();
		const messageId = randomUUID();
		const kind = options?.kind ?? "message";
		const { epicId, waveId } = parseTopicName(topicName);
		const topicInsertId = randomUUID();
		await this.runWrite(() => {
			db.prepare(`INSERT INTO conduit_topics (id, name, epic_id, wave_id, created_by, created_at)
       VALUES (?, ?, ?, ?, ?, ?)
       ON CONFLICT(name) DO NOTHING`).run(topicInsertId, topicName, epicId, waveId ?? null, agentId, nowIso);
			const topic = db.prepare("SELECT id FROM conduit_topics WHERE name = ?").get(topicName);
			if (!topic) throw new Error(`LocalTransport.publishToTopic: could not resolve topic "${topicName}"`);
			db.prepare(`INSERT INTO conduit_topic_messages (id, topic_id, from_agent_id, kind, content, payload, created_at)
       VALUES (?, ?, ?, ?, ?, ?, ?)`).run(messageId, topic.id, agentId, kind, content, options?.payload != null ? JSON.stringify(options.payload) : null, nowIso);
		});
		this.notifyTopicHandlers(topicName, {
			id: messageId,
			from: agentId,
			fromPeerId: agentId,
			toPeerId: null,
			content,
			kind,
			threadId: topicName,
			payload: options?.payload,
			timestamp: nowIso
		});
		return { messageId };
	}
	/**
	* Register a real-time handler for messages on a named topic.
	*
	* In-process publishers notify the handler synchronously via `publishToTopic()`.
	* Cross-process publishers are picked up via a 1-second polling interval
	* that is started on the first `onTopic()` call and stopped when all
	* topic handlers are removed.
	*
	* @param topicName - Topic name to watch.
	* @param handler   - Callback invoked for each incoming message.
	* @returns Unsubscribe function that removes this handler.
	* @task T1252
	*/
	onTopic(topicName, handler) {
		this.ensureConnected();
		const state = this.state;
		if (!state.topicHandlers.has(topicName)) state.topicHandlers.set(topicName, /* @__PURE__ */ new Set());
		state.topicHandlers.get(topicName).add(handler);
		if (!state.topicPollTimer) state.topicPollTimer = setInterval(() => {
			this.pollAndNotifyTopics();
		}, 1e3);
		return () => {
			const handlers = state.topicHandlers.get(topicName);
			if (handlers) {
				handlers.delete(handler);
				if (handlers.size === 0) {
					state.topicHandlers.delete(topicName);
					state.topicLastSeen.delete(topicName);
				}
			}
			if (state.topicHandlers.size === 0 && state.topicPollTimer) {
				clearInterval(state.topicPollTimer);
				state.topicPollTimer = null;
			}
		};
	}
	/**
	* Unsubscribe this agent from a named topic.
	*
	* Removes the `topic_subscriptions` row; does not delete the topic or messages.
	* In-process `onTopic` handlers are NOT automatically removed — callers should
	* call the unsubscribe function returned by `onTopic()` before `unsubscribeTopic()`.
	*
	* @param topicName - Topic name to leave.
	* @task T1252
	*/
	async unsubscribeTopic(topicName) {
		this.ensureConnected();
		const { db, agentId } = this.state;
		const topic = db.prepare("SELECT id FROM conduit_topics WHERE name = ?").get(topicName);
		if (!topic) return;
		await this.runWrite(() => {
			db.prepare("DELETE FROM conduit_topic_subscriptions WHERE topic_id = ? AND agent_id = ?").run(topic.id, agentId);
		});
	}
	/**
	* Poll for new topic messages and deliver to registered handlers.
	*
	* Queries each topic with registered handlers for messages newer than
	* `topicLastSeen`. Updates `topicLastSeen` after each poll. Called
	* periodically by the `topicPollTimer`.
	*
	* @task T1252
	*/
	async pollTopic(topicName, options) {
		this.ensureConnected();
		const { db } = this.state;
		const limit = options?.limit ?? 50;
		const since = options?.since ?? "";
		const topic = db.prepare("SELECT id FROM conduit_topics WHERE name = ?").get(topicName);
		if (!topic) return [];
		return db.prepare(`SELECT id, from_agent_id, kind, content, payload, created_at
         FROM conduit_topic_messages
         WHERE topic_id = ? AND created_at > ?
         ORDER BY created_at ASC
         LIMIT ?`).all(topic.id, since, limit).map((r) => ({
			id: r.id,
			from: r.from_agent_id,
			fromPeerId: r.from_agent_id,
			toPeerId: null,
			content: r.content,
			kind: r.kind,
			threadId: topicName,
			payload: r.payload != null ? JSON.parse(r.payload) : void 0,
			timestamp: r.created_at
		}));
	}
	/**
	* Check whether conduit.db is available for local transport.
	*
	* Used by factory.ts to decide whether to use LocalTransport.
	*
	* @task T356
	* @epic T310
	* @param cwd - Optional working directory override (defaults to process.cwd()).
	* @returns `true` if conduit.db exists at the expected path.
	*/
	static isAvailable(cwd) {
		let dbPath;
		try {
			dbPath = getConduitDbPath(resolveOrCwd(cwd));
		} catch {
			return false;
		}
		return existsSync(dbPath);
	}
	/** Poll for new messages and notify subscribers (cross-process sync). */
	async pollAndNotify() {
		if (!this.state || this.state.subscribers.size === 0) return;
		const messages = await this.poll({ limit: 20 });
		for (const msg of messages) this.notifySubscribers(msg);
		if (messages.length > 0) await this.ack(messages.map((m) => m.id));
	}
	/** Poll all watched topics for new messages and notify in-process handlers. */
	async pollAndNotifyTopics() {
		if (!this.state || this.state.topicHandlers.size === 0) return;
		for (const [topicName] of this.state.topicHandlers) {
			const since = this.state.topicLastSeen.get(topicName) ?? "";
			const messages = await this.pollTopic(topicName, {
				limit: 50,
				since
			});
			if (messages.length > 0) {
				const lastTs = messages[messages.length - 1];
				if (lastTs?.timestamp) this.state.topicLastSeen.set(topicName, lastTs.timestamp);
				for (const msg of messages) this.notifyTopicHandlers(topicName, msg);
			}
		}
	}
	/** Notify all handlers registered for a specific topic. */
	notifyTopicHandlers(topicName, message) {
		if (!this.state) return;
		const handlers = this.state.topicHandlers.get(topicName);
		if (!handlers) return;
		for (const handler of handlers) try {
			handler(message);
		} catch {}
	}
	/** Notify all active subscribers of a new message. */
	notifySubscribers(message) {
		if (!this.state) return;
		for (const handler of this.state.subscribers) try {
			handler(message);
		} catch {}
	}
	/**
	* Ensure a DM conversation exists between two agents.
	*
	* Conversations store participants as a comma-separated TEXT field.
	* We search for existing private conversations containing both agents.
	*
	* @returns The conversation ID.
	*/
	ensureDmConversation(fromAgentId, toAgentId) {
		const { db } = this.state;
		const sortedParticipants = [fromAgentId, toAgentId].sort().join(",");
		const existing = db.prepare(`SELECT id FROM conduit_conversations
         WHERE visibility = 'private' AND participants = ?
         LIMIT 1`).get(sortedParticipants);
		if (existing) return existing.id;
		const convId = randomUUID();
		const nowIso = (/* @__PURE__ */ new Date()).toISOString();
		db.prepare(`INSERT INTO conduit_conversations (id, participants, visibility, message_count, created_at, updated_at)
       VALUES (?, ?, 'private', 0, ?, ?)`).run(convId, sortedParticipants, nowIso, nowIso);
		return convId;
	}
	/** Throw if not connected. */
	ensureConnected() {
		if (!this.state) throw new Error("LocalTransport not connected. Call connect() first.");
	}
	/**
	* Seam 3 (T11627): conduit.db is a raw bypass writer (project-tier, sidesteps
	* the tasks chokepoint via its own `openFreshConduitDb` handle). Hold the
	* project `bulk` lease around a synchronous write block so conduit writes
	* serialize against other writers on the same scope. `off` mode → pass-through
	* (busy_timeout serializes as before). The wrapped `fn` is synchronous because
	* every conduit write is a synchronous `db.prepare(...).run(...)`.
	*
	* @param fn - The synchronous write block to run under the lease.
	* @returns The value returned by `fn`.
	*/
	runWrite(fn) {
		return withWriterLease("project", "bulk", async () => fn());
	}
};
//#endregion
//#region ../core/dist/hooks/handlers/memory-bridge-refresh.js
/**
* Shared memory bridge refresh helper for hook handlers.
*
* Provides a config-gated, debounced wrapper around refreshMemoryBridge().
* Prevents rapid regeneration of .cleo/memory-bridge.md when multiple
* lifecycle events fire in quick succession (e.g. session end + task complete).
*
* Debounce: max one refresh per 30 seconds across all callers in the process.
*
* @task T138
* @epic T134
*/
/** Debounce window in milliseconds. */
var DEBOUNCE_MS = 3e4;
/** Timestamp of the last successful refresh call (module-level singleton). */
var lastRefreshTime = 0;
/**
* Refresh the memory bridge if autoRefresh is enabled and the debounce window
* has elapsed. Reads config via loadConfig() (cascaded). Never throws.
*
* @param projectRoot - Absolute path to the project root directory.
*/
async function maybeRefreshMemoryBridge(projectRoot) {
	try {
		const { loadConfig } = await import('./config2-CdsX487B.js');
		if (!(await loadConfig(projectRoot)).brain?.memoryBridge?.autoRefresh) return;
		const now = Date.now();
		if (now - lastRefreshTime < DEBOUNCE_MS) return;
		lastRefreshTime = now;
		const { refreshMemoryBridge } = await import('./memory-bridge-C_YgF01K.js');
		await refreshMemoryBridge(projectRoot);
	} catch {}
}
//#endregion
//#region ../core/dist/hooks/handlers/session-hooks.js
/**
* Session Hook Handlers - Phase 2D of T5237
*
* Handlers that capture session lifecycle events.
* Auto-registers on module load.
*
* T138: Triggers memory bridge refresh on session start and end.
* T139: Regenerates bridge with session scope on start.
* T144: Extracts transcript observations on session end.
* T5158: Auto-snapshots SQLite databases (tasks.db + brain.db) via
*        VACUUM INTO on SessionEnd to preserve a recovery point now that
*        the databases are no longer tracked in project git (ADR-013).
* T527: Removed duplicate session observeBrain writes — session data already
*       lives in the sessions table; writing it again to brain_observations
*       was pure noise.
* T549 Wave 3-E: Fire-and-forget sleep-time consolidation on session end.
*       Runs after backup (priority 5) so brain.db snapshot is captured first.
* T554: Fire-and-forget LLM reflector on session end. Runs at priority 4
*       (after consolidation at priority 5) to synthesize final session knowledge.
* T732: Write `transcript_pending_extraction` tombstone on session end (priority 3).
*       Runs after reflector so it is the last scheduled operation. Records the
*       session JSONL path so `cleo transcript scan --pending` can list queued work.
* T1263: Append session journal entry at priority 2 (last in pipeline).
*        Absorbs T1262 session-end hook: calls scanBrainNoise and embeds
*        doctorSummary in each session_end journal entry.
*        MUST use `await` (not setImmediate) — process exits after this hook.
*/
/**
* Handle SessionStart - refresh memory bridge on session start.
*
* T138: Refresh memory bridge on session start.
* T139: Regenerate bridge with session scope context.
* T527: Removed duplicate observeBrain write — session data is already
*       persisted in the sessions table.
*/
async function handleSessionStart(projectRoot, _payload) {
	await maybeRefreshMemoryBridge(projectRoot);
}
/**
* Handle SessionEnd - run post-session tasks and refresh memory bridge.
*
* T138: Refresh memory bridge after session ends.
* T144: Extract transcript observations via cross-provider adapter.
* T527: Removed duplicate observeBrain write — session data is already
*       persisted in the sessions table.
*/
async function handleSessionEnd(projectRoot, payload) {
	if (!process.env["VITEST"]) try {
		const { gradeSession } = await import('./session-grade-DMqUSt10.js');
		await gradeSession(payload.sessionId, projectRoot);
	} catch {}
	try {
		const { loadConfig } = await import('./config2-CdsX487B.js');
		if ((await loadConfig(projectRoot)).brain?.autoCapture) {
			const { AdapterManager } = await import('./adapters2-Dqs264RK.js');
			const hookProvider = AdapterManager.getInstance(projectRoot).getActive()?.hooks;
			if (hookProvider && typeof hookProvider.getTranscript === "function") {
				const transcript = await hookProvider.getTranscript(payload.sessionId, projectRoot);
				if (transcript) {
					const { extractFromTranscript } = await import('./auto-extract-rR2baYuu.js');
					await extractFromTranscript(projectRoot, payload.sessionId, transcript);
				}
			}
		}
	} catch {}
	await maybeRefreshMemoryBridge(projectRoot);
}
/**
* Handle SessionEnd - snapshot SQLite databases to `.cleo/backups/sqlite/`.
*
* ADR-013 / T5158: `.cleo/tasks.db` and `.cleo/brain.db` are NOT tracked in
* project git, so we need an out-of-band recovery mechanism. This handler
* calls `vacuumIntoBackupAll` with `force: true` at every session end to
* guarantee a fresh point-in-time snapshot of both databases.
*
* Rotation (MAX_SNAPSHOTS = 10 per database) is handled inside
* `sqlite-backup.ts`. Failures here are non-fatal: a backup error must
* never block session end.
*
* The `vacuumIntoBackupAll` import is deferred to call time so tests that
* auto-load session-hooks (e.g. via `handlers/index.ts`) do not have to
* mock every transitive dependency of `sqlite-backup.ts` at hoisted
* `vi.mock` time.
*/
async function handleSessionEndBackup(projectRoot, _payload) {
	try {
		const { vacuumIntoBackupAll } = await import('./sqlite-backup-BLAnRqch.js');
		await vacuumIntoBackupAll({
			cwd: projectRoot,
			force: true
		});
	} catch {}
}
/**
* Handle SessionEnd — fire-and-forget sleep-time memory consolidation.
*
* T549 Wave 3-E: Runs the full consolidation pipeline (dedup, quality recompute,
* tier promotion, contradiction detection, soft eviction, graph strengthening,
* summary generation) in the background after the session backup has completed.
*
* Uses setImmediate to yield control so the session end flow completes before
* consolidation begins. Consolidation errors are caught and logged to console.warn
* — they MUST NOT block session end or throw to callers.
*
* Priority 5 ensures this runs last (after backup at priority 10).
*/
async function handleSessionEndConsolidation(projectRoot, _payload) {
	setImmediate(async () => {
		try {
			const { runConsolidation } = await import('./brain-lifecycle-CWp_g1sm.js');
			await runConsolidation(projectRoot);
		} catch (err) {
			console.warn("[consolidation] Session-end consolidation failed:", err);
		}
	});
}
/**
* Handle SessionEnd — fire-and-forget LLM Observer compression.
*
* T740: GAP-6 fix — Observer was only triggered on task completion when
* observation count >= threshold (default 10). Sessions with < 10 observations
* never got compression. This hook runs Observer unconditionally at session end
* with a threshold override of 1, ensuring even short sessions get compressed.
*
* Priority 4.5 (between consolidation at 5 and reflector at 4) so Observer
* compresses the raw observations before Reflector synthesizes patterns/learnings
* from them. This matches the intended pipeline order.
*
* Uses setImmediate to yield control before the LLM call. Errors are caught
* and logged — they MUST NOT block session end or throw to callers.
*/
async function handleSessionEndObserver(projectRoot, payload) {
	setImmediate(async () => {
		try {
			const { runObserver } = await import('./observer-reflector-CZJKLCI_.js');
			await runObserver(projectRoot, payload.sessionId, { thresholdOverride: 1 });
		} catch (err) {
			console.warn("[observer] Session-end observer failed:", err);
		}
	});
}
/**
* Handle SessionEnd — fire-and-forget LLM reflector synthesis.
*
* T554: Runs the Reflector after the consolidation pass (priority 5) to
* synthesize session observations into durable patterns and learnings.
*
* Uses setImmediate to yield control before the LLM call. Errors are caught
* and logged — they MUST NOT block session end or throw to callers.
*
* Priority 4 ensures this runs after consolidation (priority 5).
*/
async function handleSessionEndReflector(projectRoot, payload) {
	setImmediate(async () => {
		try {
			const { runReflector } = await import('./observer-reflector-CZJKLCI_.js');
			await runReflector(projectRoot, payload.sessionId);
		} catch (err) {
			console.warn("[reflector] Session-end reflector failed:", err);
		}
	});
}
/**
* Handle SessionEnd — write a `transcript_pending_extraction` record to brain_observations.
*
* T732: Records the session JSONL path so the warm-tier extractor and
* `cleo transcript scan --pending` can locate queued sessions.
*
* The record is idempotent: re-running session end for the same session_id
* updates the timestamp but does not create a duplicate (content-hash dedup
* in observeBrain handles this automatically).
*
* Priority 3 — runs after reflector (priority 4) so it is the last step in
* the session-end pipeline.
*
* Uses setImmediate to yield so the session-end response reaches the CLI
* before the brain write occurs.
*/
async function handleSessionEndTranscriptSchedule(projectRoot, payload) {
	setImmediate(async () => {
		try {
			const { findSessionTranscriptPath } = await import('./transcript-scanner-kJWhWmlg.js');
			const filePath = await findSessionTranscriptPath(payload.sessionId);
			if (!filePath) return;
			const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
			await observeBrain(projectRoot, {
				title: `transcript_pending_extraction:${payload.sessionId}`,
				text: `Session ${payload.sessionId} transcript queued for warm-tier extraction. File: ${filePath}`,
				type: "discovery",
				sourceType: "agent",
				sourceSessionId: payload.sessionId
			});
		} catch (err) {
			console.warn("[transcript-schedule] Failed to queue transcript:", err);
		}
	});
}
/**
* Handle SessionEnd — append a session journal entry to `.cleo/session-journals/`.
*
* T1263 PSYCHE E6: Writes a `session_end` JSONL entry capturing session metadata
* and the compact result of `scanBrainNoise` (T1262 absorption).
*
* **MUST use synchronous `await`** — this hook runs at priority 2, immediately
* before process.exit. Using `setImmediate` here would silently drop the write.
*
* Runs at priority 2 — the last synchronous hook in the session-end pipeline,
* ensuring all prior work (backup, consolidation, observer, reflector, transcript)
* has been scheduled before the journal entry is written.
*/
async function handleSessionEndJournal(projectRoot, payload) {
	try {
		const { SESSION_JOURNAL_SCHEMA_VERSION } = await import('./dist3-vdmzrGzO.js');
		const { appendSessionJournalEntry } = await import('./session-journal-Br8tw7nG.js');
		let doctorSummary;
		try {
			const { scanBrainNoise } = await import('./brain-doctor-BVUWGUKG.js');
			const scanResult = await scanBrainNoise(projectRoot);
			doctorSummary = {
				isClean: scanResult.isClean,
				findingsCount: scanResult.findings.length,
				patterns: scanResult.findings.map((f) => f.pattern),
				totalScanned: scanResult.totalScanned
			};
		} catch {}
		const agentIdentifier = process.env.CLEO_AGENT_ID ?? process.env.CLAUDE_CODE_AGENT_ID ?? void 0;
		await appendSessionJournalEntry(projectRoot, {
			schemaVersion: SESSION_JOURNAL_SCHEMA_VERSION,
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			sessionId: payload.sessionId,
			eventType: "session_end",
			agentIdentifier,
			providerId: payload.providerId,
			duration: payload.duration,
			tasksCompleted: payload.tasksCompleted,
			...doctorSummary !== void 0 ? { doctorSummary } : {}
		});
	} catch {}
}
hooks.register({
	id: "brain-session-start",
	event: "SessionStart",
	handler: handleSessionStart,
	priority: 100
});
hooks.register({
	id: "brain-session-end",
	event: "SessionEnd",
	handler: handleSessionEnd,
	priority: 100
});
hooks.register({
	id: "backup-session-end",
	event: "SessionEnd",
	handler: handleSessionEndBackup,
	priority: 10
});
hooks.register({
	id: "consolidation-session-end",
	event: "SessionEnd",
	handler: handleSessionEndConsolidation,
	priority: 5
});
hooks.register({
	id: "observer-session-end",
	event: "SessionEnd",
	handler: handleSessionEndObserver,
	priority: 4.5
});
hooks.register({
	id: "reflector-session-end",
	event: "SessionEnd",
	handler: handleSessionEndReflector,
	priority: 4
});
hooks.register({
	id: "transcript-schedule-session-end",
	event: "SessionEnd",
	handler: handleSessionEndTranscriptSchedule,
	priority: 3
});
hooks.register({
	id: "journal-session-end",
	event: "SessionEnd",
	handler: handleSessionEndJournal,
	priority: 2
});
//#endregion
//#region ../core/dist/hooks/handlers/task-hooks.js
/**
* Task Hook Handlers - Phase 2D of T5237
*
* Handlers that capture task lifecycle events to BRAIN via memory.observe.
* Auto-registers on module load.
*
* T138: Triggers memory bridge refresh after task completion.
* T554: Triggers LLM observer after task completion when observation count ≥ threshold.
*/
/**
* Handle PreToolUse (maps to task.start in CLEO, canonical: was onToolStart)
*/
async function handleToolStart(projectRoot, payload) {
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	try {
		await observeBrain(projectRoot, {
			text: `Started work on ${payload.taskId}: ${payload.taskTitle}`,
			title: `Task start: ${payload.taskId}`,
			type: "change",
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err) && !isNoProjectError(err)) throw err;
	}
}
/**
* Handle PostToolUse (maps to task.complete in CLEO, canonical: was onToolComplete)
*
* T138: Refresh memory bridge after task completion.
* T554: Fire-and-forget LLM observer when observation count ≥ threshold.
*/
async function handleToolComplete(projectRoot, payload) {
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	try {
		await observeBrain(projectRoot, {
			text: `Task ${payload.taskId} completed with status: ${payload.status}`,
			title: `Task complete: ${payload.taskId}`,
			type: "change",
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err) && !isNoProjectError(err)) throw err;
	}
	setImmediate(async () => {
		try {
			const { runObserver } = await import('./observer-reflector-CZJKLCI_.js');
			await runObserver(projectRoot);
		} catch {}
	});
	setImmediate(async () => {
		try {
			const { correlateOutcomes } = await import('./quality-feedback-2etyCgV3.js');
			await correlateOutcomes(projectRoot);
		} catch {}
	});
	await maybeRefreshMemoryBridge(projectRoot);
}
hooks.register({
	id: "brain-tool-start",
	event: "PreToolUse",
	handler: handleToolStart,
	priority: 100
});
hooks.register({
	id: "brain-tool-complete",
	event: "PostToolUse",
	handler: handleToolComplete,
	priority: 100
});
//#endregion
//#region ../core/dist/hooks/handlers/error-hooks.js
/**
* Error Hook Handlers - Wave 2 of T5237
*
* Captures operation errors to BRAIN via memory.observe.
* Includes infinite-loop guard to prevent onError triggering onError.
* Auto-registers on module load.
*/
/**
* Handle PostToolUseFailure — capture operation errors to BRAIN.
*
* Includes an infinite-loop guard: if the payload carries a `_fromHook`
* metadata marker the handler skips immediately to prevent the cycle
* `PostToolUseFailure → observeBrain → PostToolUseFailure`. All
* `observeBrain` errors are also silently suppressed for the same reason.
*
* Never throws. All brain-write errors are swallowed so error hooks
* never block the original error path.
*
* @param projectRoot - Absolute path to the project root directory.
* @param payload     - PostToolUseFailure event payload.
*
* @task T166
* @epic T134
*/
async function handleError(projectRoot, payload) {
	if (payload.metadata?.["_fromHook"]) return;
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	const domainOp = payload.domain && payload.operation ? `${payload.domain}.${payload.operation}` : "unknown";
	try {
		await observeBrain(projectRoot, {
			text: `Error in ${domainOp}: ${payload.message}\nCode: ${payload.errorCode}${payload.gateway ? `\nGateway: ${payload.gateway}` : ""}`,
			title: `Error: ${domainOp}`,
			type: "discovery",
			sourceType: "agent"
		});
	} catch (_err) {}
}
hooks.register({
	id: "brain-error",
	event: "PostToolUseFailure",
	handler: handleError,
	priority: 100
});
//#endregion
//#region ../core/dist/hooks/handlers/file-hooks.js
/**
* File Change Hook Handlers - Wave 2 of T5237
*
* Captures file change events to BRAIN via memory.observe.
* Includes 5-second dedup and path-based filtering to avoid noisy writes.
* Auto-registers on module load.
*
* Disabled by default. Enable via:
*   - Config: brain.captureFiles = true  (checked first)
*   - Env:    CLEO_BRAIN_CAPTURE_FILES=true  (overrides config)
*/
/** 5-second dedup map: filePath -> last dispatch timestamp */
var recentWrites = /* @__PURE__ */ new Map();
var DEDUP_INTERVAL_MS = 5e3;
/** Path patterns to exclude from brain observations (internal/noisy files). */
var SKIP_PATTERNS = [
	/\.cleo\//,
	/\.cleo\\/,
	/\/\.temp\//,
	/cleo-test-/,
	/cleo-edge-/,
	/node_modules\//,
	/\.git\//,
	/dist\//
];
/** Check whether a relative path should be skipped. */
function shouldSkipPath(relativePath) {
	return SKIP_PATTERNS.some((pattern) => pattern.test(relativePath));
}
/**
* Check whether file-change capture is enabled.
*
* Resolution order (first truthy wins):
*   1. CLEO_BRAIN_CAPTURE_FILES env var (explicit override)
*   2. brain.captureFiles project config value
*
* Defaults to false when neither is set.
*/
async function isFileCaptureEnabled(projectRoot) {
	const envOverride = process.env["CLEO_BRAIN_CAPTURE_FILES"];
	if (envOverride !== void 0) return envOverride === "true";
	try {
		const { loadConfig } = await import('./config2-CdsX487B.js');
		return (await loadConfig(projectRoot)).brain?.captureFiles ?? false;
	} catch {
		return false;
	}
}
/**
* Handle Notification (file-change variant) - capture file changes to BRAIN
*
* Gated behind brain.captureFiles config or CLEO_BRAIN_CAPTURE_FILES env var.
* Env var takes precedence over config for backward compatibility.
* Deduplicates rapid writes to the same file within a 5-second window.
* Filters out .cleo/ internal files and test temp directories.
* Converts absolute paths to project-relative paths.
*/
async function handleFileChange(projectRoot, payload) {
	if (!payload.filePath || !payload.changeType) return;
	if (!await isFileCaptureEnabled(projectRoot)) return;
	const now = Date.now();
	const lastWrite = recentWrites.get(payload.filePath);
	if (lastWrite && now - lastWrite < DEDUP_INTERVAL_MS) return;
	recentWrites.set(payload.filePath, now);
	const relativePath = (isAbsolute(payload.filePath) ? relative(projectRoot, payload.filePath) : payload.filePath).replaceAll("\\", "/");
	if (shouldSkipPath(relativePath)) return;
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	try {
		await observeBrain(projectRoot, {
			text: `File ${payload.changeType}: ${relativePath}${payload.sizeBytes != null ? ` (${payload.sizeBytes} bytes)` : ""}`,
			title: `File changed: ${relativePath}`,
			type: "change",
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err)) throw err;
	}
}
hooks.register({
	id: "brain-file-change",
	event: "Notification",
	handler: handleFileChange,
	priority: 100
});
//#endregion
//#region ../core/dist/hooks/handlers/notification-hooks.js
/**
* System Notification Hook Handler
*
* Captures message-bearing system notifications as BRAIN observations.
* File-change notifications (filePath + changeType) are handled by
* file-hooks.ts; this module only handles Notification payloads with
* a `message` field.
*
* Gated behind brain.autoCapture config. Never throws.
* Auto-registers on module load.
*
* @task T166
*/
/**
* Handle Notification — capture system notifications as BRAIN observations.
*
* Only fires for Notification payloads that carry a message field (i.e. system
* notifications). File-change notifications (filePath + changeType) are
* handled exclusively by file-hooks.ts and are skipped here to avoid
* double-capture.
*
* Gated behind brain.autoCapture config. Never throws.
*
* @param projectRoot - Absolute path to the project root directory.
* @param payload     - Notification event payload.
*
* @task T166
*/
async function handleSystemNotification(projectRoot, payload) {
	if (payload.filePath || payload.changeType) return;
	if (!payload.message) return;
	if (!await isAutoCaptureEnabled(projectRoot)) return;
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	try {
		await observeBrain(projectRoot, {
			text: `System notification: ${payload.message}`,
			title: `Notification: ${payload.message.slice(0, 60)}`,
			type: "discovery",
			sourceSessionId: payload.sessionId,
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err)) throw err;
	}
}
hooks.register({
	id: "brain-system-notification",
	event: "Notification",
	handler: handleSystemNotification,
	priority: 90
});
//#endregion
//#region ../core/dist/hooks/handlers/work-capture-hooks.js
/**
* Active-Work Capture Hook Handlers
*
* Captures dispatch mutation events to BRAIN during active work, closing
* the gap between lifecycle-only capture (session end, task complete) and
* continuous capture. Only WRITE operations that represent meaningful work
* are captured — reads and operations already handled by lifecycle hooks
* are skipped to avoid flooding brain.db.
*
* Disabled by default. Enable via:
*   - Config: brain.captureWork = true  (checked first)
*   - Env:    CLEO_BRAIN_CAPTURE_WORK=true  (overrides config)
*
* Auto-registers on module load.
*
* @task T142
*/
/**
* Check whether active-work capture is enabled.
*
* Resolution order (first truthy wins):
*   1. CLEO_BRAIN_CAPTURE_WORK env var (explicit override)
*   2. brain.captureWork project config value
*
* Defaults to false when neither is set.
*/
async function isWorkCaptureEnabled(projectRoot) {
	const envOverride = process.env["CLEO_BRAIN_CAPTURE_WORK"];
	if (envOverride !== void 0) return envOverride === "true";
	try {
		const { loadConfig } = await import('./config2-CdsX487B.js');
		return (await loadConfig(projectRoot)).brain?.captureWork ?? false;
	} catch {
		return false;
	}
}
/**
* Mutations that represent novel work not already captured by lifecycle hooks.
*
* Excluded (already captured elsewhere):
*   - tasks.complete  → task-hooks.ts (PostToolUse)
*   - session.start   → session-hooks.ts (SessionStart)
*   - session.end     → session-hooks.ts (SessionEnd)
*   - memory.brain.observe → observeBrain itself writes to brain; self-loop
*
* All query gateway operations are excluded by the gateway check before
* this set is consulted.
*/
var CAPTURE_OPERATIONS = new Set([
	"tasks.add",
	"tasks.update",
	"tasks.move",
	"tasks.link",
	"tasks.unlink",
	"tasks.label",
	"tasks.unlabel",
	"tasks.note"
]);
/**
* Determine whether a mutate operation should be captured.
*
* Only `mutate` gateway operations in CAPTURE_OPERATIONS are captured.
* All `query` gateway calls are skipped (reads produce no novel work).
*
* @param gateway - 'query' or 'mutate'
* @param domain  - e.g. 'tasks'
* @param operation - e.g. 'add'
*/
function shouldCapture(gateway, domain, operation) {
	if (gateway !== "mutate") return false;
	const key = `${domain}.${operation}`;
	return CAPTURE_OPERATIONS.has(key);
}
/**
* Handle PromptSubmit — log incoming mutation intents to BRAIN.
*
* Only fires for mutate operations in CAPTURE_OPERATIONS.
* Gated behind brain.captureWork config (or CLEO_BRAIN_CAPTURE_WORK env).
*
* @param projectRoot - Absolute path to the project root
* @param payload     - PromptSubmit event payload
*/
async function handleWorkPromptSubmit(projectRoot, payload) {
	if (!shouldCapture(payload.gateway, payload.domain, payload.operation)) return;
	if (!await isWorkCaptureEnabled(projectRoot)) return;
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	try {
		await observeBrain(projectRoot, {
			text: `Dispatch intent: ${payload.gateway}:${payload.domain}.${payload.operation}${payload.source ? ` (from ${payload.source})` : ""}`,
			title: `Work intent: ${payload.domain}.${payload.operation}`,
			type: "discovery",
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err)) throw err;
	}
}
/**
* Handle ResponseComplete — capture completed mutations to BRAIN.
*
* Only fires for successful mutate operations in CAPTURE_OPERATIONS.
* Failures are skipped — the intent was already captured by handleWorkPromptSubmit.
* Gated behind brain.captureWork config (or CLEO_BRAIN_CAPTURE_WORK env).
*
* @param projectRoot - Absolute path to the project root
* @param payload     - ResponseComplete event payload
*/
async function handleWorkResponseComplete(projectRoot, payload) {
	if (!shouldCapture(payload.gateway, payload.domain, payload.operation)) return;
	if (!payload.success) return;
	if (!await isWorkCaptureEnabled(projectRoot)) return;
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	try {
		const durationNote = payload.durationMs != null ? ` (${payload.durationMs}ms)` : "";
		await observeBrain(projectRoot, {
			text: `Dispatch complete: ${payload.gateway}:${payload.domain}.${payload.operation}${durationNote}`,
			title: `Work done: ${payload.domain}.${payload.operation}`,
			type: "change",
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err)) throw err;
	}
}
hooks.register({
	id: "work-capture-prompt-submit",
	event: "PromptSubmit",
	handler: handleWorkPromptSubmit,
	priority: 90
});
hooks.register({
	id: "work-capture-response-complete",
	event: "ResponseComplete",
	handler: handleWorkResponseComplete,
	priority: 90
});
//#endregion
//#region ../core/dist/hooks/handlers/agent-hooks.js
/**
* Agent (Subagent) Lifecycle Hook Handlers
*
* Captures SubagentStart and SubagentStop events to BRAIN so that
* multi-agent orchestration runs leave an auditable trail of which
* subagents were spawned, what tasks they were assigned, and how
* they completed.
*
* Gated behind brain.autoCapture config. Never throws — all errors are
* swallowed so that brain capture never blocks agent orchestration.
*
* Auto-registers on module load.
*
* @task T166
* @epic T134
*/
/**
* Handle SubagentStart — log subagent spawn as a BRAIN observation.
*
* Records the agent ID, role, and task assignment so orchestrators can
* trace which agents were active in a given session.
*
* Gated behind brain.autoCapture config. Never throws.
*
* @param projectRoot - Absolute path to the project root directory.
* @param payload     - SubagentStart event payload.
*
* @task T166
* @epic T134
*/
async function handleSubagentStart(projectRoot, payload) {
	if (!await isAutoCaptureEnabled(projectRoot)) return;
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	const rolePart = payload.role ? ` role=${payload.role}` : "";
	const taskPart = payload.taskId ? ` task=${payload.taskId}` : "";
	try {
		await observeBrain(projectRoot, {
			text: `Subagent spawned: ${payload.agentId}${rolePart}${taskPart}`,
			title: `Subagent start: ${payload.agentId}`,
			type: "discovery",
			sourceSessionId: payload.sessionId,
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err)) throw err;
	}
}
/**
* Handle SubagentStop — log subagent completion result as a BRAIN observation.
*
* Records the agent ID, completion status, assigned task, and optional
* summary reference so orchestrators can audit subagent outcomes.
*
* Gated behind brain.autoCapture config. Never throws.
*
* @param projectRoot - Absolute path to the project root directory.
* @param payload     - SubagentStop event payload.
*
* @task T166
* @epic T134
*/
async function handleSubagentStop(projectRoot, payload) {
	if (!await isAutoCaptureEnabled(projectRoot)) return;
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	const statusPart = payload.status ? ` status=${payload.status}` : "";
	const taskPart = payload.taskId ? ` task=${payload.taskId}` : "";
	const summaryPart = payload.summary ? `\nSummary: ${payload.summary}` : "";
	try {
		await observeBrain(projectRoot, {
			text: `Subagent completed: ${payload.agentId}${statusPart}${taskPart}${summaryPart}`,
			title: `Subagent stop: ${payload.agentId}`,
			type: "change",
			sourceSessionId: payload.sessionId,
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err)) throw err;
	}
}
hooks.register({
	id: "brain-subagent-start",
	event: "SubagentStart",
	handler: handleSubagentStart,
	priority: 100
});
hooks.register({
	id: "brain-subagent-stop",
	event: "SubagentStop",
	handler: handleSubagentStop,
	priority: 100
});
//#endregion
//#region ../core/dist/hooks/handlers/context-hooks.js
/**
* Context Compaction Hook Handlers
*
* Captures PreCompact and PostCompact events to BRAIN so that agents
* can later observe when context compactions occurred and what state
* preceded them. This is especially useful for long sessions where
* context is compacted multiple times.
*
* PreCompact saves a session context snapshot before compaction begins.
* PostCompact records that compaction occurred and the resulting token counts.
*
* Gated behind brain.autoCapture config. Never throws — all errors are
* swallowed so brain capture never blocks context compaction.
*
* Auto-registers on module load.
*
* @task T166
* @epic T134
*/
/**
* Handle PreCompact — snapshot current session memory context to BRAIN.
*
* Fires immediately before context compaction begins. Records the token
* count and compaction reason so the brain retains context about what
* was in scope before compaction.
*
* Gated behind brain.autoCapture config. Never throws.
*
* @param projectRoot - Absolute path to the project root directory.
* @param payload     - PreCompact event payload.
*
* @task T166
* @epic T134
*/
async function handlePreCompact(projectRoot, payload) {
	if (!await isAutoCaptureEnabled(projectRoot)) return;
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	const tokensPart = payload.tokensBefore != null ? ` (~${payload.tokensBefore.toLocaleString()} tokens)` : "";
	const reasonPart = payload.reason ? ` Reason: ${payload.reason}` : "";
	try {
		await observeBrain(projectRoot, {
			text: `Context compaction about to begin${tokensPart}.${reasonPart}`,
			title: "Pre-compaction context snapshot",
			type: "discovery",
			sourceSessionId: payload.sessionId,
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err)) throw err;
	}
}
/**
* Handle PostCompact — record compaction completion to BRAIN.
*
* Fires immediately after context compaction completes. Records the
* before/after token counts so agents can correlate observations made
* before compaction with those made after.
*
* Gated behind brain.autoCapture config. Never throws.
*
* @param projectRoot - Absolute path to the project root directory.
* @param payload     - PostCompact event payload.
*
* @task T166
* @epic T134
*/
async function handlePostCompact(projectRoot, payload) {
	if (!await isAutoCaptureEnabled(projectRoot)) return;
	const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
	const statusPart = payload.success ? "succeeded" : "failed";
	const beforePart = payload.tokensBefore != null ? ` before=${payload.tokensBefore.toLocaleString()}` : "";
	const afterPart = payload.tokensAfter != null ? ` after=${payload.tokensAfter.toLocaleString()}` : "";
	try {
		await observeBrain(projectRoot, {
			text: `Context compaction ${statusPart}${beforePart}${afterPart}`,
			title: "Post-compaction record",
			type: "change",
			sourceSessionId: payload.sessionId,
			sourceType: "agent"
		});
	} catch (err) {
		if (!isMissingBrainSchemaError(err)) throw err;
	}
}
hooks.register({
	id: "brain-pre-compact",
	event: "PreCompact",
	handler: handlePreCompact,
	priority: 100
});
hooks.register({
	id: "brain-post-compact",
	event: "PostCompact",
	handler: handlePostCompact,
	priority: 100
});
//#endregion
//#region ../core/dist/hooks/handlers/watchdog-hooks.js
/**
* Watchdog Scheduler Hook Handlers
*
* Starts a periodic health-check timer when a session begins and stops it when
* the session ends. One watchdog per project root, enforced by a module-level
* Map keyed on the resolved project root.
*
* Each 60-second tick:
* 1. Detects agents with a stale heartbeat (> STALE_THRESHOLD_MS)
* 2. Records each crash as a learning event in brain.db
* 3. Attempts recovery (status reset to 'starting') for all crashed agents
* 4. Fires the `onPatrol` hook for observability
*
* Best-effort contract: the watchdog NEVER crashes or throws. Any tick failure
* is swallowed so the timer continues. Any error in the start/stop handlers is
* also swallowed so it never blocks session operations.
*
* Gated behind `brain.autoCapture` config — the watchdog does NOT start unless
* auto-capture is enabled.
*
* Auto-registers on module load.
*
* @task T549
* @epic T5149
*/
/** One active timer per project root (resolved path). */
var activeWatchdogs = /* @__PURE__ */ new Map();
/** Interval between watchdog ticks in milliseconds. */
var WATCHDOG_INTERVAL_MS = 6e4;
/**
* Execute one watchdog patrol tick.
*
* Steps:
* 1. detectCrashedAgents — find and mark stale agents as 'crashed'
* 2. processAgentLifecycleEvent — write failure events to brain.db for each
* 3. recoverCrashedAgents — reset crashed agents to 'starting'
* 4. hooks.dispatch('onPatrol') — emit observability event
*
* Any thrown error is caught and logged as a warning; the timer continues.
*
* @param projectRoot - Absolute path to the project root directory.
*/
async function runWatchdogTick(projectRoot) {
	try {
		const { detectCrashedAgents, STALE_THRESHOLD_MS } = await import('./health-monitor-CaxN1A-_.js');
		const { recoverCrashedAgents } = await import('./retry-Bq7dqsY8.js');
		const { processAgentLifecycleEvent } = await import('./execution-learning-D7yNT9EV.js');
		const crashed = await detectCrashedAgents(STALE_THRESHOLD_MS, projectRoot);
		for (const agent of crashed) try {
			await processAgentLifecycleEvent({
				agentId: agent.id,
				agentType: agent.agentType ?? "custom",
				taskId: agent.taskId ?? "unknown",
				taskType: "unknown",
				outcome: "failure",
				errorMessage: "Heartbeat timeout — agent presumed crashed",
				errorType: "retriable",
				sessionId: agent.sessionId ?? void 0
			}, projectRoot);
		} catch (err) {
			if (!isMissingBrainSchemaError(err)) console.warn("[watchdog] Failed to record agent crash event:", err);
		}
		if (crashed.length > 0) await recoverCrashedAgents(STALE_THRESHOLD_MS, projectRoot);
		const patrolPayload = {
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			watcherId: "health-watchdog",
			patrolType: "health",
			scope: `crashed=${crashed.length}`
		};
		await hooks.dispatch("onPatrol", projectRoot, patrolPayload);
	} catch (err) {
		console.warn("[watchdog] Tick failed:", err);
	}
}
/**
* Handle SessionStart — start the watchdog timer for this project root.
*
* Idempotent: if a watchdog is already running for this root (e.g. a second
* session in the same process), the existing timer is reused.
*
* Gated behind `brain.autoCapture` — watchdog does not start unless auto-capture
* is enabled. This aligns with all other brain-capture hooks.
*
* @param projectRoot - Absolute path to the project root directory.
* @param _payload - SessionStart payload (unused).
*/
async function handleWatchdogStart(projectRoot, _payload) {
	try {
		if (!await isAutoCaptureEnabled(projectRoot)) return;
		if (activeWatchdogs.has(projectRoot)) return;
		const timer = setInterval(() => {
			runWatchdogTick(projectRoot);
		}, WATCHDOG_INTERVAL_MS);
		activeWatchdogs.set(projectRoot, timer);
	} catch {}
}
/**
* Handle SessionEnd — stop the watchdog timer for this project root.
*
* Safe to call when no watchdog is running (no-op).
*
* @param projectRoot - Absolute path to the project root directory.
* @param _payload - SessionEnd payload (unused).
*/
async function handleWatchdogStop(projectRoot, _payload) {
	try {
		const timer = activeWatchdogs.get(projectRoot);
		if (timer) {
			clearInterval(timer);
			activeWatchdogs.delete(projectRoot);
		}
	} catch {}
}
hooks.register({
	id: "watchdog-session-start",
	event: "SessionStart",
	handler: handleWatchdogStart,
	priority: 50
});
hooks.register({
	id: "watchdog-session-end",
	event: "SessionEnd",
	handler: handleWatchdogStop,
	priority: 50
});
//#endregion
//#region ../core/dist/hooks/handlers/intelligence-hooks.js
/**
* Intelligence Hook Handlers — Wave 5-D/E of T549
*
* Best-effort intelligence side-effects wired into task lifecycle events.
*
* On task start → calculate risk; if riskScore exceeds the HIGH threshold
* (≥ 0.8), store a warning observation so future agents see it.
*
* All handlers are fire-and-forget: failures MUST NOT propagate or block the
* operation that triggered them.
*
* @task T549
* @epic T549
*/
var RISK_HIGH = .8;
/**
* Handle task start: calculate risk and store a warning observation if the
* riskScore is at or above the HIGH threshold (≥ 0.8).
*
* Uses deferred imports so this module can be imported in environments where
* not all transitive dependencies (brain.db, tasks.db) are present.
*
* Best-effort: any error is silently swallowed.
*/
async function handleTaskStartIntelligence(projectRoot, payload) {
	try {
		const { getTaskAccessor } = await import('./data-accessor-CVbz8X_7.js').then(function (n) { return n.d; });
		const { getBrainAccessor } = await import('./memory-accessor-BXAc4VKA.js');
		const { calculateTaskRisk } = await import('./prediction-D1lSGJ4c.js');
		const { observeBrain } = await import('./brain-retrieval2-CM6hsYog.js');
		const [accessor, brain] = await Promise.all([getTaskAccessor(projectRoot), getBrainAccessor(projectRoot)]);
		const risk = await calculateTaskRisk(payload.taskId, accessor, brain);
		if (risk.riskScore >= RISK_HIGH) {
			const level = risk.riskScore >= .95 ? "critical" : "high";
			const factorNames = risk.factors.map((f) => f.name).join(", ");
			await observeBrain(projectRoot, {
				text: `Risk alert for ${payload.taskId} (score ${risk.riskScore.toFixed(2)}): ${factorNames}. ${risk.recommendation}`,
				title: `Task risk: ${level} — ${payload.taskId}`,
				type: "discovery",
				sourceType: "agent"
			});
		}
	} catch {}
}
hooks.register({
	id: "intelligence-task-start",
	event: "PreToolUse",
	handler: handleTaskStartIntelligence,
	priority: 50
});
//#endregion
//#region ../core/dist/hooks/handlers/conduit-hooks.js
/**
* Conduit Messaging Hook Handlers
*
* Captures orchestration lifecycle events (SubagentStart, SubagentStop,
* SessionEnd) and writes structured messages to conduit.db via LocalTransport.
* This is the DECOUPLED approach: hooks observe orchestration events without
* any changes to the orchestrate engine itself.
*
* Message format (JSON string stored as conduit message content):
*   { type, from, to, content, taskId, timestamp }
*
* All handlers are best-effort — failures are silently swallowed so that
* conduit writes NEVER crash or block agent orchestration.
*
* Auto-registers on module load.
*
* @task T268
*/
/** Well-known system agent ID used as the "from" sender for lifecycle messages. */
var SYSTEM_AGENT_ID = "cleo-orchestrator";
/** Well-known broadcast recipient for lifecycle events. */
var BROADCAST_RECIPIENT = "cleo-system";
/**
* Build the content string for a SubagentStart conduit message.
*
* @param payload - SubagentStart event payload.
* @returns JSON-serialised message content string.
*/
function buildSpawnMessageContent(payload) {
	return JSON.stringify({
		type: "subagent.spawn",
		from: SYSTEM_AGENT_ID,
		to: payload.agentId,
		content: `Subagent spawned: ${payload.agentId}${payload.role ? ` (${payload.role})` : ""}`,
		taskId: payload.taskId ?? null,
		timestamp: payload.timestamp
	});
}
/**
* Build the content string for a SubagentStop conduit message.
*
* @param payload - SubagentStop event payload.
* @returns JSON-serialised message content string.
*/
function buildCompletionMessageContent(payload) {
	return JSON.stringify({
		type: "subagent.complete",
		from: payload.agentId,
		to: BROADCAST_RECIPIENT,
		content: `Subagent completed: ${payload.agentId} status=${payload.status ?? "unknown"}${payload.taskId ? ` task=${payload.taskId}` : ""}`,
		taskId: payload.taskId ?? null,
		timestamp: payload.timestamp
	});
}
/**
* Build the content string for a SessionEnd handoff conduit message.
*
* @param payload  - SessionEnd event payload.
* @param nextTask - Optional next suggested task ID from the session.
* @returns JSON-serialised message content string.
*/
function buildHandoffMessageContent(payload, nextTask) {
	return JSON.stringify({
		type: "session.handoff",
		from: SYSTEM_AGENT_ID,
		to: BROADCAST_RECIPIENT,
		content: `Session ended: ${payload.sessionId}${nextTask ? ` \u2014 next task: ${nextTask}` : ""}`,
		taskId: nextTask ?? null,
		timestamp: payload.timestamp
	});
}
/**
* Attempt to create and connect a LocalTransport for conduit.db.
*
* Returns null when conduit.db is unavailable (not yet initialised), or when
* the connect call fails, so callers can bail out gracefully without throwing.
*
* The `transportFactory` parameter exists for testing: callers can inject a
* mock constructor so no real conduit.db is required in unit tests.
*
* @param projectRoot      - Absolute path to the project root directory.
* @param transportFactory - Optional factory used to construct the transport.
*                           Defaults to the `LocalTransport` class.
*/
async function tryGetLocalTransport(projectRoot, transportFactory = LocalTransport) {
	try {
		if (!transportFactory.isAvailable(projectRoot)) return null;
		const transport = new transportFactory();
		await transport.connect({
			agentId: SYSTEM_AGENT_ID,
			apiKey: "",
			apiBaseUrl: "local"
		});
		return transport;
	} catch {
		return null;
	}
}
/**
* Handle SubagentStart — send a spawn message to conduit.db.
*
* Writes a `subagent.spawn` message from `cleo-orchestrator` to the
* spawned agent ID so orchestrators and watchers can observe the event.
*
* Best-effort: failures are swallowed and logged at debug level.
*
* @param projectRoot - Absolute path to the project root directory.
* @param payload     - SubagentStart event payload.
*/
async function handleConduitSubagentStart(projectRoot, payload) {
	const transport = await tryGetLocalTransport(projectRoot);
	if (!transport) return;
	try {
		const content = buildSpawnMessageContent(payload);
		await transport.push(payload.agentId, content);
	} catch (err) {
		getLogger("conduit-hooks").debug({
			err,
			agentId: payload.agentId
		}, "conduit spawn write failed");
	} finally {
		try {
			await transport.disconnect();
		} catch {}
	}
}
/**
* Handle SubagentStop — send a completion message to conduit.db.
*
* Writes a `subagent.complete` message from the stopped agent to
* `cleo-system` so orchestrators can audit completion outcomes.
*
* Best-effort: failures are swallowed and logged at debug level.
*
* @param projectRoot - Absolute path to the project root directory.
* @param payload     - SubagentStop event payload.
*/
async function handleConduitSubagentStop(projectRoot, payload) {
	const transport = await tryGetLocalTransport(projectRoot);
	if (!transport) return;
	try {
		const content = buildCompletionMessageContent(payload);
		await transport.push(BROADCAST_RECIPIENT, content);
	} catch (err) {
		getLogger("conduit-hooks").debug({
			err,
			agentId: payload.agentId
		}, "conduit complete write failed");
	} finally {
		try {
			await transport.disconnect();
		} catch {}
	}
}
/**
* Handle SessionEnd — send a handoff message if a next task is suggested.
*
* Writes a `session.handoff` message to `cleo-system` when the session
* metadata includes a `nextTask` suggestion (stored in `metadata.nextTask`).
* The message lets waiting agents pick up where the session left off.
*
* Best-effort: failures are swallowed and logged at debug level.
*
* @param projectRoot - Absolute path to the project root directory.
* @param payload     - SessionEnd event payload.
*/
async function handleConduitSessionEnd(projectRoot, payload) {
	const transport = await tryGetLocalTransport(projectRoot);
	if (!transport) return;
	try {
		const content = buildHandoffMessageContent(payload, typeof payload.metadata?.nextTask === "string" ? payload.metadata.nextTask : void 0);
		await transport.push(BROADCAST_RECIPIENT, content);
	} catch (err) {
		getLogger("conduit-hooks").debug({
			err,
			sessionId: payload.sessionId
		}, "conduit handoff write failed");
	} finally {
		try {
			await transport.disconnect();
		} catch {}
	}
}
hooks.register({
	id: "conduit-subagent-start",
	event: "SubagentStart",
	handler: handleConduitSubagentStart,
	priority: 50
});
hooks.register({
	id: "conduit-subagent-stop",
	event: "SubagentStop",
	handler: handleConduitSubagentStop,
	priority: 50
});
hooks.register({
	id: "conduit-session-end",
	event: "SessionEnd",
	handler: handleConduitSessionEnd,
	priority: 8
});
//#endregion
//#region ../core/dist/sessions/agent-session-adapter.js
/**
* CLEO wrapper around `llmtxt/sdk.AgentSession` — T947 Step 2.
*
* Satisfies owner Constraint #4 (zero primitive duplication) per GitHub
* issue #96: CLEO no longer rolls its own session-level audit receipts,
* crypto-random session IDs, or manual state-transition writes. Those
* primitives come from llmtxt's {@link AgentSession}, and this module
* only adapts them to CLEO's project layout.
*
* Responsibilities:
*   - Open an {@link AgentSession} backed by a lightweight `standalone`
*     llmtxt backend scoped to `<projectRoot>/.cleo/llmtxt/`, ONLY when
*     llmtxt's optional peer dependencies (`better-sqlite3`,
*     `drizzle-orm/better-sqlite3`) are installed.
*   - Wrap a caller-provided async unit of work in `session.contribute()`
*     so the receipt's `eventCount` and (optionally) `documentIds` are
*     populated with programmatic proof.
*   - Close the session and emit a {@link ContributionReceipt}. The
*     receipt is appended to `.cleo/audit/receipts.jsonl` alongside the
*     existing ADR-051 evidence trail (`.cleo/audit/gates.jsonl`,
*     `.cleo/audit/force-bypass.jsonl`) so all tamper-evident records
*     live in one place on disk.
*   - Degrade gracefully when llmtxt peer deps are absent: every entry
*     point resolves to `{ result, receipt: null }` and NEVER throws,
*     so `cleo complete` / `cleo session` behaviour is unchanged in
*     minimal environments.
*
* Non-goals:
*   - Remote sync. We intentionally choose `topology: 'standalone'` so
*     the backend is a per-project SQLite file with zero network
*     surface. Cross-machine receipt replication is a Wave B concern.
*   - Replacing the existing CLEO `Session` record in `tasks.db`. This
*     adapter runs ALONGSIDE `startSession` / `endSession`; CLEO owns
*     session metadata, llmtxt owns the signed receipt.
*
* @task T947
* @adr ADR-051 (Gate Integrity & Evidence) — receipts are the session-
*      level companion to per-gate audit records.
* @see packages/core/src/tasks/gate-audit.ts
* @see packages/core/src/store/llmtxt-blob-adapter.ts (mirror peer-dep
*      lazy-load pattern)
*/
/**
* Resolve the absolute path of the receipts audit log.
*
* Mirrors the shape of {@link getGateAuditPath} so operators can glob
* `.cleo/audit/*.jsonl` to find every ADR-051 artifact.
*
* @param projectRoot - Absolute path to the project root.
* @returns Absolute path to `.cleo/audit/receipts.jsonl`.
*
* @task T947
* @adr ADR-051 §6
*/
function getReceiptsAuditPath(projectRoot) {
	return resolve(projectRoot, ".cleo", "audit", "receipts.jsonl");
}
/**
* Open an llmtxt AgentSession, or return `null` if llmtxt peer deps are
* absent. NEVER throws — peer-dep failure, filesystem failure, and
* backend-open failure all degrade to `null` and are logged at debug.
*
* The returned handle MUST be closed via {@link closeAgentSession} to
* flush the receipt and release SQLite handles.
*
* @param options - Session options (see {@link AgentSessionAdapterOptions}).
* @returns A handle, or `null` when the adapter cannot be initialised.
*
* @task T947
*/
async function openAgentSession(options = {}) {
	const projectRoot = resolveOrCwd(options.projectRoot);
	const agentId = options.agentId ?? process.env.CLEO_AGENT_ID ?? "cleo";
	let createBackendFn;
	let AgentSessionCtor;
	try {
		const [llmtxtMod, sdkMod] = await Promise.all([import('llmtxt'), import('llmtxt/sdk')]);
		createBackendFn = llmtxtMod.createBackend;
		AgentSessionCtor = sdkMod.AgentSession;
	} catch {
		return null;
	}
	let backend;
	try {
		backend = await createBackendFn({
			topology: "standalone",
			storagePath: resolve(projectRoot, ".cleo", "llmtxt")
		});
		await backend.open();
	} catch {
		return null;
	}
	try {
		const session = new AgentSessionCtor({
			backend,
			agentId,
			...options.sessionId ? { sessionId: options.sessionId } : {},
			...options.label ? { label: options.label } : {}
		});
		await session.open();
		return {
			session,
			projectRoot,
			closeBackend: async () => {
				try {
					await backend.close();
				} catch {}
			}
		};
	} catch {
		await backend.close().catch(() => {});
		return null;
	}
}
/**
* Close a previously-opened AgentSession and persist the receipt.
*
* Behaviour:
*   - Calls `session.close()` which emits a {@link ContributionReceipt}
*     (llmtxt handles all state transitions and Ed25519 signing once
*     llmtxt T461 ships).
*   - Appends the receipt to `.cleo/audit/receipts.jsonl` as a single
*     JSON line (matches `.cleo/audit/gates.jsonl`).
*   - Returns the receipt so callers (e.g. `cleo complete`) can surface
*     `receiptId` on the CLI envelope.
*   - Swallows `SESSION_CLOSE_PARTIAL` errors: the partial receipt is
*     still returned and persisted, matching llmtxt's best-effort
*     close contract.
*   - Returns `null` on any unexpected throw so teardown NEVER breaks
*     the caller.
*
* @param handle - Handle previously returned by {@link openAgentSession}.
* @returns The emitted receipt, or `null` on failure.
*
* @task T947
* @adr ADR-051 §6
*/
async function closeAgentSession(handle) {
	let receipt = null;
	try {
		receipt = await handle.session.close();
	} catch (err) {
		const maybePartial = err;
		if (maybePartial?.receipt !== void 0) receipt = maybePartial.receipt;
		else receipt = null;
	}
	if (receipt !== null) try {
		await persistReceipt(handle.projectRoot, receipt);
	} catch {}
	await handle.closeBackend();
	return receipt;
}
/**
* Wrap an async function inside an AgentSession, returning both the
* function's result and a persisted receipt (when peer deps allow).
*
* Contract:
*   - The wrapped `fn` ALWAYS runs. Its result is returned verbatim on
*     success, and its thrown error is re-thrown verbatim on failure.
*   - When peer deps are present, `fn` is invoked inside
*     {@link AgentSession.contribute}, so its returned object is
*     scanned for `documentId` / `documentIds` fields (see llmtxt spec
*     §3.3) and eventCount is incremented.
*   - When peer deps are absent, `fn` runs unwrapped and the returned
*     `receipt` is `null`.
*   - On error, no receipt is emitted (the session is closed with
*     zero eventCount — llmtxt spec §3.3 MUST NOT increment on error).
*     The error propagates.
*
* @typeParam T - Return type of the wrapped function.
* @param options - Adapter options (see {@link AgentSessionAdapterOptions}).
* @param fn - The async work unit to wrap.
* @returns An envelope with `result` (always present on success) and
*          `receipt` (null when peer deps unavailable).
*
* @example
* ```ts
* const { result, receipt } = await wrapWithAgentSession(
*   { sessionId, agentId: 'cleo', projectRoot: '/repo' },
*   async () => {
*     return applyTaskCompletion(task);
*   },
* );
* if (receipt) console.log(receipt.sessionId, receipt.eventCount);
* ```
*
* @task T947
* @adr ADR-051 §6
*/
async function wrapWithAgentSession(options, fn) {
	const handle = await openAgentSession(options);
	if (handle === null) return {
		result: await fn(),
		receipt: null
	};
	let result;
	try {
		result = await handle.session.contribute(async () => fn());
	} catch (err) {
		await handle.session.close().catch(() => {});
		await handle.closeBackend();
		throw err;
	}
	const receipt = await closeAgentSession(handle);
	return {
		result,
		receipt
	};
}
/**
* Append a {@link ContributionReceipt} to `.cleo/audit/receipts.jsonl`.
*
* Creates the audit directory on first use. Serialises the receipt as
* a single JSON line so the file remains valid JSON-lines.
*
* @param projectRoot - Absolute path to the project root.
* @param receipt - Receipt emitted by {@link AgentSession.close}.
*
* @internal
* @task T947
*/
async function persistReceipt(projectRoot, receipt) {
	const path = getReceiptsAuditPath(projectRoot);
	await mkdir(dirname(path), { recursive: true });
	await appendFile(path, `${JSON.stringify(receipt)}\n`, { encoding: "utf-8" });
}
//#endregion
//#region ../core/dist/render/ansi.js
/**
* Minimal ANSI escape primitives used by the shared render helpers.
*
* Kept tiny and self-contained so `@cleocode/core/render` has zero coupling
* to the cleo CLI's larger colors module. Respects `NO_COLOR` and
* `FORCE_COLOR` envs the same way (https://no-color.org).
*
* @task T10129
*/
/** Whether ANSI color escape codes should be emitted. */
var colorsEnabled = (() => {
	if (process.env["NO_COLOR"] !== void 0) return false;
	if (process.env["FORCE_COLOR"] !== void 0) return true;
	return process.stdout.isTTY === true;
})();
function ansi(code) {
	return colorsEnabled ? code : "";
}
/** Bold text. */
var BOLD = ansi("\x1B[1m");
/** Dim text. */
var DIM = ansi("\x1B[2m");
/** Reset (no-color). */
var NC = ansi("\x1B[0m");
//#endregion
//#region ../core/dist/render/helpers.js
/** Default single-glyph ellipsis appended by {@link truncateString}. */
var DEFAULT_ELLIPSIS = "…";
/**
* Truncate a plain (non-ANSI) string to at most `max` characters, appending an
* ellipsis when truncation occurs.
*
* This is the single SSoT for scalar-string truncation — it replaces the 6+
* ad-hoc `truncate(str, max)` helpers that were copy-pasted across
* `output-mode.ts`, `briefing.ts`, `memory/public-api.ts`, `gate-runner.ts`,
* and friends. The result length never exceeds `max`: the ellipsis is counted
* against the budget, so `truncateString('abcdef', 4)` returns `'abc…'`.
*
* For ANSI-colored content use {@link truncateVisible} (which counts visible
* columns and never splits an escape sequence); for windowing an *array* use
* {@link truncated}.
*
* @param value - The string to truncate. `null` / `undefined` collapse to `''`.
* @param max - Maximum length of the returned string (including the ellipsis).
* @param ellipsis - The suffix appended when truncated. Defaults to `'…'`.
*   Pass `'...'` for ASCII contexts.
* @returns The original string when it fits, otherwise a truncated string of
*   length ≤ `max`.
*
* @example
* ```ts
* truncateString('a very long title', 8);        // 'a very…'
* truncateString('a very long title', 8, '...');  // 'a ve...'
* truncateString(null, 10);                        // ''
* ```
*
* @task T11353
* @epic T11285
*/
function truncateString(value, max, ellipsis = DEFAULT_ELLIPSIS) {
	if (!value) return "";
	if (max <= 0) return "";
	if (value.length <= max) return value;
	if (ellipsis.length >= max) return value.slice(0, max);
	return value.slice(0, max - ellipsis.length) + ellipsis;
}
/**
* Read sessions from accessor or JSON file.
* Internal helper — not exposed via dispatch contracts.
* @task T4463
*/
async function readSessions(cwd, accessor) {
	return (accessor ?? await getTaskAccessor(cwd)).loadSessions();
}
/**
* Get current session status.
* Normalized Core signature: (projectRoot, params) → Result.
* @task T1450
*/
async function sessionStatus(projectRoot, _params) {
	return (await readSessions(projectRoot, await getTaskAccessor(projectRoot))).filter((s) => s.status === "active").sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())[0] ?? null;
}
/**
* List sessions with optional filtering.
* Normalized Core signature: (projectRoot, params) → Result.
* @task T1450
*/
async function listSessions(projectRoot, params) {
	let sessions = await readSessions(projectRoot, await getTaskAccessor(projectRoot));
	if (params.status) sessions = sessions.filter((s) => s.status === params.status);
	sessions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
	if (params.limit) sessions = sessions.slice(0, params.limit);
	for (const s of sessions) Object.assign(s, { _next: sessionListItemNext(s.id) });
	return sessions;
}

export { BOLD as B, DIM as D, NC as N, getLastHandoff as g, isAutoCaptureEnabled as i, listSessions as l, readSessions as r, sessionStatus as s, truncateString as t, wrapWithAgentSession as w };
//# sourceMappingURL=sessions-j73dkbCI.js.map
