/** * State types and file helpers for mission-control * * Types for state.json, run.json, and file read/write operations. * All tool mutations go through here. */ import * as fs from "fs"; import * as path from "path"; // ============================================================================ // Status Types // ============================================================================ export type PhaseStatus = "pending" | "in_progress" | "done" | "removed"; export type TaskStatus = "pending" | "in_progress" | "done" | "failed" | "removed"; export type RunStatus = "in_progress" | "done" | "failed" | "paused"; // ============================================================================ // State Interfaces // ============================================================================ /** * Volatile UI state - stored in .pi/mission-control/state.json * Resets on every new pi session */ export interface State { active_run_id: string | null; current_phase: string; current_status_message: string; } /** * Task artifact paths */ export interface TaskPaths { contract: string; worker_output: string; auditor_report: string; } /** * Task definition - stored within Phase in run.json */ export interface Task { id: string; name: string; status: TaskStatus; started_at: string | null; finish_at: string | null; file: string; paths: TaskPaths; } /** * Phase definition - stored in run.json */ export interface Phase { id: string; name: string; status: PhaseStatus; started_at: string | null; finish_at: string | null; file: string; tasks: Task[]; } /** * Artifact references in run.json */ export interface Artifacts { requirements: string; architecture: string; validation: string; } /** * Persistent run state - stored in .pi/mission-control/runs//run.json */ export interface Run { run_id: string; started_at: string; finish_at: string | null; status: RunStatus; artifacts: Artifacts; phases: Phase[]; } // ============================================================================ // Default State Values // ============================================================================ export const defaultState: State = { active_run_id: null, current_phase: "idle", current_status_message: "" }; // ============================================================================ // Path Helpers // ============================================================================ /** * Get the project root (cwd) */ export function getProjectRoot(): string { return process.cwd(); } /** * Get the base mission-control directory path */ export function getMissionControlDir(): string { return path.join(getProjectRoot(), ".pi", "mission-control"); } /** * Get the state.json file path */ export function getStateFilePath(): string { return path.join(getMissionControlDir(), "state.json"); } /** * Get the memory directory path */ export function getMemoryDir(): string { return path.join(getMissionControlDir(), "memory"); } /** * Get the long-term memory file path */ export function getLongTermMemoryPath(): string { return path.join(getMemoryDir(), "long_term.md"); } /** * Get the runs directory path */ export function getRunsDir(): string { return path.join(getMissionControlDir(), "runs"); } /** * Get a specific run directory path */ export function getRunDir(runId: string): string { return path.join(getRunsDir(), runId); } /** * Get the run.json file path for a specific run */ export function getRunFilePath(runId: string): string { return path.join(getRunDir(runId), "run.json"); } /** * Get the tasks directory path for a specific run */ export function getTasksDir(runId: string): string { return path.join(getRunDir(runId), "tasks"); } /** * Get a specific task directory path */ export function getTaskDir(runId: string, taskId: string): string { return path.join(getTasksDir(runId), taskId); } // ============================================================================ // File I/O Helpers // ============================================================================ /** * Ensure a directory exists (recursive) */ export function ensureDir(dirPath: string): void { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } /** * Read and parse JSON file */ export function readJson(filePath: string): T | null { try { if (!fs.existsSync(filePath)) { return null; } const content = fs.readFileSync(filePath, "utf-8"); return JSON.parse(content) as T; } catch (error) { console.error(`Error reading JSON file ${filePath}:`, error); return null; } } /** * Write JSON file (with pretty formatting) */ export function writeJson(filePath: string, data: T): void { ensureDir(path.dirname(filePath)); fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); } /** * Read a text file */ export function readText(filePath: string): string | null { try { if (!fs.existsSync(filePath)) { return null; } return fs.readFileSync(filePath, "utf-8"); } catch (error) { console.error(`Error reading text file ${filePath}:`, error); return null; } } /** * Write a text file */ export function writeText(filePath: string, content: string): void { ensureDir(path.dirname(filePath)); fs.writeFileSync(filePath, content, "utf-8"); } /** * Check if a file exists */ export function fileExists(filePath: string): boolean { return fs.existsSync(filePath); } /** * Copy a file from source to destination (skip if exists when specified) */ export function copyFile(src: string, dest: string, skipIfExists = false): boolean { if (skipIfExists && fs.existsSync(dest)) { return false; } ensureDir(path.dirname(dest)); fs.copyFileSync(src, dest); return true; } /** * Copy a directory recursively (skip existing files when specified) * Returns count of files copied */ export function copyDir(src: string, dest: string, skipIfExists = false): number { if (!fs.existsSync(src)) { return 0; } ensureDir(dest); let copiedCount = 0; const entries = fs.readdirSync(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { copiedCount += copyDir(srcPath, destPath, skipIfExists); } else if (entry.isFile()) { if (copyFile(srcPath, destPath, skipIfExists)) { copiedCount++; } } } return copiedCount; } // ============================================================================ // State File Operations // ============================================================================ /** * Read the current state.json * Returns default state if file doesn't exist */ export function readState(): State { const statePath = getStateFilePath(); const state = readJson(statePath); return state ?? { ...defaultState }; } /** * Write state.json */ export function writeState(state: State): void { const statePath = getStateFilePath(); writeJson(statePath, state); } /** * Update specific fields in state.json */ export function updateState(updates: Partial): State { const state = readState(); const updatedState = { ...state, ...updates }; writeState(updatedState); return updatedState; } // ============================================================================ // Run File Operations // ============================================================================ /** * Read a run.json file */ export function readRun(runId: string): Run | null { const runPath = getRunFilePath(runId); return readJson(runPath); } /** * Write a run.json file */ export function writeRun(run: Run): void { const runPath = getRunFilePath(run.run_id); writeJson(runPath, run); } /** * Update specific fields in a run */ export function updateRun(runId: string, updates: Partial): Run | null { const run = readRun(runId); if (!run) { return null; } const updatedRun = { ...run, ...updates }; writeRun(updatedRun); return updatedRun; } // ============================================================================ // Timestamp Helpers // ============================================================================ /** * Get current ISO timestamp */ export function getTimestamp(): string { return new Date().toISOString(); } /** * Generate a run ID from current timestamp * Format: run-YYYYMMDD-HHmmss */ export function generateRunId(): string { const now = new Date(); const date = now.toISOString().slice(0, 10).replace(/-/g, ""); const time = now.toTimeString().slice(0, 8).replace(/:/g, ""); return `run-${date}-${time}`; } // ============================================================================ // Phase Helpers // ============================================================================ /** * Find a phase by ID in a run */ export function findPhase(run: Run, phaseId: string): Phase | undefined { return run.phases.find(p => p.id === phaseId); } /** * Generate the next phase ID */ export function generatePhaseId(run: Run): string { const phaseCount = run.phases.length; return `phase${phaseCount + 1}`; } /** * Create a new phase */ export function createPhase(id: string, name: string, file: string): Phase { return { id, name, status: "pending", started_at: null, finish_at: null, file, tasks: [] }; } /** * Add a phase to a run */ export function addPhaseToRun(run: Run, phase: Phase): Run { return { ...run, phases: [...run.phases, phase] }; } /** * Update a phase in a run */ export function updatePhaseInRun(run: Run, phaseId: string, updates: Partial): Run { return { ...run, phases: run.phases.map(p => p.id === phaseId ? { ...p, ...updates } : p ) }; } // ============================================================================ // Task Helpers // ============================================================================ /** * Find a task by ID in a run (searches all phases) */ export function findTask(run: Run, taskId: string): { phase: Phase; task: Task; phaseIndex: number; taskIndex: number } | null { for (let phaseIndex = 0; phaseIndex < run.phases.length; phaseIndex++) { const phase = run.phases[phaseIndex]; const taskIndex = phase.tasks.findIndex(t => t.id === taskId); if (taskIndex !== -1) { return { phase, task: phase.tasks[taskIndex], phaseIndex, taskIndex }; } } return null; } /** * Generate the next task ID for a phase */ export function generateTaskId(phase: Phase): string { const taskCount = phase.tasks.length; return `${phase.id}-task${taskCount + 1}`; } /** * Create a new task */ export function createTask(id: string, name: string, file: string): Task { const taskDir = path.join("tasks", id); return { id, name, status: "pending", started_at: null, finish_at: null, file, paths: { contract: path.join(taskDir, "contract.md"), worker_output: path.join(taskDir, "worker-output.md"), auditor_report: path.join(taskDir, "auditor-report.md") } }; } /** * Add a task to a phase within a run */ export function addTaskToRun(run: Run, phaseId: string, task: Task): Run { return { ...run, phases: run.phases.map(p => p.id === phaseId ? { ...p, tasks: [...p.tasks, task] } : p ) }; } /** * Update a task in a run */ export function updateTaskInRun(run: Run, taskId: string, updates: Partial): Run { return { ...run, phases: run.phases.map(p => ({ ...p, tasks: p.tasks.map(t => t.id === taskId ? { ...t, ...updates } : t ) })) }; } /** * Check if all tasks in a phase are done */ export function areAllTasksDone(phase: Phase): boolean { if (phase.tasks.length === 0) return false; return phase.tasks.every(t => t.status === "done"); } /** * Check if all phases in a run are done */ export function areAllPhasesDone(run: Run): boolean { if (run.phases.length === 0) return false; return run.phases.every(p => p.status === "done"); } // ============================================================================ // Run Discovery // ============================================================================ /** * List all run IDs in the runs directory */ export function listRunIds(): string[] { const runsDir = getRunsDir(); if (!fs.existsSync(runsDir)) { return []; } return fs.readdirSync(runsDir, { withFileTypes: true }) .filter(entry => entry.isDirectory() && entry.name.startsWith("run-")) .map(entry => entry.name) .sort(); } /** * List all runs with their basic info */ export function listRuns(): Array<{ runId: string; run: Run | null }> { const runIds = listRunIds(); return runIds.map(runId => ({ runId, run: readRun(runId) })); } // ============================================================================ // Scaffold Check // ============================================================================ /** * Check if mission-control has been scaffolded */ export function isScaffolded(): boolean { return fileExists(getMissionControlDir()); } // ============================================================================ // Initialization Helpers // ============================================================================ /** * Create the initial run.json structure */ export function createInitialRun(runId: string): Run { const now = getTimestamp(); return { run_id: runId, started_at: now, finish_at: null, status: "in_progress", artifacts: { requirements: "00-requirements.md", architecture: "01-architecture.md", validation: "02-validation.md" }, phases: [] }; } /** * Create the base mission-control directory structure */ export function createBaseStructure(): void { ensureDir(getMissionControlDir()); ensureDir(getMemoryDir()); ensureDir(getRunsDir()); } /** * Create a new run directory structure */ export function createRunStructure(runId: string): void { const runDir = getRunDir(runId); ensureDir(runDir); ensureDir(getTasksDir(runId)); } // ============================================================================ // Status Transition Logic // ============================================================================ /** * Get the appropriate timestamp field to update based on status */ export function getTimestampForStatus( status: PhaseStatus | TaskStatus, currentStatus: PhaseStatus | TaskStatus ): { started_at?: string; finish_at?: string } | null { const now = getTimestamp(); // Transition to in_progress sets started_at if (status === "in_progress" && currentStatus !== "in_progress") { return { started_at: now }; } // Transition to terminal state sets finish_at const terminalStatuses: Array = ["done", "failed", "removed"]; if (terminalStatuses.includes(status) && !terminalStatuses.includes(currentStatus)) { return { finish_at: now }; } return null; }