/**
 * 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 checkpoint.
 */
import {
	appendFileSync,
	existsSync,
	mkdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { atomicWriteSync } from "./atomic-write.js";
import { emitJournalEvent } from "./journal.js";
import { sfRoot } from "./paths.js";
import { loadPrompt } from "./prompt-loader.js";

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

const MAX_RENDERED_ITEMS = 12;
const DEFAULT_SOLVER_MAX_ITERATIONS = 25;
const MIN_SOLVER_MAX_ITERATIONS = 1;
const MAX_SOLVER_MAX_ITERATIONS = 100000;
const DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS = 4;
export const MAX_CHECKPOINTS_PER_ITERATION = 5;
const SOLVER_CHECKPOINT_SCHEMA_VERSION = 1;
const SOLVER_STEERING_SCHEMA_VERSION = 1;
const STALL_THRESHOLD_ITERATIONS = 3;
const LOOP_DETECTION_WINDOW = 5;
const ROLLING_SUMMARY_WINDOW = 5;
const CLOSE_PHASE_LOOKAHEAD = 3;

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();
}

/**
 * Produce a short fingerprint for a checkpoint summary used in loop detection.
 * Normalises whitespace and lower-cases so minor phrasing differences don't
 * break deduplication, then takes the first 120 chars as the hash key.
 */
function summaryFingerprint(summary) {
	if (!summary || typeof summary !== "string") return "";
	return summary.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 120);
}

/**
 * Detect a stalled solver loop: true when all recent fingerprints are identical,
 * indicating the agent is repeating the same action without making progress.
 */
export function detectSolverLoop(recentSummaryHashes) {
	if (!Array.isArray(recentSummaryHashes)) return false;
	const window = recentSummaryHashes.slice(-LOOP_DETECTION_WINDOW);
	if (window.length < LOOP_DETECTION_WINDOW) return false;
	const first = window[0];
	if (!first) return false;
	return window.every((h) => h === first);
}

/**
 * Determine the current solver phase from iteration counters.
 * Returns "orient" | "execute" | "close".
 *
 * Purpose: phase-aware prompts sharpen agent focus — orient in early iters,
 * execute in the middle, and close hard in the final stretch.
 *
 * Consumer: buildAutonomousSolverPromptBlock.
 */
export function getSolverPhase(iteration, maxIterations) {
	const iter = Number(iteration) || 1;
	const max = Number(maxIterations) || DEFAULT_SOLVER_MAX_ITERATIONS;
	if (iter <= 2) return "orient";
	if (iter >= max - CLOSE_PHASE_LOOKAHEAD + 1) return "close";
	return "execute";
}

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");
}

const REQUIRED_PDD_FIELDS = [
	["purpose", "Purpose"],
	["consumer", "Consumer"],
	["contract", "Contract"],
	["failureBoundary", "Failure boundary"],
	["evidence", "Evidence"],
	["nonGoals", "Non-goals"],
	["invariants", "Invariants"],
	["assumptions", "Assumptions"],
];

/**
 * Return missing PDD field labels for a checkpoint payload.
 *
 * Purpose: make ADR-0000's purpose gate executable instead of leaving the
 * eight-field contract as prompt-only guidance.
 *
 * Consumer: assessAutonomousSolverTurn before accepting continue/complete
 * checkpoint outcomes.
 */
export function missingPddFieldLabels(pdd = {}) {
	return REQUIRED_PDD_FIELDS.filter(
		([key]) => !String(pdd?.[key] ?? "").trim(),
	).map(([, label]) => label);
}

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,
		// Stall and loop tracking carried across iterations
		iterationsSinceProgress: sameUnit(existing, unitType, unitId)
			? Number(existing.iterationsSinceProgress) || 0
			: 0,
		lastProgressAt: sameUnit(existing, unitType, unitId)
			? (existing.lastProgressAt ?? null)
			: null,
		recentSummaryHashes: sameUnit(existing, unitType, unitId)
			? Array.isArray(existing.recentSummaryHashes)
				? existing.recentSummaryHashes
				: []
			: [],
		recentCheckpointSummaries: sameUnit(existing, unitType, unitId)
			? Array.isArray(existing.recentCheckpointSummaries)
				? existing.recentCheckpointSummaries
				: []
			: [],
		// Safety cap: how many checkpoints have been written this iteration
		checkpointCountThisIteration: 0,
		// Per-unit budget for executor-refusal-driven re-dispatches. Reset when
		// the unit changes; persists across iterations of the same unit so a
		// resumed run does not silently get a fresh budget.
		executorRefusalEscalations: sameUnit(existing, unitType, unitId)
			? Number(existing.executorRefusalEscalations) || 0
			: 0,
	};
	writeState(basePath, state);
	return state;
}

