/**
 * autonomous-solver.ts — PDD-shaped checkpoint contract for autonomous units.
 *
 * Purpose: make long-running autonomous work explicit, resumable, and auditable
 * instead of relying on a single agent turn to either finish or silently drift.
 *
 * Consumer: auto/phases.js injects the contract into each autonomous unit, and
 * bootstrap/db-tools.js records agent checkpoints via sf_autonomous_checkpoint.
 */
import {
	appendFileSync,
	mkdirSync,
	readFileSync,
	writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { atomicWriteSync } from "./atomic-write.js";
import { sfRoot } from "./paths.js";

export const AUTONOMOUS_SOLVER_OUTCOMES = [
	"continue",
	"complete",
	"blocked",
	"decide",
];

const MAX_RENDERED_ITEMS = 12;
const DEFAULT_SOLVER_MAX_ITERATIONS = 30000;
const MIN_SOLVER_MAX_ITERATIONS = 1;
const MAX_SOLVER_MAX_ITERATIONS = 100000;
const DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS = 4;
const SOLVER_CHECKPOINT_SCHEMA_VERSION = 1;
const SOLVER_STEERING_SCHEMA_VERSION = 1;

function solverDir(basePath) {
	return join(sfRoot(basePath), "runtime", "autonomous-solver");
}

function statePath(basePath) {
	return join(solverDir(basePath), "active.json");
}

function projectionPath(basePath) {
	return join(solverDir(basePath), "LOOP.md");
}

function historyPath(basePath) {
	return join(solverDir(basePath), "iterations.jsonl");
}

function steeringPath(basePath) {
	return join(solverDir(basePath), "steering.jsonl");
}

function nowIso() {
	return new Date().toISOString();
}

function sanitizeList(value) {
	if (!Array.isArray(value)) return [];
	return value.map((item) => String(item).trim()).filter(Boolean);
}

function readJson(path) {
	try {
		return JSON.parse(readFileSync(path, "utf-8"));
	} catch {
		return null;
	}
}

function sameUnit(state, unitType, unitId) {
	return state?.unitType === unitType && state?.unitId === unitId;
}

function clampNumber(value, min, max, fallback) {
	const n = Number(value);
	if (!Number.isFinite(n)) return fallback;
	return Math.max(min, Math.min(max, Math.floor(n)));
}

/**
 * Resolve the bounded autonomous solver iteration limit.
 *
 * Purpose: prevent a misconfigured or vague unit from retrying forever while
 * still letting projects raise the ceiling for large, explicitly bounded work.
 *
 * Consumer: runUnitPhase before dispatch and preferences resolution tests.
 */
export function getConfiguredAutonomousSolverMaxIterations(preferences) {
	return clampNumber(
		preferences?.auto_supervisor?.solver_max_iterations,
		MIN_SOLVER_MAX_ITERATIONS,
		MAX_SOLVER_MAX_ITERATIONS,
		DEFAULT_SOLVER_MAX_ITERATIONS,
	);
}

function renderList(items, fallback) {
	const list = sanitizeList(items).slice(0, MAX_RENDERED_ITEMS);
	if (list.length === 0) return `- ${fallback}`;
	return list.map((item) => `- ${item}`).join("\n");
}

function renderPdd(pdd = {}) {
	return [
		"## PDD Contract",
		`- Purpose: ${pdd.purpose || "not recorded yet"}`,
		`- Consumer: ${pdd.consumer || "not recorded yet"}`,
		`- Contract: ${pdd.contract || "not recorded yet"}`,
		`- Failure boundary: ${pdd.failureBoundary || "not recorded yet"}`,
		`- Evidence: ${pdd.evidence || "not recorded yet"}`,
		`- Non-goals: ${pdd.nonGoals || "not recorded yet"}`,
		`- Invariants: ${pdd.invariants || "not recorded yet"}`,
		`- Assumptions: ${pdd.assumptions || "not recorded yet"}`,
	].join("\n");
}

function renderProjection(state) {
	const checkpoint = state.latestCheckpoint ?? {};
	return [
		"# Autonomous Solver Loop",
		"",
		`- Unit: ${state.unitType} ${state.unitId}`,
		`- Status: ${state.status}`,
		`- Iteration: ${state.iteration} of ${state.maxIterations}`,
		`- Started: ${state.startedAt}`,
		`- Updated: ${state.updatedAt}`,
		"",
		"## Last Checkpoint",
		`- Outcome: ${checkpoint.outcome ?? "none"}`,
		`- Summary: ${checkpoint.summary ?? "none yet"}`,
		checkpoint.blockerReason ? `- Blocker: ${checkpoint.blockerReason}` : "",
		checkpoint.decisionQuestion
			? `- Decision needed: ${checkpoint.decisionQuestion}`
			: "",
		"",
		"## Completed This Iteration",
		renderList(checkpoint.completedItems, "Nothing recorded yet."),
		"",
		"## Remaining",
		renderList(
			checkpoint.remainingItems,
			"Unknown until the agent checkpoints.",
		),
		"",
		renderPdd(checkpoint.pdd),
		"",
		"## Verification Evidence",
		renderList(
			checkpoint.verificationEvidence,
			"No verification evidence recorded yet.",
		),
		"",
	]
		.filter((line) => line !== "")
		.join("\n");
}

function writeState(basePath, state) {
	const dir = solverDir(basePath);
	mkdirSync(dir, { recursive: true });
	atomicWriteSync(statePath(basePath), `${JSON.stringify(state, null, 2)}\n`);
	atomicWriteSync(projectionPath(basePath), renderProjection(state));
}

/**
 * Start or advance the persisted autonomous solver iteration for a unit.
 *
 * Purpose: each autonomous dispatch gets an explicit iteration number and
 * durable loop projection, so retries and restarts have concrete state.
 *
 * Consumer: runUnitPhase before sending the unit prompt.
 */
export function beginAutonomousSolverIteration(
	basePath,
	unitType,
	unitId,
	options = {},
) {
	const existing = readJson(statePath(basePath));
	const priorIteration =
		sameUnit(existing, unitType, unitId) && existing.status !== "complete"
			? Number(existing.iteration ?? 0)
			: 0;
	const maxIterations = clampNumber(
		options.maxIterations ?? existing?.maxIterations,
		MIN_SOLVER_MAX_ITERATIONS,
		MAX_SOLVER_MAX_ITERATIONS,
		DEFAULT_SOLVER_MAX_ITERATIONS,
	);
	const state = {
		unitType,
		unitId,
		status: "running",
		iteration: Math.max(1, priorIteration + 1),
		maxIterations,
		startedAt: sameUnit(existing, unitType, unitId)
			? existing.startedAt || nowIso()
			: nowIso(),
		updatedAt: nowIso(),
		latestCheckpoint: sameUnit(existing, unitType, unitId)
			? (existing.latestCheckpoint ?? null)
			: null,
		missingCheckpointRetry: null,
	};
	writeState(basePath, state);
	return state;
}

/**
 * Build the PDD autonomous solver prompt block appended to unit prompts.
 *
 * Purpose: bind every autonomous unit to bounded iterations, evidence, stop
 * signals, and the eight PDD fields instead of open-ended hidden retries.
 *
 * Consumer: runUnitPhase prompt injection.
 */
export function buildAutonomousSolverPromptBlock(state) {
	return [
		"## Autonomous Solver Loop Contract",
		"",
		`You are inside /autonomous iteration ${state.iteration} of ${state.maxIterations} for ${state.unitType} ${state.unitId}.`,
		"",
		"This is SF's built-in solver loop. It is not a separate Ralph workflow. Work one bounded, useful chunk; preserve enough state for the next autonomous iteration to continue without guessing.",
		"",
		"Hard requirement: before ending the turn, call the actual `sf_autonomous_checkpoint` tool. Writing SUMMARY.md, LOOP.md, task files, chat prose, or any other artifact is useful evidence, but it is not a checkpoint and does not satisfy this requirement.",
		"",
		"Call `sf_autonomous_checkpoint` with:",
		'- `outcome: "complete"` only when this unit\'s normal completion tool/artifact is also done.',
		'- `outcome: "continue"` when you made real progress but more autonomous iterations are needed.',
		'- `outcome: "blocked"` when the next step cannot proceed without unavailable facts, credentials, or a broken environment.',
		'- `outcome: "decide"` when there is a material product/architecture choice that must not be decided autonomously.',
		"",
		"Checkpoint the eight PDD fields every time:",
		"- Purpose: why this behavior exists and what value it protects.",
		"- Consumer: who or what uses it in production.",
		"- Contract: the observable behavior or artifact boundary.",
		"- Failure boundary: what failures must be contained or surfaced.",
		"- Evidence: commands, files, tests, or runtime observations proving progress.",
		"- Non-goals: what you intentionally did not solve this iteration.",
		"- Invariants: rules that must remain true across iterations.",
		"- Assumptions: uncertain facts you relied on and how to falsify them later.",
		"",
		"If you are executing an `execute-task` unit and the task is finished, `sf_task_complete` remains mandatory; `sf_autonomous_checkpoint` does not replace it.",
		"If you need another iteration, leave exact remaining items in the checkpoint rather than ending with vague prose.",
		"Your final autonomous action should be the checkpoint tool call unless a required completion tool such as sf_task_complete must be called immediately before it.",
	].join("\n");
}

/**
 * Record a solver checkpoint and update the markdown projection.
 *
 * Purpose: turn the agent's end-of-iteration status into structured autonomous
 * state that can be inspected, gated, and resumed.
 *
 * Consumer: sf_autonomous_checkpoint tool.
 */
export function appendAutonomousSolverCheckpoint(basePath, params) {
	const state =
		readJson(statePath(basePath)) ??
		beginAutonomousSolverIteration(basePath, params.unitType, params.unitId);
	const checkpoint = {
		schemaVersion: SOLVER_CHECKPOINT_SCHEMA_VERSION,
		ts: nowIso(),
		unitType: params.unitType,
		unitId: params.unitId,
		iteration: sameUnit(state, params.unitType, params.unitId)
			? state.iteration
			: 1,
		outcome: params.outcome,
		summary: String(params.summary ?? "").trim(),
		completedItems: sanitizeList(params.completedItems),
		remainingItems: sanitizeList(params.remainingItems),
		verificationEvidence: sanitizeList(params.verificationEvidence),
		blockerReason: params.blockerReason
			? String(params.blockerReason).trim()
			: undefined,
		decisionQuestion: params.decisionQuestion
			? String(params.decisionQuestion).trim()
			: undefined,
		pdd: {
			purpose: String(params.pdd?.purpose ?? "").trim(),
			consumer: String(params.pdd?.consumer ?? "").trim(),
			contract: String(params.pdd?.contract ?? "").trim(),
			failureBoundary: String(params.pdd?.failureBoundary ?? "").trim(),
			evidence: String(params.pdd?.evidence ?? "").trim(),
			nonGoals: String(params.pdd?.nonGoals ?? "").trim(),
			invariants: String(params.pdd?.invariants ?? "").trim(),
			assumptions: String(params.pdd?.assumptions ?? "").trim(),
		},
	};
	const nextState = {
		...state,
		unitType: params.unitType,
		unitId: params.unitId,
		status:
			params.outcome === "complete"
				? "complete"
				: params.outcome === "blocked" || params.outcome === "decide"
					? "paused"
					: "running",
		updatedAt: checkpoint.ts,
		latestCheckpoint: checkpoint,
	};
	mkdirSync(dirname(historyPath(basePath)), { recursive: true });
	writeFileSync(historyPath(basePath), `${JSON.stringify(checkpoint)}\n`, {
		flag: "a",
	});
	writeState(basePath, nextState);
	return checkpoint;
}

/**
 * Read the current persisted autonomous solver state.
 *
 * Purpose: status surfaces and loop enforcement need one structured source for
 * the active solver unit instead of scraping markdown projections.
 *
 * Consumer: /status, sf-progress, and runUnitPhase.
 */
export function readAutonomousSolverState(basePath) {
	return readJson(statePath(basePath));
}

/**
 * Record that a missing checkpoint repair dispatch has already been attempted.
 *
 * Purpose: enforce the checkpoint contract with one repair chance while
 * preventing an unbounded missing-checkpoint redispatch loop.
 *
 * Consumer: runUnitPhase after the first unit turn omits sf_autonomous_checkpoint.
 */
export function recordAutonomousSolverMissingCheckpointRetry(
	basePath,
	unitType,
	unitId,
	diagnosis = null,
) {
	const state = readJson(statePath(basePath));
	if (!sameUnit(state, unitType, unitId)) return null;
	const ts = nowIso();
	const attempts = getMissingCheckpointRepairAttempts(state);
	const nextAttempt = attempts.length + 1;
	const nextState = {
		...state,
		status: "running",
		updatedAt: ts,
		missingCheckpointRepairs: [
			...attempts,
			{
				attempt: nextAttempt,
				iteration: state.iteration,
				ts,
				mode: missingCheckpointRepairMode(nextAttempt),
				...(diagnosis ? { diagnosis } : {}),
			},
		],
		missingCheckpointRetry: {
			iteration: state.iteration,
			ts,
			attempt: nextAttempt,
			mode: missingCheckpointRepairMode(nextAttempt),
			...(diagnosis ? { diagnosis } : {}),
		},
	};
	writeState(basePath, nextState);
	return nextState;
}

/**
 * Classify why a solver turn omitted the checkpoint tool.
 *
 * Purpose: make the missing-checkpoint repair prompt adaptive and turn repeated
 * model failure patterns into self-feedback instead of manual operator forensics.
 *
 * Consumer: runUnitPhase before repair redispatch and before missing-checkpoint
 * pause/self-feedback.
 */
export function classifyAutonomousSolverMissingCheckpointFailure(messages) {
	const text = stringifyMessages(messages);
	const lower = text.toLowerCase();
	if (!text.trim()) {
		return {
			classification: "no-transcript",
			summary: "No agent-end transcript was available to classify.",
			evidence: "",
		};
	}
	const mentionsCheckpoint = lower.includes("sf_autonomous_checkpoint");
	const mentionsToolUnavailable =
		/(unknown|unavailable|not available|not found|no such) tool/.test(lower) ||
		(lower.includes("sf_autonomous_checkpoint") &&
			/(unavailable|not available|not found|unknown)/.test(lower));
	const mentionsToolFailure =
		lower.includes("error in sf_autonomous_checkpoint") ||
		(lower.includes("sf_autonomous_checkpoint") &&
			/(failed|error|exception|invalid)/.test(lower));
	const mentionsFileSubstitute =
		/\bsummary\.md\b/i.test(text) ||
		/\bloop\.md\b/i.test(text) ||
		/\bt\d+[-_].*summary\.md\b/i.test(text) ||
		lower.includes("checkpoint is now saved to") ||
		lower.includes("checkpoint saved to") ||
		lower.includes("summary file");
	const falselyClaimsSaved =
		(lower.includes("checkpoint") ||
			lower.includes("sf_autonomous_checkpoint")) &&
		/(saved|recorded|complete|now saved)/.test(lower);
	if (mentionsToolUnavailable) {
		return {
			classification: "checkpoint-tool-unavailable",
			summary: "The transcript suggests the checkpoint tool was unavailable.",
			evidence: truncateEvidence(text),
		};
	}
	if (mentionsToolFailure) {
		return {
			classification: "checkpoint-tool-failed",
			summary: "The transcript suggests the checkpoint tool call failed.",
			evidence: truncateEvidence(text),
		};
	}
	if (mentionsFileSubstitute) {
		return {
			classification: "file-substituted-for-checkpoint",
			summary:
				"The agent wrote or referenced a summary/projection file instead of calling the checkpoint tool.",
			evidence: truncateEvidence(text),
		};
	}
	if (falselyClaimsSaved) {
		return {
			classification: "claimed-checkpoint-without-tool",
			summary:
				"The agent claimed checkpoint completion without a successful checkpoint record.",
			evidence: truncateEvidence(text),
		};
	}
	if (mentionsCheckpoint) {
		return {
			classification: "mentioned-checkpoint-without-tool",
			summary:
				"The agent discussed sf_autonomous_checkpoint but did not record a checkpoint.",
			evidence: truncateEvidence(text),
		};
	}
	return {
		classification: "no-checkpoint-tool-call",
		summary: "The agent ended without calling sf_autonomous_checkpoint.",
		evidence: truncateEvidence(text),
	};
}

/**
 * Classify the completed solver turn into the next loop action.
 *
 * Purpose: make checkpoint outcomes authoritative for autonomous scheduling
 * instead of letting artifact heuristics silently override blocked, decision,
 * continue, or missing-checkpoint states.
 *
 * Consumer: runUnitPhase immediately after each unit turn.
 */
export function assessAutonomousSolverTurn(basePath, unitType, unitId) {
	const state = readJson(statePath(basePath));
	if (!sameUnit(state, unitType, unitId)) {
		return {
			action: "missing-checkpoint-retry",
			reason: "solver-missing-state",
			state,
		};
	}
	const checkpoint = state.latestCheckpoint ?? null;
	const hasCurrentCheckpoint =
		checkpoint?.unitType === unitType &&
		checkpoint?.unitId === unitId &&
		Number(checkpoint?.iteration) === Number(state.iteration);
	if (!hasCurrentCheckpoint) {
		const repairAttempts = getMissingCheckpointRepairAttempts(state).filter(
			(attempt) => Number(attempt.iteration) === Number(state.iteration),
		).length;
		if (repairAttempts >= DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS) {
			return {
				action: "pause",
				reason: "solver-missing-checkpoint",
				state,
				repairAttempts,
				maxRepairAttempts: DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS,
			};
		}
		return {
			action: "missing-checkpoint-retry",
			reason: "solver-missing-checkpoint",
			state,
			repairAttempt: repairAttempts + 1,
			maxRepairAttempts: DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS,
		};
	}
	if (
		state.iteration >= state.maxIterations &&
		checkpoint.outcome !== "complete"
	) {
		return {
			action: "pause",
			reason: "solver-max-iterations",
			state,
			checkpoint,
		};
	}
	if (checkpoint.outcome === "blocked" || checkpoint.outcome === "decide") {
		return {
			action: "pause",
			reason: `solver-${checkpoint.outcome}`,
			state,
			checkpoint,
		};
	}
	return {
		action: checkpoint.outcome === "continue" ? "continue" : "complete",
		reason: `solver-${checkpoint.outcome}`,
		state,
		checkpoint,
	};
}

/**
 * Append user steering for the next autonomous solver iteration.
 *
 * Purpose: active /steer must redirect the next bounded iteration without
 * interrupting the current tool batch or forcing an immediate agent turn.
 *
 * Consumer: /steer while autonomous mode is active.
 */
export function appendAutonomousSolverSteering(basePath, text, metadata = {}) {
	const trimmed = String(text ?? "").trim();
	if (!trimmed) return null;
	const entry = {
		schemaVersion: SOLVER_STEERING_SCHEMA_VERSION,
		id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
		ts: nowIso(),
		text: trimmed,
		consumedAt: null,
		...metadata,
	};
	mkdirSync(solverDir(basePath), { recursive: true });
	appendFileSync(steeringPath(basePath), `${JSON.stringify(entry)}\n`);
	return entry;
}

function readSteeringEntries(basePath) {
	try {
		return readFileSync(steeringPath(basePath), "utf-8")
			.split("\n")
			.filter((line) => line.trim())
			.map((line) => {
				try {
					return normalizeSteeringEntry(JSON.parse(line));
				} catch {
					return null;
				}
			})
			.filter(Boolean);
	} catch {
		return [];
	}
}

function normalizeSteeringEntry(entry) {
	if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
	const schemaVersion = entry.schemaVersion ?? SOLVER_STEERING_SCHEMA_VERSION;
	if (schemaVersion !== SOLVER_STEERING_SCHEMA_VERSION) return null;
	return {
		...entry,
		schemaVersion,
	};
}

/**
 * Consume pending steering exactly once for prompt injection.
 *
 * Purpose: every user steering note should influence the next solver iteration
 * once, without being replayed into every later dispatch.
 *
 * Consumer: runUnitPhase before appending the solver prompt block.
 */
export function consumePendingAutonomousSolverSteering(basePath) {
	const entries = readSteeringEntries(basePath);
	const pending = entries.filter((entry) => !entry.consumedAt);
	if (pending.length === 0) return [];
	const consumedAt = nowIso();
	const rewritten = entries.map((entry) =>
		entry.consumedAt ? entry : { ...entry, consumedAt },
	);
	mkdirSync(solverDir(basePath), { recursive: true });
	atomicWriteSync(
		steeringPath(basePath),
		rewritten.map((entry) => JSON.stringify(entry)).join("\n") + "\n",
	);
	return pending;
}

/**
 * Render consumed steering entries as a bounded prompt block.
 *
 * Purpose: keep user steering visible to the next iteration as explicit input
 * while preserving the checkpoint-driven solver contract.
 *
 * Consumer: runUnitPhase prompt injection.
 */
export function buildAutonomousSolverSteeringPromptBlock(entries) {
	const pending = Array.isArray(entries) ? entries : [];
	if (pending.length === 0) return "";
	return [
		"## Pending User Steering",
		"",
		"Apply these user overrides in this solver iteration:",
		...pending.map((entry) => `- ${String(entry.text ?? "").trim()}`),
	].join("\n");
}

/**
 * Build the one allowed repair prompt for a missing checkpoint.
 *
 * Purpose: give the agent a narrow chance to satisfy the solver contract before
 * autonomous mode pauses for inspection.
 *
 * Consumer: runUnitPhase when a turn ends without a current checkpoint.
 */
export function buildAutonomousSolverMissingCheckpointRepairPrompt(
	state,
	unitType,
	unitId,
	diagnosis = null,
	repairAttempt = 1,
	maxRepairAttempts = DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS,
) {
	const mode = missingCheckpointRepairMode(repairAttempt);
	const lines = [
		"## Checkpoint Required",
		"",
		`Your previous autonomous turn for ${unitType} ${unitId} ended without calling sf_autonomous_checkpoint for iteration ${state?.iteration ?? "unknown"}.`,
		`Repair attempt: ${repairAttempt} of ${maxRepairAttempts}.`,
		`Repair mode: ${mode}.`,
	];
	if (diagnosis?.classification) {
		lines.push(
			"",
			"Detected failure pattern:",
			`- ${diagnosis.classification}: ${diagnosis.summary ?? "missing checkpoint"}`,
		);
	}
	if (diagnosis?.classification === "no-transcript") {
		lines.push(
			"",
			"No transcript was captured from your previous turn. To reconstruct the checkpoint:",
			"1. Read .sf/runtime/autonomous-solver/LOOP.md to see what was expected.",
			"2. List files in the milestone/slice/task directories to find what artifacts exist.",
			"3. Read any SUMMARY.md or PLAN.md files to understand what progress was made.",
			"4. Based on the evidence, call sf_autonomous_checkpoint with the appropriate outcome and PDD fields.",
			"5. If you cannot determine what happened, use outcome='decide' and ask the human what the checkpoint should contain.",
		);
	} else if (repairAttempt <= 1) {
		lines.push("Do not continue implementation work in this repair turn.");
	} else {
		lines.push(
			"You may inspect transcript context and existing artifacts to reconstruct the checkpoint accurately; do not perform unrelated implementation work.",
		);
	}
	lines.push(
		"Inspect the work already performed, then call the actual sf_autonomous_checkpoint tool with the correct outcome and all eight PDD fields.",
		"Do not write a summary file as a substitute. Do not say the checkpoint is saved unless the sf_autonomous_checkpoint tool call succeeds.",
		"Your final action in this repair turn must be the sf_autonomous_checkpoint tool call.",
	);
	if (repairAttempt >= 2) {
		lines.push(
			"If artifacts show real progress, checkpoint that progress even if the prior turn did not explain it cleanly.",
		);
	}
	if (repairAttempt >= 3) {
		lines.push(
			'If your confidence that the reconstructed checkpoint is correct is below 0.98, call sf_autonomous_checkpoint with outcome="decide" and put the human acceptance question in decisionQuestion.',
		);
	}
	if (repairAttempt >= maxRepairAttempts) {
		lines.push(
			'This is the final automatic repair attempt. Prefer outcome="decide" over guessing; autonomous mode will pause with your decision question for human acceptance.',
		);
	}
	lines.push(
		"If no useful progress happened, use outcome=blocked and explain why.",
	);
	return lines.join("\n");
}

function getMissingCheckpointRepairAttempts(state) {
	if (Array.isArray(state?.missingCheckpointRepairs)) {
		return state.missingCheckpointRepairs;
	}
	if (state?.missingCheckpointRetry) return [state.missingCheckpointRetry];
	return [];
}

function missingCheckpointRepairMode(attempt) {
	if (attempt <= 1) return "strict-checkpoint-tool-only";
	if (attempt === 2) return "artifact-and-transcript-reconstruction";
	if (attempt === 3) return "confidence-gated-reconstruction";
	return "final-human-acceptance-gate";
}

function stringifyMessages(messages) {
	if (!Array.isArray(messages)) return "";
	return messages
		.map((message) => {
			if (typeof message === "string") return message;
			if (!message || typeof message !== "object") return String(message ?? "");
			if (typeof message.content === "string") return message.content;
			if (Array.isArray(message.content)) {
				return message.content
					.map((part) => {
						if (typeof part === "string") return part;
						if (part?.text) return String(part.text);
						return JSON.stringify(part);
					})
					.join("\n");
			}
			return JSON.stringify(message);
		})
		.join("\n");
}

function truncateEvidence(text, limit = 4000) {
	const value = String(text ?? "");
	return value.length > limit ? `${value.slice(0, limit)}\n[truncated]` : value;
}

/**
 * Return the latest solver checkpoint for a unit, if one exists.
 *
 * Purpose: let autonomous finalization react to semantic blocked/decision
 * outcomes without scraping prose from model output.
 *
 * Consumer: runUnitPhase after runUnit returns.
 */
export function readLatestAutonomousSolverCheckpoint(
	basePath,
	unitType,
	unitId,
) {
	const state = readJson(statePath(basePath));
	if (!sameUnit(state, unitType, unitId)) return null;
	return state.latestCheckpoint ?? null;
}
