// SF Extension — Legacy Filesystem State Derivation
// _deriveStateImpl() parses .sf/ markdown files directly; used only when
// SQLite is unavailable or the project has not been migrated.

import { existsSync, readdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { detectPendingEscalation } from "./escalation.js";
import {
	isValidTaskSummary,
	loadFile,
	parseContextDependsOn,
	parseRequirementCounts,
	parseSummary,
} from "./files.js";
import { findMilestoneIds } from "./milestone-ids.js";
import { nativeBatchParseSfFiles } from "./native-parser-bridge.js";
import { parsePlan, parseRoadmap } from "./parsers.js";
import {
	resolveMilestoneFile,
	resolveSfRootFile,
	resolveSliceFile,
	resolveSlicePath,
	resolveTaskFile,
	resolveTasksDir,
	sfRoot,
} from "./paths.js";
import { getSlicePlanBlockingIssue } from "./plan-quality.js";
import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js";
import { getSliceTasks, isDbAvailable } from "./sf-db.js";
import {
	extractContextTitle,
	isGhostMilestone,
	isMilestoneComplete,
	loadTerminalSummary,
	readMilestoneValidationVerdict,
	stripMilestonePrefix,
} from "./state-shared.js";
import { logWarning } from "./workflow-logger.js";

export async function _deriveStateImpl(basePath) {
	const diskIds = findMilestoneIds(basePath);
	const customOrder = loadQueueOrder(basePath);
	const milestoneIds = sortByQueueOrder(diskIds, customOrder);
	// ── Parallel worker isolation ──────────────────────────────────────────
	// When SF_MILESTONE_LOCK is set, this process is a parallel worker
	// scoped to a single milestone. Filter the milestone list so this worker
	// only sees its assigned milestone (all others are treated as if they
	// don't exist). This gives each worker complete isolation without
	// modifying any other state derivation logic.
	const milestoneLock = process.env.SF_MILESTONE_LOCK;
	if (milestoneLock && milestoneIds.includes(milestoneLock)) {
		milestoneIds.length = 0;
		milestoneIds.push(milestoneLock);
	}
	// ── Batch-parse file cache ──────────────────────────────────────────────
	// When the native Rust parser is available, read every .md file under .sf/
	// in one call and build an in-memory content map keyed by absolute path.
	// This eliminates O(N) individual fs.readFile calls during traversal.
	const fileContentCache = new Map();
	const sfDir = sfRoot(basePath);
	// Filesystem fallback: used when deriveStateFromDb() is not available
	// (pre-migration projects). The DB-backed path is preferred when available
	// — see deriveStateFromDb() above.
	const batchFiles = nativeBatchParseSfFiles(sfDir);
	if (batchFiles) {
		for (const f of batchFiles) {
			const absPath = resolve(sfDir, f.path);
			fileContentCache.set(absPath, f.rawContent);
		}
	}
	/**
	 * Load file content from batch cache first, falling back to disk read.
	 * Resolves the path to absolute before cache lookup.
	 */
	async function cachedLoadFile(path) {
		const abs = resolve(path);
		const cached = fileContentCache.get(abs);
		if (cached !== undefined) return cached;
		return loadFile(path);
	}
	const requirements = parseRequirementCounts(
		await cachedLoadFile(resolveSfRootFile(basePath, "REQUIREMENTS")),
	);
	if (milestoneIds.length === 0) {
		return {
			activeMilestone: null,
			activeSlice: null,
			activeTask: null,
			phase: "pre-planning",
			recentDecisions: [],
			blockers: [],
			nextAction: "No milestones found. Run /next to create one.",
			registry: [],
			requirements,
			progress: {
				milestones: { done: 0, total: 0 },
			},
		};
	}
	// ── Single-pass milestone scan ──────────────────────────────────────────
	// Parse each milestone's roadmap once, caching results. First pass determines
	// completeness for dependency resolution; second pass builds the registry.
	// With the batch cache, all file reads hit memory instead of disk.
	// Phase 1: Build roadmap cache and completeness set
	const roadmapCache = new Map();
	const completeMilestoneIds = new Set();
	// Track parked milestone IDs so Phase 2 can check without re-reading disk
	const parkedMilestoneIds = new Set();
	for (const mid of milestoneIds) {
		// Skip parked milestones — they do NOT count as complete (don't satisfy depends_on)
		// But still parse their roadmap for title extraction in Phase 2.
		const parkedFile = resolveMilestoneFile(basePath, mid, "PARKED");
		if (parkedFile) {
			parkedMilestoneIds.add(mid);
			// Cache roadmap for title extraction (but don't add to completeMilestoneIds)
			const prf = resolveMilestoneFile(basePath, mid, "ROADMAP");
			const prc = prf ? await cachedLoadFile(prf) : null;
			if (prc) roadmapCache.set(mid, parseRoadmap(prc));
			continue;
		}
		const rf = resolveMilestoneFile(basePath, mid, "ROADMAP");
		const rc = rf ? await cachedLoadFile(rf) : null;
		if (!rc) {
			const sf = resolveMilestoneFile(basePath, mid, "SUMMARY");
			if (await loadTerminalSummary(sf, cachedLoadFile))
				completeMilestoneIds.add(mid);
			continue;
		}
		const rmap = parseRoadmap(rc);
		roadmapCache.set(mid, rmap);
		if (!isMilestoneComplete(rmap)) {
			// Summary is the terminal artifact — if it exists and is terminal, the milestone is
			// complete even when roadmap checkboxes weren't ticked (#864).
			const sf = resolveMilestoneFile(basePath, mid, "SUMMARY");
			if (await loadTerminalSummary(sf, cachedLoadFile))
				completeMilestoneIds.add(mid);
			continue;
		}
		const sf = resolveMilestoneFile(basePath, mid, "SUMMARY");
		if (await loadTerminalSummary(sf, cachedLoadFile))
			completeMilestoneIds.add(mid);
	}
	// Phase 2: Build registry using cached roadmaps (no re-parsing or re-reading)
	const registry = [];
	let activeMilestone = null;
	let activeRoadmap = null;
	let activeMilestoneFound = false;
	let activeMilestoneHasDraft = false;
	for (const mid of milestoneIds) {
		// Skip parked milestones — register them as 'parked' and move on
		if (parkedMilestoneIds.has(mid)) {
			const roadmap = roadmapCache.get(mid) ?? null;
			const title = roadmap ? stripMilestonePrefix(roadmap.title) : mid;
			registry.push({ id: mid, title, status: "parked" });
			continue;
		}
		const roadmap = roadmapCache.get(mid) ?? null;
		if (!roadmap) {
			// No roadmap — check if a terminal summary exists (completed milestone without roadmap)
			const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
			const sc = await loadTerminalSummary(summaryFile, cachedLoadFile);
			if (sc) {
				const summaryTitle = parseSummary(sc).title || mid;
				registry.push({ id: mid, title: summaryTitle, status: "complete" });
				completeMilestoneIds.add(mid);
				continue;
			}
			// Failure summary or unreadable — milestone is not yet done; fall through
			// Ghost milestone (only META.json, no CONTEXT/ROADMAP/SUMMARY) — skip entirely
			if (isGhostMilestone(basePath, mid)) continue;
			// No roadmap and no summary — treat as incomplete/active
			if (!activeMilestoneFound) {
				// Check for CONTEXT-DRAFT.md to distinguish draft-seeded from blank milestones.
				// A draft seed means the milestone has discussion material but no full context yet.
				const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
				const draftFile = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT");
				if (!contextFile && draftFile) activeMilestoneHasDraft = true;
				// Extract title from CONTEXT.md or CONTEXT-DRAFT.md heading before falling back to mid.
				const contextContent = contextFile
					? await cachedLoadFile(contextFile)
					: null;
				const draftContent =
					draftFile && !contextContent ? await cachedLoadFile(draftFile) : null;
				const title = extractContextTitle(contextContent || draftContent, mid);
				// Check milestone-level dependencies before promoting to active.
				// Without this, a queued milestone with depends_on in its CONTEXT
				// or CONTEXT-DRAFT frontmatter would be promoted to active even when
				// its deps are unmet. Fall back to CONTEXT-DRAFT.md when absent (#1724).
				const deps = parseContextDependsOn(contextContent ?? draftContent);
				const depsUnmet = deps.some((dep) => !completeMilestoneIds.has(dep));
				if (depsUnmet) {
					registry.push({ id: mid, title, status: "pending", dependsOn: deps });
				} else {
					activeMilestone = { id: mid, title };
					activeMilestoneFound = true;
					registry.push({
						id: mid,
						title,
						status: "active",
						...(deps.length > 0 ? { dependsOn: deps } : {}),
					});
				}
			} else {
				// For milestones after the active one, also try to extract title from context files.
				const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
				const draftFile = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT");
				const contextContent = contextFile
					? await cachedLoadFile(contextFile)
					: null;
				const draftContent =
					draftFile && !contextContent ? await cachedLoadFile(draftFile) : null;
				const title = extractContextTitle(contextContent || draftContent, mid);
				registry.push({ id: mid, title, status: "pending" });
			}
			continue;
		}
		const title = stripMilestonePrefix(roadmap.title);
		const complete = isMilestoneComplete(roadmap);
		if (complete) {
			// All slices done — check validation and summary state
			const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
			const { terminal: validationTerminal, verdict } =
				await readMilestoneValidationVerdict(basePath, mid, cachedLoadFile);
			// needs-remediation is terminal but requires re-validation (#3596)
			const needsRevalidation =
				!validationTerminal || verdict === "needs-remediation";
			if (await loadTerminalSummary(summaryFile, cachedLoadFile)) {
				// Terminal summary → milestone is complete. The summary is the terminal artifact (#864).
				registry.push({ id: mid, title, status: "complete" });
				continue;
			}
			// Failure summary or unreadable — fall through to re-validation / active logic below
			if (needsRevalidation && !activeMilestoneFound) {
				// No terminal summary and needs (re-)validation → validating-milestone
				activeMilestone = { id: mid, title };
				activeRoadmap = roadmap;
				activeMilestoneFound = true;
				registry.push({ id: mid, title, status: "active" });
			} else if (needsRevalidation && activeMilestoneFound) {
				// Needs (re-)validation, but another milestone is already active
				registry.push({ id: mid, title, status: "pending" });
			} else if (!activeMilestoneFound) {
				// Terminal validation (pass/needs-attention) but no summary → completing-milestone
				activeMilestone = { id: mid, title };
				activeRoadmap = roadmap;
				activeMilestoneFound = true;
				registry.push({ id: mid, title, status: "active" });
			} else {
				registry.push({ id: mid, title, status: "complete" });
			}
		} else {
			// Roadmap slices not all checked — but if a terminal summary exists, the
			// milestone is still complete. The summary is the terminal artifact (#864).
			const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
			if (await loadTerminalSummary(summaryFile, cachedLoadFile)) {
				registry.push({ id: mid, title, status: "complete" });
			} else if (!activeMilestoneFound) {
				// Check milestone-level dependencies before promoting to active.
				// Fall back to CONTEXT-DRAFT.md when CONTEXT.md is absent (#1724).
				const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
				const draftFile = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT");
				const contextContent = contextFile
					? await cachedLoadFile(contextFile)
					: null;
				const draftContent =
					draftFile && !contextContent ? await cachedLoadFile(draftFile) : null;
				const deps = parseContextDependsOn(contextContent ?? draftContent);
				const depsUnmet = deps.some((dep) => !completeMilestoneIds.has(dep));
				if (depsUnmet) {
					registry.push({ id: mid, title, status: "pending", dependsOn: deps });
					// Do NOT set activeMilestoneFound — let the loop continue to the next milestone
				} else {
					activeMilestone = { id: mid, title };
					activeRoadmap = roadmap;
					activeMilestoneFound = true;
					registry.push({
						id: mid,
						title,
						status: "active",
						...(deps.length > 0 ? { dependsOn: deps } : {}),
					});
				}
			} else {
				const contextFile2 = resolveMilestoneFile(basePath, mid, "CONTEXT");
				const draftFileForDeps3 = resolveMilestoneFile(
					basePath,
					mid,
					"CONTEXT-DRAFT",
				);
				const contextOrDraftContent3 = contextFile2
					? await cachedLoadFile(contextFile2)
					: draftFileForDeps3
						? await cachedLoadFile(draftFileForDeps3)
						: null;
				const deps2 = parseContextDependsOn(contextOrDraftContent3);
				registry.push({
					id: mid,
					title,
					status: "pending",
					...(deps2.length > 0 ? { dependsOn: deps2 } : {}),
				});
			}
		}
	}
	const milestoneProgress = {
		done: registry.filter((entry) => entry.status === "complete").length,
		total: registry.length,
	};
	if (!activeMilestone) {
		// Check whether any milestones are pending (dep-blocked) or parked
		const pendingEntries = registry.filter(
			(entry) => entry.status === "pending",
		);
		const parkedEntries = registry.filter((entry) => entry.status === "parked");
		if (pendingEntries.length > 0) {
			// All incomplete milestones are dep-blocked — no progress possible
			const blockerDetails = pendingEntries
				.filter((entry) => entry.dependsOn && entry.dependsOn.length > 0)
				.map(
					(entry) =>
						`${entry.id} is waiting on unmet deps: ${entry.dependsOn.join(", ")}`,
				);
			return {
				activeMilestone: null,
				activeSlice: null,
				activeTask: null,
				phase: "blocked",
				recentDecisions: [],
				blockers:
					blockerDetails.length > 0
						? blockerDetails
						: [
								"All remaining milestones are dep-blocked but no deps listed — check CONTEXT.md files",
							],
				nextAction: "Resolve milestone dependencies before proceeding.",
				registry,
				requirements,
				progress: {
					milestones: milestoneProgress,
				},
			};
		}
		if (parkedEntries.length > 0) {
			// All non-complete milestones are parked — nothing active, but not "all complete"
			const parkedIds = parkedEntries.map((e) => e.id).join(", ");
			return {
				activeMilestone: null,
				activeSlice: null,
				activeTask: null,
				phase: "pre-planning",
				recentDecisions: [],
				blockers: [],
				nextAction: `All remaining milestones are parked (${parkedIds}). Run /unpark <id> or create a new milestone.`,
				registry,
				requirements,
				progress: {
					milestones: milestoneProgress,
				},
			};
		}
		// All real milestones were ghosts (empty registry) → treat as pre-planning
		if (registry.length === 0) {
			return {
				activeMilestone: null,
				activeSlice: null,
				activeTask: null,
				phase: "pre-planning",
				recentDecisions: [],
				blockers: [],
				nextAction: "No milestones found. Run /next to create one.",
				registry: [],
				requirements,
				progress: {
					milestones: { done: 0, total: 0 },
				},
			};
		}
		// All milestones complete
		const lastEntry = registry[registry.length - 1];
		const activeReqs = requirements.active ?? 0;
		const completionNote =
			activeReqs > 0
				? `All milestones complete. ${activeReqs} active requirement${activeReqs === 1 ? "" : "s"} in REQUIREMENTS.md ${activeReqs === 1 ? "has" : "have"} not been mapped to a milestone.`
				: "All milestones complete.";
		return {
			activeMilestone: null,
			lastCompletedMilestone: lastEntry
				? { id: lastEntry.id, title: lastEntry.title }
				: null,
			activeSlice: null,
			activeTask: null,
			phase: "complete",
			recentDecisions: [],
			blockers: [],
			nextAction: completionNote,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
			},
		};
	}
	if (!activeRoadmap) {
		// Active milestone exists but has no roadmap yet.
		// If a CONTEXT-DRAFT.md seed exists, it needs discussion before planning.
		// Otherwise, it's a blank milestone ready for initial planning.
		const phase = activeMilestoneHasDraft ? "needs-discussion" : "pre-planning";
		const nextAction = activeMilestoneHasDraft
			? `Discuss draft context for milestone ${activeMilestone.id}.`
			: `Plan milestone ${activeMilestone.id}.`;
		return {
			activeMilestone,
			activeSlice: null,
			activeTask: null,
			phase,
			recentDecisions: [],
			blockers: [],
			nextAction,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
			},
		};
	}
	// ── Zero-slice roadmap guard (#1785) ─────────────────────────────────
	// A stub roadmap (placeholder text, no slice definitions) has a truthy
	// roadmap object but an empty slices array. Without this check the
	// slice-finding loop below finds nothing and returns phase: "blocked".
	// An empty slices array means the roadmap still needs slice definitions,
	// so the correct phase is pre-planning.
	if (activeRoadmap.slices.length === 0) {
		return {
			activeMilestone,
			activeSlice: null,
			activeTask: null,
			phase: "pre-planning",
			recentDecisions: [],
			blockers: [],
			nextAction: `Milestone ${activeMilestone.id} has a roadmap but no slices defined. Add slices to the roadmap.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: { done: 0, total: 0 },
			},
		};
	}
	// Check if active milestone needs validation or completion (all slices done)
	if (isMilestoneComplete(activeRoadmap)) {
		const { terminal: validationTerminal, verdict } =
			await readMilestoneValidationVerdict(
				basePath,
				activeMilestone.id,
				cachedLoadFile,
			);
		const sliceProgress = {
			done: activeRoadmap.slices.length,
			total: activeRoadmap.slices.length,
		};
		// Force re-validation when verdict is needs-remediation — remediation slices
		// may have completed since the stale validation was written (#3596).
		if (!validationTerminal || verdict === "needs-remediation") {
			return {
				activeMilestone,
				activeSlice: null,
				activeTask: null,
				phase: "validating-milestone",
				recentDecisions: [],
				blockers: [],
				nextAction: `Validate milestone ${activeMilestone.id} before completion.`,
				registry,
				requirements,
				progress: {
					milestones: milestoneProgress,
					slices: sliceProgress,
				},
			};
		}
		return {
			activeMilestone,
			activeSlice: null,
			activeTask: null,
			phase: "completing-milestone",
			recentDecisions: [],
			blockers: [],
			nextAction: `All slices complete in ${activeMilestone.id}. Write milestone summary.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
			},
		};
	}
	const sliceProgress = {
		done: activeRoadmap.slices.filter((s) => s.done).length,
		total: activeRoadmap.slices.length,
	};
	// Find the active slice (first incomplete with deps satisfied)
	const doneSliceIds = new Set(
		activeRoadmap.slices.filter((s) => s.done).map((s) => s.id),
	);
	let activeSlice = null;
	// ── Slice-level parallel worker isolation ─────────────────────────────
	// When SF_SLICE_LOCK is set, override activeSlice to only the locked slice.
	const sliceLockLegacy = process.env.SF_SLICE_LOCK;
	if (sliceLockLegacy) {
		const lockedSlice = activeRoadmap.slices.find(
			(s) => s.id === sliceLockLegacy,
		);
		if (lockedSlice) {
			activeSlice = { id: lockedSlice.id, title: lockedSlice.title };
		} else {
			logWarning(
				"state",
				`SF_SLICE_LOCK=${sliceLockLegacy} not found in active slices — worker has no assigned work`,
			);
			return {
				activeMilestone,
				activeSlice: null,
				activeTask: null,
				phase: "blocked",
				recentDecisions: [],
				blockers: [
					`SF_SLICE_LOCK=${sliceLockLegacy} not found in active milestone slices`,
				],
				nextAction:
					"Slice lock references a non-existent slice — check orchestrator dispatch.",
				registry,
				requirements,
				progress: {
					milestones: milestoneProgress,
					slices: sliceProgress,
				},
			};
		}
	} else {
		let bestFallbackLegacy = null;
		let bestFallbackLegacySatisfied = -1;
		for (const s of activeRoadmap.slices) {
			if (s.done) continue;
			if (s.depends.every((dep) => doneSliceIds.has(dep))) {
				activeSlice = { id: s.id, title: s.title };
				break;
			}
			// Track best fallback
			const satisfied = s.depends.filter((dep) => doneSliceIds.has(dep)).length;
			if (satisfied > bestFallbackLegacySatisfied) {
				bestFallbackLegacy = s;
				bestFallbackLegacySatisfied = satisfied;
			}
		}
		// Fallback: if no slice has all deps met, pick the one with the most deps satisfied
		if (!activeSlice && bestFallbackLegacy) {
			const unmet = bestFallbackLegacy.depends.filter(
				(dep) => !doneSliceIds.has(dep),
			);
			logWarning(
				"state",
				`No slice has all deps satisfied — falling back to ${bestFallbackLegacy.id} ` +
					`(${bestFallbackLegacySatisfied}/${bestFallbackLegacy.depends.length} deps met, ` +
					`unmet: ${unmet.join(", ")})`,
			);
			activeSlice = {
				id: bestFallbackLegacy.id,
				title: bestFallbackLegacy.title,
			};
		}
	}
	if (!activeSlice) {
		return {
			activeMilestone,
			activeSlice: null,
			activeTask: null,
			phase: "blocked",
			recentDecisions: [],
			blockers: ["No slice eligible — check dependency ordering"],
			nextAction: "Resolve dependency blockers or plan next slice.",
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
			},
		};
	}
	// Check if the slice has a plan
	const planFile = resolveSliceFile(
		basePath,
		activeMilestone.id,
		activeSlice.id,
		"PLAN",
	);
	const slicePlanContent = planFile ? await cachedLoadFile(planFile) : null;
	if (!slicePlanContent) {
		return {
			activeMilestone,
			activeSlice,
			activeTask: null,
			phase: "planning",
			recentDecisions: [],
			blockers: [],
			nextAction: `Plan slice ${activeSlice.id} (${activeSlice.title}).`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
			},
		};
	}
	const slicePlan = parsePlan(slicePlanContent);
	const planQualityIssue = getSlicePlanBlockingIssue(slicePlanContent);
	if (planQualityIssue && slicePlan.tasks.length === 0) {
		return {
			activeMilestone,
			activeSlice,
			activeTask: null,
			phase: "planning",
			recentDecisions: [],
			blockers: [],
			nextAction: `Slice ${activeSlice.id} plan is incomplete (${planQualityIssue}). Re-run plan-slice with partner/combatant/architect review.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
			},
		};
	}
	// ── Reconcile stale task status for filesystem-based projects (#2514) ──
	// Heading-style tasks (### T01:) are always parsed as done=false by
	// parsePlan because the heading syntax has no checkbox. When the agent
	// writes a SUMMARY file but the plan's heading isn't converted to a
	// checkbox, the task appears incomplete forever — causing infinite
	// re-dispatch. Reconcile by checking SUMMARY files on disk.
	for (const t of slicePlan.tasks) {
		if (t.done) continue;
		const summaryPath = resolveTaskFile(
			basePath,
			activeMilestone.id,
			activeSlice.id,
			t.id,
			"SUMMARY",
		);
		if (summaryPath && existsSync(summaryPath)) {
			// Validate that the summary file has actual content (#sf-moobj36o-6rxy6e)
			const summaryContent = readFileSync(summaryPath, "utf-8");
			if (!isValidTaskSummary(summaryContent)) {
				logWarning(
					"reconcile",
					`task ${activeMilestone.id}/${activeSlice.id}/${t.id} has empty/invalid SUMMARY — skipping reconciliation`,
					{ mid: activeMilestone.id, sid: activeSlice.id, tid: t.id },
				);
				continue;
			}
			t.done = true;
			logWarning(
				"reconcile",
				`task ${activeMilestone.id}/${activeSlice.id}/${t.id} reconciled via SUMMARY on disk (#2514)`,
				{ mid: activeMilestone.id, sid: activeSlice.id, tid: t.id },
			);
		}
	}
	const taskProgress = {
		done: slicePlan.tasks.filter((t) => t.done).length,
		total: slicePlan.tasks.length,
	};
	const activeTaskEntry = slicePlan.tasks.find((t) => !t.done);
	if (!activeTaskEntry && slicePlan.tasks.length > 0) {
		// All tasks done but slice not marked complete
		return {
			activeMilestone,
			activeSlice,
			activeTask: null,
			phase: "summarizing",
			recentDecisions: [],
			blockers: [],
			nextAction: `All tasks done in ${activeSlice.id}. Write slice summary and complete slice.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
				tasks: taskProgress,
			},
		};
	}
	// Empty plan — no tasks defined yet, stay in planning phase
	if (!activeTaskEntry) {
		return {
			activeMilestone,
			activeSlice,
			activeTask: null,
			phase: "planning",
			recentDecisions: [],
			blockers: [],
			nextAction: `Slice ${activeSlice.id} has a plan file but no tasks. Add tasks to the plan.`,
			registry,
			requirements,
			progress: {
				milestones: milestoneProgress,
				slices: sliceProgress,
				tasks: taskProgress,
			},
		};
	}
	const activeTask = {
		id: activeTaskEntry.id,
		title: activeTaskEntry.title,
	};
	// ── Task plan file check (#909) ──────────────────────────────────────
	// The slice plan may reference tasks but per-task plan files may be
	// missing — e.g. when the slice plan was pre-created during roadmapping.
	// If the tasks dir exists but has literally zero files (empty dir from
	// mkdir), fall back to planning so plan-slice generates task plans.
	const tasksDir = resolveTasksDir(
		basePath,
		activeMilestone.id,
		activeSlice.id,
	);
	if (tasksDir && existsSync(tasksDir) && slicePlan.tasks.length > 0) {
		const allFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".md"));
		if (allFiles.length === 0) {
			return {
				activeMilestone,
				activeSlice,
				activeTask: null,
				phase: "planning",
				recentDecisions: [],
				blockers: [],
				nextAction: `Task plan files missing for ${activeSlice.id}. Run plan-slice to generate task plans.`,
				registry,
				requirements,
				progress: {
					milestones: milestoneProgress,
					slices: sliceProgress,
					tasks: taskProgress,
				},
			};
		}
	}
	// ── Mid-execution escalation (ADR-011 P2 — SF ADR) ────────────────
	// Pause the loop if any task in the active slice has escalation_pending=1
	// and an unresolved escalation artifact. The user must run /escalate
	// resolve before autonomous mode will continue. Falls through (returns null
	// from detectPendingEscalation) when nothing is paused — no perf cost
	// in the common path.
	{
		const dbTasks = getSliceTasks(activeMilestone.id, activeSlice.id);
		const escalatingTaskId = detectPendingEscalation(dbTasks, basePath);
		if (escalatingTaskId) {
			return {
				activeMilestone,
				activeSlice,
				activeTask: { id: escalatingTaskId, title: "" },
				phase: "escalating-task",
				recentDecisions: [],
				blockers: [
					`Task ${escalatingTaskId} requires a user decision before the loop can proceed`,
				],
				nextAction: `Run \`/escalate show ${escalatingTaskId}\` to review the options, then \`/escalate resolve ${escalatingTaskId} <choice>\` to proceed.`,
				registry,
				requirements,
				progress: {
					milestones: milestoneProgress,
					slices: sliceProgress,
				},
			};
		}
	}
	// ── Blocker detection: scan completed task summaries ──────────────────
	// If any completed task has blocker_discovered: true and no REPLAN.md
	// exists yet, transition to replanning-slice instead of executing.
	const completedTasks = slicePlan.tasks.filter((t) => t.done);
	let blockerTaskId = null;
	for (const ct of completedTasks) {
		const summaryFile = resolveTaskFile(
			basePath,
			activeMilestone.id,
			activeSlice.id,
			ct.id,
			"SUMMARY",
		);
		if (!summaryFile) continue;
		const summaryContent = await cachedLoadFile(summaryFile);
		if (!summaryContent) continue;
		const summary = parseSummary(summaryContent);
		if (summary.frontmatter.blocker_discovered) {
			blockerTaskId = ct.id;
			break;
		}
	}
	if (blockerTaskId) {
		// Loop protection: if REPLAN.md already exists, a replan was already
		// performed for this slice — skip further replanning and continue executing.
		const replanFile = resolveSliceFile(
			basePath,
			activeMilestone.id,
			activeSlice.id,
			"REPLAN",
		);
		if (!replanFile) {
			return {
				activeMilestone,
				activeSlice,
				activeTask,
				phase: "replanning-slice",
				recentDecisions: [],
				blockers: [
					`Task ${blockerTaskId} discovered a blocker requiring slice replan`,
				],
				nextAction: `Task ${blockerTaskId} reported blocker_discovered. Replan slice ${activeSlice.id} before continuing.`,
				activeWorkspace: undefined,
				registry,
				requirements,
				progress: {
					milestones: milestoneProgress,
					slices: sliceProgress,
					tasks: taskProgress,
				},
			};
		}
		// REPLAN.md exists — loop protection: fall through to normal executing
	}
	// ── REPLAN-TRIGGER detection: triage-initiated replan ──────────────────
	// Manual `/triage` writes REPLAN-TRIGGER.md when a capture is classified
	// as "replan". Detect it here and transition to replanning-slice so the
	// dispatch loop picks it up (instead of silently advancing past it).
	if (!blockerTaskId) {
		const replanTriggerFile = resolveSliceFile(
			basePath,
			activeMilestone.id,
			activeSlice.id,
			"REPLAN-TRIGGER",
		);
		if (replanTriggerFile) {
			// Same loop protection: if REPLAN.md already exists, a replan was
			// already performed — skip further replanning and continue executing.
			const replanFile = resolveSliceFile(
				basePath,
				activeMilestone.id,
				activeSlice.id,
				"REPLAN",
			);
			if (!replanFile) {
				return {
					activeMilestone,
					activeSlice,
					activeTask,
					phase: "replanning-slice",
					recentDecisions: [],
					blockers: ["Triage replan trigger detected — slice replan required"],
					nextAction: `Triage replan triggered for slice ${activeSlice.id}. Replan before continuing.`,
					activeWorkspace: undefined,
					registry,
					requirements,
					progress: {
						milestones: milestoneProgress,
						slices: sliceProgress,
						tasks: taskProgress,
					},
				};
			}
		}
	}
	// Check for interrupted work
	const sDir = resolveSlicePath(basePath, activeMilestone.id, activeSlice.id);
	const continueFile = sDir
		? resolveSliceFile(basePath, activeMilestone.id, activeSlice.id, "CONTINUE")
		: null;
	// Also check legacy continue.md
	const hasInterrupted =
		!!(continueFile && (await cachedLoadFile(continueFile))) ||
		!!(sDir && (await cachedLoadFile(join(sDir, "continue.md"))));
	return {
		activeMilestone,
		activeSlice,
		activeTask,
		phase: "executing",
		recentDecisions: [],
		blockers: [],
		nextAction: hasInterrupted
			? `Resume interrupted work on ${activeTask.id}: ${activeTask.title} in slice ${activeSlice.id}. Read continue.md first.`
			: `Execute ${activeTask.id}: ${activeTask.title} in slice ${activeSlice.id}.`,
		registry,
		requirements,
		progress: {
			milestones: milestoneProgress,
			slices: sliceProgress,
			tasks: taskProgress,
		},
	};
}