/**
 * Maximum number of executor-refusal-driven re-dispatches for a single unit
 * before the loop falls back to the legacy blocked-and-pause behavior. Three
 * gives the router two tier escalations after the initial refusal (typical
 * tier ladder depth), which is plenty without enabling runaway loops if every
 * fallback model also refuses.
 */
export const MAX_EXECUTOR_REFUSAL_ESCALATIONS = 3;

/**
 * Increment the per-unit executor-refusal escalation counter and return the
 * new count.
 *
 * Purpose: when classifyExecutorRefusal fires, the refusal handler blocks the
 * refusing model and re-dispatches the unit so selectAndApplyModel picks a
 * higher-tier alternative. This counter bounds that retry loop so an entire
 * fallback chain of refusing models cannot loop forever — once the budget is
 * exhausted, the unit pauses for operator intervention via the legacy
 * blocked-checkpoint path.
 *
 * Consumer: runUnitPhase refusal branch in auto/phases-unit.js.
 */
export function recordExecutorRefusalEscalation(basePath, unitType, unitId) {
	const state = readJson(statePath(basePath));
	if (!sameUnit(state, unitType, unitId)) return 0;
	const next = (Number(state.executorRefusalEscalations) || 0) + 1;
	writeState(basePath, {
		...state,
		executorRefusalEscalations: next,
		updatedAt: nowIso(),
	});
	return next;
}

/**
 * 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.
 * Phase-aware: ORIENT (iters 1-2) focuses on reading and planning; EXECUTE
 * (middle) on implementation; CLOSE (final 3) on verifying and wrapping up.
 * Stall/loop signals are injected when the system detects no progress.
 *
 * @param {object} state - Active solver state
 * @returns {object} Variables for the template
 */
function buildAutonomousLoopVars(state) {
	const phase = getSolverPhase(state.iteration, state.maxIterations);
	const stalled =
		Number(state.iterationsSinceProgress) >= STALL_THRESHOLD_ITERATIONS;
	const looping = detectSolverLoop(state.recentSummaryHashes);

	// ── Phase header ────────────────────────────────────────────────────────
	const phaseHeaders = {
		orient:
			"ORIENT PHASE (iterations 1-2): Your priority is to read, understand, and plan — not to write code.\n" +
			"Read all relevant artifacts: task plans, slice plans, DECISIONS.md, REQUIREMENTS.md, CONTEXT.md.\n" +
			"Identify what already exists, what must be built, and what the acceptance criteria are.\n" +
			"End this iteration with a concrete plan of action recorded in your checkpoint's remainingItems.",
		execute:
			"EXECUTE PHASE: You are in the implementation stretch. Make concrete, verifiable progress each iteration.\n" +
			"Each iteration must produce at least one new artifact, passing test, or measurable change.\n" +
			"Record what you completed and what remains — do not repeat the same actions as prior iterations.",
		close:
			`CLOSE PHASE (final ${CLOSE_PHASE_LOOKAHEAD} iterations): You are approaching the iteration budget.\n` +
			"Priority: verify all acceptance criteria, run the test suite, and confirm the unit is complete.\n" +
			'If the unit cannot be completed in the remaining iterations, checkpoint with outcome="blocked" and a precise reason.\n' +
			"Do NOT start new work — finish and verify existing work.",
	};

	const vars = {
		unitType: state.unitType,
		unitId: state.unitId,
		iteration: state.iteration,
		maxIterations: state.maxIterations,
		phaseHeader: phaseHeaders[phase],
		stallWarning: "",
		loopWarning: "",
		recentHistory: "",
	};

	// ── Stall injection ─────────────────────────────────────────────────────
	if (stalled) {
		vars.stallWarning = [
			"",
			`⚠️  STALL DETECTED: ${state.iterationsSinceProgress} iterations without new completedItems recorded.`,
			"You are repeating work without making measurable progress. Before continuing:",
			"1. Read your last checkpoint's remainingItems — are they still accurate?",
			"2. Are you blocked by something that requires a different approach?",
			"3. Try a DIFFERENT strategy from previous iterations — do not repeat the same steps.",
			"If there is a concrete blocker, use outcome='blocked' with a precise blockerReason.",
		].join("\n");
	}

	// ── Loop detection injection ─────────────────────────────────────────────
	if (looping) {
		const lastSummary = state.recentSummaryHashes?.slice(-1)[0] ?? "";
		vars.loopWarning = [
			"",
			`🔁  LOOP DETECTED: Your last ${LOOP_DETECTION_WINDOW} checkpoint summaries are identical or nearly identical.`,
			`Pattern: "${lastSummary.slice(0, 80)}..."`,
			"You are in a loop. You MUST take a different action this iteration:",
			"- Try a completely different approach or tool.",
			"- Re-read the task plan from scratch — are you solving the right problem?",
			"- If the task is actually done, checkpoint with outcome='complete'.",
			"- If you cannot break the loop, checkpoint with outcome='blocked' and name the specific obstacle.",
		].join("\n");
	}

	// ── Rolling summary of recent iterations ────────────────────────────────
	const summaries = Array.isArray(state.recentCheckpointSummaries)
		? state.recentCheckpointSummaries.filter(Boolean)
		: [];
	if (summaries.length > 0) {
		vars.recentHistory = [
			"",
			`## Recent Iteration History (last ${summaries.length})`,
			...summaries.map(
				(s, i) => `- Iter ${state.iteration - summaries.length + i}: ${s}`,
			),
		].join("\n");
	}

	return vars;
}

