import { existsSync, readFileSync } from "node:fs"; import { isAbsolute, join, relative, resolve } from "node:path"; import { getPlanChecklist, type PlanChecklist } from "./plan-checklist.js"; export type { PlanChecklist } from "./plan-checklist.js"; export { getPlanChecklist } from "./plan-checklist.js"; type BoulderWorkStatus = "active" | "paused" | "completed" | "abandoned"; type BoulderWork = { readonly activePlan: string; readonly planName: string; readonly status?: BoulderWorkStatus; readonly startedAt?: string; readonly updatedAt?: string; readonly sessionIds: readonly string[]; readonly worktreePath?: string; }; type BoulderState = { readonly works: readonly BoulderWork[]; readonly mirrorWork: BoulderWork | null; readonly hasWorksMap: boolean; }; export type ContinuationState = { readonly planName: string; readonly planPath: string; readonly boulderPath: string; readonly ledgerPath: string; readonly worktreePath: string | null; readonly checklist: PlanChecklist; }; const SESSION_ID_PREFIX_PATTERN = /^(codex|opencode):/; export function readContinuationState(cwd: string, sessionId: string): ContinuationState | null { const boulderPath = getBoulderFilePath(cwd); const boulderState = readBoulderState(boulderPath); if (boulderState === null) return null; const work = getWorkForSession(boulderState, normalizeSessionId(sessionId, "codex")); if (work === null || !isContinuableStatus(work.status)) return null; const planPath = resolveBoulderPlanPathForWork(cwd, work); const checklist = getPlanChecklist(planPath); if (checklist.total === 0) return null; return { planName: work.planName, planPath, boulderPath, ledgerPath: join(cwd, ".omo", "start-work", "ledger.jsonl"), worktreePath: work.worktreePath ?? null, checklist, }; } function readBoulderState(path: string): BoulderState | null { try { const parsed: unknown = JSON.parse(readFileSync(path, "utf8")); return parseBoulderState(parsed); } catch (error) { if (error instanceof Error) return null; throw error; } } function parseBoulderState(value: unknown): BoulderState | null { if (!isRecord(value)) return null; const works: BoulderWork[] = []; const worksValue = value["works"]; const hasWorksMap = isRecord(worksValue); if (hasWorksMap) { for (const workValue of Object.values(worksValue)) { const work = parseBoulderWork(workValue); if (work !== null) works.push(work); } } const mirrorWork = parseBoulderWork(value); if (works.length === 0 && mirrorWork === null) return null; return { works, mirrorWork, hasWorksMap }; } function parseBoulderWork(value: unknown): BoulderWork | null { if (!isRecord(value)) return null; const activePlan = value["active_plan"]; const planName = value["plan_name"]; if (typeof activePlan !== "string") return null; const status = parseBoulderWorkStatus(value["status"]); const sessionIds = parseSessionIds(value["session_ids"]); const worktreePath = value["worktree_path"]; const startedAt = value["started_at"]; const updatedAt = value["updated_at"]; return { activePlan, planName: typeof planName === "string" ? planName : activePlan, sessionIds, ...(status === undefined ? {} : { status }), ...(typeof startedAt === "string" ? { startedAt } : {}), ...(typeof updatedAt === "string" ? { updatedAt } : {}), ...(typeof worktreePath === "string" ? { worktreePath } : {}), }; } function getWorkForSession(state: BoulderState, normalizedSessionId: string): BoulderWork | null { let newestWork: BoulderWork | null = null; let newestWorkMs = 0; for (const work of state.works) { if (!work.sessionIds.includes(normalizedSessionId)) continue; const workMs = parseIsoToMs(work.updatedAt ?? work.startedAt) ?? 0; if (newestWork === null || workMs > newestWorkMs) { newestWork = work; newestWorkMs = workMs; } } if (newestWork !== null) return newestWork; if (state.hasWorksMap) return null; if (state.mirrorWork?.sessionIds.includes(normalizedSessionId) === true) return state.mirrorWork; return null; } function resolveBoulderPlanPathForWork(cwd: string, work: BoulderWork): string { const absolutePlanPath = resolveTrackedPath(cwd, work.activePlan); const worktreePath = work.worktreePath?.trim(); if (worktreePath === undefined || worktreePath.length === 0) return absolutePlanPath; const relativePlanPath = relative(resolve(cwd), absolutePlanPath); if (relativePlanPath.length === 0 || relativePlanPath.startsWith("..") || isAbsolute(relativePlanPath)) { return absolutePlanPath; } const worktreePlanPath = resolve(resolveTrackedPath(cwd, worktreePath), relativePlanPath); return existsSync(worktreePlanPath) ? worktreePlanPath : absolutePlanPath; } function resolveTrackedPath(baseDirectory: string, trackedPath: string): string { return isAbsolute(trackedPath) ? resolve(trackedPath) : resolve(baseDirectory, trackedPath); } function parseBoulderWorkStatus(value: unknown): BoulderWorkStatus | undefined { if (value === "active" || value === "paused" || value === "completed" || value === "abandoned") return value; return undefined; } function parseSessionIds(value: unknown): readonly string[] { if (!Array.isArray(value)) return []; const sessionIds: string[] = []; for (const item of value) { if (typeof item === "string") sessionIds.push(normalizeSessionId(item)); } return sessionIds; } function normalizeSessionId(sessionId: string, platform: "codex" | "opencode" = "opencode"): string { if (SESSION_ID_PREFIX_PATTERN.test(sessionId)) return sessionId; return `${platform}:${sessionId}`; } function parseIsoToMs(value: string | undefined): number | null { if (value === undefined) return null; const parsed = Date.parse(value); return Number.isNaN(parsed) ? null : parsed; } function isContinuableStatus(status: BoulderWorkStatus | undefined): boolean { return status === "active" || status === "paused"; } function getBoulderFilePath(cwd: string): string { return join(cwd, ".omo", "boulder.json"); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }