import { promises as fs } from 'node:fs'; import path from 'node:path'; import { loadMission } from './mission.js'; import { parseTasksFile } from './tasks-file.js'; import type { Mission, MissionSession, MissionStatusSummary, MissionTask, TaskDetail, } from './types.js'; const SESSION_LOCK_FILE = 'session.lock'; interface SessionLockState { session_id?: string; runtime?: string; pid?: number; started_at?: string; milestone_id?: string; } function tasksFilePath(mission: Mission): string { if (path.isAbsolute(mission.tasksFile)) { return mission.tasksFile; } return path.join(mission.projectPath, mission.tasksFile); } function sessionLockPath(mission: Mission): string { const orchestratorDir = path.isAbsolute(mission.orchestratorDir) ? mission.orchestratorDir : path.join(mission.projectPath, mission.orchestratorDir); return path.join(orchestratorDir, SESSION_LOCK_FILE); } function isPidAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } async function readTasks(mission: Mission): Promise { try { const content = await fs.readFile(tasksFilePath(mission), 'utf8'); return parseTasksFile(content); } catch (error) { if ( typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'ENOENT' ) { return []; } throw error; } } async function readActiveSession(mission: Mission): Promise { let lockRaw: string; try { lockRaw = await fs.readFile(sessionLockPath(mission), 'utf8'); } catch (error) { if ( typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'ENOENT' ) { return undefined; } throw error; } const lock = JSON.parse(lockRaw) as SessionLockState; if ( typeof lock.session_id !== 'string' || (lock.runtime !== 'claude' && lock.runtime !== 'codex') || typeof lock.started_at !== 'string' ) { return undefined; } const pid = typeof lock.pid === 'number' ? lock.pid : undefined; if (pid !== undefined && pid > 0 && !isPidAlive(pid)) { return undefined; } const existingSession = mission.sessions.find((session) => session.sessionId === lock.session_id); if (existingSession !== undefined) { return existingSession; } return { sessionId: lock.session_id, runtime: lock.runtime, pid, startedAt: lock.started_at, milestoneId: lock.milestone_id, }; } export async function getMissionStatus(mission: Mission): Promise { const freshMission = await loadMission(mission.projectPath); const tasks = await readTasks(freshMission); const done = tasks.filter((task) => task.status === 'done').length; const inProgress = tasks.filter((task) => task.status === 'in-progress').length; const pending = tasks.filter((task) => task.status === 'not-started').length; const blocked = tasks.filter((task) => task.status === 'blocked').length; const cancelled = tasks.filter((task) => task.status === 'cancelled').length; const nextTask = tasks.find((task) => task.status === 'not-started'); const completedMilestones = freshMission.milestones.filter( (milestone) => milestone.status === 'completed', ).length; const currentMilestone = freshMission.milestones.find((milestone) => milestone.status === 'in-progress') ?? freshMission.milestones.find((milestone) => milestone.status === 'pending'); const activeSession = await readActiveSession(freshMission); return { mission: { id: freshMission.id, name: freshMission.name, status: freshMission.status, projectPath: freshMission.projectPath, }, milestones: { total: freshMission.milestones.length, completed: completedMilestones, current: currentMilestone, }, tasks: { total: tasks.length, done, inProgress, pending, blocked, cancelled, }, nextTaskId: nextTask?.id, activeSession, }; } export async function getTaskStatus(mission: Mission, taskId: string): Promise { const freshMission = await loadMission(mission.projectPath); const tasks = await readTasks(freshMission); const matches = tasks.filter((task) => task.id === taskId); if (matches.length === 0) { throw new Error(`Task not found: ${taskId}`); } if (matches.length > 1) { throw new Error(`Duplicate task IDs found: ${taskId}`); } const summary = await getMissionStatus(freshMission); return { missionId: freshMission.id, task: matches[0]!, isNextTask: summary.nextTaskId === taskId, activeSession: summary.activeSession, }; }