/**
 * 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.
 * Phase-aware: ORIENT (iters 1-2) focuses on reading and planning; EXECUTE
 * (middle) on implementation; CLOSE (final 3) on verifying and wrapping up.
 * Stall/loop signals are injected when the system detects no progress.
 *
 * Consumer: runUnitPhase prompt injection (solver pass).
 */
export function buildAutonomousSolverPromptBlock(state, vars = {}) {
	const templateVars = {
		workingDirectory: "/repo", // Fallback for tests or missing context
		...buildAutonomousLoopVars(state),
		...vars,
	};
	return loadPrompt("autonomous-solver-contract", templateVars);
}

/**
 * Build the executor prompt block (no checkpoint requirement).
 *
 * Purpose: the executor focuses on doing the unit work. A separate solver pass
 * reads the executor transcript and emits the canonical checkpoint.
 *
 * Consumer: runUnitPhase prompt injection (executor pass).
 */
export function buildAutonomousExecutorPromptBlock(state, vars = {}) {
	const templateVars = {
		workingDirectory: "/repo", // Fallback for tests or missing context
		...buildAutonomousLoopVars(state),
		...vars,
	};
	return loadPrompt("autonomous-executor-contract", templateVars);
}

/**
 * Build the solver pass prompt that reads an executor transcript.
 *
 * Purpose: give the stable solver model the executor transcript and instruct it
 * to classify what happened and emit the canonical checkpoint.
 *
 * Consumer: runUnitPhase after the executor pass returns.
 */
export function buildSolverPassPrompt(
	executorTranscript,
	state,
	unitType,
	unitId,
) {
	const transcriptText = stringifyMessages(executorTranscript);
	const refusal = classifyExecutorRefusal(executorTranscript);

	const vars = {
		unitType,
		unitId,
		iteration: state?.iteration ?? "unknown",
		maxIterations: state?.maxIterations ?? "unknown",
		executorTranscript: transcriptText,
		refusalMarker: refusal
			? [
					`⚠️  Refusal pattern detected: ${refusal.pattern}.`,
					"The executor refused the task. Emit outcome='blocked' with blockerReason='executor-refused'.",
					"",
				].join("\n")
			: "",
	};

	return loadPrompt("autonomous-solver-pass", vars);
}

