UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

271 lines 11.6 kB
/** * Autopilot Shared State Module * * Centralizes state management, validation, and task discovery * for both CLI command and MCP tools. Eliminates code duplication. * * ADR-072: Autopilot Integration * Security: Addresses prototype pollution, NaN bypass, input validation */ import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, renameSync } from 'node:fs'; import { resolve, join } from 'node:path'; import { homedir } from 'node:os'; // ── Constants ───────────────────────────────────────────────── export const STATE_DIR = '.claude-flow/data'; export const STATE_FILE = `${STATE_DIR}/autopilot-state.json`; export const LOG_FILE = `${STATE_DIR}/autopilot-log.json`; /** Maximum entries kept in state.history (prevents unbounded growth) */ const MAX_HISTORY_ENTRIES = 50; /** Maximum entries kept in the event log */ const MAX_LOG_ENTRIES = 1000; /** Allowlist for valid task sources */ export const VALID_TASK_SOURCES = new Set(['team-tasks', 'swarm-tasks', 'file-checklist']); /** Terminal task statuses */ export const TERMINAL_STATUSES = new Set(['completed', 'done', 'cancelled', 'skipped', 'failed']); // ── Validation Helpers ──────────────────────────────────────── /** * Sanitize a parsed JSON object to prevent prototype pollution. * Removes __proto__, constructor, and prototype keys recursively. */ function sanitizeObject(obj) { if (obj === null || typeof obj !== 'object') return obj; if (Array.isArray(obj)) return obj.map(sanitizeObject); const clean = {}; for (const key of Object.keys(obj)) { if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue; clean[key] = sanitizeObject(obj[key]); } return clean; } /** * Safe JSON.parse that prevents prototype pollution. */ export function safeJsonParse(raw) { return sanitizeObject(JSON.parse(raw)); } /** * Validate and coerce a numeric parameter. Returns the default if * the input is NaN, undefined, or outside the allowed range. */ export function validateNumber(value, min, max, defaultValue) { if (value === undefined || value === null) return defaultValue; const num = Number(value); if (!Number.isFinite(num)) return defaultValue; return Math.min(Math.max(min, Math.round(num)), max); } /** * Validate task sources against the allowlist. * Returns only valid sources; falls back to defaults if none are valid. */ export function validateTaskSources(sources) { const defaults = ['team-tasks', 'swarm-tasks', 'file-checklist']; if (!Array.isArray(sources)) return defaults; const valid = sources .filter((s) => typeof s === 'string') .map(s => s.trim()) .filter(s => VALID_TASK_SOURCES.has(s)); return valid.length > 0 ? valid : defaults; } // ── State Management ────────────────────────────────────────── export function getDefaultState() { return { sessionId: randomUUID(), enabled: false, startTime: Date.now(), iterations: 0, maxIterations: 50, timeoutMinutes: 240, taskSources: ['team-tasks', 'swarm-tasks', 'file-checklist'], lastCheck: null, history: [], }; } export function loadState() { const filePath = resolve(STATE_FILE); const defaults = getDefaultState(); try { if (existsSync(filePath)) { const raw = safeJsonParse(readFileSync(filePath, 'utf-8')); const merged = { ...defaults, ...raw }; // Re-validate fields that could be tampered with merged.maxIterations = validateNumber(merged.maxIterations, 1, 1000, 50); merged.timeoutMinutes = validateNumber(merged.timeoutMinutes, 1, 1440, 240); merged.iterations = validateNumber(merged.iterations, 0, 1000, 0); merged.taskSources = validateTaskSources(merged.taskSources); // Cap history to prevent unbounded growth if (Array.isArray(merged.history) && merged.history.length > MAX_HISTORY_ENTRIES) { merged.history = merged.history.slice(-MAX_HISTORY_ENTRIES); } return merged; } } catch { // Corrupted state file — return defaults } return defaults; } export function saveState(state) { const dir = resolve(STATE_DIR); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); // Cap history before saving if (state.history.length > MAX_HISTORY_ENTRIES) { state.history = state.history.slice(-MAX_HISTORY_ENTRIES); } const tmpFile = resolve(STATE_FILE) + '.tmp'; writeFileSync(tmpFile, JSON.stringify(state, null, 2)); renameSync(tmpFile, resolve(STATE_FILE)); } export function appendLog(entry) { const filePath = resolve(LOG_FILE); const dir = resolve(STATE_DIR); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); let log = []; try { if (existsSync(filePath)) { log = safeJsonParse(readFileSync(filePath, 'utf-8')); if (!Array.isArray(log)) log = []; } } catch { log = []; } log.push(entry); if (log.length > MAX_LOG_ENTRIES) log = log.slice(-MAX_LOG_ENTRIES); const tmpFile = filePath + '.tmp'; writeFileSync(tmpFile, JSON.stringify(log, null, 2)); renameSync(tmpFile, filePath); } export function loadLog() { const filePath = resolve(LOG_FILE); try { if (existsSync(filePath)) { const result = safeJsonParse(readFileSync(filePath, 'utf-8')); return Array.isArray(result) ? result : []; } } catch { // Corrupted log — return empty } return []; } // ── Task Discovery ──────────────────────────────────────────── export function discoverTasks(sources) { const tasks = []; // Only process valid sources const validSources = sources.filter(s => VALID_TASK_SOURCES.has(s)); for (const source of validSources) { if (source === 'team-tasks') { const tasksDir = join(homedir(), '.claude', 'tasks'); try { if (existsSync(tasksDir)) { const teams = readdirSync(tasksDir, { withFileTypes: true }); for (const team of teams) { if (!team.isDirectory()) continue; const teamDir = join(tasksDir, team.name); const files = readdirSync(teamDir).filter((f) => f.endsWith('.json')); for (const file of files) { try { const data = safeJsonParse(readFileSync(join(teamDir, file), 'utf-8')); tasks.push({ id: String(data.id || file.replace('.json', '')), subject: String(data.subject || data.title || file), status: String(data.status || 'unknown'), source: 'team-tasks', }); } catch { /* skip individual file */ } } } } } catch { /* skip source */ } } if (source === 'swarm-tasks') { const swarmFile = resolve('.claude-flow/swarm-tasks.json'); try { if (existsSync(swarmFile)) { const data = safeJsonParse(readFileSync(swarmFile, 'utf-8')); const swarmTasks = Array.isArray(data) ? data : (data.tasks || []); for (const t of swarmTasks) { if (t && typeof t === 'object') { const task = t; tasks.push({ id: String(task.id || task.taskId || `swarm-${tasks.length}`), subject: String(task.subject || task.description || task.name || 'Unnamed task'), status: String(task.status || 'unknown'), source: 'swarm-tasks', }); } } } } catch { /* skip source */ } } if (source === 'file-checklist') { const checklistFile = resolve('.claude-flow/data/checklist.json'); try { if (existsSync(checklistFile)) { const data = safeJsonParse(readFileSync(checklistFile, 'utf-8')); const items = Array.isArray(data) ? data : (data.items || []); for (const item of items) { if (item && typeof item === 'object') { const i = item; tasks.push({ id: String(i.id || `check-${tasks.length}`), subject: String(i.subject || i.text || i.description || 'Unnamed item'), status: String(i.status || (i.done ? 'completed' : 'pending')), source: 'file-checklist', }); } } } } catch { /* skip source */ } } } return tasks; } // ── Progress Helpers ────────────────────────────────────────── export function isTerminal(status) { return TERMINAL_STATUSES.has(status.toLowerCase()); } export function getProgress(tasks) { const completed = tasks.filter(t => isTerminal(t.status)).length; const total = tasks.length; const percent = total === 0 ? 100 : Math.round((completed / total) * 100); const incomplete = tasks.filter(t => !isTerminal(t.status)); return { completed, total, percent, incomplete }; } // ── Reward Calculation ──────────────────────────────────────── export function calculateReward(iterations, durationMs) { const iterFactor = (1 - iterations / (iterations + 10)) * 0.6; const timeFactor = (1 - Math.min(durationMs / 3600000, 1)) * 0.4; return Math.round((iterFactor + timeFactor) * 100) / 100; } // ── Learning Integration ────────────────────────────────────── export async function tryLoadLearning() { try { const modPath = 'agentic-flow/dist/coordination/autopilot-learning.js'; const mod = await import(/* webpackIgnore: true */ modPath).catch(() => null); if (mod?.AutopilotLearning) { const instance = new mod.AutopilotLearning(); if (await instance.initialize()) return instance; } } catch { /* not available */ } return null; } //# sourceMappingURL=autopilot-state.js.map