/**
 * Detect whether an executor transcript contains zero meaningful work.
 *
 * Purpose: no-op iterations (continue checkpoint with zero file/tool activity)
 * must not satisfy the repair gate.
 *
 * Consumer: assessAutonomousSolverTurn to reject no-op continues.
 *
 * Implementation: structural inspection only. We look for evidence that the
 * executor actually invoked tools, in either of the two message shapes used
 * across SF's provider runtimes:
 *
 *   1. Anthropic-style: `msg.content` is an array of blocks; tool activity
 *      shows as `{ type: "tool_use", name: ... }` (assistant) or
 *      `{ type: "tool_result", ... }` (user/tool role). This is the shape
 *      Claude messages take when stored in pi's agent_end events (see
 *      undo.js:431-447 which uses the same pattern to extract tool_result
 *      content).
 *   2. OpenAI-style: `msg.tool_calls` array on the assistant message and
 *      `msg.role === "tool"` (or "tool_result") with `msg.name` on the
 *      reply. Used by OpenAI-compatible providers.
 *
 * A `checkpoint` tool call by itself doesn't count as work — that's the
 * protocol step, not the unit deliverable. Any other named tool counts.
 *
 * We deliberately do NOT grep prose ("File edited", "```diff", …). Prose
 * patterns are runtime-specific and produce false negatives that mark real
 * work as no-op, which would synthesize a blocker over completed iterations.
 * If a transcript has zero structural tool activity, it really is a no-op
 * even if its prose is plausible.
 */
export function isNoOpExecutorTranscript(messages) {
	if (!Array.isArray(messages) || messages.length === 0) return true;

	// Refusal is always a no-op
	if (classifyExecutorRefusal(messages)) return true;

	const isWorkToolName = (name) => {
		if (!name || typeof name !== "string") return false;
		// `checkpoint` is the protocol; the executor calling it is not unit work.
		// (Per ADR-0079 the executor isn't even supposed to call it.) Anything
		// else — reads, writes, bash, complete_task, save_summary — counts.
		return name !== "checkpoint";
	};

	for (const msg of messages) {
		if (!msg || typeof msg !== "object") continue;

		// ── Anthropic-style: content is an array of typed blocks ──
		if (Array.isArray(msg.content)) {
			for (const block of msg.content) {
				if (!block || typeof block !== "object") continue;
				if (block.type === "tool_use" && isWorkToolName(block.name)) {
					return false;
				}
			}
		}

		// ── OpenAI-style: msg.tool_calls on assistant ──
		if (Array.isArray(msg.tool_calls)) {
			for (const tc of msg.tool_calls) {
				const name = tc?.function?.name ?? tc?.name ?? "";
				if (isWorkToolName(name)) return false;
			}
		}

		// ── OpenAI-style: tool reply rows ──
		if (msg.role === "tool" || msg.role === "tool_result") {
			if (isWorkToolName(msg.name)) return false;
		}
	}

	return true;
}

/**
 * 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: checkpoint tool.
 */
export function appendAutonomousSolverCheckpoint(basePath, params) {
	const persisted = readJson(statePath(basePath));
	const state =
		persisted ??
		beginAutonomousSolverIteration(basePath, params.unitType, params.unitId);
	// ── Sticky identity guard ──
	// The orchestrator owns the active unit identity (it called
	// beginAutonomousSolverIteration with the canonical unitType/unitId).
	// If the agent's checkpoint call passes a *different* unitType/unitId
	// because it guessed wrong (real-world: minimax/M2.1 stuck at
	// 2026-05-13 calling checkpoint with `parallel-research` /
	// `1-ci-build-pipeline/parallel-research` / `research-slice 1-...`
	// — three different strings — none matching the orchestrator's
	// active identity), the previous implementation silently overwrote
	// state.unitType/unitId with the wrong claim. assessAutonomousSolverTurn
	// then failed sameUnit() against the orchestrator's identity, fired
	// missing-checkpoint-retry, the agent re-checkpointed with another
	// wrong guess, and the loop ran indefinitely (60+ wasted calls).
	//
	// Fix: when there is an active running/paused state, pin the
	// checkpoint to the active state's identity instead of trusting the
	// agent's claim. Surface the mismatch on the checkpoint payload so it
	// is visible in traces.
	const hasActiveIdentity =
		persisted &&
		persisted.unitType &&
		persisted.unitId &&
		persisted.status !== "complete";
	const isMismatch =
		hasActiveIdentity && !sameUnit(persisted, params.unitType, params.unitId);
	if (isMismatch) {
		emitJournalEvent(basePath, {
			flowId: `${state.unitType}-${state.unitId}-${Date.now()}`,
			seq: 1,
			ts: nowIso(),
			eventType: "checkpoint-identity-mismatch",
			data: {
				claimedUnitType: params.unitType,
				claimedUnitId: params.unitId,
				pinnedToUnitType: state.unitType,
				pinnedToUnitId: state.unitId,
			},
		});
	}
	const effectiveUnitType = isMismatch ? state.unitType : params.unitType;
	const effectiveUnitId = isMismatch ? state.unitId : params.unitId;
	const checkpoint = {
		schemaVersion: SOLVER_CHECKPOINT_SCHEMA_VERSION,
		ts: nowIso(),
		unitType: effectiveUnitType,
		unitId: effectiveUnitId,
		// Iteration must match the orchestrator's current iteration so
		// assessAutonomousSolverTurn's hasCurrentCheckpoint check passes
		// and the outcome (especially `complete`) is honored.
		iteration: state.iteration,
		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(),
		},
		// Diagnostic: when the agent's claim differs from the active unit,
		// record both so trace consumers can flag the model's confusion.
		...(isMismatch
			? {
					mismatchedIdentity: {
						claimedUnitType: String(params.unitType ?? ""),
						claimedUnitId: String(params.unitId ?? ""),
						pinnedToActive: { unitType: state.unitType, unitId: state.unitId },
					},
				}
			: {}),
	};
	const nextState = {
		...state,
		unitType: effectiveUnitType,
		unitId: effectiveUnitId,
		status:
			params.outcome === "complete"
				? "complete"
				: params.outcome === "blocked"
					? "paused"
					: "running",
		updatedAt: checkpoint.ts,
		latestCheckpoint: checkpoint,
		// ── Stall tracking ────────────────────────────────────────────────────
		// Progress is measured by whether completedItems grew vs the prior checkpoint.
		// Stall counter resets on any real progress; increments otherwise.
		...(() => {
			const priorCompleted = sanitizeList(
				state.latestCheckpoint?.completedItems,
			).length;
			const newCompleted = checkpoint.completedItems.length;
			const madeProgress = newCompleted > priorCompleted;
			const prevStall = Number(state.iterationsSinceProgress) || 0;
			return {
				iterationsSinceProgress: madeProgress ? 0 : prevStall + 1,
				lastProgressAt: madeProgress
					? checkpoint.ts
					: (state.lastProgressAt ?? checkpoint.ts),
			};
		})(),
		// ── Loop detection: ring buffer of last N summary fingerprints ─────────
		recentSummaryHashes: [
			...(Array.isArray(state.recentSummaryHashes)
				? state.recentSummaryHashes
				: []),
			summaryFingerprint(checkpoint.summary),
		].slice(-LOOP_DETECTION_WINDOW),
		// ── Rolling summary window: last N checkpoint summaries for context ────
		recentCheckpointSummaries: [
			...(Array.isArray(state.recentCheckpointSummaries)
				? state.recentCheckpointSummaries
				: []),
			checkpoint.summary,
		].slice(-ROLLING_SUMMARY_WINDOW),
		// Increment checkpoint count for this iteration (safety cap)
		checkpointCountThisIteration: (state.checkpointCountThisIteration || 0) + 1,
	};
	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));
}

/**
 * Clear the active autonomous solver projection when its owning auto.lock is gone.
 *
 * Purpose: prevent a crashed or timed-out autonomous owner from leaving
 * `.sf/runtime/autonomous-solver/active.json` in `running`, which makes later
 * status and dispatch paths believe a unit is still live.
 *
 * Consumer: UOK diagnostics repair when no live auto.lock owner exists.
 */
export function clearAutonomousSolverState(basePath) {
	rmSync(statePath(basePath), { force: true });
	rmSync(projectionPath(basePath), { force: true });
}

/**
 * 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 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;
}

/**
 * Detect that the executor model refused the task outright (rather than
 * attempting and failing the protocol).
 *
 * Why: when a routed executor model (e.g. a code-completion model like
 * Codestral) lacks the agentic capabilities required for the unit, it emits a
 * generic refusal — "I'm sorry, I currently don't have the necessary tools to
 * assist with that specific request." The existing missing-checkpoint repair
 * loop will dutifully re-prompt the same model until it emits a syntactically
 * valid checkpoint with zero work, fabricating forward progress. Refusal must
 * be caught earlier and surfaced as a `blocked` outcome so the loop pauses (or
 * the executor model can be escalated on retry) rather than synthesizing a
 * `continue` over no work.
 *
 * Returns null when no refusal pattern is detected.
 *
 * Consumer: runUnitPhase short-circuits the repair loop on a positive match.
 */
export function classifyExecutorRefusal(messages) {
	const text = stringifyMessages(messages);
	if (!text.trim()) return null;
	const lower = text.toLowerCase();
	const patterns = [
		{
			id: "apology-no-tools",
			regex:
				/i(?:'m| am)\s+sorry[^.]{0,80}(?:don't|do not|cannot|can't)\s+have\s+(?:the\s+)?(?:necessary\s+)?tools?/i,
		},
		{
			id: "cannot-assist",
			regex:
				/i\s+(?:cannot|can't|am unable to|won't be able to)\s+(?:assist|help)\s+with\s+(?:that|this)/i,
		},
		{
			id: "not-able-to-help",
			regex:
				/i\s+(?:am\s+not\s+able\s+to|do not have the ability to|don't have the ability to)\s+(?:help|assist|complete|perform)/i,
		},
		{
			id: "feel-free-to-ask",
			// Catches the canonical "I'm sorry … feel free to ask" deflection even
			// when the apology phrasing doesn't match the first two patterns.
			regex:
				/(?:i(?:'m| am)\s+sorry|i\s+apologi[sz]e)[\s\S]{0,200}feel\s+free\s+to\s+ask/i,
		},
		{
			id: "outside-capabilities",
			regex:
				/(?:that's|that is|this is)\s+(?:outside|beyond)\s+(?:my|the)\s+(?:capabilities|abilities|scope)/i,
		},
	];
	for (const pattern of patterns) {
		if (pattern.regex.test(lower) || pattern.regex.test(text)) {
			return {
				classification: "executor-refused",
				pattern: pattern.id,
				summary:
					"The executor model refused the task rather than attempting it. This is a capability/routing problem, not a protocol problem — repairing the prompt will not produce progress.",
				evidence: truncateEvidence(text),
			};
		}
	}
	return null;
}

/**
 * Memoized lookup: is the `checkpoint` tool registered in the SF extension
 * manifest? Used by `classifyAutonomousSolverMissingCheckpointFailure` to
 * disambiguate "agent says tool is unavailable" from "agent mentioned a real
 * tool but did not call it."
 *
 * Why memoized: the previous implementation read the manifest from disk on
 * every classifier call, with CWD-sensitive path probing — surprising hidden
 * I/O inside what reads like a pure function, and a test-unfriendly coupling.
 * The manifest does not change while the process is running, so a single
 * memoized read at first call is correct and fast.
 *
 * Callers that want test-time control (or are running in environments where
 * the manifest can't be located, e.g. CI fixtures) pass an explicit
 * `checkpointToolRegistered` override to the classifier instead — no need to
 * stub the filesystem.
 */
let _checkpointToolRegisteredCache = null;
function isCheckpointToolRegisteredFromManifest() {
	if (_checkpointToolRegisteredCache !== null) {
		return _checkpointToolRegisteredCache;
	}
	try {
		const manifestPath = join(
			process.cwd(),
			"dist",
			"resources",
			"extensions",
			"sf",
			"extension-manifest.json",
		);
		const srcManifestPath = join(
			process.cwd(),
			"src",
			"resources",
			"extensions",
			"sf",
			"extension-manifest.json",
		);
		const manifestContent = existsSync(manifestPath)
			? readFileSync(manifestPath, "utf-8")
			: existsSync(srcManifestPath)
				? readFileSync(srcManifestPath, "utf-8")
				: null;
		if (!manifestContent) {
			_checkpointToolRegisteredCache = false;
			return false;
		}
		const manifest = JSON.parse(manifestContent);
		_checkpointToolRegisteredCache =
			Array.isArray(manifest?.provides?.tools) &&
			manifest.provides.tools.includes("checkpoint");
		return _checkpointToolRegisteredCache;
	} catch {
		_checkpointToolRegisteredCache = false;
		return false;
	}
}

/**
 * Test-only escape hatch to reset the manifest-lookup memoization. Tests that
 * exercise the classifier under different "is checkpoint registered" assumptions
 * should prefer the explicit `options.checkpointToolRegistered` override on
 * `classifyAutonomousSolverMissingCheckpointFailure` — this function exists
 * only as a safety net for tests that need to clear a polluted module cache.
 */
export function _resetCheckpointToolRegisteredCacheForTests() {
	_checkpointToolRegisteredCache = null;
}

/**
 * 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.
 *
 * @param {Array} messages
 * @param {object} [options]
 * @param {boolean} [options.checkpointToolRegistered] - Explicit override for
 *   the "is the checkpoint tool registered in our manifest?" question. When
 *   omitted, falls back to the memoized manifest lookup. Tests should pass
 *   this explicitly so they don't depend on CWD or on-disk dist/ state.
 */
export function classifyAutonomousSolverMissingCheckpointFailure(
	messages,
	options = {},
) {
	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("checkpoint");
	// Resolve "is checkpoint registered" — explicit override wins, otherwise
	// fall back to the memoized manifest lookup.
	const checkpointToolIsRegistered =
		typeof options.checkpointToolRegistered === "boolean"
			? options.checkpointToolRegistered
			: isCheckpointToolRegisteredFromManifest();
	const mentionsToolUnavailable =
		/(unknown|unavailable|not available|not found|no such) tool/.test(lower) ||
		(lower.includes("checkpoint") &&
			/(unavailable|not available|not found|unknown)/.test(lower));
	const mentionsToolFailure =
		lower.includes("error in checkpoint") ||
		(lower.includes("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("checkpoint")) &&
		/(saved|recorded|complete|now saved)/.test(lower);
	if (mentionsToolUnavailable) {
		// Tool reported as unavailable but IS registered in manifest — agent mentioned
		// it without calling it. Reclassify to avoid repeated self-referential repair.
		return {
			classification: checkpointToolIsRegistered
				? "mentioned-checkpoint-without-tool"
				: "checkpoint-tool-unavailable",
			summary: checkpointToolIsRegistered
				? "Agent reported tool unavailable but it is registered — classified as mentioned-without-call."
				: "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 checkpoint but did not record a checkpoint.",
			evidence: truncateEvidence(text),
		};
	}
	return {
		classification: "no-checkpoint-tool-call",
		summary: "The agent ended without calling 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,
	executorMessages = null,
) {
	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,
		};
	}
	// Hard cap on excessive checkpoints within a single iteration. This check
	// must run before repair classifications such as empty/no-op continue; once
	// the cap is reached, launching another repair turn only amplifies the loop.
	if (
		(state.checkpointCountThisIteration || 0) >= MAX_CHECKPOINTS_PER_ITERATION
	) {
		return {
			action: "pause",
			reason: "solver-excessive-checkpoints",
			state,
			checkpoint,
			checkpointCount: state.checkpointCountThisIteration,
			maxCheckpointCount: MAX_CHECKPOINTS_PER_ITERATION,
		};
	}
	const missingPdd = missingPddFieldLabels(checkpoint.pdd);
	if (checkpoint.outcome !== "blocked" && missingPdd.length > 0) {
		return {
			action: "pause",
			reason: "solver-purpose-gate",
			state,
			checkpoint,
			missingPddFields: missingPdd,
			message: `BLOCKED: purpose unclear - ${missingPdd.join(", ")}`,
		};
	}
	if (
		(checkpoint.outcome === "continue" || checkpoint.outcome === "decide") &&
		(checkpoint.remainingItems?.length ?? 0) === 0
	) {
		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-empty-continue",
				state,
				repairAttempts,
				maxRepairAttempts: DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS,
				checkpoint,
			};
		}
		return {
			action: "missing-checkpoint-retry",
			reason: "solver-empty-continue",
			state,
			repairAttempt: repairAttempts + 1,
			maxRepairAttempts: DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS,
			checkpoint,
		};
	}
	if (
		state.iteration >= state.maxIterations &&
		checkpoint.outcome !== "complete"
	) {
		return {
			action: "pause",
			reason: "solver-max-iterations",
			state,
			checkpoint,
		};
	}
	if (checkpoint.outcome === "blocked") {
		return {
			action: "pause",
			reason: `solver-${checkpoint.outcome}`,
			state,
			checkpoint,
		};
	}
	// No-op detection: a continue with zero work is not real progress
	if (
		(checkpoint.outcome === "continue" || checkpoint.outcome === "decide") &&
		executorMessages &&
		isNoOpExecutorTranscript(executorMessages)
	) {
		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-noop-continue",
				state,
				repairAttempts,
				maxRepairAttempts: DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS,
				checkpoint,
			};
		}
		return {
			action: "missing-checkpoint-retry",
			reason: "solver-noop-continue",
			state,
			repairAttempt: repairAttempts + 1,
			maxRepairAttempts: DEFAULT_MISSING_CHECKPOINT_REPAIR_ATTEMPTS,
			checkpoint,
		};
	}
	// "decide" is treated as "continue": agent reconstructs best-effort and moves on
	return {
		action:
			checkpoint.outcome === "continue" || checkpoint.outcome === "decide"
				? "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);

	// ── Lead paragraph: classification drives the repair strategy ─────────────
	// The diagnosis is the most actionable signal — put it first so the agent's
	// attention lands on the specific failure mode before generic instructions.
	const lines = ["## Checkpoint Required — Repair Needed"];
	if (
		diagnosis?.classification &&
		diagnosis.classification !== "no-transcript"
	) {
		const classificationLabels = {
			"checkpoint-tool-unavailable":
				"⚠️  checkpoint appeared unavailable — but it is ALWAYS registered at runtime. Call it now without searching for it. If you don't see it in your tool list, that is a model perception error; the tool will work.",
			"checkpoint-tool-failed":
				"⚠️  The checkpoint tool call failed with an error. Fix the input (check required fields, types) and call it again.",
			"file-substituted-for-checkpoint":
				"⚠️  You wrote a summary or projection file instead of calling checkpoint. Writing files is not a checkpoint. Call the tool.",
			"claimed-checkpoint-without-tool":
				"⚠️  You stated the checkpoint was saved but no tool call succeeded. Do not describe or narrate the checkpoint — call checkpoint now.",
			"mentioned-checkpoint-without-tool":
				"⚠️  You discussed checkpoint without calling it. Discussion is not execution. Call the tool.",
			"no-checkpoint-tool-call":
				"⚠️  You ended your turn without calling checkpoint at all. This is required. Call it now.",
		};
		const label =
			classificationLabels[diagnosis.classification] ??
			`⚠️  Failure pattern: ${diagnosis.classification} — ${diagnosis.summary ?? "missing checkpoint"}`;
		lines.push("", label);
	} else if (diagnosis?.classification === "no-transcript") {
		lines.push(
			"",
			`⚠️  Failure pattern: no-transcript — No agent-end transcript was available. Reconstruct the checkpoint from artifacts.`,
		);
	}
	lines.push(
		"",
		`Unit: ${unitType} ${unitId} · Iteration: ${state?.iteration ?? "unknown"} · Repair attempt: ${repairAttempt} of ${maxRepairAttempts} · Mode: ${mode}`,
	);
	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 checkpoint with the appropriate outcome and PDD fields.",
			"5. Based on the evidence, call checkpoint with the appropriate outcome and PDD fields.",
			"6. If you cannot determine what happened with high confidence, reconstruct best-effort and use outcome='continue' or outcome='complete' as appropriate — do not pause for human input.",
		);
		lines.push(
			"",
			"**Low-confidence reconstruction guidance**:",
			"- Use outcome='continue' when evidence is sparse or ambiguous — reconstruct best-effort and let the loop proceed",
			"- Use outcome='complete' only when there is clear evidence the task was finished",
			"- Use outcome='blocked' only when there is a hard blocker that prevents forward progress",
			"- Never use the decide outcome — reconstruct autonomously even under uncertainty",
		);
	} 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 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 checkpoint tool call succeeds.",
		"Your final action in this repair turn must be the 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, use outcome='continue' and describe the uncertainty in the notes — do not pause for human input.",
		);
	}
	if (repairAttempt >= maxRepairAttempts) {
		lines.push(
			"This is the final automatic repair attempt. Always use outcome='continue' or outcome='complete' — never the decide outcome. Reconstruct best-effort and let the autonomous loop continue.",
		);
		lines.push(
			"**Final guidance**: Commit to the most plausible interpretation of the evidence and checkpoint with that outcome. Do not pause for human review.",
		);
	}
	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;
}
