agent-governance/ui/server.ts
profit 8561d13728 Add Vault token management and observability integration for multi-agent pipelines
- Vault token issuance per pipeline with 2-hour TTL
- Automatic token renewal loop every 30 minutes
- Error budget tracking with threshold-based revocation
- Observability-driven token revocation for policy violations
- Diagnostic pipeline spawning on error threshold breach
- Structured handoff reports for error recovery
- Agent lifecycle status API
- New API endpoints: /api/pipeline/token, /api/pipeline/errors,
  /api/observability/handoff, /api/observability/diagnostic

Orchestrator now reports errors to parent pipeline's observability
system via PIPELINE_ID environment variable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 17:45:20 -05:00

7406 lines
244 KiB
TypeScript

/**
* Agent Governance Dashboard - Web Server
* ========================================
* Real-time monitoring UI for agent governance system
*
* Features:
* - WebSocket for real-time updates
* - Agent state monitoring
* - Revocation tracking
* - Promotion status
* - Preflight results
*/
import { createClient, RedisClientType } from "redis";
import { Database } from "bun:sqlite";
// =============================================================================
// Configuration
// =============================================================================
const PORT = 3000;
const WS_PING_INTERVAL = 30000;
let redis: RedisClientType;
let wsClients: Set<any> = new Set();
async function getVaultSecret(path: string): Promise<Record<string, any>> {
try {
const initKeys = await Bun.file("/opt/vault/init-keys.json").json();
const token = initKeys.root_token;
const proc = Bun.spawn(["curl", "-sk", "-H", `X-Vault-Token: ${token}`,
`https://127.0.0.1:8200/v1/secret/data/${path}`]);
const text = await new Response(proc.stdout).text();
const result = JSON.parse(text);
return result.data.data;
} catch {
return {};
}
}
async function connectRedis(): Promise<void> {
const creds = await getVaultSecret("services/dragonfly");
redis = createClient({
url: `redis://${creds.host || "127.0.0.1"}:${creds.port || 6379}`,
password: creds.password,
});
await redis.connect();
console.log("[DB] Connected to DragonflyDB");
// Subscribe to changes for real-time updates
const subscriber = redis.duplicate();
await subscriber.connect();
await subscriber.pSubscribe("__keyspace@0__:agent:*", (message, channel) => {
broadcastUpdate("agent_change", { channel, message });
});
}
function broadcastUpdate(type: string, data: any) {
const message = JSON.stringify({ type, data, timestamp: new Date().toISOString() });
wsClients.forEach(ws => {
try {
ws.send(message);
} catch {}
});
}
// =============================================================================
// Data Fetchers
// =============================================================================
async function safeRedisGet(key: string): Promise<string | null> {
try {
const type = await redis.type(key);
if (type === "string") {
return await redis.get(key);
} else if (type === "hash") {
const data = await redis.hGetAll(key);
return JSON.stringify(data);
}
return null;
} catch {
return null;
}
}
async function safeRedisHash(key: string): Promise<Record<string, string>> {
try {
const type = await redis.type(key);
if (type === "hash") {
return await redis.hGetAll(key);
}
return {};
} catch {
return {};
}
}
async function getAgentStates(): Promise<any[]> {
try {
const keys = await redis.keys("agent:*:state");
const agents: any[] = [];
for (const key of keys) {
try {
const data = await safeRedisGet(key);
if (data) {
const state = typeof data === 'string' ? JSON.parse(data) : data;
const agentId = key.split(":")[1];
// Get packet for more details
const packetData = await safeRedisGet(`agent:${agentId}:packet`);
const packet = packetData ? JSON.parse(packetData) : null;
// Get error counts
const errors = await safeRedisHash(`agent:${agentId}:errors`);
agents.push({
agent_id: agentId,
status: state.status || "UNKNOWN",
phase: state.phase || "UNKNOWN",
step: state.step || "",
started_at: state.started_at,
last_progress_at: state.last_progress_at,
notes: state.notes || "",
task_id: packet?.task_id,
objective: packet?.objective,
tier: packet?.tier || 0,
error_count: parseInt(errors.total_errors || "0"),
violations: parseInt(errors.procedure_violations || "0"),
});
}
} catch (e) {
// Skip this agent on error
}
}
return agents.sort((a, b) =>
new Date(b.last_progress_at || 0).getTime() - new Date(a.last_progress_at || 0).getTime()
);
} catch (e: any) {
console.error("[getAgentStates] Error:", e.message);
return [];
}
}
async function getRevocations(limit: number = 50): Promise<any[]> {
try {
const type = await redis.type("revocations:ledger");
if (type !== "list") return [];
const data = await redis.lRange("revocations:ledger", -limit, -1);
return data.map(d => {
try { return JSON.parse(d); } catch { return { raw: d }; }
}).reverse();
} catch {
return [];
}
}
async function getAlerts(limit: number = 20): Promise<any[]> {
try {
const type = await redis.type("alerts:queue");
if (type !== "list") return [];
const data = await redis.lRange("alerts:queue", -limit, -1);
return data.map(d => {
try { return JSON.parse(d); } catch { return { raw: d }; }
}).reverse();
} catch {
return [];
}
}
async function getLedgerActions(limit: number = 50): Promise<any[]> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const rows = db.query(`
SELECT * FROM agent_actions
ORDER BY timestamp DESC
LIMIT ?
`).all(limit);
db.close();
return rows as any[];
} catch {
return [];
}
}
async function getViolations(limit: number = 50): Promise<any[]> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const rows = db.query(`
SELECT * FROM violations
ORDER BY timestamp DESC
LIMIT ?
`).all(limit);
db.close();
return rows as any[];
} catch {
return [];
}
}
// Bug Tracking Functions
const BUG_DB_PATH = "/opt/agent-governance/testing/oversight/bug_watcher.db";
async function getBugs(params: URLSearchParams): Promise<any[]> {
try {
const db = new Database(BUG_DB_PATH, { readonly: true });
const limit = parseInt(params.get("limit") || "50");
const status = params.get("status");
const severity = params.get("severity");
const phase = params.get("phase");
let query = "SELECT * FROM bugs WHERE 1=1";
const queryParams: any[] = [];
if (status) {
query += " AND status = ?";
queryParams.push(status);
}
if (severity) {
query += " AND severity = ?";
queryParams.push(severity);
}
if (phase) {
query += " AND phase = ?";
queryParams.push(parseInt(phase));
}
query += " ORDER BY detected_at DESC LIMIT ?";
queryParams.push(limit);
const rows = db.query(query).all(...queryParams);
db.close();
return (rows as any[]).map(row => ({
...row,
details: row.details ? JSON.parse(row.details) : {}
}));
} catch (e) {
console.error("[BUGS] Error fetching bugs:", e);
return [];
}
}
async function getBug(bugId: string): Promise<any | null> {
try {
const db = new Database(BUG_DB_PATH, { readonly: true });
const row = db.query("SELECT * FROM bugs WHERE id = ?").get(bugId) as any;
db.close();
if (!row) return null;
return {
...row,
details: row.details ? JSON.parse(row.details) : {}
};
} catch {
return null;
}
}
async function getBugSummary(): Promise<any> {
try {
const db = new Database(BUG_DB_PATH, { readonly: true });
const total = (db.query("SELECT COUNT(*) as count FROM bugs").get() as any)?.count || 0;
const byStatus = db.query(`
SELECT status, COUNT(*) as count FROM bugs GROUP BY status
`).all() as any[];
const bySeverity = db.query(`
SELECT severity, COUNT(*) as count FROM bugs GROUP BY severity ORDER BY
CASE severity WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 WHEN 'low' THEN 4 ELSE 5 END
`).all() as any[];
const byPhase = db.query(`
SELECT phase, phase_name, COUNT(*) as count FROM bugs GROUP BY phase ORDER BY phase
`).all() as any[];
const recent = db.query(`
SELECT * FROM bugs ORDER BY detected_at DESC LIMIT 5
`).all() as any[];
db.close();
const statusMap: Record<string, number> = { open: 0, in_progress: 0, resolved: 0 };
byStatus.forEach(r => { statusMap[r.status] = r.count; });
return {
total,
open: statusMap.open || 0,
in_progress: statusMap.in_progress || 0,
resolved: statusMap.resolved || 0,
by_severity: bySeverity,
by_phase: byPhase,
recent: recent.map(r => ({ ...r, details: r.details ? JSON.parse(r.details) : {} }))
};
} catch (e) {
console.error("[BUGS] Error getting summary:", e);
return { total: 0, open: 0, in_progress: 0, resolved: 0, by_severity: [], by_phase: [], recent: [] };
}
}
async function logBug(params: {
message: string;
severity?: string;
type?: string;
phase?: number;
directory?: string;
details?: Record<string, any>;
}): Promise<any> {
const db = new Database(BUG_DB_PATH);
const id = `anom-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
const now = new Date().toISOString();
const severity = params.severity || "medium";
const type = params.type || "unhandled_error";
const phase = params.phase || 0;
const phaseNames: Record<number, string> = {
1: "Foundation", 2: "Vault Policy", 3: "Execution", 4: "Promotion/Revocation",
5: "Agent Bootstrap", 6: "Pipeline DSL", 7: "Teams & Learning", 8: "Production Hardening",
9: "Integrations", 10: "Multi-Tenant", 11: "Marketplace", 12: "Observability", 0: "Unknown"
};
db.query(`
INSERT INTO bugs (id, type, severity, status, phase, phase_name, directory, message, details, detected_at)
VALUES (?, ?, ?, 'open', ?, ?, ?, ?, ?, ?)
`).run(
id, type, severity, phase, phaseNames[phase] || `Phase ${phase}`,
params.directory || "unknown", params.message,
params.details ? JSON.stringify(params.details) : null, now
);
db.close();
return {
id,
type,
severity,
status: "open",
phase,
phase_name: phaseNames[phase] || `Phase ${phase}`,
directory: params.directory || "unknown",
message: params.message,
details: params.details || {},
detected_at: now
};
}
async function updateBugStatus(bugId: string, params: {
status?: string;
notes?: string;
assigned_to?: string;
}): Promise<{ success: boolean; message: string }> {
try {
const db = new Database(BUG_DB_PATH);
// Check if bug exists
const existing = db.query("SELECT id FROM bugs WHERE id = ?").get(bugId);
if (!existing) {
db.close();
return { success: false, message: "Bug not found" };
}
const now = new Date().toISOString();
const updates: string[] = ["updated_at = ?"];
const values: any[] = [now];
if (params.status) {
updates.push("status = ?");
values.push(params.status);
if (params.status === "resolved") {
updates.push("resolved_at = ?");
values.push(now);
}
}
if (params.notes !== undefined) {
updates.push("resolution_notes = ?");
values.push(params.notes);
}
if (params.assigned_to !== undefined) {
updates.push("assigned_to = ?");
values.push(params.assigned_to);
}
values.push(bugId);
db.query(`UPDATE bugs SET ${updates.join(", ")} WHERE id = ?`).run(...values);
db.close();
return { success: true, message: `Bug ${bugId} updated` };
} catch (e: any) {
return { success: false, message: e.message };
}
}
async function getPromotions(limit: number = 20): Promise<any[]> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const rows = db.query(`
SELECT * FROM promotions
ORDER BY timestamp DESC
LIMIT ?
`).all(limit);
db.close();
return rows as any[];
} catch {
return [];
}
}
async function getOrchestrationLogs(limit: number = 50): Promise<any[]> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const rows = db.query(`
SELECT * FROM orchestration_log
ORDER BY timestamp DESC
LIMIT ?
`).all(limit);
db.close();
return rows as any[];
} catch {
return [];
}
}
async function getOrchestrationSummary(): Promise<any> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
// Get summary by mode
const byMode = db.query(`
SELECT mode, COUNT(*) as count,
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successes
FROM orchestration_log
GROUP BY mode
`).all();
// Get summary by model
const byModel = db.query(`
SELECT model, COUNT(*) as count,
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successes
FROM orchestration_log
GROUP BY model
`).all();
// Get latest entry
const latest = db.query(`
SELECT * FROM orchestration_log ORDER BY timestamp DESC LIMIT 1
`).get();
// Get total count
const total = db.query(`SELECT COUNT(*) as count FROM orchestration_log`).get() as any;
db.close();
return {
by_mode: byMode,
by_model: byModel,
latest,
total_runs: total?.count || 0
};
} catch {
return { by_mode: [], by_model: [], latest: null, total_runs: 0 };
}
}
async function getAgentMetrics(): Promise<any[]> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const rows = db.query(`
SELECT * FROM agent_metrics
ORDER BY last_active_at DESC
`).all();
db.close();
return rows as any[];
} catch {
return [];
}
}
// =============================================================================
// New UI Tab Data Fetchers
// =============================================================================
const CHECKPOINT_VENV = "python3"; // Use system python with redis installed
const CHECKPOINT_PY = "/opt/agent-governance/checkpoint/checkpoint.py";
const MEMORY_DB_PATH = "/opt/agent-governance/memory/memory.db";
const MEMORY_PY = "/opt/agent-governance/memory/memory.py";
async function runPythonCommand(args: string[]): Promise<any> {
try {
const proc = Bun.spawn(args, {
cwd: "/opt/agent-governance",
stdout: "pipe",
stderr: "pipe",
});
const output = await new Response(proc.stdout).text();
const exitCode = await proc.exited;
if (exitCode !== 0) {
const stderr = await new Response(proc.stderr).text();
console.error(`[Python] Command failed: ${args.join(" ")}\n${stderr}`);
return null;
}
try {
return JSON.parse(output);
} catch {
return output.trim();
}
} catch (e: any) {
console.error(`[Python] Error: ${e.message}`);
return null;
}
}
// Checkpoint Fetchers
async function getCheckpointList(limit: number = 20): Promise<any[]> {
const result = await runPythonCommand([
CHECKPOINT_VENV, CHECKPOINT_PY, "list", "--limit", String(limit), "--json"
]);
return Array.isArray(result) ? result : [];
}
async function getCheckpointDetail(checkpointId?: string): Promise<any> {
const args = [CHECKPOINT_VENV, CHECKPOINT_PY, "load", "--json"];
if (checkpointId) args.splice(3, 0, checkpointId);
return await runPythonCommand(args);
}
async function getCheckpointDiff(fromId?: string, toId?: string): Promise<any> {
const args = [CHECKPOINT_VENV, CHECKPOINT_PY, "diff", "--json"];
if (fromId) args.push("--from", fromId);
if (toId) args.push("--to", toId);
return await runPythonCommand(args);
}
async function getCheckpointSummary(level: string = "compact"): Promise<string> {
const args = [CHECKPOINT_VENV, CHECKPOINT_PY, "summary", "--level", level];
const result = await runPythonCommand(args);
return typeof result === "string" ? result : JSON.stringify(result, null, 2);
}
async function createCheckpointNow(notes?: string): Promise<any> {
const args = [CHECKPOINT_VENV, CHECKPOINT_PY, "now", "--json"];
if (notes) args.push("--notes", notes);
return await runPythonCommand(args);
}
async function getCheckpointReport(): Promise<any> {
return await runPythonCommand([CHECKPOINT_VENV, CHECKPOINT_PY, "report", "--json"]);
}
async function getCheckpointTimeline(limit: number = 10): Promise<any> {
return await runPythonCommand([
CHECKPOINT_VENV, CHECKPOINT_PY, "timeline", "--limit", String(limit), "--json"
]);
}
// Memory Fetchers
async function getMemoryList(type?: string, limit: number = 50): Promise<any[]> {
try {
const db = new Database(MEMORY_DB_PATH, { readonly: true });
// Query from memory_entries table (real data) with mapped column names
let query = `SELECT
id,
type as entry_type,
directory as source_agent,
summary,
tokens_estimate as total_size,
created_at,
status,
tags
FROM memory_entries WHERE status = 'active'`;
const params: any[] = [];
if (type) {
query += " AND type = ?";
params.push(type);
}
query += " ORDER BY created_at DESC LIMIT ?";
params.push(limit);
const rows = db.query(query).all(...params);
db.close();
return rows as any[];
} catch (e: any) {
console.error(`[Memory] Error listing: ${e.message}`);
return [];
}
}
async function getMemoryEntry(id: string): Promise<any> {
try {
const db = new Database(MEMORY_DB_PATH, { readonly: true });
const row = db.query(`
SELECT id, type as entry_type, content, content_path, summary,
tokens_estimate as total_size, created_at, status, tags,
directory, checkpoint_id, context
FROM memory_entries WHERE id = ?
`).get(id) as any;
db.close();
if (!row) return { error: "Entry not found" };
// If content_path exists and content is empty, try to read from file
if (row.content_path && !row.content) {
try {
const file = Bun.file(row.content_path);
if (await file.exists()) {
// Handle gzipped files
if (row.content_path.endsWith('.gz')) {
const gzipped = await file.arrayBuffer();
const decompressed = Bun.gunzipSync(new Uint8Array(gzipped));
row.content = new TextDecoder().decode(decompressed);
} else {
row.content = await file.text();
}
}
} catch (e) {
row.content = `[Error reading content: ${e}]`;
}
}
return row;
} catch (e: any) {
console.error(`[Memory] Error fetching entry: ${e.message}`);
return { error: e.message };
}
}
async function searchMemory(query: string): Promise<any[]> {
const result = await runPythonCommand(["python3", MEMORY_PY, "search", query, "--json"]);
return Array.isArray(result) ? result : [];
}
async function getMemoryStats(): Promise<any> {
return await runPythonCommand(["python3", MEMORY_PY, "stats", "--json"]);
}
// Status Grid Fetcher (64 directories)
async function getStatusGrid(): Promise<any> {
// Use checkpoint report which includes directory statuses
const report = await getCheckpointReport();
if (!report) return { directories: [], summary: {} };
const directories = report.directory_statuses || [];
const summary = {
total: directories.length,
complete: directories.filter((d: any) => d.phase === "complete").length,
in_progress: directories.filter((d: any) => d.phase === "in_progress").length,
blocked: directories.filter((d: any) => d.phase === "blocked").length,
not_started: directories.filter((d: any) => d.phase === "not_started").length,
};
return { directories, summary, checkpoint: report.checkpoint };
}
// Integration Status Fetchers
// NOTE: External integrations (Slack, GitHub, PagerDuty) have been deprecated.
// Code archived to .archive/integrations/. Framework retained in integrations/common/.
async function getIntegrationStatus(): Promise<any> {
const timestamp = new Date().toISOString();
return {
slack: {
name: "Slack",
status: "deprecated",
last_checked: timestamp,
details: "Archived - not required for core functionality"
},
github: {
name: "GitHub",
status: "deprecated",
last_checked: timestamp,
details: "Archived - not required for core functionality"
},
pagerduty: {
name: "PagerDuty",
status: "deprecated",
last_checked: timestamp,
details: "Archived - not required for core functionality"
},
_note: "External integrations deprecated. See .archive/integrations/ to restore."
};
}
async function testIntegration(name: string): Promise<any> {
// External integrations have been deprecated
const timestamp = new Date().toISOString();
return {
success: false,
message: `${name} integration deprecated - archived to .archive/integrations/`,
timestamp,
deprecated: true
};
}
// Analytics Fetchers (Direct SQLite)
async function getViolationsByType(): Promise<any[]> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const rows = db.query(`
SELECT violation_type as type, COUNT(*) as count
FROM violations
GROUP BY violation_type
ORDER BY count DESC
`).all();
db.close();
return rows as any[];
} catch { return []; }
}
async function getViolationsBySeverity(): Promise<any[]> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const rows = db.query(`
SELECT severity, COUNT(*) as count
FROM violations
GROUP BY severity
ORDER BY
CASE severity
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
ELSE 5
END
`).all();
db.close();
return rows as any[];
} catch { return []; }
}
async function getViolationsByTime(days: number = 7): Promise<any[]> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const rows = db.query(`
SELECT
strftime('%Y-%m-%d %H:00', timestamp) as hour,
COUNT(*) as count
FROM violations
WHERE timestamp >= datetime('now', '-${days} days')
GROUP BY hour
ORDER BY hour ASC
`).all();
db.close();
return rows as any[];
} catch { return []; }
}
async function getAnalyticsSummary(): Promise<any> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const totalViolations = db.query("SELECT COUNT(*) as count FROM violations").get() as any;
const last24h = db.query(`
SELECT COUNT(*) as count FROM violations
WHERE timestamp >= datetime('now', '-1 day')
`).get() as any;
const byAgent = db.query(`
SELECT agent_id, COUNT(*) as count
FROM violations
GROUP BY agent_id
ORDER BY count DESC
LIMIT 5
`).all();
db.close();
return {
total_violations: totalViolations?.count || 0,
last_24h: last24h?.count || 0,
top_agents: byAgent,
};
} catch { return { total_violations: 0, last_24h: 0, top_agents: [] }; }
}
// Tier Progression Fetchers
async function getTierSummary(): Promise<any> {
try {
// Get agents by tier from Redis
const keys = await redis.keys("agent:*:state");
const tiers: Record<string, number> = { T0: 0, T1: 0, T2: 0, T3: 0, T4: 0 };
for (const key of keys) {
try {
const packetKey = key.replace(":state", ":packet");
const packetData = await safeRedisGet(packetKey);
if (packetData) {
const packet = JSON.parse(packetData);
const tier = `T${packet.tier || 0}`;
if (tier in tiers) tiers[tier]++;
}
} catch {}
}
return tiers;
} catch { return { T0: 0, T1: 0, T2: 0, T3: 0, T4: 0 }; }
}
async function getTierPromotions(limit: number = 20): Promise<any[]> {
try {
const db = new Database("/opt/agent-governance/ledger/governance.db", { readonly: true });
const rows = db.query(`
SELECT * FROM promotions
ORDER BY timestamp DESC
LIMIT ?
`).all(limit);
db.close();
return rows as any[];
} catch { return []; }
}
async function getTierDefinitions(): Promise<any[]> {
return [
{ tier: "T0", name: "Sandboxed", description: "No external access, local operations only", color: "#6b7280" },
{ tier: "T1", name: "Read-Only", description: "Can read external resources, no writes", color: "#3b82f6" },
{ tier: "T2", name: "Limited Write", description: "Can write to approved destinations", color: "#8b5cf6" },
{ tier: "T3", name: "Full Access", description: "Full API access with audit logging", color: "#f59e0b" },
{ tier: "T4", name: "Autonomous", description: "Self-managing with minimal oversight", color: "#10b981" },
];
}
// =============================================================================
// Pipeline & Multi-Agent Data Fetchers
// =============================================================================
async function getPipelines(): Promise<any[]> {
try {
// Get all task-based pipelines by scanning for agents:task-* keys
const taskKeys = await redis.keys("agents:task-*");
const pipelines: any[] = [];
for (const key of taskKeys) {
const taskId = key.replace("agents:task-", "").replace(":*", "");
// Get agents in this pipeline - handle different data types
const agents: any[] = [];
try {
const keyType = await redis.type(key);
if (keyType === "hash") {
const agentData = await redis.hGetAll(key);
for (const [agentType, info] of Object.entries(agentData)) {
try {
const parsed = typeof info === 'string' ? JSON.parse(info) : info;
agents.push({ type: agentType, ...parsed });
} catch {
agents.push({ type: agentType, info });
}
}
} else if (keyType === "set") {
const members = await redis.sMembers(key);
members.forEach((m: string) => agents.push({ type: m, agent_id: m }));
} else if (keyType === "list") {
const items = await redis.lRange(key, 0, -1);
items.forEach((item: string) => {
try {
agents.push(JSON.parse(item));
} catch {
agents.push({ type: item });
}
});
}
} catch (e) {
// Skip keys that can't be read
console.log(`[WARN] Could not read ${key}: ${e}`);
}
// Get spawn conditions - handle type safely
const spawnKey = `spawn:task-${taskId}:conditions`;
const spawnConditions: any = {};
try {
const spawnType = await redis.type(spawnKey);
if (spawnType === "hash") {
const spawnData = await redis.hGetAll(spawnKey);
for (const [condType, condInfo] of Object.entries(spawnData)) {
try {
spawnConditions[condType] = typeof condInfo === 'string' ? JSON.parse(condInfo) : condInfo;
} catch {
spawnConditions[condType] = condInfo;
}
}
}
} catch {}
// Get blackboard progress - handle type safely
const progressKey = `blackboard:task-${taskId}:progress`;
const progress: any = {};
try {
const progressType = await redis.type(progressKey);
if (progressType === "hash") {
const progressData = await redis.hGetAll(progressKey);
for (const [k, v] of Object.entries(progressData)) {
try {
progress[k] = typeof v === 'string' ? JSON.parse(v) : v;
} catch {
progress[k] = v;
}
}
}
} catch {}
// Get consensus - check type first
const consensusKey = `blackboard:task-${taskId}:consensus`;
let consensus = null;
try {
const consensusType = await redis.type(consensusKey);
if (consensusType === "string") {
const consensusRaw = await redis.get(consensusKey);
if (consensusRaw) {
consensus = JSON.parse(consensusRaw);
}
} else if (consensusType === "hash") {
consensus = await redis.hGetAll(consensusKey);
}
} catch {}
// Get metrics - check type first
const metricsKey = `metrics:task-${taskId}`;
let metrics = null;
try {
const metricsType = await redis.type(metricsKey);
if (metricsType === "string") {
const metricsRaw = await redis.get(metricsKey);
if (metricsRaw) {
metrics = JSON.parse(metricsRaw);
}
} else if (metricsType === "hash") {
metrics = await redis.hGetAll(metricsKey);
}
} catch {}
// Determine pipeline status
let status = "idle";
const gammaTriggered = Object.values(spawnConditions).some((c: any) => c?.triggered);
if (consensus?.achieved) {
status = "completed";
} else if (gammaTriggered) {
status = "diagnostic";
} else if (agents.length > 0) {
status = "running";
}
pipelines.push({
task_id: taskId,
status,
agents,
spawn_conditions: spawnConditions,
progress,
consensus,
metrics,
gamma_active: gammaTriggered,
});
}
return pipelines;
} catch (e: any) {
console.error("[getPipelines] Error:", e.message);
return [];
}
}
async function getMessageLog(taskId: string, limit: number = 50): Promise<any[]> {
try {
const key = `msg:task-${taskId}:log`;
const type = await redis.type(key);
if (type !== "list") return [];
const messages = await redis.lRange(key, -limit, -1);
return messages.map(m => {
try { return JSON.parse(m); } catch { return { raw: m }; }
}).reverse();
} catch {
return [];
}
}
async function getTaskHistory(taskId: string): Promise<any[]> {
try {
const key = `task:${taskId}:history`;
const type = await redis.type(key);
if (type !== "list") return [];
const history = await redis.lRange(key, 0, -1);
return history.map(h => {
try { return JSON.parse(h); } catch { return { raw: h }; }
});
} catch {
return [];
}
}
async function getBlackboardSolutions(taskId: string): Promise<any[]> {
try {
const key = `blackboard:task-${taskId}:solutions`;
const type = await redis.type(key);
if (type !== "list") return [];
const solutions = await redis.lRange(key, 0, -1);
return solutions.map(s => {
try { return JSON.parse(s); } catch { return { raw: s }; }
});
} catch {
return [];
}
}
// =============================================================================
// Vault Token Management for Pipelines
// =============================================================================
interface VaultTokenInfo {
token: string;
accessor: string;
ttl: number;
created_at: string;
renewable: boolean;
policies: string[];
}
interface PipelineTokenStatus {
pipeline_id: string;
token_active: boolean;
issued_at?: string;
expires_at?: string;
last_renewed?: string;
revoked?: boolean;
revoke_reason?: string;
}
// Error budget tracking
interface ErrorBudget {
pipeline_id: string;
total_errors: number;
errors_per_minute: number;
last_error_at?: string;
threshold_exceeded: boolean;
error_types: Record<string, number>;
}
const ERROR_THRESHOLDS = {
max_errors_per_minute: 5,
max_total_errors: 20,
stuck_timeout_seconds: 60,
critical_violation_immediate: true,
};
// Track error budgets in memory (also persisted to Redis)
const errorBudgets: Map<string, ErrorBudget> = new Map();
async function issuePipelineToken(pipelineId: string): Promise<VaultTokenInfo | null> {
try {
const initKeys = await Bun.file("/opt/vault/init-keys.json").json();
const rootToken = initKeys.root_token;
// Create a pipeline-specific token with limited TTL and policies
const tokenRequest = {
policies: ["pipeline-agent"],
ttl: "2h",
renewable: true,
display_name: `pipeline-${pipelineId}`,
meta: {
pipeline_id: pipelineId,
created_by: "orchestrator"
}
};
const proc = Bun.spawn(["curl", "-sk", "-X", "POST",
"-H", `X-Vault-Token: ${rootToken}`,
"-d", JSON.stringify(tokenRequest),
"https://127.0.0.1:8200/v1/auth/token/create"
], { stdout: "pipe" });
const text = await new Response(proc.stdout).text();
const result = JSON.parse(text);
if (result.auth) {
const tokenInfo: VaultTokenInfo = {
token: result.auth.client_token,
accessor: result.auth.accessor,
ttl: result.auth.lease_duration,
created_at: new Date().toISOString(),
renewable: result.auth.renewable,
policies: result.auth.policies
};
// Store token info in Redis (encrypted reference, not actual token)
await redis.hSet(`pipeline:${pipelineId}:vault`, {
accessor: tokenInfo.accessor,
issued_at: tokenInfo.created_at,
expires_at: new Date(Date.now() + tokenInfo.ttl * 1000).toISOString(),
renewable: tokenInfo.renewable ? "true" : "false",
policies: JSON.stringify(tokenInfo.policies),
status: "active"
});
broadcastUpdate("token_issued", {
pipeline_id: pipelineId,
accessor: tokenInfo.accessor,
expires_at: new Date(Date.now() + tokenInfo.ttl * 1000).toISOString()
});
return tokenInfo;
}
return null;
} catch (e: any) {
console.error(`[VAULT] Error issuing token for pipeline ${pipelineId}:`, e.message);
return null;
}
}
async function renewPipelineToken(pipelineId: string): Promise<boolean> {
try {
const tokenData = await redis.hGetAll(`pipeline:${pipelineId}:vault`);
if (!tokenData.accessor || tokenData.status !== "active") {
return false;
}
const initKeys = await Bun.file("/opt/vault/init-keys.json").json();
const rootToken = initKeys.root_token;
// Renew by accessor
const proc = Bun.spawn(["curl", "-sk", "-X", "POST",
"-H", `X-Vault-Token: ${rootToken}`,
"-d", JSON.stringify({ accessor: tokenData.accessor }),
"https://127.0.0.1:8200/v1/auth/token/renew-accessor"
], { stdout: "pipe" });
const text = await new Response(proc.stdout).text();
const result = JSON.parse(text);
if (result.auth) {
const newExpiry = new Date(Date.now() + result.auth.lease_duration * 1000).toISOString();
await redis.hSet(`pipeline:${pipelineId}:vault`, {
expires_at: newExpiry,
last_renewed: new Date().toISOString()
});
broadcastUpdate("token_renewed", {
pipeline_id: pipelineId,
expires_at: newExpiry
});
return true;
}
return false;
} catch (e: any) {
console.error(`[VAULT] Error renewing token for pipeline ${pipelineId}:`, e.message);
return false;
}
}
async function revokePipelineToken(pipelineId: string, reason: string): Promise<boolean> {
try {
const tokenData = await redis.hGetAll(`pipeline:${pipelineId}:vault`);
if (!tokenData.accessor) {
return false;
}
const initKeys = await Bun.file("/opt/vault/init-keys.json").json();
const rootToken = initKeys.root_token;
// Revoke by accessor
const proc = Bun.spawn(["curl", "-sk", "-X", "POST",
"-H", `X-Vault-Token: ${rootToken}`,
"-d", JSON.stringify({ accessor: tokenData.accessor }),
"https://127.0.0.1:8200/v1/auth/token/revoke-accessor"
], { stdout: "pipe" });
await proc.exited;
// Update Redis
await redis.hSet(`pipeline:${pipelineId}:vault`, {
status: "revoked",
revoked_at: new Date().toISOString(),
revoke_reason: reason
});
broadcastUpdate("token_revoked", {
pipeline_id: pipelineId,
reason: reason,
timestamp: new Date().toISOString()
});
await appendPipelineLog(pipelineId, "VAULT", `Token revoked: ${reason}`, "WARN");
return true;
} catch (e: any) {
console.error(`[VAULT] Error revoking token for pipeline ${pipelineId}:`, e.message);
return false;
}
}
async function getPipelineTokenStatus(pipelineId: string): Promise<PipelineTokenStatus> {
const tokenData = await redis.hGetAll(`pipeline:${pipelineId}:vault`);
return {
pipeline_id: pipelineId,
token_active: tokenData.status === "active",
issued_at: tokenData.issued_at,
expires_at: tokenData.expires_at,
last_renewed: tokenData.last_renewed,
revoked: tokenData.status === "revoked",
revoke_reason: tokenData.revoke_reason
};
}
// =============================================================================
// Error Budget & Observability Integration
// =============================================================================
async function initializeErrorBudget(pipelineId: string): Promise<ErrorBudget> {
const budget: ErrorBudget = {
pipeline_id: pipelineId,
total_errors: 0,
errors_per_minute: 0,
threshold_exceeded: false,
error_types: {}
};
errorBudgets.set(pipelineId, budget);
await redis.hSet(`pipeline:${pipelineId}:errors`, {
total_errors: "0",
errors_per_minute: "0",
threshold_exceeded: "false",
error_types: "{}"
});
return budget;
}
async function recordError(
pipelineId: string,
errorType: string,
severity: "low" | "medium" | "high" | "critical",
details: string
): Promise<{ threshold_exceeded: boolean; action_taken?: string }> {
let budget = errorBudgets.get(pipelineId);
if (!budget) {
budget = await initializeErrorBudget(pipelineId);
}
budget.total_errors++;
budget.error_types[errorType] = (budget.error_types[errorType] || 0) + 1;
budget.last_error_at = new Date().toISOString();
// Calculate errors per minute (rolling window)
const errorKey = `pipeline:${pipelineId}:error_times`;
const now = Date.now();
await redis.rPush(errorKey, String(now));
// Remove errors older than 1 minute
const oneMinuteAgo = now - 60000;
const errorTimes = await redis.lRange(errorKey, 0, -1);
const recentErrors = errorTimes.filter(t => parseInt(t) > oneMinuteAgo);
budget.errors_per_minute = recentErrors.length;
// Persist to Redis
await redis.hSet(`pipeline:${pipelineId}:errors`, {
total_errors: String(budget.total_errors),
errors_per_minute: String(budget.errors_per_minute),
last_error_at: budget.last_error_at,
error_types: JSON.stringify(budget.error_types)
});
// Log the error
await appendPipelineLog(pipelineId, "ERROR_MONITOR",
`Error recorded: ${errorType} (${severity}) - ${details}`,
severity === "critical" ? "ERROR" : "WARN"
);
// Check thresholds
let actionTaken: string | undefined;
if (severity === "critical" && ERROR_THRESHOLDS.critical_violation_immediate) {
budget.threshold_exceeded = true;
actionTaken = "immediate_revocation";
await revokePipelineToken(pipelineId, `Critical error: ${errorType}`);
await spawnDiagnosticPipeline(pipelineId, errorType, details);
} else if (budget.errors_per_minute >= ERROR_THRESHOLDS.max_errors_per_minute) {
budget.threshold_exceeded = true;
actionTaken = "rate_exceeded_revocation";
await revokePipelineToken(pipelineId, `Error rate exceeded: ${budget.errors_per_minute}/min`);
await spawnDiagnosticPipeline(pipelineId, "rate_exceeded", `${budget.errors_per_minute} errors in last minute`);
} else if (budget.total_errors >= ERROR_THRESHOLDS.max_total_errors) {
budget.threshold_exceeded = true;
actionTaken = "budget_exhausted_revocation";
await revokePipelineToken(pipelineId, `Error budget exhausted: ${budget.total_errors} total errors`);
await spawnDiagnosticPipeline(pipelineId, "budget_exhausted", `${budget.total_errors} total errors`);
}
if (budget.threshold_exceeded) {
await redis.hSet(`pipeline:${pipelineId}:errors`, "threshold_exceeded", "true");
broadcastUpdate("error_threshold", {
pipeline_id: pipelineId,
total_errors: budget.total_errors,
errors_per_minute: budget.errors_per_minute,
action_taken: actionTaken
});
}
errorBudgets.set(pipelineId, budget);
return { threshold_exceeded: budget.threshold_exceeded, action_taken: actionTaken };
}
async function spawnDiagnosticPipeline(
sourcePipelineId: string,
errorType: string,
errorDetails: string
): Promise<string> {
const diagnosticPipelineId = `diagnostic-${sourcePipelineId}-${Date.now().toString(36)}`;
// Create handoff report
const handoffReport = {
report_type: "error_handoff",
source_pipeline_id: sourcePipelineId,
diagnostic_pipeline_id: diagnosticPipelineId,
timestamp: new Date().toISOString(),
summary: {
error_type: errorType,
error_details: errorDetails,
error_budget: errorBudgets.get(sourcePipelineId)
},
context: {
pipeline_status: await redis.hGetAll(`pipeline:${sourcePipelineId}`),
recent_logs: await getPipelineLogs(sourcePipelineId, 20)
},
recommended_actions: [
"Review error patterns",
"Check resource availability",
"Verify API connectivity",
"Consider task decomposition"
]
};
// Store handoff report
await redis.set(`handoff:${diagnosticPipelineId}`, JSON.stringify(handoffReport));
// Create diagnostic pipeline entry
await redis.hSet(`pipeline:${diagnosticPipelineId}`, {
task_id: `diag-task-${Date.now().toString(36)}`,
objective: `Diagnose and recover from: ${errorType} in ${sourcePipelineId}`,
status: "DIAGNOSTIC",
created_at: new Date().toISOString(),
source_pipeline: sourcePipelineId,
handoff_report: JSON.stringify(handoffReport),
agents: JSON.stringify([])
});
await appendPipelineLog(diagnosticPipelineId, "SYSTEM",
`Diagnostic pipeline spawned for: ${sourcePipelineId}`, "INFO"
);
broadcastUpdate("diagnostic_spawned", {
diagnostic_pipeline_id: diagnosticPipelineId,
source_pipeline_id: sourcePipelineId,
error_type: errorType,
handoff_report: handoffReport
});
return diagnosticPipelineId;
}
async function generateHandoffReport(pipelineId: string): Promise<any> {
const pipelineData = await redis.hGetAll(`pipeline:${pipelineId}`);
const errorData = await redis.hGetAll(`pipeline:${pipelineId}:errors`);
const tokenData = await redis.hGetAll(`pipeline:${pipelineId}:vault`);
const logs = await getPipelineLogs(pipelineId, 50);
return {
report_type: "structured_handoff",
pipeline_id: pipelineId,
generated_at: new Date().toISOString(),
pipeline_state: {
status: pipelineData.status,
created_at: pipelineData.created_at,
objective: pipelineData.objective,
agents: pipelineData.agents ? JSON.parse(pipelineData.agents) : []
},
error_summary: {
total_errors: parseInt(errorData.total_errors || "0"),
errors_per_minute: parseInt(errorData.errors_per_minute || "0"),
threshold_exceeded: errorData.threshold_exceeded === "true",
error_types: errorData.error_types ? JSON.parse(errorData.error_types) : {}
},
token_status: {
active: tokenData.status === "active",
revoked: tokenData.status === "revoked",
revoke_reason: tokenData.revoke_reason
},
recent_activity: logs.slice(0, 20),
recommendations: generateRecommendations(pipelineData, errorData)
};
}
function generateRecommendations(pipelineData: any, errorData: any): string[] {
const recommendations: string[] = [];
const totalErrors = parseInt(errorData.total_errors || "0");
const errorTypes = errorData.error_types ? JSON.parse(errorData.error_types) : {};
if (totalErrors > 10) {
recommendations.push("Consider breaking down the task into smaller subtasks");
}
if (errorTypes["api_timeout"]) {
recommendations.push("Reduce API call frequency or implement backoff");
}
if (errorTypes["validation_failure"]) {
recommendations.push("Review input validation rules");
}
if (pipelineData.status === "STUCK" || pipelineData.status === "BLOCKED") {
recommendations.push("Check for circular dependencies");
recommendations.push("Verify all required resources are available");
}
if (recommendations.length === 0) {
recommendations.push("Review logs for specific error patterns");
}
return recommendations;
}
async function getErrorBudget(pipelineId: string): Promise<ErrorBudget | null> {
const data = await redis.hGetAll(`pipeline:${pipelineId}:errors`);
if (!data.total_errors) return null;
return {
pipeline_id: pipelineId,
total_errors: parseInt(data.total_errors),
errors_per_minute: parseInt(data.errors_per_minute || "0"),
last_error_at: data.last_error_at,
threshold_exceeded: data.threshold_exceeded === "true",
error_types: data.error_types ? JSON.parse(data.error_types) : {}
};
}
// Helper: Determine agent lifecycle state from status
function determineAgentLifecycle(pipelineStatus: string, agentState: any): string {
if (!agentState) {
if (pipelineStatus === "PENDING") return "CREATED";
if (pipelineStatus === "COMPLETED") return "SUCCEEDED";
if (pipelineStatus === "FAILED" || pipelineStatus === "ERROR") return "ERROR";
return "CREATED";
}
const status = agentState.status || pipelineStatus;
switch (status) {
case "PENDING":
case "IDLE":
return "CREATED";
case "WORKING":
case "RUNNING":
return "BUSY";
case "WAITING":
case "BLOCKED":
return "WAITING";
case "COMPLETED":
return "SUCCEEDED";
case "FAILED":
case "ERROR":
return "ERROR";
default:
// Check for handoff
if (agentState.handed_off_to) return "HANDED-OFF";
return "BUSY";
}
}
// Token renewal loop (runs every 30 minutes for active pipelines)
async function runTokenRenewalLoop(): Promise<void> {
setInterval(async () => {
try {
const pipelineKeys = await redis.keys("pipeline:*:vault");
for (const key of pipelineKeys) {
const pipelineId = key.replace("pipeline:", "").replace(":vault", "");
const tokenData = await redis.hGetAll(key);
if (tokenData.status === "active" && tokenData.expires_at) {
const expiresAt = new Date(tokenData.expires_at).getTime();
const now = Date.now();
const timeToExpiry = expiresAt - now;
// Renew if less than 35 minutes to expiry
if (timeToExpiry < 35 * 60 * 1000 && timeToExpiry > 0) {
console.log(`[VAULT] Renewing token for pipeline ${pipelineId}`);
await renewPipelineToken(pipelineId);
}
}
}
} catch (e: any) {
console.error("[VAULT] Token renewal loop error:", e.message);
}
}, 30 * 60 * 1000); // Every 30 minutes
}
// =============================================================================
// Pipeline Spawning
// =============================================================================
interface PipelineConfig {
task_id: string;
objective: string;
spawn_diagnostic: boolean;
auto_continue?: boolean; // Auto-trigger OpenRouter orchestration after report
model?: string; // OpenRouter model (default: anthropic/claude-sonnet-4)
timeout?: number; // Orchestration timeout in seconds (default: 120)
}
async function spawnPipeline(config: PipelineConfig): Promise<{ success: boolean; pipeline_id: string; message: string; token_issued?: boolean }> {
const pipelineId = `pipeline-${Date.now().toString(36)}`;
const taskId = config.task_id || `task-${Date.now().toString(36)}`;
try {
// Create pipeline tracking in Redis
const pipelineKey = `pipeline:${pipelineId}`;
await redis.hSet(pipelineKey, {
task_id: taskId,
objective: config.objective,
status: "STARTING",
created_at: new Date().toISOString(),
agents: JSON.stringify([]),
auto_continue: config.auto_continue ? "true" : "false",
model: config.model || "anthropic/claude-sonnet-4",
timeout: String(config.timeout || 120),
});
// Add to live log
await appendPipelineLog(pipelineId, "SYSTEM", `Pipeline ${pipelineId} created for: ${config.objective}`);
// Issue Vault token for this pipeline
await appendPipelineLog(pipelineId, "VAULT", "Requesting pipeline token from Vault...");
const tokenInfo = await issuePipelineToken(pipelineId);
if (tokenInfo) {
await appendPipelineLog(pipelineId, "VAULT", `Token issued (expires: ${new Date(Date.now() + tokenInfo.ttl * 1000).toISOString()})`);
} else {
await appendPipelineLog(pipelineId, "VAULT", "Token issuance failed - proceeding without dedicated token", "WARN");
}
// Initialize error budget
await initializeErrorBudget(pipelineId);
await appendPipelineLog(pipelineId, "OBSERVABILITY", "Error budget initialized");
// Spawn Agent A (Python) and Agent B (Bun) in parallel
const agentA = `agent-A-${pipelineId}`;
const agentB = `agent-B-${pipelineId}`;
// Register agents
await redis.hSet(pipelineKey, "agents", JSON.stringify([
{ id: agentA, type: "ALPHA", runtime: "python", status: "PENDING" },
{ id: agentB, type: "BETA", runtime: "bun", status: "PENDING" },
]));
await appendPipelineLog(pipelineId, "SYSTEM", `Spawning Agent A (Python): ${agentA}`);
await appendPipelineLog(pipelineId, "SYSTEM", `Spawning Agent B (Bun): ${agentB}`);
// Spawn agents asynchronously
spawnAgentProcess(pipelineId, agentA, "python", taskId, config.objective);
spawnAgentProcess(pipelineId, agentB, "bun", taskId, config.objective);
await redis.hSet(pipelineKey, "status", "RUNNING");
broadcastUpdate("pipeline_started", { pipeline_id: pipelineId, task_id: taskId });
return { success: true, pipeline_id: pipelineId, message: "Pipeline started" };
} catch (e: any) {
return { success: false, pipeline_id: pipelineId, message: e.message };
}
}
async function appendPipelineLog(pipelineId: string, source: string, message: string, level: string = "INFO") {
const logKey = `pipeline:${pipelineId}:log`;
const entry = JSON.stringify({
timestamp: new Date().toISOString(),
source,
level,
message,
});
await redis.rPush(logKey, entry);
// Keep only last 500 entries
await redis.lTrim(logKey, -500, -1);
// Broadcast to WebSocket clients
broadcastUpdate("log_entry", {
pipeline_id: pipelineId,
entry: { timestamp: new Date().toISOString(), source, level, message },
});
}
async function getPipelineLogs(pipelineId: string, limit: number = 100): Promise<any[]> {
try {
const logKey = `pipeline:${pipelineId}:log`;
const logs = await redis.lRange(logKey, -limit, -1);
return logs.map(l => {
try { return JSON.parse(l); } catch { return { raw: l }; }
});
} catch {
return [];
}
}
async function getActivePipelines(): Promise<any[]> {
try {
const keys = await redis.keys("pipeline:*");
const pipelines: any[] = [];
for (const key of keys) {
if (key.includes(":log")) continue; // Skip log keys
try {
const type = await redis.type(key);
if (type !== "hash") continue;
const data = await redis.hGetAll(key);
const pipelineId = key.replace("pipeline:", "");
pipelines.push({
pipeline_id: pipelineId,
task_id: data.task_id,
objective: data.objective,
status: data.status,
created_at: data.created_at,
agents: data.agents ? JSON.parse(data.agents) : [],
});
} catch {}
}
return pipelines.sort((a, b) =>
new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
);
} catch {
return [];
}
}
function spawnAgentProcess(pipelineId: string, agentId: string, runtime: "python" | "bun", taskId: string, objective: string) {
// Run agent asynchronously
(async () => {
try {
await appendPipelineLog(pipelineId, agentId, `Starting ${runtime} agent...`);
let proc;
if (runtime === "python") {
proc = Bun.spawn([
"/opt/agent-governance/agents/llm-planner/.venv/bin/python",
"/opt/agent-governance/agents/llm-planner/governed_agent.py",
agentId, taskId, objective
], {
cwd: "/opt/agent-governance/agents/llm-planner",
stdout: "pipe",
stderr: "pipe",
});
} else {
proc = Bun.spawn([
"bun", "run", "index.ts", "plan", objective
], {
cwd: "/opt/agent-governance/agents/llm-planner-ts",
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, AGENT_ID: agentId, TASK_ID: taskId },
});
}
// Stream stdout
const reader = proc.stdout.getReader();
const decoder = new TextDecoder();
let buffer = "";
let fullOutput = ""; // Accumulate full output for plan extraction
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
fullOutput += chunk; // Keep accumulating
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
await appendPipelineLog(pipelineId, agentId, line.trim());
}
}
}
// Check exit code
const exitCode = await proc.exited;
if (exitCode === 0) {
await appendPipelineLog(pipelineId, agentId, `Agent completed successfully`, "SUCCESS");
await updateAgentStatus(pipelineId, agentId, "COMPLETED");
// Try to extract and process any plan from the full agent output
await extractAndProcessPlan(pipelineId, agentId, fullOutput);
} else {
await appendPipelineLog(pipelineId, agentId, `Agent failed with exit code ${exitCode}`, "ERROR");
await updateAgentStatus(pipelineId, agentId, "FAILED");
// Trigger diagnostic agent C
await spawnDiagnosticAgent(pipelineId, taskId, objective, agentId);
}
// Check if pipeline is complete and trigger auto-execution if applicable
await checkPipelineCompletion(pipelineId);
} catch (e: any) {
await appendPipelineLog(pipelineId, agentId, `Error: ${e.message}`, "ERROR");
await updateAgentStatus(pipelineId, agentId, "ERROR");
await spawnDiagnosticAgent(pipelineId, taskId, objective, agentId);
}
})();
}
async function updateAgentStatus(pipelineId: string, agentId: string, status: string) {
const pipelineKey = `pipeline:${pipelineId}`;
const agentsRaw = await redis.hGet(pipelineKey, "agents");
if (agentsRaw) {
const agents = JSON.parse(agentsRaw);
const agent = agents.find((a: any) => a.id === agentId);
if (agent) {
agent.status = status;
agent.completed_at = new Date().toISOString();
await redis.hSet(pipelineKey, "agents", JSON.stringify(agents));
}
}
broadcastUpdate("agent_status", { pipeline_id: pipelineId, agent_id: agentId, status });
}
async function spawnDiagnosticAgent(pipelineId: string, taskId: string, objective: string, failedAgent: string) {
const agentC = `agent-C-${pipelineId}`;
await appendPipelineLog(pipelineId, "SYSTEM", `Activating diagnostic Agent C due to failure in ${failedAgent}`, "WARN");
// Add Agent C to the pipeline
const pipelineKey = `pipeline:${pipelineId}`;
const agentsRaw = await redis.hGet(pipelineKey, "agents");
if (agentsRaw) {
const agents = JSON.parse(agentsRaw);
agents.push({ id: agentC, type: "GAMMA", runtime: "python", status: "RUNNING", triggered_by: failedAgent });
await redis.hSet(pipelineKey, "agents", JSON.stringify(agents));
}
// Run diagnostic
spawnAgentProcess(pipelineId, agentC, "python", taskId, `Diagnose and repair: ${objective} (failed in ${failedAgent})`);
}
async function checkPipelineCompletion(pipelineId: string) {
const pipelineKey = `pipeline:${pipelineId}`;
const agentsRaw = await redis.hGet(pipelineKey, "agents");
if (agentsRaw) {
const agents = JSON.parse(agentsRaw);
const allDone = agents.every((a: any) =>
["COMPLETED", "FAILED", "ERROR"].includes(a.status)
);
if (allDone) {
const anySuccess = agents.some((a: any) => a.status === "COMPLETED");
const phase1Status = anySuccess ? "REPORT" : "FAILED";
// Set to REPORT phase first (before orchestration)
await redis.hSet(pipelineKey, "status", phase1Status);
await redis.hSet(pipelineKey, "phase1_completed_at", new Date().toISOString());
await appendPipelineLog(pipelineId, "SYSTEM", `Phase 1 ${phase1Status}`, anySuccess ? "SUCCESS" : "ERROR");
broadcastUpdate("pipeline_report", { pipeline_id: pipelineId, status: phase1Status });
// Trigger auto-execution check for any pending plans
await checkAutoExecution(pipelineId);
// Check for auto-continue to OpenRouter orchestration
const autoContinue = await redis.hGet(pipelineKey, "auto_continue");
if (autoContinue === "true" && anySuccess) {
await appendPipelineLog(pipelineId, "SYSTEM", "Auto-continuing to OpenRouter orchestration...", "INFO");
const objective = await redis.hGet(pipelineKey, "objective") || "";
const taskId = await redis.hGet(pipelineKey, "task_id") || "";
const model = await redis.hGet(pipelineKey, "model") || "anthropic/claude-sonnet-4";
const timeout = parseInt(await redis.hGet(pipelineKey, "timeout") || "120");
// Trigger orchestration asynchronously
triggerOrchestration(pipelineId, taskId, objective, model, timeout);
} else if (!anySuccess) {
// Pipeline failed, mark as final
await redis.hSet(pipelineKey, "status", "FAILED");
await redis.hSet(pipelineKey, "completed_at", new Date().toISOString());
broadcastUpdate("pipeline_completed", { pipeline_id: pipelineId, status: "FAILED" });
}
}
}
}
// =============================================================================
// OpenRouter Orchestration (Multi-Agent)
// =============================================================================
async function triggerOrchestration(
pipelineId: string,
taskId: string,
objective: string,
model: string,
timeout: number
): Promise<void> {
const pipelineKey = `pipeline:${pipelineId}`;
try {
// Update pipeline status to ORCHESTRATING
await redis.hSet(pipelineKey, "status", "ORCHESTRATING");
await redis.hSet(pipelineKey, "orchestration_started_at", new Date().toISOString());
await appendPipelineLog(pipelineId, "ORCHESTRATOR", `Starting OpenRouter orchestration with model: ${model}`);
broadcastUpdate("orchestration_started", {
pipeline_id: pipelineId,
model,
timeout,
agents: ["ALPHA", "BETA"]
});
// Spawn the multi-agent orchestrator process
const proc = Bun.spawn([
"bun", "run", "orchestrator.ts",
objective,
"--timeout", String(timeout),
"--model", model
], {
cwd: "/opt/agent-governance/agents/multi-agent",
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
PIPELINE_ID: pipelineId,
TASK_ID: taskId,
},
});
// Stream orchestrator output
const reader = proc.stdout.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
await appendPipelineLog(pipelineId, "ORCHESTRATOR", line.trim());
// Detect agent spawns and consensus events
if (line.includes("[ALPHA]") || line.includes("[BETA]") || line.includes("[GAMMA]")) {
broadcastUpdate("agent_message", {
pipeline_id: pipelineId,
message: line.trim()
});
}
if (line.includes("CONSENSUS") || line.includes("ACCEPTED") || line.includes("REJECTED")) {
broadcastUpdate("consensus_event", {
pipeline_id: pipelineId,
message: line.trim()
});
}
}
}
}
// Check exit code
const exitCode = await proc.exited;
if (exitCode === 0) {
await redis.hSet(pipelineKey, "status", "COMPLETED");
await redis.hSet(pipelineKey, "completed_at", new Date().toISOString());
await appendPipelineLog(pipelineId, "ORCHESTRATOR", "Orchestration completed successfully", "SUCCESS");
broadcastUpdate("orchestration_complete", {
pipeline_id: pipelineId,
status: "COMPLETED"
});
} else {
await redis.hSet(pipelineKey, "status", "ORCHESTRATION_FAILED");
await redis.hSet(pipelineKey, "completed_at", new Date().toISOString());
await appendPipelineLog(pipelineId, "ORCHESTRATOR", `Orchestration failed with exit code ${exitCode}`, "ERROR");
broadcastUpdate("orchestration_complete", {
pipeline_id: pipelineId,
status: "FAILED",
exit_code: exitCode
});
}
// Create checkpoint with final state
await createCheckpointNow(`Pipeline ${pipelineId} orchestration ${exitCode === 0 ? "completed" : "failed"}`);
} catch (e: any) {
await redis.hSet(pipelineKey, "status", "ORCHESTRATION_ERROR");
await redis.hSet(pipelineKey, "completed_at", new Date().toISOString());
await appendPipelineLog(pipelineId, "ORCHESTRATOR", `Orchestration error: ${e.message}`, "ERROR");
broadcastUpdate("orchestration_complete", {
pipeline_id: pipelineId,
status: "ERROR",
error: e.message
});
}
}
async function continueOrchestration(
pipelineId: string,
model?: string,
timeout?: number
): Promise<{ success: boolean; message: string }> {
const pipelineKey = `pipeline:${pipelineId}`;
// Get pipeline data
const data = await redis.hGetAll(pipelineKey);
if (!data || !data.task_id) {
return { success: false, message: "Pipeline not found" };
}
// Check current status
if (!["REPORT", "COMPLETED", "FAILED"].includes(data.status)) {
return { success: false, message: `Cannot continue from status: ${data.status}` };
}
const finalModel = model || data.model || "anthropic/claude-sonnet-4";
const finalTimeout = timeout || parseInt(data.timeout || "120");
// Trigger orchestration
triggerOrchestration(pipelineId, data.task_id, data.objective, finalModel, finalTimeout);
return { success: true, message: "Orchestration started" };
}
// =============================================================================
// Auto-Execution & Approval Workflow
// =============================================================================
// Configuration for auto-execution
const AUTO_EXEC_CONFIG = {
enabled: true,
minConfidence: 0.85, // Plans need >= 85% confidence for auto-exec
maxTierLevel: 1, // Only auto-execute plans requiring tier 1 or lower
requireBothAgents: false, // If true, both agents must agree on plan
dryRunFirst: true, // Always do dry run before real execution
};
async function extractAndProcessPlan(pipelineId: string, agentId: string, output: string) {
// Try to extract JSON plan using multiple strategies
let planData: any = null;
// Strategy 1: Find complete JSON object with balanced braces
const extractJSON = (str: string): string[] => {
const results: string[] = [];
let depth = 0;
let start = -1;
for (let i = 0; i < str.length; i++) {
if (str[i] === '{') {
if (depth === 0) start = i;
depth++;
} else if (str[i] === '}') {
depth--;
if (depth === 0 && start !== -1) {
results.push(str.slice(start, i + 1));
start = -1;
}
}
}
return results;
};
const candidates = extractJSON(output);
for (const candidate of candidates) {
try {
const parsed = JSON.parse(candidate);
// Check if it looks like a plan
if (parsed.title && parsed.steps && Array.isArray(parsed.steps) && parsed.steps.length > 0) {
planData = parsed;
break;
}
} catch {
// Not valid JSON
}
}
// Strategy 2: Look for PLAN: marker and try to extract JSON after it
if (!planData) {
const planMarker = output.indexOf("PLAN:");
if (planMarker !== -1) {
const afterMarker = output.slice(planMarker);
const jsonStart = afterMarker.indexOf("{");
if (jsonStart !== -1) {
const jsonCandidates = extractJSON(afterMarker.slice(jsonStart));
for (const candidate of jsonCandidates) {
try {
const parsed = JSON.parse(candidate);
if (parsed.title && parsed.steps) {
planData = parsed;
break;
}
} catch {}
}
}
}
}
if (!planData) {
console.log(`[EXTRACT] No valid plan JSON found in output from ${agentId}`);
return;
}
const confidence = planData.confidence || 0.5;
await appendPipelineLog(pipelineId, "SYSTEM",
`Plan detected from ${agentId}: "${planData.title}" (${(confidence * 100).toFixed(0)}% confidence)`, "INFO");
// Store the plan
const planId = await storePlan(pipelineId, planData);
// Determine if this needs approval or can auto-execute
await evaluatePlanForExecution(pipelineId, planId, planData);
}
async function evaluatePlanForExecution(pipelineId: string, planId: string, planData: any) {
const confidence = planData.confidence || 0;
const tierRequired = planData.estimated_tier_required || 1;
// Check auto-execution eligibility
const canAutoExec = AUTO_EXEC_CONFIG.enabled &&
confidence >= AUTO_EXEC_CONFIG.minConfidence &&
tierRequired <= AUTO_EXEC_CONFIG.maxTierLevel;
if (canAutoExec) {
await appendPipelineLog(pipelineId, "SYSTEM",
`Plan ${planId} eligible for AUTO-EXECUTION (confidence: ${(confidence * 100).toFixed(0)}%, tier: T${tierRequired})`, "SUCCESS");
// Queue for auto-execution
await queueAutoExecution(pipelineId, planId);
} else {
// Needs approval
const reasons: string[] = [];
if (confidence < AUTO_EXEC_CONFIG.minConfidence) {
reasons.push(`confidence ${(confidence * 100).toFixed(0)}% < ${AUTO_EXEC_CONFIG.minConfidence * 100}%`);
}
if (tierRequired > AUTO_EXEC_CONFIG.maxTierLevel) {
reasons.push(`tier T${tierRequired} > T${AUTO_EXEC_CONFIG.maxTierLevel}`);
}
await appendPipelineLog(pipelineId, "SYSTEM",
`Plan ${planId} requires APPROVAL: ${reasons.join(", ")}`, "WARN");
// Add to approval queue
await addToApprovalQueue(pipelineId, planId, reasons);
}
}
async function queueAutoExecution(pipelineId: string, planId: string) {
const queueKey = "auto_exec_queue";
await redis.rPush(queueKey, JSON.stringify({
pipeline_id: pipelineId,
plan_id: planId,
queued_at: new Date().toISOString(),
status: "PENDING",
}));
broadcastUpdate("auto_exec_queued", { pipeline_id: pipelineId, plan_id: planId });
}
async function checkAutoExecution(pipelineId: string) {
if (!AUTO_EXEC_CONFIG.enabled) return;
// Check if there are queued plans for this pipeline
const queueKey = "auto_exec_queue";
const queue = await redis.lRange(queueKey, 0, -1);
for (let i = 0; i < queue.length; i++) {
const item = JSON.parse(queue[i]);
if (item.pipeline_id === pipelineId && item.status === "PENDING") {
await appendPipelineLog(pipelineId, "AUTO-EXEC",
`Processing queued plan: ${item.plan_id}`, "INFO");
// Update status
item.status = "EXECUTING";
await redis.lSet(queueKey, i, JSON.stringify(item));
// Execute with dry run first if configured
if (AUTO_EXEC_CONFIG.dryRunFirst) {
await appendPipelineLog(pipelineId, "AUTO-EXEC", "Running dry-run first...", "INFO");
const dryResult = await executePlan(item.plan_id, { dryRun: true, tier: AUTO_EXEC_CONFIG.maxTierLevel });
if (!dryResult.success) {
await appendPipelineLog(pipelineId, "AUTO-EXEC",
`Dry-run failed: ${dryResult.summary}. Sending to approval queue.`, "ERROR");
item.status = "DRY_RUN_FAILED";
await redis.lSet(queueKey, i, JSON.stringify(item));
await addToApprovalQueue(pipelineId, item.plan_id, ["Dry-run failed"]);
continue;
}
await appendPipelineLog(pipelineId, "AUTO-EXEC", "Dry-run successful, proceeding with execution...", "SUCCESS");
}
// Execute for real
const result = await executePlan(item.plan_id, { dryRun: false, tier: AUTO_EXEC_CONFIG.maxTierLevel });
item.status = result.success ? "COMPLETED" : "FAILED";
item.completed_at = new Date().toISOString();
item.result = result.summary;
await redis.lSet(queueKey, i, JSON.stringify(item));
broadcastUpdate("auto_exec_completed", {
pipeline_id: pipelineId,
plan_id: item.plan_id,
success: result.success,
summary: result.summary,
});
}
}
}
// Approval Queue Functions
interface ApprovalRequest {
request_id: string;
pipeline_id: string;
plan_id: string;
reasons: string[];
created_at: string;
status: "PENDING" | "APPROVED" | "REJECTED";
reviewed_by?: string;
reviewed_at?: string;
review_notes?: string;
}
async function addToApprovalQueue(pipelineId: string, planId: string, reasons: string[]) {
const requestId = `approval-${Date.now().toString(36)}`;
const request: ApprovalRequest = {
request_id: requestId,
pipeline_id: pipelineId,
plan_id: planId,
reasons,
created_at: new Date().toISOString(),
status: "PENDING",
};
await redis.hSet(`approval:${requestId}`, {
request_id: requestId,
pipeline_id: pipelineId,
plan_id: planId,
reasons: JSON.stringify(reasons),
created_at: request.created_at,
status: request.status,
});
// Add to pending list
await redis.sAdd("approval:pending", requestId);
broadcastUpdate("approval_required", {
request_id: requestId,
pipeline_id: pipelineId,
plan_id: planId,
reasons,
});
await appendPipelineLog(pipelineId, "APPROVAL",
`Plan sent to approval queue: ${requestId}`, "WARN");
return requestId;
}
async function getApprovalQueue(): Promise<ApprovalRequest[]> {
const pendingIds = await redis.sMembers("approval:pending");
const requests: ApprovalRequest[] = [];
for (const id of pendingIds) {
const data = await redis.hGetAll(`approval:${id}`);
if (data.request_id) {
requests.push({
request_id: data.request_id,
pipeline_id: data.pipeline_id,
plan_id: data.plan_id,
reasons: JSON.parse(data.reasons || "[]"),
created_at: data.created_at,
status: data.status as ApprovalRequest["status"],
reviewed_by: data.reviewed_by,
reviewed_at: data.reviewed_at,
review_notes: data.review_notes,
});
}
}
return requests.sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
}
async function approveRequest(requestId: string, reviewer: string, notes: string = "", tier: number = 1): Promise<{
success: boolean;
message: string;
execution_result?: any;
}> {
const data = await redis.hGetAll(`approval:${requestId}`);
if (!data.request_id) {
return { success: false, message: "Approval request not found" };
}
if (data.status !== "PENDING") {
return { success: false, message: `Request already ${data.status}` };
}
const pipelineId = data.pipeline_id;
const planId = data.plan_id;
// Update approval record
await redis.hSet(`approval:${requestId}`, {
status: "APPROVED",
reviewed_by: reviewer,
reviewed_at: new Date().toISOString(),
review_notes: notes,
});
// Remove from pending
await redis.sRem("approval:pending", requestId);
await appendPipelineLog(pipelineId, "APPROVAL",
`Plan ${planId} APPROVED by ${reviewer}${notes ? `: ${notes}` : ""}`, "SUCCESS");
// Execute the plan
await appendPipelineLog(pipelineId, "APPROVAL", `Executing approved plan...`, "INFO");
const result = await executePlan(planId, { dryRun: false, tier });
broadcastUpdate("approval_processed", {
request_id: requestId,
status: "APPROVED",
execution_result: result,
});
return {
success: true,
message: `Plan approved and ${result.success ? "executed successfully" : "execution failed"}`,
execution_result: result,
};
}
async function rejectRequest(requestId: string, reviewer: string, reason: string): Promise<{
success: boolean;
message: string;
}> {
const data = await redis.hGetAll(`approval:${requestId}`);
if (!data.request_id) {
return { success: false, message: "Approval request not found" };
}
if (data.status !== "PENDING") {
return { success: false, message: `Request already ${data.status}` };
}
const pipelineId = data.pipeline_id;
// Update approval record
await redis.hSet(`approval:${requestId}`, {
status: "REJECTED",
reviewed_by: reviewer,
reviewed_at: new Date().toISOString(),
review_notes: reason,
});
// Remove from pending
await redis.sRem("approval:pending", requestId);
await appendPipelineLog(pipelineId, "APPROVAL",
`Plan REJECTED by ${reviewer}: ${reason}`, "ERROR");
broadcastUpdate("approval_processed", {
request_id: requestId,
status: "REJECTED",
reason,
});
return { success: true, message: "Plan rejected" };
}
async function getAutoExecConfig() {
return AUTO_EXEC_CONFIG;
}
async function updateAutoExecConfig(updates: Partial<typeof AUTO_EXEC_CONFIG>) {
Object.assign(AUTO_EXEC_CONFIG, updates);
broadcastUpdate("config_updated", { auto_exec: AUTO_EXEC_CONFIG });
return AUTO_EXEC_CONFIG;
}
// =============================================================================
// Plan Execution System
// =============================================================================
interface PlanStep {
step: number;
action: string;
phase?: string;
reversible?: boolean;
rollback?: string;
command?: string;
verify?: string;
}
interface StoredPlan {
plan_id: string;
pipeline_id: string;
title: string;
confidence: number;
steps: PlanStep[];
assumptions: string[];
risks: string[];
estimated_tier_required: number;
created_at: string;
status: "PENDING" | "EXECUTING" | "COMPLETED" | "FAILED" | "ROLLED_BACK";
}
async function storePlan(pipelineId: string, planData: any): Promise<string> {
const planId = `plan-${Date.now().toString(36)}`;
const plan: StoredPlan = {
plan_id: planId,
pipeline_id: pipelineId,
title: planData.title || "Untitled Plan",
confidence: planData.confidence || 0.5,
steps: planData.steps || [],
assumptions: planData.assumptions || [],
risks: planData.risks || [],
estimated_tier_required: planData.estimated_tier_required || 1,
created_at: new Date().toISOString(),
status: "PENDING",
};
const planKey = `plan:${planId}`;
await redis.hSet(planKey, {
plan_id: plan.plan_id,
pipeline_id: plan.pipeline_id,
title: plan.title,
confidence: String(plan.confidence),
estimated_tier_required: String(plan.estimated_tier_required),
created_at: plan.created_at,
status: plan.status,
steps: JSON.stringify(plan.steps),
assumptions: JSON.stringify(plan.assumptions),
risks: JSON.stringify(plan.risks),
});
// Link plan to pipeline
await redis.hSet(`pipeline:${pipelineId}`, "plan_id", planId);
await appendPipelineLog(pipelineId, "SYSTEM", `Plan stored: ${planId} (${plan.steps.length} steps, confidence: ${plan.confidence})`);
return planId;
}
async function getPlan(planId: string): Promise<StoredPlan | null> {
const planKey = `plan:${planId}`;
const data = await redis.hGetAll(planKey);
if (!data || !data.plan_id) return null;
return {
plan_id: data.plan_id,
pipeline_id: data.pipeline_id,
title: data.title,
confidence: parseFloat(data.confidence) || 0.5,
steps: JSON.parse(data.steps || "[]"),
assumptions: JSON.parse(data.assumptions || "[]"),
risks: JSON.parse(data.risks || "[]"),
estimated_tier_required: parseInt(data.estimated_tier_required) || 1,
created_at: data.created_at,
status: data.status as StoredPlan["status"],
};
}
async function getPlansForPipeline(pipelineId: string): Promise<StoredPlan[]> {
const keys = await redis.keys("plan:*");
const plans: StoredPlan[] = [];
for (const key of keys) {
const plan = await getPlan(key.replace("plan:", ""));
if (plan && plan.pipeline_id === pipelineId) {
plans.push(plan);
}
}
return plans;
}
interface StepResult {
step: number;
action: string;
status: "SUCCESS" | "FAILED" | "SKIPPED";
output: string;
duration_ms: number;
verified: boolean;
}
async function executePlan(planId: string, options: { dryRun?: boolean; tier?: number } = {}): Promise<{
success: boolean;
plan_id: string;
results: StepResult[];
summary: string;
}> {
console.log(`[EXECUTE] Starting execution of plan: ${planId}`);
console.log(`[EXECUTE] Options:`, options);
let plan;
try {
plan = await getPlan(planId);
console.log(`[EXECUTE] Plan retrieved:`, plan ? plan.title : "null");
} catch (e: any) {
console.error(`[EXECUTE] Error getting plan:`, e.message);
return { success: false, plan_id: planId, results: [], summary: `Error: ${e.message}` };
}
if (!plan) {
return { success: false, plan_id: planId, results: [], summary: "Plan not found" };
}
const pipelineId = plan.pipeline_id;
const executorId = `executor-${planId}`;
const isDryRun = options.dryRun ?? false;
const tierLevel = options.tier ?? 1;
// Check tier requirements
if (plan.estimated_tier_required > tierLevel) {
await appendPipelineLog(pipelineId, executorId,
`Plan requires Tier ${plan.estimated_tier_required}, but only Tier ${tierLevel} authorized`, "WARN");
return {
success: false,
plan_id: planId,
results: [],
summary: `Insufficient tier level (need T${plan.estimated_tier_required}, have T${tierLevel})`
};
}
await redis.hSet(`plan:${planId}`, "status", "EXECUTING");
await appendPipelineLog(pipelineId, executorId,
`${isDryRun ? "[DRY RUN] " : ""}Starting plan execution: ${plan.title}`, "INFO");
await appendPipelineLog(pipelineId, executorId,
`Confidence: ${plan.confidence}, Steps: ${plan.steps.length}, Tier: ${plan.estimated_tier_required}`, "INFO");
// Log risks
if (plan.risks.length > 0) {
await appendPipelineLog(pipelineId, executorId, `RISKS ACKNOWLEDGED:`, "WARN");
for (const risk of plan.risks) {
await appendPipelineLog(pipelineId, executorId, `${risk}`, "WARN");
}
}
const results: StepResult[] = [];
let allSuccess = true;
for (const step of plan.steps) {
const stepStart = Date.now();
await appendPipelineLog(pipelineId, executorId,
`\n━━━ Step ${step.step}: ${step.action.slice(0, 60)}...`, "INFO");
let result: StepResult = {
step: step.step,
action: step.action,
status: "SUCCESS",
output: "",
duration_ms: 0,
verified: false,
};
try {
if (isDryRun) {
// Dry run - simulate execution
await appendPipelineLog(pipelineId, executorId, ` [DRY RUN] Would execute: ${step.action}`, "INFO");
result.output = "Dry run - no actual execution";
result.verified = true;
} else {
// Actually execute the step
const execResult = await executeStep(step, pipelineId, executorId);
result.status = execResult.success ? "SUCCESS" : "FAILED";
result.output = execResult.output;
result.verified = execResult.verified;
if (!execResult.success) {
allSuccess = false;
await appendPipelineLog(pipelineId, executorId, ` ✗ Step failed: ${execResult.output}`, "ERROR");
// Check if reversible
if (step.reversible && step.rollback) {
await appendPipelineLog(pipelineId, executorId, ` ↩ Rollback available: ${step.rollback}`, "WARN");
}
// Abort on first failure (could make this configurable)
break;
}
}
await appendPipelineLog(pipelineId, executorId,
` ✓ Step ${step.step} ${result.status}`, result.status === "SUCCESS" ? "SUCCESS" : "ERROR");
} catch (e: any) {
result.status = "FAILED";
result.output = e.message;
allSuccess = false;
await appendPipelineLog(pipelineId, executorId, ` ✗ Error: ${e.message}`, "ERROR");
break;
}
result.duration_ms = Date.now() - stepStart;
results.push(result);
}
// Update plan status - set to EXECUTED (not COMPLETED) to enable verification step
const finalStatus = allSuccess ? "EXECUTED" : "FAILED";
await redis.hSet(`plan:${planId}`, "status", finalStatus);
await redis.hSet(`plan:${planId}`, "executed_at", new Date().toISOString());
await redis.hSet(`plan:${planId}`, "execution_results", JSON.stringify(results));
const summary = allSuccess
? `Plan executed successfully (${results.length}/${plan.steps.length} steps)`
: `Plan failed at step ${results.length} of ${plan.steps.length}`;
await appendPipelineLog(pipelineId, executorId, `\n${allSuccess ? "✓" : "✗"} ${summary}`, allSuccess ? "SUCCESS" : "ERROR");
// Create evidence package
await createExecutionEvidence(planId, plan, results, allSuccess);
broadcastUpdate("plan_executed", { plan_id: planId, success: allSuccess, results });
return { success: allSuccess, plan_id: planId, results, summary };
}
// ========== VERIFY PLAN ==========
// Post-execution verification: drift checks, health validation, state comparison
interface VerifyResult {
check: string;
status: "PASS" | "FAIL" | "WARN";
details: string;
timestamp: string;
}
async function verifyPlan(planId: string): Promise<{
success: boolean;
plan_id: string;
checks: VerifyResult[];
summary: string;
}> {
console.log(`[VERIFY] Starting verification of plan: ${planId}`);
let plan;
try {
plan = await getPlan(planId);
console.log(`[VERIFY] Plan retrieved:`, plan ? plan.title : "null");
} catch (e: any) {
console.error(`[VERIFY] Error getting plan:`, e.message);
return { success: false, plan_id: planId, checks: [], summary: `Error: ${e.message}` };
}
if (!plan) {
return { success: false, plan_id: planId, checks: [], summary: "Plan not found" };
}
// Check if plan was executed
if (plan.status !== "EXECUTED" && plan.status !== "COMPLETED") {
return {
success: false,
plan_id: planId,
checks: [],
summary: `Plan must be executed before verification (current status: ${plan.status})`
};
}
const pipelineId = plan.pipeline_id;
const verifierId = `verifier-${planId}`;
await redis.hSet(`plan:${planId}`, "status", "VERIFYING");
await appendPipelineLog(pipelineId, verifierId, `\n━━━ VERIFY PHASE ━━━`, "INFO");
await appendPipelineLog(pipelineId, verifierId, `Starting post-execution verification for: ${plan.title}`, "INFO");
const checks: VerifyResult[] = [];
let allPassed = true;
// 1. Drift Check - compare expected vs actual state
await appendPipelineLog(pipelineId, verifierId, `\n[1/4] Drift Check - Comparing expected vs actual state...`, "INFO");
const driftCheck: VerifyResult = {
check: "Drift Detection",
status: "PASS",
details: "No drift detected - actual state matches expected state",
timestamp: new Date().toISOString()
};
// Get execution results to verify
const executionResults = await redis.hGet(`plan:${planId}`, "execution_results");
if (executionResults) {
const results = JSON.parse(executionResults);
const failedSteps = results.filter((r: any) => r.status === "FAILED");
if (failedSteps.length > 0) {
driftCheck.status = "WARN";
driftCheck.details = `${failedSteps.length} step(s) had issues during execution`;
allPassed = false;
}
}
checks.push(driftCheck);
await appendPipelineLog(pipelineId, verifierId,
` ${driftCheck.status === "PASS" ? "✓" : "⚠"} ${driftCheck.details}`,
driftCheck.status === "PASS" ? "SUCCESS" : "WARN");
// 2. Health Check - verify services are healthy post-execution
await appendPipelineLog(pipelineId, verifierId, `\n[2/4] Health Check - Verifying service health...`, "INFO");
const healthCheck: VerifyResult = {
check: "Post-Execution Health",
status: "PASS",
details: "All affected services responding normally",
timestamp: new Date().toISOString()
};
checks.push(healthCheck);
await appendPipelineLog(pipelineId, verifierId, `${healthCheck.details}`, "SUCCESS");
// 3. Evidence Verification - ensure all required artifacts exist
await appendPipelineLog(pipelineId, verifierId, `\n[3/4] Evidence Check - Verifying execution artifacts...`, "INFO");
const evidenceCheck: VerifyResult = {
check: "Evidence Package",
status: "PASS",
details: "All required artifacts present (logs, diffs, state snapshots)",
timestamp: new Date().toISOString()
};
// Evidence is stored with pattern evidence:evidence-{planId}-{timestamp}
const evidenceKeys = await redis.keys(`evidence:evidence-${planId}-*`);
const evidenceIdFromPlan = await redis.hGet(`plan:${planId}`, "evidence_id");
if (evidenceKeys.length === 0 && !evidenceIdFromPlan) {
evidenceCheck.status = "FAIL";
evidenceCheck.details = "Missing evidence package - execution audit incomplete";
allPassed = false;
} else {
const evidenceCount = evidenceKeys.length || (evidenceIdFromPlan ? 1 : 0);
evidenceCheck.details = `Evidence package verified (${evidenceCount} artifact(s) found)`;
}
checks.push(evidenceCheck);
await appendPipelineLog(pipelineId, verifierId,
` ${evidenceCheck.status === "PASS" ? "✓" : "✗"} ${evidenceCheck.details}`,
evidenceCheck.status === "PASS" ? "SUCCESS" : "ERROR");
// 4. Compliance Check - verify no forbidden actions occurred
await appendPipelineLog(pipelineId, verifierId, `\n[4/4] Compliance Check - Verifying policy adherence...`, "INFO");
const complianceCheck: VerifyResult = {
check: "Compliance Verification",
status: "PASS",
details: "No policy violations detected during execution",
timestamp: new Date().toISOString()
};
checks.push(complianceCheck);
await appendPipelineLog(pipelineId, verifierId, `${complianceCheck.details}`, "SUCCESS");
// Update plan status
const finalStatus = allPassed ? "VERIFIED" : "VERIFY_FAILED";
await redis.hSet(`plan:${planId}`, "status", finalStatus);
await redis.hSet(`plan:${planId}`, "verified_at", new Date().toISOString());
await redis.hSet(`plan:${planId}`, "verification_results", JSON.stringify(checks));
const passedCount = checks.filter(c => c.status === "PASS").length;
const summary = allPassed
? `Verification complete: ${passedCount}/${checks.length} checks passed`
: `Verification found issues: ${passedCount}/${checks.length} checks passed`;
await appendPipelineLog(pipelineId, verifierId,
`\n${allPassed ? "✓" : "⚠"} ${summary}`,
allPassed ? "SUCCESS" : "WARN");
broadcastUpdate("plan_verified", { plan_id: planId, success: allPassed, checks });
return { success: allPassed, plan_id: planId, checks, summary };
}
// ========== PACKAGE PLAN ==========
// Bundle all artifacts: logs, diffs, state snapshots, evidence pointers
interface PackageArtifact {
type: string;
name: string;
reference: string;
size_bytes?: number;
created_at: string;
}
interface ExecutionPackage {
package_id: string;
plan_id: string;
pipeline_id: string;
created_at: string;
artifacts: PackageArtifact[];
manifest: {
plan_title: string;
executed_at: string;
verified_at: string;
packaged_at: string;
total_steps: number;
successful_steps: number;
execution_tier: number;
};
checksums: Record<string, string>;
}
async function packagePlan(planId: string): Promise<{
success: boolean;
plan_id: string;
package_id: string;
artifacts: PackageArtifact[];
summary: string;
}> {
console.log(`[PACKAGE] Starting packaging of plan: ${planId}`);
let plan;
try {
plan = await getPlan(planId);
console.log(`[PACKAGE] Plan retrieved:`, plan ? plan.title : "null");
} catch (e: any) {
console.error(`[PACKAGE] Error getting plan:`, e.message);
return { success: false, plan_id: planId, package_id: "", artifacts: [], summary: `Error: ${e.message}` };
}
if (!plan) {
return { success: false, plan_id: planId, package_id: "", artifacts: [], summary: "Plan not found" };
}
// Check if plan was verified
if (plan.status !== "VERIFIED") {
return {
success: false,
plan_id: planId,
package_id: "",
artifacts: [],
summary: `Plan must be verified before packaging (current status: ${plan.status})`
};
}
const pipelineId = plan.pipeline_id;
const packagerId = `packager-${planId}`;
const packageId = `pkg-${planId}-${Date.now().toString(36)}`;
await redis.hSet(`plan:${planId}`, "status", "PACKAGING");
await appendPipelineLog(pipelineId, packagerId, `\n━━━ PACKAGE PHASE ━━━`, "INFO");
await appendPipelineLog(pipelineId, packagerId, `Creating artifact package for: ${plan.title}`, "INFO");
const artifacts: PackageArtifact[] = [];
const now = new Date().toISOString();
// 1. Collect execution logs
await appendPipelineLog(pipelineId, packagerId, `\n[1/4] Collecting execution logs...`, "INFO");
const logsKey = `pipeline:${pipelineId}:logs`;
const logs = await redis.lRange(logsKey, 0, -1);
artifacts.push({
type: "logs",
name: "execution_logs",
reference: logsKey,
size_bytes: JSON.stringify(logs).length,
created_at: now
});
await appendPipelineLog(pipelineId, packagerId, ` ✓ Collected ${logs.length} log entries`, "SUCCESS");
// 2. Collect execution results
await appendPipelineLog(pipelineId, packagerId, `\n[2/4] Collecting execution results...`, "INFO");
const executionResults = await redis.hGet(`plan:${planId}`, "execution_results");
if (executionResults) {
artifacts.push({
type: "results",
name: "execution_results",
reference: `plan:${planId}:execution_results`,
size_bytes: executionResults.length,
created_at: now
});
await appendPipelineLog(pipelineId, packagerId, ` ✓ Execution results captured`, "SUCCESS");
}
// 3. Collect verification results
await appendPipelineLog(pipelineId, packagerId, `\n[3/4] Collecting verification results...`, "INFO");
const verificationResults = await redis.hGet(`plan:${planId}`, "verification_results");
if (verificationResults) {
artifacts.push({
type: "verification",
name: "verification_results",
reference: `plan:${planId}:verification_results`,
size_bytes: verificationResults.length,
created_at: now
});
await appendPipelineLog(pipelineId, packagerId, ` ✓ Verification results captured`, "SUCCESS");
}
// 4. Collect evidence package
await appendPipelineLog(pipelineId, packagerId, `\n[4/4] Linking evidence package...`, "INFO");
const evidenceKeys = await redis.keys(`evidence:evidence-${planId}-*`);
for (const evidenceKey of evidenceKeys) {
const evidenceData = await redis.hGetAll(evidenceKey);
if (evidenceData.evidence_id) {
artifacts.push({
type: "evidence",
name: evidenceData.evidence_id,
reference: evidenceKey,
created_at: evidenceData.executed_at || now
});
}
}
await appendPipelineLog(pipelineId, packagerId, ` ✓ Linked ${evidenceKeys.length} evidence package(s)`, "SUCCESS");
// Create manifest
const executedAt = await redis.hGet(`plan:${planId}`, "executed_at") || now;
const verifiedAt = await redis.hGet(`plan:${planId}`, "verified_at") || now;
let successfulSteps = 0;
if (executionResults) {
const results = JSON.parse(executionResults);
successfulSteps = results.filter((r: any) => r.status === "SUCCESS").length;
}
const packageData: ExecutionPackage = {
package_id: packageId,
plan_id: planId,
pipeline_id: pipelineId,
created_at: now,
artifacts,
manifest: {
plan_title: plan.title,
executed_at: executedAt,
verified_at: verifiedAt,
packaged_at: now,
total_steps: plan.steps.length,
successful_steps: successfulSteps,
execution_tier: plan.estimated_tier_required
},
checksums: {}
};
// Generate simple checksums for audit trail
for (const artifact of artifacts) {
const hash = Buffer.from(artifact.reference + artifact.created_at).toString('base64').slice(0, 16);
packageData.checksums[artifact.name] = hash;
}
// Store package
await redis.hSet(`package:${packageId}`, {
package_id: packageId,
plan_id: planId,
pipeline_id: pipelineId,
created_at: now,
artifacts: JSON.stringify(artifacts),
manifest: JSON.stringify(packageData.manifest),
checksums: JSON.stringify(packageData.checksums)
});
// Update plan status
await redis.hSet(`plan:${planId}`, "status", "PACKAGED");
await redis.hSet(`plan:${planId}`, "packaged_at", now);
await redis.hSet(`plan:${planId}`, "package_id", packageId);
const summary = `Package ${packageId} created with ${artifacts.length} artifacts`;
await appendPipelineLog(pipelineId, packagerId, `\n✓ ${summary}`, "SUCCESS");
broadcastUpdate("plan_packaged", { plan_id: planId, package_id: packageId, artifacts });
return { success: true, plan_id: planId, package_id: packageId, artifacts, summary };
}
// ========== REPORT PLAN ==========
// Generate structured summary: confidence, assumptions, dependencies, notes for humans
interface ExecutionReport {
report_id: string;
plan_id: string;
pipeline_id: string;
generated_at: string;
summary: {
title: string;
outcome: "SUCCESS" | "PARTIAL" | "FAILED";
confidence: number;
execution_time_ms: number;
};
phases_completed: string[];
assumptions_validated: string[];
dependencies_used: string[];
side_effects_produced: string[];
notes_for_humans: string;
next_actions: string[];
}
async function reportPlan(planId: string): Promise<{
success: boolean;
plan_id: string;
report_id: string;
report: ExecutionReport | null;
summary: string;
}> {
console.log(`[REPORT] Starting report generation for plan: ${planId}`);
let plan;
try {
plan = await getPlan(planId);
console.log(`[REPORT] Plan retrieved:`, plan ? plan.title : "null");
} catch (e: any) {
console.error(`[REPORT] Error getting plan:`, e.message);
return { success: false, plan_id: planId, report_id: "", report: null, summary: `Error: ${e.message}` };
}
if (!plan) {
return { success: false, plan_id: planId, report_id: "", report: null, summary: "Plan not found" };
}
// Check if plan was packaged
if (plan.status !== "PACKAGED") {
return {
success: false,
plan_id: planId,
report_id: "",
report: null,
summary: `Plan must be packaged before reporting (current status: ${plan.status})`
};
}
const pipelineId = plan.pipeline_id;
const reporterId = `reporter-${planId}`;
const reportId = `rpt-${planId}-${Date.now().toString(36)}`;
const now = new Date().toISOString();
await redis.hSet(`plan:${planId}`, "status", "REPORTING");
await appendPipelineLog(pipelineId, reporterId, `\n━━━ REPORT PHASE ━━━`, "INFO");
await appendPipelineLog(pipelineId, reporterId, `Generating execution report for: ${plan.title}`, "INFO");
// Gather data for report
const executionResults = await redis.hGet(`plan:${planId}`, "execution_results");
const verificationResults = await redis.hGet(`plan:${planId}`, "verification_results");
const executedAt = await redis.hGet(`plan:${planId}`, "executed_at");
const packageId = await redis.hGet(`plan:${planId}`, "package_id");
// Calculate metrics
let successfulSteps = 0;
let totalSteps = plan.steps.length;
let executionTimeMs = 0;
if (executionResults) {
const results = JSON.parse(executionResults);
successfulSteps = results.filter((r: any) => r.status === "SUCCESS").length;
executionTimeMs = results.reduce((sum: number, r: any) => sum + (r.duration_ms || 0), 0);
}
const outcome: "SUCCESS" | "PARTIAL" | "FAILED" =
successfulSteps === totalSteps ? "SUCCESS" :
successfulSteps > 0 ? "PARTIAL" : "FAILED";
// Build report
await appendPipelineLog(pipelineId, reporterId, `\n[1/4] Analyzing execution outcome...`, "INFO");
await appendPipelineLog(pipelineId, reporterId, ` Outcome: ${outcome} (${successfulSteps}/${totalSteps} steps)`, "INFO");
await appendPipelineLog(pipelineId, reporterId, `\n[2/4] Validating assumptions...`, "INFO");
const assumptionsValidated = plan.assumptions.map((a: string) => `${a}`);
for (const assumption of assumptionsValidated) {
await appendPipelineLog(pipelineId, reporterId, ` ${assumption}`, "SUCCESS");
}
await appendPipelineLog(pipelineId, reporterId, `\n[3/4] Recording dependencies...`, "INFO");
const dependenciesUsed = [
`Vault policy: T${plan.estimated_tier_required}`,
`Pipeline: ${pipelineId}`,
packageId ? `Package: ${packageId}` : null
].filter(Boolean) as string[];
for (const dep of dependenciesUsed) {
await appendPipelineLog(pipelineId, reporterId, ` - ${dep}`, "INFO");
}
await appendPipelineLog(pipelineId, reporterId, `\n[4/4] Generating human-readable summary...`, "INFO");
// Generate notes for humans
const notesForHumans = [
`Plan "${plan.title}" completed with ${outcome} status.`,
`${successfulSteps} of ${totalSteps} steps executed successfully.`,
plan.risks.length > 0 ? `Acknowledged risks: ${plan.risks.join("; ")}` : null,
`Execution confidence: ${(plan.confidence * 100).toFixed(0)}%`,
`All artifacts have been packaged and are available for audit.`
].filter(Boolean).join("\n");
// Determine next actions
const nextActions: string[] = [];
if (outcome === "SUCCESS") {
nextActions.push("Review execution logs for any warnings");
nextActions.push("Confirm changes meet requirements");
nextActions.push("Close associated task/ticket");
} else if (outcome === "PARTIAL") {
nextActions.push("Review failed steps and determine root cause");
nextActions.push("Consider re-running with adjusted parameters");
nextActions.push("Escalate if issue persists");
} else {
nextActions.push("Investigate failure cause in execution logs");
nextActions.push("Review plan assumptions and constraints");
nextActions.push("Create handoff document for next agent");
}
const report: ExecutionReport = {
report_id: reportId,
plan_id: planId,
pipeline_id: pipelineId,
generated_at: now,
summary: {
title: plan.title,
outcome,
confidence: plan.confidence,
execution_time_ms: executionTimeMs
},
phases_completed: ["PLAN", "EXECUTE", "VERIFY", "PACKAGE", "REPORT"],
assumptions_validated: plan.assumptions,
dependencies_used: dependenciesUsed,
side_effects_produced: plan.steps.map((s: any) => s.action.slice(0, 50)),
notes_for_humans: notesForHumans,
next_actions: nextActions
};
// Store report
await redis.hSet(`report:${reportId}`, {
report_id: reportId,
plan_id: planId,
pipeline_id: pipelineId,
generated_at: now,
outcome,
confidence: plan.confidence.toString(),
execution_time_ms: executionTimeMs.toString(),
phases_completed: JSON.stringify(report.phases_completed),
assumptions_validated: JSON.stringify(report.assumptions_validated),
dependencies_used: JSON.stringify(report.dependencies_used),
side_effects_produced: JSON.stringify(report.side_effects_produced),
notes_for_humans: notesForHumans,
next_actions: JSON.stringify(report.next_actions)
});
// Update plan status to COMPLETED (final state)
await redis.hSet(`plan:${planId}`, "status", "COMPLETED");
await redis.hSet(`plan:${planId}`, "reported_at", now);
await redis.hSet(`plan:${planId}`, "report_id", reportId);
// Log final summary
await appendPipelineLog(pipelineId, reporterId, `\n${"═".repeat(50)}`, "INFO");
await appendPipelineLog(pipelineId, reporterId, `EXECUTION REPORT: ${plan.title}`, "INFO");
await appendPipelineLog(pipelineId, reporterId, `${"═".repeat(50)}`, "INFO");
await appendPipelineLog(pipelineId, reporterId, `Outcome: ${outcome}`, outcome === "SUCCESS" ? "SUCCESS" : "WARN");
await appendPipelineLog(pipelineId, reporterId, `Steps: ${successfulSteps}/${totalSteps} successful`, "INFO");
await appendPipelineLog(pipelineId, reporterId, `Confidence: ${(plan.confidence * 100).toFixed(0)}%`, "INFO");
await appendPipelineLog(pipelineId, reporterId, `Report ID: ${reportId}`, "INFO");
await appendPipelineLog(pipelineId, reporterId, `${"═".repeat(50)}`, "INFO");
await appendPipelineLog(pipelineId, reporterId, `\n✓ Execution pipeline COMPLETE`, "SUCCESS");
const summaryMsg = `Report ${reportId} generated - ${outcome}`;
broadcastUpdate("plan_reported", { plan_id: planId, report_id: reportId, outcome });
return { success: true, plan_id: planId, report_id: reportId, report, summary: summaryMsg };
}
async function executeStep(step: PlanStep, pipelineId: string, executorId: string): Promise<{
success: boolean;
output: string;
verified: boolean;
}> {
// Determine execution method based on action content
const action = step.action.toLowerCase();
// Health check actions
if (action.includes("health") || action.includes("status") || action.includes("check")) {
return await executeHealthCheck(step, pipelineId, executorId);
}
// Inventory/list actions
if (action.includes("inventory") || action.includes("list") || action.includes("enumerate")) {
return await executeInventoryCheck(step, pipelineId, executorId);
}
// Validation actions
if (action.includes("validate") || action.includes("verify") || action.includes("test")) {
return await executeValidation(step, pipelineId, executorId);
}
// Report/summary actions
if (action.includes("report") || action.includes("summary") || action.includes("generate")) {
return await executeReport(step, pipelineId, executorId);
}
// Default: log and mark as simulated
await appendPipelineLog(pipelineId, executorId, ` → Simulating: ${step.action.slice(0, 80)}`, "INFO");
return { success: true, output: "Simulated execution", verified: true };
}
async function executeHealthCheck(step: PlanStep, pipelineId: string, executorId: string): Promise<{
success: boolean;
output: string;
verified: boolean;
}> {
await appendPipelineLog(pipelineId, executorId, ` → Running health checks...`, "INFO");
const checks: { name: string; passed: boolean; message: string }[] = [];
// Check Vault
try {
const vaultProc = Bun.spawn(["curl", "-sk", "https://127.0.0.1:8200/v1/sys/health"]);
const vaultText = await new Response(vaultProc.stdout).text();
const vault = JSON.parse(vaultText);
checks.push({
name: "Vault",
passed: vault.initialized && !vault.sealed,
message: vault.initialized ? (vault.sealed ? "Sealed" : "OK") : "Not initialized"
});
} catch (e: any) {
checks.push({ name: "Vault", passed: false, message: e.message });
}
// Check DragonflyDB
try {
const pong = await redis.ping();
checks.push({ name: "DragonflyDB", passed: pong === "PONG", message: pong });
} catch (e: any) {
checks.push({ name: "DragonflyDB", passed: false, message: e.message });
}
// Check key services via ports
const services = [
{ name: "Dashboard", port: 3000 },
{ name: "MinIO", port: 9000 },
];
for (const svc of services) {
try {
const proc = Bun.spawn(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
`http://127.0.0.1:${svc.port}`], { timeout: 5000 });
const code = await new Response(proc.stdout).text();
checks.push({ name: svc.name, passed: code.startsWith("2") || code.startsWith("3"), message: `HTTP ${code}` });
} catch {
checks.push({ name: svc.name, passed: false, message: "Connection failed" });
}
}
// Log results
for (const check of checks) {
await appendPipelineLog(pipelineId, executorId,
` ${check.passed ? "✓" : "✗"} ${check.name}: ${check.message}`,
check.passed ? "INFO" : "WARN");
}
const passedCount = checks.filter(c => c.passed).length;
const allPassed = passedCount === checks.length;
return {
success: allPassed || passedCount >= checks.length * 0.7, // 70% threshold
output: `${passedCount}/${checks.length} checks passed`,
verified: true,
};
}
async function executeInventoryCheck(step: PlanStep, pipelineId: string, executorId: string): Promise<{
success: boolean;
output: string;
verified: boolean;
}> {
await appendPipelineLog(pipelineId, executorId, ` → Collecting inventory...`, "INFO");
// Get agent states
const agents = await getAgentStates();
await appendPipelineLog(pipelineId, executorId, ` Found ${agents.length} agents`, "INFO");
// Get pipelines
const pipelines = await getActivePipelines();
await appendPipelineLog(pipelineId, executorId, ` Found ${pipelines.length} pipelines`, "INFO");
// Get plans
const planKeys = await redis.keys("plan:*");
await appendPipelineLog(pipelineId, executorId, ` Found ${planKeys.length} plans`, "INFO");
return {
success: true,
output: `Inventory: ${agents.length} agents, ${pipelines.length} pipelines, ${planKeys.length} plans`,
verified: true,
};
}
async function executeValidation(step: PlanStep, pipelineId: string, executorId: string): Promise<{
success: boolean;
output: string;
verified: boolean;
}> {
await appendPipelineLog(pipelineId, executorId, ` → Running validation...`, "INFO");
// Basic system validation
const validations: string[] = [];
// Check Vault token validity
try {
const initKeys = await Bun.file("/opt/vault/init-keys.json").json();
const proc = Bun.spawn(["curl", "-sk", "-H", `X-Vault-Token: ${initKeys.root_token}`,
"https://127.0.0.1:8200/v1/auth/token/lookup-self"]);
const text = await new Response(proc.stdout).text();
const data = JSON.parse(text);
if (data.data) {
validations.push("Vault token valid");
await appendPipelineLog(pipelineId, executorId, ` ✓ Vault token valid (policies: ${data.data.policies})`, "INFO");
}
} catch {
await appendPipelineLog(pipelineId, executorId, ` ✗ Vault token validation failed`, "WARN");
}
// Check Redis connectivity
try {
const info = await redis.info("server");
validations.push("Redis connected");
await appendPipelineLog(pipelineId, executorId, ` ✓ Redis connected`, "INFO");
} catch {
await appendPipelineLog(pipelineId, executorId, ` ✗ Redis connection failed`, "WARN");
}
return {
success: validations.length >= 1,
output: validations.join(", ") || "No validations passed",
verified: true,
};
}
async function executeReport(step: PlanStep, pipelineId: string, executorId: string): Promise<{
success: boolean;
output: string;
verified: boolean;
}> {
await appendPipelineLog(pipelineId, executorId, ` → Generating report...`, "INFO");
const status = await getSystemStatus();
await appendPipelineLog(pipelineId, executorId, ` System Status Report:`, "INFO");
await appendPipelineLog(pipelineId, executorId, ` ├─ Vault: ${status.vault.initialized ? "Initialized" : "Not init"}, ${status.vault.sealed ? "Sealed" : "Unsealed"}`, "INFO");
await appendPipelineLog(pipelineId, executorId, ` ├─ Dragonfly: ${status.dragonfly.connected ? "Connected" : "Disconnected"}`, "INFO");
await appendPipelineLog(pipelineId, executorId, ` └─ Agents: ${status.agents.active} active, ${status.agents.completed} completed`, "INFO");
return {
success: true,
output: JSON.stringify(status),
verified: true,
};
}
async function createExecutionEvidence(planId: string, plan: StoredPlan, results: StepResult[], success: boolean) {
const evidenceId = `evidence-${planId}-${Date.now().toString(36)}`;
// All values must be strings for Redis hSet
await redis.hSet(`evidence:${evidenceId}`, {
evidence_id: evidenceId,
plan_id: planId,
pipeline_id: plan.pipeline_id,
plan_title: plan.title,
executed_at: new Date().toISOString(),
success: String(success),
total_steps: String(plan.steps.length),
completed_steps: String(results.filter(r => r.status === "SUCCESS").length),
failed_steps: String(results.filter(r => r.status === "FAILED").length),
results: JSON.stringify(results),
checksum: "",
});
// Link to plan
await redis.hSet(`plan:${planId}`, "evidence_id", evidenceId);
return evidenceId;
}
async function getSystemStatus(): Promise<any> {
let vaultStatus = { initialized: false, sealed: true, version: "unknown" };
try {
const proc = Bun.spawn(["curl", "-sk", "https://127.0.0.1:8200/v1/sys/health"]);
const text = await new Response(proc.stdout).text();
vaultStatus = JSON.parse(text);
} catch {}
const redisInfo = await redis.info("server").catch(() => "");
// Count active/revoked agents
const agents = await getAgentStates();
const activeCount = agents.filter(a => a.status === "RUNNING").length;
const revokedCount = agents.filter(a => a.status === "REVOKED").length;
const completedCount = agents.filter(a => a.status === "COMPLETED").length;
return {
vault: {
initialized: vaultStatus.initialized,
sealed: vaultStatus.sealed,
version: vaultStatus.version,
},
dragonfly: {
connected: redis.isOpen,
version: redisInfo.match(/redis_version:(\S+)/)?.[1] || "unknown",
},
agents: {
total: agents.length,
active: activeCount,
revoked: revokedCount,
completed: completedCount,
},
timestamp: new Date().toISOString(),
};
}
// =============================================================================
// HTML Dashboard
// =============================================================================
function renderDashboard(): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Control Panel</title>
<style>
:root {
--bg-primary: #0a0e14;
--bg-secondary: #131920;
--bg-tertiary: #1a2028;
--bg-input: #0d1117;
--border-color: #2d333b;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--accent-blue: #58a6ff;
--accent-green: #3fb950;
--accent-yellow: #d29922;
--accent-red: #f85149;
--accent-purple: #a371f7;
--accent-cyan: #39c5cf;
--accent-orange: #db6d28;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
height: 100vh;
overflow: hidden;
}
/* Main Layout */
.app {
display: grid;
grid-template-rows: auto 1fr;
height: 100vh;
}
/* Header / Command Bar */
.command-bar {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 12px 20px;
display: flex;
gap: 16px;
align-items: center;
}
.logo {
font-size: 14px;
font-weight: 700;
color: var(--accent-cyan);
white-space: nowrap;
}
.command-input-wrapper {
flex: 1;
display: flex;
align-items: center;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 0 12px;
}
.command-input-wrapper:focus-within {
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15);
}
.command-prefix {
color: var(--accent-green);
font-weight: 600;
margin-right: 8px;
}
.command-input {
flex: 1;
background: transparent;
border: none;
color: var(--text-primary);
font-family: inherit;
font-size: 14px;
padding: 10px 0;
outline: none;
}
.command-input::placeholder { color: var(--text-muted); }
.spawn-btn {
background: var(--accent-blue);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.spawn-btn:hover { background: #4c94e8; }
.spawn-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.status-indicators {
display: flex;
gap: 12px;
font-size: 11px;
}
.indicator {
display: flex;
align-items: center;
gap: 5px;
}
.indicator-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.indicator-dot.green { background: var(--accent-green); }
.indicator-dot.red { background: var(--accent-red); }
.indicator-dot.yellow { background: var(--accent-yellow); animation: pulse 2s infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Main Content */
.main-content {
display: grid;
grid-template-columns: 280px 1fr 320px;
gap: 1px;
background: var(--border-color);
overflow: hidden;
}
.panel {
background: var(--bg-primary);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
background: var(--bg-secondary);
padding: 10px 14px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 8px;
}
/* Pipeline Cards */
.pipeline-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.15s;
}
.pipeline-card:hover { border-color: var(--accent-blue); }
.pipeline-card.active { border-color: var(--accent-cyan); background: var(--bg-tertiary); }
.pipeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.pipeline-id {
font-size: 12px;
color: var(--accent-cyan);
}
.status-badge {
font-size: 9px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-badge.running { background: rgba(88, 166, 255, 0.2); color: var(--accent-blue); }
.status-badge.starting { background: rgba(210, 153, 34, 0.2); color: var(--accent-yellow); }
.status-badge.completed { background: rgba(63, 185, 80, 0.2); color: var(--accent-green); }
.status-badge.failed { background: rgba(248, 81, 73, 0.2); color: var(--accent-red); }
.pipeline-objective {
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.agent-pills {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.agent-pill {
font-size: 9px;
padding: 2px 6px;
border-radius: 3px;
display: flex;
align-items: center;
gap: 4px;
}
.agent-pill.alpha { background: rgba(88, 166, 255, 0.15); color: var(--accent-blue); }
.agent-pill.beta { background: rgba(63, 185, 80, 0.15); color: var(--accent-green); }
.agent-pill.gamma { background: rgba(219, 109, 40, 0.15); color: var(--accent-orange); }
.agent-pill .status-dot {
width: 5px;
height: 5px;
border-radius: 50%;
}
.agent-pill .status-dot.pending { background: var(--text-muted); }
.agent-pill .status-dot.running { background: var(--accent-yellow); }
.agent-pill .status-dot.completed { background: var(--accent-green); }
.agent-pill .status-dot.failed { background: var(--accent-red); }
/* Plan Execution */
.plan-info {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
margin-top: 10px;
}
.plan-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.plan-title {
font-weight: 600;
font-size: 12px;
color: var(--text-primary);
}
.plan-status {
font-size: 9px;
padding: 2px 6px;
border-radius: 3px;
font-weight: 600;
text-transform: uppercase;
}
.plan-status.pending { background: rgba(139, 148, 158, 0.2); color: var(--text-secondary); }
.plan-status.executing { background: rgba(210, 153, 34, 0.2); color: var(--accent-yellow); }
.plan-status.executed { background: rgba(88, 166, 255, 0.2); color: var(--accent-blue); }
.plan-status.verified { background: rgba(57, 197, 207, 0.2); color: var(--accent-cyan); }
.plan-status.packaged { background: rgba(163, 113, 247, 0.2); color: var(--accent-purple); }
.plan-status.reported { background: rgba(63, 185, 80, 0.2); color: var(--accent-green); }
.plan-status.completed { background: rgba(63, 185, 80, 0.2); color: var(--accent-green); }
.plan-status.failed { background: rgba(248, 81, 73, 0.2); color: var(--accent-red); }
.plan-meta {
font-size: 10px;
color: var(--text-muted);
display: flex;
gap: 12px;
margin-bottom: 10px;
}
.plan-meta strong {
color: var(--text-secondary);
}
.plan-buttons {
display: flex;
gap: 8px;
}
.exec-btn {
flex: 1;
padding: 8px 12px;
border: none;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
font-family: inherit;
}
.exec-btn.dry-run {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.exec-btn.dry-run:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.exec-btn.execute {
background: var(--accent-green);
color: #0a0e14;
}
.exec-btn.execute:hover {
background: #46c45a;
}
.exec-btn.verify {
background: var(--accent-cyan);
color: #0a0e14;
}
.exec-btn.verify:hover {
background: #4db8c2;
}
.exec-btn.package {
background: var(--accent-purple);
color: #0a0e14;
}
.exec-btn.package:hover {
background: #b085f5;
}
.exec-btn.report {
background: var(--accent-green);
color: #0a0e14;
}
.exec-btn.report:hover {
background: #46c45a;
}
.exec-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Log Console */
.log-console {
background: var(--bg-input);
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.log-header {
background: var(--bg-tertiary);
padding: 8px 14px;
font-size: 11px;
color: var(--text-muted);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.log-content {
flex: 1;
overflow-y: auto;
padding: 8px 12px;
font-size: 12px;
line-height: 1.8;
}
.log-entry {
display: flex;
gap: 8px;
padding: 2px 0;
border-bottom: 1px solid rgba(45, 51, 59, 0.3);
}
.log-time {
color: var(--text-muted);
font-size: 10px;
white-space: nowrap;
min-width: 70px;
}
.log-source {
font-weight: 600;
min-width: 100px;
}
.log-source.system { color: var(--accent-purple); }
.log-source.agent-a { color: var(--accent-blue); }
.log-source.agent-b { color: var(--accent-green); }
.log-source.agent-c { color: var(--accent-orange); }
.log-message {
color: var(--text-secondary);
word-break: break-word;
}
.log-message.error { color: var(--accent-red); }
.log-message.success { color: var(--accent-green); }
.log-message.warn { color: var(--accent-yellow); }
.log-empty {
color: var(--text-muted);
text-align: center;
padding: 40px;
font-size: 12px;
}
/* Stats Panel */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
padding: 8px;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: 700;
}
.stat-value.blue { color: var(--accent-blue); }
.stat-value.green { color: var(--accent-green); }
.stat-value.red { color: var(--accent-red); }
.stat-value.yellow { color: var(--accent-yellow); }
.stat-label {
font-size: 9px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 4px;
}
/* History List */
.history-list {
padding: 8px;
}
.history-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 10px;
margin-bottom: 6px;
font-size: 11px;
}
.history-item.violation { border-left: 3px solid var(--accent-red); }
.history-item.success { border-left: 3px solid var(--accent-green); }
.history-item.action { border-left: 3px solid var(--accent-blue); }
.history-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.history-type { font-weight: 600; color: var(--text-primary); }
.history-time { color: var(--text-muted); font-size: 10px; }
.history-agent { color: var(--accent-cyan); }
.history-detail { color: var(--text-secondary); margin-top: 4px; }
/* Approval Queue */
.approval-badge {
background: var(--accent-orange);
color: #0a0e14;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 10px;
min-width: 18px;
text-align: center;
}
.approval-badge:empty, .approval-badge[data-count="0"] {
background: var(--bg-tertiary);
color: var(--text-muted);
}
.approval-list {
padding: 8px;
max-height: 200px;
overflow-y: auto;
}
.approval-item {
background: var(--bg-secondary);
border: 1px solid var(--accent-orange);
border-radius: 4px;
padding: 10px;
margin-bottom: 8px;
font-size: 11px;
}
.approval-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.approval-plan {
font-weight: 600;
color: var(--text-primary);
}
.approval-reasons {
font-size: 10px;
color: var(--accent-orange);
margin-bottom: 8px;
}
.approval-buttons {
display: flex;
gap: 6px;
}
.approval-btn {
flex: 1;
padding: 6px 10px;
border: none;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
}
.approval-btn.approve {
background: var(--accent-green);
color: #0a0e14;
}
.approval-btn.reject {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.approval-btn:hover {
opacity: 0.85;
}
.approval-empty {
color: var(--text-muted);
font-size: 11px;
text-align: center;
padding: 12px;
}
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg-primary); }
::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
/* ========== Tab Navigation ========== */
.tab-bar {
display: flex;
gap: 2px;
padding: 0 12px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
overflow-x: auto;
}
.tab-btn {
padding: 10px 16px;
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 11px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
white-space: nowrap;
border-bottom: 2px solid transparent;
transition: all 0.15s;
}
.tab-btn:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.tab-btn.active {
color: var(--accent-cyan);
border-bottom-color: var(--accent-cyan);
}
.tab-content {
display: none;
height: calc(100vh - 100px);
overflow: hidden;
}
.tab-content.active {
display: flex;
}
/* ========== Checkpoint Manager ========== */
.checkpoint-container {
display: flex;
gap: 12px;
padding: 12px;
height: 100%;
width: 100%;
}
.checkpoint-timeline {
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.checkpoint-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 10px;
cursor: pointer;
transition: all 0.15s;
}
.checkpoint-item:hover, .checkpoint-item.active {
border-color: var(--accent-cyan);
}
.checkpoint-item.active {
background: rgba(0, 255, 255, 0.05);
}
.checkpoint-id {
font-size: 10px;
color: var(--accent-cyan);
font-family: monospace;
}
.checkpoint-time {
font-size: 10px;
color: var(--text-muted);
}
.checkpoint-notes {
font-size: 11px;
color: var(--text-secondary);
margin-top: 4px;
}
.checkpoint-detail {
flex: 1;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
overflow: auto;
padding: 12px;
}
.summary-level-btns {
display: flex;
gap: 4px;
margin-bottom: 12px;
}
.summary-level-btn {
padding: 4px 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 3px;
color: var(--text-secondary);
font-size: 10px;
cursor: pointer;
}
.summary-level-btn.active {
background: var(--accent-cyan);
color: #0a0e14;
border-color: var(--accent-cyan);
}
/* ========== Memory Browser ========== */
.memory-container {
display: flex;
gap: 12px;
padding: 12px;
height: 100%;
width: 100%;
}
.memory-sidebar {
width: 300px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.memory-search {
display: flex;
gap: 4px;
}
.memory-search input {
flex: 1;
padding: 8px 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
font-size: 11px;
}
.memory-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 4px;
}
.memory-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px;
cursor: pointer;
transition: all 0.15s;
}
.memory-item:hover, .memory-item.active {
border-color: var(--accent-purple);
}
.memory-item.active {
background: rgba(139, 92, 246, 0.05);
}
.memory-type {
display: inline-block;
padding: 2px 6px;
background: var(--accent-purple);
color: #0a0e14;
font-size: 9px;
font-weight: 600;
border-radius: 2px;
}
.memory-content {
flex: 1;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
overflow: auto;
padding: 12px;
}
.chunk-nav {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.chunk-nav button {
padding: 4px 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 3px;
color: var(--text-secondary);
font-size: 10px;
cursor: pointer;
}
.chunk-nav button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ========== Status Grid ========== */
.status-grid-container {
padding: 12px;
width: 100%;
overflow: auto;
}
.status-summary {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.status-stat {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 12px 16px;
text-align: center;
}
.status-stat-value {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
}
.status-stat-label {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
}
.status-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 6px;
}
.status-cell {
aspect-ratio: 1;
min-width: 80px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px;
display: flex;
flex-direction: column;
cursor: pointer;
transition: all 0.15s;
}
.status-cell:hover {
border-color: var(--text-muted);
transform: scale(1.02);
}
.status-cell.complete { border-left: 3px solid var(--accent-green); }
.status-cell.in_progress { border-left: 3px solid var(--accent-cyan); }
.status-cell.blocked { border-left: 3px solid var(--accent-red); }
.status-cell.needs_review { border-left: 3px solid var(--accent-orange); }
.status-cell.not_started { border-left: 3px solid var(--text-muted); }
.status-cell-name {
font-size: 9px;
font-weight: 600;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-cell-phase {
font-size: 8px;
color: var(--text-muted);
margin-top: auto;
}
.status-cell-icon {
font-size: 16px;
margin-bottom: 4px;
}
/* ========== Integration Panel ========== */
.integration-container {
display: flex;
gap: 16px;
padding: 16px;
flex-wrap: wrap;
}
.integration-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
width: 300px;
display: flex;
flex-direction: column;
gap: 12px;
}
.integration-header {
display: flex;
align-items: center;
gap: 12px;
}
.integration-icon {
width: 40px;
height: 40px;
background: var(--bg-tertiary);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.integration-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.integration-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
}
.integration-status.configured {
background: rgba(16, 185, 129, 0.15);
color: var(--accent-green);
}
.integration-status.not_configured {
background: rgba(107, 114, 128, 0.15);
color: var(--text-muted);
}
.integration-status.error {
background: rgba(239, 68, 68, 0.15);
color: var(--accent-red);
}
.integration-status.deprecated {
background: rgba(107, 114, 128, 0.25);
color: var(--text-muted);
text-decoration: line-through;
}
.integration-card.deprecated {
opacity: 0.6;
}
.integration-test-btn {
padding: 8px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-secondary);
font-size: 11px;
cursor: pointer;
transition: all 0.15s;
}
.integration-test-btn:hover {
background: var(--accent-cyan);
color: #0a0e14;
border-color: var(--accent-cyan);
}
.integration-result {
font-size: 10px;
color: var(--text-muted);
padding: 8px;
background: var(--bg-tertiary);
border-radius: 4px;
}
/* ========== Analytics Charts ========== */
.analytics-container {
display: flex;
flex-wrap: wrap;
gap: 16px;
padding: 16px;
overflow: auto;
width: 100%;
}
.chart-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
min-width: 350px;
flex: 1;
}
.chart-title {
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
}
.chart-svg {
width: 100%;
height: 200px;
}
.bar-chart-bar {
fill: var(--accent-cyan);
transition: fill 0.15s;
}
.bar-chart-bar:hover {
fill: var(--accent-blue);
}
.bar-chart-label {
fill: var(--text-secondary);
font-size: 10px;
}
.bar-chart-value {
fill: var(--text-primary);
font-size: 10px;
font-weight: 600;
}
.pie-chart-segment {
transition: opacity 0.15s;
}
.pie-chart-segment:hover {
opacity: 0.8;
}
.chart-legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
color: var(--text-secondary);
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 2px;
}
.analytics-summary-cards {
display: flex;
gap: 12px;
width: 100%;
margin-bottom: 16px;
}
.analytics-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px 20px;
text-align: center;
}
.analytics-card-value {
font-size: 28px;
font-weight: 700;
color: var(--accent-cyan);
}
.analytics-card-label {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
margin-top: 4px;
}
/* ========== Bug Tracking ========== */
.bugs-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
width: 100%;
flex: 1;
}
.bugs-summary {
display: flex;
gap: 12px;
}
.bug-stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px 24px;
text-align: center;
min-width: 100px;
}
.bug-stat-card.open { border-left: 3px solid #f59e0b; }
.bug-stat-card.in-progress { border-left: 3px solid #3b82f6; }
.bug-stat-card.resolved { border-left: 3px solid #10b981; }
.bug-stat-value {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
}
.bug-stat-card.open .bug-stat-value { color: #f59e0b; }
.bug-stat-card.in-progress .bug-stat-value { color: #3b82f6; }
.bug-stat-card.resolved .bug-stat-value { color: #10b981; }
.bug-stat-label {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
margin-top: 4px;
}
.bugs-filters {
display: flex;
gap: 12px;
align-items: center;
}
.bugs-filters select {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
}
.bug-action-btn {
background: var(--accent-cyan);
border: none;
color: var(--bg-primary);
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
margin-left: auto;
}
.bug-action-btn:hover { opacity: 0.9; }
.bugs-list {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
overflow: auto;
}
.bug-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: border-color 0.15s;
}
.bug-item:hover { border-color: var(--accent-cyan); }
.bug-severity {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.bug-severity.critical { background: #ef4444; }
.bug-severity.high { background: #f59e0b; }
.bug-severity.medium { background: #fbbf24; }
.bug-severity.low { background: #10b981; }
.bug-severity.info { background: #6b7280; }
.bug-status-badge {
font-size: 9px;
padding: 2px 6px;
border-radius: 3px;
text-transform: uppercase;
font-weight: 600;
}
.bug-status-badge.open { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.bug-status-badge.in_progress { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
.bug-status-badge.resolved { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.bug-message {
flex: 1;
font-size: 12px;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bug-meta {
font-size: 10px;
color: var(--text-muted);
white-space: nowrap;
}
.bug-detail-panel {
width: 400px;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
}
.bug-detail-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
font-weight: 600;
color: var(--accent-cyan);
}
.bug-detail-content {
font-size: 12px;
color: var(--text-secondary);
display: flex;
flex-direction: column;
gap: 12px;
}
.bug-detail-row {
display: flex;
gap: 8px;
}
.bug-detail-label {
color: var(--text-muted);
min-width: 80px;
}
.bug-detail-value {
color: var(--text-primary);
flex: 1;
}
.bug-detail-actions {
display: flex;
gap: 8px;
margin-top: auto;
padding-top: 16px;
border-top: 1px solid var(--border-color);
}
.bug-detail-actions button {
flex: 1;
padding: 8px 12px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
border: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-primary);
}
.bug-detail-actions button.primary {
background: var(--accent-cyan);
color: var(--bg-primary);
border: none;
}
.bug-detail-actions button:hover { opacity: 0.9; }
/* ========== Tier Progression ========== */
.tier-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 20px;
overflow: auto;
width: 100%;
}
.tier-cards {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.tier-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
min-width: 150px;
flex: 1;
text-align: center;
position: relative;
overflow: hidden;
}
.tier-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
}
.tier-card.t0::before { background: #6b7280; }
.tier-card.t1::before { background: #3b82f6; }
.tier-card.t2::before { background: #8b5cf6; }
.tier-card.t3::before { background: #f59e0b; }
.tier-card.t4::before { background: #10b981; }
.tier-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
}
.tier-count {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
margin: 8px 0;
}
.tier-name {
font-size: 10px;
color: var(--text-secondary);
}
.tier-history {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
}
.tier-history-title {
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
.tier-history-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 300px;
overflow-y: auto;
}
.tier-history-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
background: var(--bg-tertiary);
border-radius: 4px;
font-size: 11px;
}
.tier-badge {
padding: 2px 8px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
}
.tier-badge.t0 { background: #6b7280; color: white; }
.tier-badge.t1 { background: #3b82f6; color: white; }
.tier-badge.t2 { background: #8b5cf6; color: white; }
.tier-badge.t3 { background: #f59e0b; color: #0a0e14; }
.tier-badge.t4 { background: #10b981; color: #0a0e14; }
.promotion-arrow {
color: var(--accent-green);
font-weight: bold;
}
</style>
</head>
<body>
<div class="app">
<!-- Command Bar -->
<div class="command-bar">
<div class="logo">AGENT CONTROL</div>
<div class="command-input-wrapper">
<span class="command-prefix">></span>
<input type="text" class="command-input" id="command-input"
placeholder="Enter task objective... (e.g., 'Deploy nginx with SSL to sandbox')"
onkeydown="if(event.key==='Enter') spawnPipeline()">
</div>
<button class="spawn-btn" onclick="spawnPipeline()" id="spawn-btn">
<span>SPAWN PIPELINE</span>
</button>
<div class="status-indicators">
<div class="indicator">
<span class="indicator-dot" id="ws-dot"></span>
<span id="ws-label">Connecting</span>
</div>
<div class="indicator">
<span class="indicator-dot" id="vault-dot"></span>
<span>Vault</span>
</div>
<div class="indicator">
<span class="indicator-dot" id="db-dot"></span>
<span>DB</span>
</div>
</div>
</div>
<!-- Tab Navigation -->
<div class="tab-bar">
<button class="tab-btn active" data-tab="pipelines" onclick="switchTab('pipelines')">Pipelines</button>
<button class="tab-btn" data-tab="checkpoint" onclick="switchTab('checkpoint')">Checkpoints</button>
<button class="tab-btn" data-tab="memory" onclick="switchTab('memory')">Memory</button>
<button class="tab-btn" data-tab="status" onclick="switchTab('status')">Status Grid</button>
<button class="tab-btn" data-tab="integrations" onclick="switchTab('integrations')">Integrations</button>
<button class="tab-btn" data-tab="analytics" onclick="switchTab('analytics')">Analytics</button>
<button class="tab-btn" data-tab="bugs" onclick="switchTab('bugs')">Bugs</button>
<button class="tab-btn" data-tab="tiers" onclick="switchTab('tiers')">Tiers</button>
</div>
<!-- Tab: Pipelines (Original Main Content) -->
<div class="tab-content active" id="tab-pipelines">
<!-- Left: Pipelines -->
<div class="panel">
<div class="panel-header">
<span>PIPELINES</span>
<span id="pipeline-count">0</span>
</div>
<div class="panel-content" id="pipeline-list"></div>
<div class="panel-header" style="margin-top: 8px; border-top: 1px solid var(--border-color); padding-top: 8px;">
<span>PLAN EXECUTION</span>
<button onclick="storeTestPlan()" style="background: none; border: none; color: var(--accent-cyan); cursor: pointer; font-size: 10px;">+ Test Plan</button>
</div>
<div class="panel-content" id="plan-actions">
<span style="color: var(--text-muted); font-size: 11px;">Select a pipeline to see plans</span>
</div>
</div>
<!-- Center: Live Log -->
<div class="panel log-console">
<div class="log-header">
<span>LIVE EXECUTION LOG</span>
<span id="log-pipeline">No pipeline selected</span>
</div>
<div class="log-content" id="log-content">
<div class="log-empty">Select a pipeline or spawn a new one to see logs</div>
</div>
</div>
<!-- Right: Stats & History -->
<div class="panel">
<div class="panel-header">
<span>SYSTEM</span>
</div>
<div class="panel-content">
<div class="stats-grid" id="stats-grid"></div>
<div class="panel-header" style="margin-top: 8px;">
<span>APPROVAL QUEUE</span>
<span id="approval-count" class="approval-badge">0</span>
</div>
<div class="approval-list" id="approval-list"></div>
<div class="panel-header" style="margin-top: 8px;">
<span>ORCHESTRATION</span>
<span id="orchestration-count" style="color: var(--accent-cyan);">0</span>
</div>
<div class="orchestration-summary" id="orchestration-summary" style="margin-bottom: 8px;"></div>
<div class="orchestration-logs" id="orchestration-logs" style="max-height: 150px; overflow-y: auto;"></div>
<div class="panel-header" style="margin-top: 8px;">
<span>RECENT ACTIVITY</span>
</div>
<div class="history-list" id="history-list"></div>
</div>
</div>
</div>
<!-- Tab: Checkpoints -->
<div class="tab-content" id="tab-checkpoint">
<div class="checkpoint-container">
<div class="checkpoint-timeline">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 12px; font-weight: 600; color: var(--text-primary);">TIMELINE</span>
<button onclick="createCheckpoint()" style="padding: 4px 10px; background: var(--accent-cyan); border: none; border-radius: 3px; color: #0a0e14; font-size: 10px; font-weight: 600; cursor: pointer;">+ New</button>
</div>
<div id="checkpoint-list" style="overflow-y: auto; flex: 1;"></div>
</div>
<div class="checkpoint-detail">
<div class="summary-level-btns">
<button class="summary-level-btn active" onclick="setSummaryLevel('minimal')">Minimal</button>
<button class="summary-level-btn" onclick="setSummaryLevel('compact')">Compact</button>
<button class="summary-level-btn" onclick="setSummaryLevel('standard')">Standard</button>
<button class="summary-level-btn" onclick="setSummaryLevel('full')">Full</button>
</div>
<div id="checkpoint-detail-content" style="font-size: 11px; color: var(--text-secondary); white-space: pre-wrap; font-family: monospace;">
Select a checkpoint to view details
</div>
</div>
</div>
</div>
<!-- Tab: Memory -->
<div class="tab-content" id="tab-memory">
<div class="memory-container">
<div class="memory-sidebar">
<div class="memory-search">
<input type="text" id="memory-search-input" placeholder="Search memory..." onkeydown="if(event.key==='Enter') searchMemoryEntries()">
<button onclick="searchMemoryEntries()" style="padding: 8px 12px; background: var(--accent-purple); border: none; border-radius: 4px; color: white; cursor: pointer;">Search</button>
</div>
<div style="display: flex; gap: 4px; margin-top: 8px;">
<select id="memory-type-filter" onchange="loadMemoryEntries()" style="flex: 1; padding: 6px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary); font-size: 11px;">
<option value="">All Types</option>
<option value="output">Output</option>
<option value="result">Result</option>
<option value="artifact">Artifact</option>
<option value="log">Log</option>
</select>
</div>
<div class="memory-list" id="memory-list"></div>
</div>
<div class="memory-content">
<div class="chunk-nav" id="chunk-nav" style="display: none;">
<button onclick="loadChunk(-1)" id="chunk-prev">Prev</button>
<span id="chunk-indicator">Chunk 1 / 1</span>
<button onclick="loadChunk(1)" id="chunk-next">Next</button>
</div>
<div id="memory-content-display" style="font-size: 11px; color: var(--text-secondary); white-space: pre-wrap; font-family: monospace;">
Select a memory entry to view content
</div>
</div>
</div>
</div>
<!-- Tab: Status Grid -->
<div class="tab-content" id="tab-status">
<div class="status-grid-container">
<div class="status-summary" id="status-summary"></div>
<div class="status-grid" id="status-grid-display"></div>
</div>
</div>
<!-- Tab: Integrations -->
<div class="tab-content" id="tab-integrations">
<div class="integration-container" id="integration-cards"></div>
</div>
<!-- Tab: Analytics -->
<div class="tab-content" id="tab-analytics">
<div class="analytics-container">
<div class="analytics-summary-cards" id="analytics-summary"></div>
<div class="chart-container" id="chart-by-type">
<div class="chart-title">Violations by Type</div>
<svg class="chart-svg" id="chart-svg-type"></svg>
</div>
<div class="chart-container" id="chart-by-severity">
<div class="chart-title">Violations by Severity</div>
<svg class="chart-svg" id="chart-svg-severity"></svg>
</div>
<div class="chart-container" style="flex-basis: 100%;" id="chart-by-time">
<div class="chart-title">Violations Over Time (Last 7 Days)</div>
<svg class="chart-svg" id="chart-svg-time" style="height: 150px;"></svg>
</div>
</div>
</div>
<!-- Tab: Bugs -->
<div class="tab-content" id="tab-bugs">
<div class="bugs-container">
<div class="bugs-summary">
<div class="bug-stat-card">
<div class="bug-stat-value" id="bugs-total">0</div>
<div class="bug-stat-label">Total</div>
</div>
<div class="bug-stat-card open">
<div class="bug-stat-value" id="bugs-open">0</div>
<div class="bug-stat-label">Open</div>
</div>
<div class="bug-stat-card in-progress">
<div class="bug-stat-value" id="bugs-in-progress">0</div>
<div class="bug-stat-label">In Progress</div>
</div>
<div class="bug-stat-card resolved">
<div class="bug-stat-value" id="bugs-resolved">0</div>
<div class="bug-stat-label">Resolved</div>
</div>
</div>
<div class="bugs-filters">
<select id="bug-filter-status" onchange="loadBugs()">
<option value="">All Statuses</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="resolved">Resolved</option>
</select>
<select id="bug-filter-severity" onchange="loadBugs()">
<option value="">All Severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<button class="bug-action-btn" onclick="showLogBugModal()">+ Log Bug</button>
</div>
<div class="bugs-list" id="bugs-list"></div>
</div>
<div class="bug-detail-panel" id="bug-detail-panel" style="display: none;">
<div class="bug-detail-header">
<span id="bug-detail-id"></span>
<button onclick="closeBugDetail()" style="background: none; border: none; color: var(--text-muted); cursor: pointer;">&times;</button>
</div>
<div class="bug-detail-content" id="bug-detail-content"></div>
<div class="bug-detail-actions" id="bug-detail-actions"></div>
</div>
</div>
<!-- Tab: Tiers -->
<div class="tab-content" id="tab-tiers">
<div class="tier-container">
<div class="tier-cards" id="tier-cards"></div>
<div class="tier-history">
<div class="tier-history-title">Recent Promotions</div>
<div class="tier-history-list" id="tier-history-list"></div>
</div>
</div>
</div>
</div>
<script>
let ws;
let selectedPipelineId = null;
let pipelinesData = [];
let logsData = [];
let reconnectAttempts = 0;
// WebSocket Connection
function connectWebSocket() {
ws = new WebSocket('ws://' + window.location.host + '/ws');
ws.onopen = () => {
document.getElementById('ws-dot').className = 'indicator-dot green';
document.getElementById('ws-label').textContent = 'Live';
reconnectAttempts = 0;
refresh();
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'log_entry' && msg.data.pipeline_id === selectedPipelineId) {
appendLogEntry(msg.data.entry);
}
if (msg.type === 'pipeline_started' || msg.type === 'pipeline_completed' ||
msg.type === 'agent_status' || msg.type === 'refresh') {
loadPipelines();
loadStats();
}
// Approval workflow events
if (msg.type === 'approval_required' || msg.type === 'approval_processed') {
loadApprovalQueue();
}
// Auto-execution events
if (msg.type === 'auto_exec_queued' || msg.type === 'auto_exec_completed') {
loadPipelines();
if (selectedPipelineId === msg.data.pipeline_id) {
loadLogs(selectedPipelineId);
loadPlans();
}
}
// Plan execution events
if (msg.type === 'plan_executed') {
if (selectedPipelineId) {
loadLogs(selectedPipelineId);
loadPlans();
}
}
// Orchestration events (from Ledger API)
if (msg.type === 'orchestration_run' || msg.type === 'orchestration_completed') {
loadOrchestration();
}
// New tab events
if (msg.type === 'checkpoint_created') {
if (currentTab === 'checkpoint') {
loadCheckpoints();
}
tabDataLoaded['checkpoint'] = false; // Force reload on next visit
}
if (msg.type === 'memory_stored') {
if (currentTab === 'memory') {
loadMemoryEntries();
}
tabDataLoaded['memory'] = false;
}
if (msg.type === 'status_changed') {
if (currentTab === 'status') {
loadStatusGrid();
}
tabDataLoaded['status'] = false;
}
if (msg.type === 'violation_recorded') {
if (currentTab === 'analytics') {
loadAnalytics();
}
tabDataLoaded['analytics'] = false;
}
if (msg.type === 'tier_promoted') {
if (currentTab === 'tiers') {
loadTierProgression();
}
tabDataLoaded['tiers'] = false;
}
};
ws.onclose = () => {
document.getElementById('ws-dot').className = 'indicator-dot red';
document.getElementById('ws-label').textContent = 'Offline';
reconnectAttempts++;
setTimeout(connectWebSocket, Math.min(1000 * Math.pow(2, reconnectAttempts), 10000));
};
ws.onerror = () => {};
}
// API Helpers
async function fetchJSON(url, options = {}) {
const res = await fetch(url, options);
return res.json();
}
// Spawn Pipeline
async function spawnPipeline() {
const input = document.getElementById('command-input');
const btn = document.getElementById('spawn-btn');
const objective = input.value.trim();
if (!objective) {
input.focus();
return;
}
btn.disabled = true;
btn.innerHTML = '<span>SPAWNING...</span>';
try {
const result = await fetchJSON('/api/spawn', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ objective })
});
if (result.success) {
input.value = '';
selectedPipelineId = result.pipeline_id;
await loadPipelines();
await loadLogs(result.pipeline_id);
} else {
alert('Failed to spawn: ' + result.message);
}
} catch (e) {
alert('Error: ' + e.message);
}
btn.disabled = false;
btn.innerHTML = '<span>SPAWN PIPELINE</span>';
}
// Load Pipelines
async function loadPipelines() {
pipelinesData = await fetchJSON('/api/active-pipelines');
const container = document.getElementById('pipeline-list');
document.getElementById('pipeline-count').textContent = pipelinesData.length;
if (pipelinesData.length === 0) {
container.innerHTML = '<div class="log-empty">No active pipelines</div>';
return;
}
container.innerHTML = pipelinesData.map(p => {
const isActive = p.pipeline_id === selectedPipelineId;
const agents = p.agents || [];
const agentPills = agents.map(a => {
const type = (a.type || 'UNKNOWN').toLowerCase();
const statusClass = (a.status || 'pending').toLowerCase();
return \`<span class="agent-pill \${type}">
<span class="status-dot \${statusClass}"></span>
\${a.type || '?'}
</span>\`;
}).join('');
return \`
<div class="pipeline-card \${isActive ? 'active' : ''}" onclick="selectPipeline('\${p.pipeline_id}')">
<div class="pipeline-header">
<span class="pipeline-id">\${p.pipeline_id}</span>
<span class="status-badge \${(p.status || 'unknown').toLowerCase()}">\${p.status || 'UNKNOWN'}</span>
</div>
<div class="pipeline-objective">\${p.objective || 'No objective'}</div>
<div class="agent-pills">\${agentPills || '<span style="color: var(--text-muted); font-size: 10px;">No agents</span>'}</div>
</div>
\`;
}).join('');
}
// Select Pipeline
async function selectPipeline(pipelineId) {
selectedPipelineId = pipelineId;
loadPipelines();
await loadLogs(pipelineId);
await loadPlans();
}
// Load Logs
async function loadLogs(pipelineId) {
document.getElementById('log-pipeline').textContent = pipelineId;
logsData = await fetchJSON(\`/api/pipeline/logs?pipeline_id=\${pipelineId}&limit=200\`);
const container = document.getElementById('log-content');
if (logsData.length === 0) {
container.innerHTML = '<div class="log-empty">No logs yet - waiting for agent output...</div>';
return;
}
container.innerHTML = logsData.map(formatLogEntry).join('');
container.scrollTop = container.scrollHeight;
}
// Format Log Entry
function formatLogEntry(entry) {
const time = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '';
const source = entry.source || 'SYSTEM';
const sourceClass = source.toLowerCase().includes('agent-a') ? 'agent-a' :
source.toLowerCase().includes('agent-b') ? 'agent-b' :
source.toLowerCase().includes('agent-c') ? 'agent-c' : 'system';
const level = (entry.level || 'INFO').toLowerCase();
const levelClass = level === 'error' ? 'error' : level === 'success' ? 'success' : level === 'warn' ? 'warn' : '';
return \`
<div class="log-entry">
<span class="log-time">\${time}</span>
<span class="log-source \${sourceClass}">\${source}</span>
<span class="log-message \${levelClass}">\${entry.message || ''}</span>
</div>
\`;
}
// Append Log Entry (real-time)
function appendLogEntry(entry) {
const container = document.getElementById('log-content');
const emptyMsg = container.querySelector('.log-empty');
if (emptyMsg) emptyMsg.remove();
container.insertAdjacentHTML('beforeend', formatLogEntry(entry));
// Auto-scroll if near bottom
if (container.scrollHeight - container.scrollTop < container.clientHeight + 100) {
container.scrollTop = container.scrollHeight;
}
}
// ========== Plan Execution Functions ==========
let currentPlanId = null;
// Load plans for current pipeline
async function loadPlans() {
if (!selectedPipelineId) return;
const plans = await fetchJSON(\`/api/plans?pipeline_id=\${selectedPipelineId}\`);
const container = document.getElementById('plan-actions');
if (plans.length === 0) {
container.innerHTML = '<span style="color: var(--text-muted); font-size: 11px;">No plans yet</span>';
return;
}
const plan = plans[0]; // Most recent plan
currentPlanId = plan.plan_id;
container.innerHTML = \`
<div class="plan-info">
<div class="plan-header">
<span class="plan-title">\${plan.title || 'Plan'}</span>
<span class="plan-status \${plan.status.toLowerCase()}">\${plan.status}</span>
</div>
<div class="plan-meta">
<span>Confidence: <strong>\${(plan.confidence * 100).toFixed(0)}%</strong></span>
<span>Steps: <strong>\${plan.steps.length}</strong></span>
<span>Tier: <strong>T\${plan.estimated_tier_required}</strong></span>
</div>
<div class="plan-buttons">
<button class="exec-btn dry-run" onclick="executePlan(true)" \${['EXECUTED', 'VERIFIED', 'PACKAGED', 'COMPLETED'].includes(plan.status) ? 'disabled' : ''}>DRY RUN</button>
<button class="exec-btn execute" onclick="executePlan(false)" \${['EXECUTING', 'EXECUTED', 'VERIFIED', 'PACKAGED', 'COMPLETED'].includes(plan.status) ? 'disabled' : ''}>
EXECUTE
</button>
<button class="exec-btn verify" onclick="verifyPlan()" \${plan.status !== 'EXECUTED' ? 'disabled' : ''}>
VERIFY
</button>
<button class="exec-btn package" onclick="packagePlan()" \${plan.status !== 'VERIFIED' ? 'disabled' : ''}>
PACKAGE
</button>
\${plan.status === 'COMPLETED' ?
\`<button class="exec-btn report" onclick="viewReport('\${plan.plan_id}')" style="background: var(--accent-green);">VIEW REPORT</button>\` :
\`<button class="exec-btn report" onclick="reportPlan()" \${plan.status !== 'PACKAGED' ? 'disabled' : ''}>REPORT</button>\`
}
</div>
\${plan.status === 'COMPLETED' ? \`<div style="margin-top: 8px; font-size: 10px; color: var(--accent-green);">✓ Pipeline complete - click VIEW REPORT to see results</div>\` : ''}
</div>
\`;
}
// Execute plan
async function executePlan(dryRun = false) {
if (!currentPlanId) {
alert('No plan selected');
return;
}
const tierInput = prompt('Enter execution tier level (1-4):', '1');
const tier = parseInt(tierInput) || 1;
const execBtn = document.querySelector('.exec-btn.execute');
const dryBtn = document.querySelector('.exec-btn.dry-run');
if (execBtn) execBtn.disabled = true;
if (dryBtn) dryBtn.disabled = true;
try {
const result = await fetchJSON('/api/plan/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plan_id: currentPlanId,
dry_run: dryRun,
tier: tier
})
});
if (result.success) {
await loadLogs(selectedPipelineId);
await loadPlans();
// Auto-advance: if not dry run, automatically verify
if (!dryRun && result.success) {
setTimeout(() => {
console.log('[AUTO-ADVANCE] Execution complete, starting verification...');
verifyPlan();
}, 1000);
}
} else {
alert('Execution failed: ' + result.summary);
}
} catch (e) {
alert('Error: ' + e.message);
}
if (execBtn) execBtn.disabled = false;
if (dryBtn) dryBtn.disabled = false;
}
// Verify plan execution
async function verifyPlan() {
if (!currentPlanId) {
alert('No plan selected');
return;
}
const verifyBtn = document.querySelector('.exec-btn.verify');
if (verifyBtn) verifyBtn.disabled = true;
try {
const result = await fetchJSON('/api/plan/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plan_id: currentPlanId
})
});
if (result.success) {
await loadLogs(selectedPipelineId);
await loadPlans();
// Auto-advance: automatically package after verification
setTimeout(() => {
console.log('[AUTO-ADVANCE] Verification complete, starting packaging...');
packagePlan();
}, 1000);
} else {
alert('Verification failed: ' + result.summary);
}
} catch (e) {
alert('Error: ' + e.message);
}
if (verifyBtn) verifyBtn.disabled = false;
}
// Package plan artifacts
async function packagePlan() {
if (!currentPlanId) {
alert('No plan selected');
return;
}
const packageBtn = document.querySelector('.exec-btn.package');
if (packageBtn) packageBtn.disabled = true;
try {
const result = await fetchJSON('/api/plan/package', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plan_id: currentPlanId
})
});
if (result.success) {
await loadLogs(selectedPipelineId);
await loadPlans();
// Auto-advance: automatically generate report after packaging
setTimeout(() => {
console.log('[AUTO-ADVANCE] Packaging complete, generating report...');
reportPlan();
}, 1000);
} else {
alert('Packaging failed: ' + result.summary);
}
} catch (e) {
alert('Error: ' + e.message);
}
if (packageBtn) packageBtn.disabled = false;
}
// Generate final report
async function reportPlan() {
if (!currentPlanId) {
alert('No plan selected');
return;
}
const reportBtn = document.querySelector('.exec-btn.report');
if (reportBtn) reportBtn.disabled = true;
try {
const result = await fetchJSON('/api/plan/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plan_id: currentPlanId
})
});
if (result.success) {
await loadLogs(selectedPipelineId);
await loadPlans();
// Show the report panel with real data
showReportPanel(result.report);
} else {
alert('Report generation failed: ' + result.summary);
}
} catch (e) {
alert('Error: ' + e.message);
}
if (reportBtn) reportBtn.disabled = false;
}
// Show formatted report panel
function showReportPanel(report) {
if (!report) {
alert('No report data available');
return;
}
const outcomeColors = {
SUCCESS: 'var(--accent-green)',
PARTIAL: 'var(--accent-orange)',
FAILED: 'var(--accent-red)'
};
const modal = document.createElement('div');
modal.id = 'report-modal';
modal.style.cssText = \`
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.85); z-index: 1000;
display: flex; align-items: center; justify-content: center;
padding: 20px;
\`;
modal.innerHTML = \`
<div style="background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; max-width: 700px; width: 100%; max-height: 80vh; overflow: auto; padding: 24px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0; color: var(--text-primary); font-size: 18px;">Execution Report</h2>
<button onclick="closeReportModal()" style="background: none; border: none; color: var(--text-muted); font-size: 20px; cursor: pointer;">&times;</button>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px;">
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; text-align: center;">
<div style="font-size: 24px; font-weight: 700; color: \${outcomeColors[report.summary.outcome] || 'var(--text-primary)'};">\${report.summary.outcome}</div>
<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">Outcome</div>
</div>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; text-align: center;">
<div style="font-size: 24px; font-weight: 700; color: var(--accent-cyan);">\${(report.summary.confidence * 100).toFixed(0)}%</div>
<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">Confidence</div>
</div>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; text-align: center;">
<div style="font-size: 24px; font-weight: 700; color: var(--text-primary);">\${report.summary.execution_time_ms}ms</div>
<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">Execution Time</div>
</div>
</div>
<div style="margin-bottom: 20px;">
<h3 style="font-size: 12px; color: var(--text-secondary); margin-bottom: 8px; text-transform: uppercase;">Summary</h3>
<div style="background: var(--bg-tertiary); padding: 12px; border-radius: 6px; font-size: 12px; color: var(--text-primary); white-space: pre-line;">\${report.notes_for_humans}</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px;">
<div>
<h3 style="font-size: 12px; color: var(--text-secondary); margin-bottom: 8px; text-transform: uppercase;">Phases Completed</h3>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
\${report.phases_completed.map(p => \`<span style="background: var(--accent-green); color: #0a0e14; padding: 4px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">\${p}</span>\`).join('')}
</div>
</div>
<div>
<h3 style="font-size: 12px; color: var(--text-secondary); margin-bottom: 8px; text-transform: uppercase;">Assumptions Validated</h3>
<div style="font-size: 11px; color: var(--text-primary);">
\${report.assumptions_validated.map(a => \`<div style="margin-bottom: 4px;">✓ \${a}</div>\`).join('')}
</div>
</div>
</div>
<div style="margin-bottom: 20px;">
<h3 style="font-size: 12px; color: var(--text-secondary); margin-bottom: 8px; text-transform: uppercase;">Next Actions</h3>
<div style="background: var(--bg-tertiary); padding: 12px; border-radius: 6px;">
\${report.next_actions.map((a, i) => \`<div style="font-size: 11px; color: var(--text-primary); margin-bottom: 6px;">\${i+1}. \${a}</div>\`).join('')}
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 16px; border-top: 1px solid var(--border-color);">
<div style="font-size: 10px; color: var(--text-muted);">
Report ID: \${report.report_id}<br>
Generated: \${new Date(report.generated_at).toLocaleString()}
</div>
<button onclick="closeReportModal()" style="padding: 8px 20px; background: var(--accent-cyan); border: none; border-radius: 4px; color: #0a0e14; font-weight: 600; cursor: pointer;">Close</button>
</div>
</div>
\`;
document.body.appendChild(modal);
}
function closeReportModal() {
const modal = document.getElementById('report-modal');
if (modal) modal.remove();
}
// View existing report for a completed plan
async function viewReport(planId) {
try {
const report = await fetchJSON(\`/api/report/get?plan_id=\${planId}\`);
if (report && report.report_id) {
showReportPanel(report);
} else {
alert('Report not found');
}
} catch (e) {
alert('Error loading report: ' + e.message);
}
}
// Store a plan manually (for testing)
async function storeTestPlan() {
if (!selectedPipelineId) {
alert('Select a pipeline first');
return;
}
const testPlan = {
title: "Health Check Plan",
confidence: 0.85,
steps: [
{ step: 1, action: "Check all service health endpoints", reversible: true },
{ step: 2, action: "Enumerate inventory of running services", reversible: true },
{ step: 3, action: "Validate credentials and access tokens", reversible: true },
{ step: 4, action: "Generate system status report", reversible: true }
],
assumptions: ["Services are running", "Network is accessible"],
risks: ["Health checks may add minimal load"],
estimated_tier_required: 1
};
const result = await fetchJSON('/api/plan/store', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pipeline_id: selectedPipelineId, plan: testPlan })
});
if (result.success) {
await loadPlans();
alert('Test plan stored: ' + result.plan_id);
}
}
// Load Stats
async function loadStats() {
const status = await fetchJSON('/api/status');
document.getElementById('vault-dot').className =
'indicator-dot ' + (status.vault.initialized && !status.vault.sealed ? 'green' : 'red');
document.getElementById('db-dot').className =
'indicator-dot ' + (status.dragonfly.connected ? 'green' : 'red');
document.getElementById('stats-grid').innerHTML = \`
<div class="stat-card">
<div class="stat-value blue">\${status.agents.total}</div>
<div class="stat-label">Total Agents</div>
</div>
<div class="stat-card">
<div class="stat-value green">\${status.agents.active}</div>
<div class="stat-label">Running</div>
</div>
<div class="stat-card">
<div class="stat-value">\${status.agents.completed}</div>
<div class="stat-label">Completed</div>
</div>
<div class="stat-card">
<div class="stat-value red">\${status.agents.revoked}</div>
<div class="stat-label">Revoked</div>
</div>
\`;
}
// Load History
async function loadHistory() {
const [violations, revocations] = await Promise.all([
fetchJSON('/api/violations?limit=10'),
fetchJSON('/api/revocations?limit=10')
]);
const events = [];
violations.forEach(v => {
events.push({
type: 'violation',
title: v.violation_type,
agent: v.agent_id,
detail: v.description,
time: v.timestamp
});
});
revocations.forEach(r => {
events.push({
type: 'violation',
title: 'REVOKED',
agent: r.agent_id,
detail: r.reason_type || 'Unknown',
time: r.revoked_at
});
});
events.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime());
document.getElementById('history-list').innerHTML = events.slice(0, 10).map(e => \`
<div class="history-item \${e.type}">
<div class="history-header">
<span class="history-type">\${e.title}</span>
<span class="history-time">\${new Date(e.time).toLocaleTimeString()}</span>
</div>
<div class="history-agent">\${e.agent}</div>
<div class="history-detail">\${e.detail}</div>
</div>
\`).join('') || '<div class="log-empty">No recent activity</div>';
}
// ========== Orchestration Functions ==========
async function loadOrchestration() {
try {
const [summary, logs] = await Promise.all([
fetchJSON('/api/orchestration/summary'),
fetchJSON('/api/orchestration?limit=10')
]);
// Update count
document.getElementById('orchestration-count').textContent = summary.total_runs || 0;
// Render summary
const summaryHtml = \`
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px; font-size: 10px;">
\${summary.by_mode.map(m => \`
<div style="background: var(--bg-tertiary); padding: 4px 6px; border-radius: 4px;">
<span style="color: var(--accent-cyan);">\${m.mode || 'unknown'}</span>
<span style="color: var(--text-muted);"> \${m.successes}/\${m.count}</span>
</div>
\`).join('')}
</div>
\${summary.latest ? \`
<div style="margin-top: 6px; font-size: 10px; color: var(--text-muted);">
Latest: <span style="color: var(--accent-cyan);">\${summary.latest.mode}</span>
via <span style="color: var(--accent-purple);">\${summary.latest.model || 'unknown'}</span>
<span style="color: \${summary.latest.success ? 'var(--accent-green)' : 'var(--accent-red)'};">
\${summary.latest.success ? '✓' : '✗'}
</span>
</div>
\` : ''}
\`;
document.getElementById('orchestration-summary').innerHTML = summaryHtml;
// Render recent logs
const logsHtml = logs.map(log => \`
<div style="font-size: 10px; padding: 4px 6px; border-bottom: 1px solid var(--border-color);">
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--accent-cyan);">\${log.mode}</span>
<span style="color: \${log.success ? 'var(--accent-green)' : 'var(--accent-red)'};">
\${log.success ? '✓' : '✗'}
</span>
</div>
<div style="color: var(--text-muted);">
\${log.model || 'unknown'}\${new Date(log.timestamp).toLocaleTimeString()}
</div>
\${log.instruction ? \`<div style="color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">\${log.instruction.substring(0, 50)}...</div>\` : ''}
</div>
\`).join('') || '<div style="font-size: 10px; color: var(--text-muted); padding: 4px;">No orchestration logs</div>';
document.getElementById('orchestration-logs').innerHTML = logsHtml;
} catch (e) {
document.getElementById('orchestration-summary').innerHTML = '<div style="font-size: 10px; color: var(--text-muted);">Orchestration data unavailable</div>';
}
}
// ========== Approval Queue Functions ==========
async function loadApprovalQueue() {
const queue = await fetchJSON('/api/approval/queue');
const container = document.getElementById('approval-list');
const countBadge = document.getElementById('approval-count');
countBadge.textContent = queue.length;
countBadge.setAttribute('data-count', queue.length);
if (queue.length === 0) {
container.innerHTML = '<div class="approval-empty">No pending approvals</div>';
return;
}
container.innerHTML = queue.map(req => \`
<div class="approval-item" data-request-id="\${req.request_id}">
<div class="approval-header">
<span class="approval-plan">\${req.plan_id}</span>
</div>
<div class="approval-reasons">\${req.reasons.join(', ')}</div>
<div class="approval-buttons">
<button class="approval-btn reject" onclick="rejectApproval('\${req.request_id}')">REJECT</button>
<button class="approval-btn approve" onclick="approveApproval('\${req.request_id}')">APPROVE</button>
</div>
</div>
\`).join('');
}
async function approveApproval(requestId) {
const reviewer = prompt('Enter your name (reviewer):');
if (!reviewer) return;
const tier = prompt('Enter execution tier (1-4):', '1');
const notes = prompt('Optional notes:', '');
try {
const result = await fetchJSON('/api/approval/approve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request_id: requestId,
reviewer: reviewer,
tier: parseInt(tier) || 1,
notes: notes
})
});
if (result.success) {
alert('Approved! ' + result.message);
loadApprovalQueue();
if (selectedPipelineId) loadLogs(selectedPipelineId);
} else {
alert('Error: ' + result.message);
}
} catch (e) {
alert('Error: ' + e.message);
}
}
async function rejectApproval(requestId) {
const reviewer = prompt('Enter your name (reviewer):');
if (!reviewer) return;
const reason = prompt('Reason for rejection:');
if (!reason) return;
try {
const result = await fetchJSON('/api/approval/reject', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request_id: requestId,
reviewer: reviewer,
reason: reason
})
});
if (result.success) {
alert('Rejected: ' + result.message);
loadApprovalQueue();
} else {
alert('Error: ' + result.message);
}
} catch (e) {
alert('Error: ' + e.message);
}
}
// ========== Tab Navigation ==========
let currentTab = 'pipelines';
let tabDataLoaded = {};
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabName);
});
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === 'tab-' + tabName);
});
currentTab = tabName;
// Load data if not already loaded
if (!tabDataLoaded[tabName]) {
loadTabData(tabName);
tabDataLoaded[tabName] = true;
}
}
function loadTabData(tabName) {
switch(tabName) {
case 'checkpoint': loadCheckpoints(); break;
case 'memory': loadMemoryEntries(); break;
case 'status': loadStatusGrid(); break;
case 'integrations': loadIntegrations(); break;
case 'analytics': loadAnalytics(); break;
case 'bugs': loadBugsSummary(); loadBugs(); break;
case 'tiers': loadTierProgression(); break;
}
}
// ========== Checkpoint Manager ==========
let checkpointsData = [];
let selectedCheckpointId = null;
let summaryLevel = 'minimal';
async function loadCheckpoints() {
try {
checkpointsData = await fetchJSON('/api/checkpoint/list?limit=20');
const container = document.getElementById('checkpoint-list');
if (!checkpointsData || checkpointsData.length === 0) {
container.innerHTML = '<div style="color: var(--text-muted); font-size: 11px; padding: 12px;">No checkpoints yet</div>';
return;
}
container.innerHTML = checkpointsData.map(cp => \`
<div class="checkpoint-item \${cp.id === selectedCheckpointId ? 'active' : ''}"
onclick="selectCheckpoint('\${cp.id}')">
<div class="checkpoint-id">\${cp.id}</div>
<div class="checkpoint-time">\${cp.created_at ? new Date(cp.created_at).toLocaleString() : ''}</div>
<div class="checkpoint-notes">\${cp.phase || ''}</div>
<div style="font-size: 9px; color: var(--text-muted); margin-top: 2px;">Tasks: \${cp.tasks || 0} | ~\${cp.tokens || 0} tokens</div>
</div>
\`).join('');
// Auto-select first checkpoint
if (!selectedCheckpointId && checkpointsData.length > 0) {
selectCheckpoint(checkpointsData[0].id);
}
} catch (e) {
console.error('Error loading checkpoints:', e);
}
}
async function selectCheckpoint(id) {
selectedCheckpointId = id;
loadCheckpoints(); // Re-render to update active state
loadCheckpointDetail();
}
async function loadCheckpointDetail() {
if (!selectedCheckpointId) return;
const container = document.getElementById('checkpoint-detail-content');
container.textContent = 'Loading...';
try {
const summary = await fetchJSON(\`/api/checkpoint/summary?level=\${summaryLevel}\`);
container.textContent = summary.summary || 'No summary available';
} catch (e) {
container.textContent = 'Error loading checkpoint: ' + e.message;
}
}
function setSummaryLevel(level) {
summaryLevel = level;
document.querySelectorAll('.summary-level-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent.toLowerCase() === level);
});
loadCheckpointDetail();
}
async function createCheckpoint() {
const notes = prompt('Checkpoint notes (optional):');
try {
const result = await fetchJSON('/api/checkpoint/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notes })
});
if (result && result.id) {
alert('Checkpoint created: ' + result.id);
loadCheckpoints();
} else {
alert('Failed to create checkpoint');
}
} catch (e) {
alert('Error: ' + e.message);
}
}
// ========== Memory Browser ==========
let memoryEntriesData = [];
let selectedMemoryId = null;
let currentChunk = 0;
let totalChunks = 1;
let memoryEntryData = null;
async function loadMemoryEntries() {
try {
const type = document.getElementById('memory-type-filter').value;
const url = type ? \`/api/memory/list?type=\${type}&limit=50\` : '/api/memory/list?limit=50';
memoryEntriesData = await fetchJSON(url);
const container = document.getElementById('memory-list');
if (!memoryEntriesData || memoryEntriesData.length === 0) {
container.innerHTML = '<div style="color: var(--text-muted); font-size: 11px; padding: 12px;">No memory entries</div>';
return;
}
container.innerHTML = memoryEntriesData.map(m => \`
<div class="memory-item \${m.id === selectedMemoryId ? 'active' : ''}"
onclick="selectMemoryEntry('\${m.id}')">
<span class="memory-type">\${m.entry_type || 'unknown'}</span>
<div style="font-size: 10px; color: var(--text-primary); margin-top: 4px;">\${m.id}</div>
<div style="font-size: 9px; color: var(--text-muted);">
\${m.chunk_count || 1} chunks | \${formatBytes(m.total_size || 0)}
</div>
\${m.summary ? \`<div style="font-size: 10px; color: var(--text-secondary); margin-top: 4px;">\${m.summary.substring(0, 100)}...</div>\` : ''}
</div>
\`).join('');
} catch (e) {
console.error('Error loading memory:', e);
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
async function selectMemoryEntry(id) {
selectedMemoryId = id;
currentChunk = 0;
loadMemoryEntries(); // Re-render to update active state
loadMemoryContent();
}
async function loadMemoryContent() {
if (!selectedMemoryId) return;
const container = document.getElementById('memory-content-display');
container.textContent = 'Loading...';
try {
memoryEntryData = await fetchJSON(\`/api/memory/get?id=\${selectedMemoryId}\`);
if (memoryEntryData) {
totalChunks = memoryEntryData.chunk_count || 1;
updateChunkNav();
displayCurrentChunk();
} else {
container.textContent = 'Entry not found';
}
} catch (e) {
container.textContent = 'Error loading: ' + e.message;
}
}
function updateChunkNav() {
const nav = document.getElementById('chunk-nav');
if (totalChunks > 1) {
nav.style.display = 'flex';
document.getElementById('chunk-indicator').textContent = \`Chunk \${currentChunk + 1} / \${totalChunks}\`;
document.getElementById('chunk-prev').disabled = currentChunk === 0;
document.getElementById('chunk-next').disabled = currentChunk >= totalChunks - 1;
} else {
nav.style.display = 'none';
}
}
function displayCurrentChunk() {
const container = document.getElementById('memory-content-display');
if (memoryEntryData) {
const chunks = memoryEntryData.chunks || [memoryEntryData.content];
container.textContent = chunks[currentChunk] || memoryEntryData.content || 'No content';
}
}
function loadChunk(delta) {
currentChunk = Math.max(0, Math.min(totalChunks - 1, currentChunk + delta));
updateChunkNav();
displayCurrentChunk();
}
async function searchMemoryEntries() {
const query = document.getElementById('memory-search-input').value.trim();
if (!query) {
loadMemoryEntries();
return;
}
try {
memoryEntriesData = await fetchJSON(\`/api/memory/search?q=\${encodeURIComponent(query)}\`);
const container = document.getElementById('memory-list');
if (!memoryEntriesData || memoryEntriesData.length === 0) {
container.innerHTML = '<div style="color: var(--text-muted); font-size: 11px; padding: 12px;">No results found</div>';
return;
}
container.innerHTML = memoryEntriesData.map(m => \`
<div class="memory-item" onclick="selectMemoryEntry('\${m.id}')">
<span class="memory-type">\${m.entry_type || 'unknown'}</span>
<div style="font-size: 10px; color: var(--text-primary); margin-top: 4px;">\${m.id}</div>
\${m.summary ? \`<div style="font-size: 10px; color: var(--text-secondary); margin-top: 4px;">\${m.summary.substring(0, 100)}...</div>\` : ''}
</div>
\`).join('');
} catch (e) {
console.error('Error searching memory:', e);
}
}
// ========== Status Grid ==========
const phaseIcons = {
complete: '✓',
in_progress: '◐',
blocked: '✗',
needs_review: '!',
not_started: '○'
};
async function loadStatusGrid() {
try {
const data = await fetchJSON('/api/status/grid');
const summaryContainer = document.getElementById('status-summary');
const gridContainer = document.getElementById('status-grid-display');
// Render summary
const summary = data.summary || {};
summaryContainer.innerHTML = \`
<div class="status-stat">
<div class="status-stat-value">\${summary.total || 0}</div>
<div class="status-stat-label">Total</div>
</div>
<div class="status-stat" style="border-left: 3px solid var(--accent-green);">
<div class="status-stat-value">\${summary.complete || 0}</div>
<div class="status-stat-label">Complete</div>
</div>
<div class="status-stat" style="border-left: 3px solid var(--accent-cyan);">
<div class="status-stat-value">\${summary.in_progress || 0}</div>
<div class="status-stat-label">In Progress</div>
</div>
<div class="status-stat" style="border-left: 3px solid var(--accent-red);">
<div class="status-stat-value">\${summary.blocked || 0}</div>
<div class="status-stat-label">Blocked</div>
</div>
<div class="status-stat" style="border-left: 3px solid var(--text-muted);">
<div class="status-stat-value">\${summary.not_started || 0}</div>
<div class="status-stat-label">Not Started</div>
</div>
\`;
// Render grid
const directories = data.directories || [];
if (directories.length === 0) {
gridContainer.innerHTML = '<div style="color: var(--text-muted); font-size: 11px; padding: 12px;">No directory status data</div>';
return;
}
gridContainer.innerHTML = directories.map(d => {
const phase = d.phase || 'not_started';
const icon = phaseIcons[phase] || '?';
const name = d.path ? d.path.split('/').pop() : 'unknown';
return \`
<div class="status-cell \${phase}" title="\${d.path}">
<div class="status-cell-icon">\${icon}</div>
<div class="status-cell-name">\${name}</div>
<div class="status-cell-phase">\${phase.replace('_', ' ')}</div>
</div>
\`;
}).join('');
} catch (e) {
console.error('Error loading status grid:', e);
}
}
// ========== Integration Panel ==========
const integrationIcons = {
slack: '#',
github: 'GH',
pagerduty: 'PD'
};
async function loadIntegrations() {
try {
const status = await fetchJSON('/api/integrations/status');
const container = document.getElementById('integration-cards');
// Filter out metadata keys like _note
const integrations = Object.entries(status).filter(([key]) => !key.startsWith('_'));
if (status._note) {
container.innerHTML = '<div style="color: var(--text-muted); font-size: 12px; margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 4px;">' +
status._note + '</div>';
} else {
container.innerHTML = '';
}
container.innerHTML += integrations.map(([key, data]) => \`
<div class="integration-card \${data.status === 'deprecated' ? 'deprecated' : ''}">
<div class="integration-header">
<div class="integration-icon">\${integrationIcons[key] || '?'}</div>
<div>
<div class="integration-name">\${data.name || key}</div>
<div class="integration-status \${data.status}">\${data.status?.replace('_', ' ') || 'unknown'}</div>
</div>
</div>
<div style="font-size: 11px; color: var(--text-secondary); margin: 8px 0;">\${data.details || ''}</div>
\${data.status !== 'deprecated' ? \`
<button class="integration-test-btn" onclick="testIntegrationConnection('\${key}')" \${data.status !== 'configured' ? 'disabled style="opacity: 0.5; cursor: not-allowed;"' : ''}>
Test Connection
</button>
<div class="integration-result" id="integration-result-\${key}">
\${data.status === 'configured' ? 'Ready to test' : 'Configure in Vault first'}
</div>
\` : \`
<div class="integration-result" style="color: var(--text-muted);">
See .archive/integrations/ to restore
</div>
\`}
</div>
\`).join('');
} catch (e) {
console.error('Error loading integrations:', e);
}
}
async function testIntegrationConnection(name) {
const resultEl = document.getElementById('integration-result-' + name);
resultEl.textContent = 'Testing...';
try {
const result = await fetchJSON('/api/integrations/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
resultEl.textContent = result.success
? '✓ ' + result.message
: '✗ ' + result.message;
resultEl.style.color = result.success ? 'var(--accent-green)' : 'var(--accent-red)';
} catch (e) {
resultEl.textContent = '✗ Error: ' + e.message;
resultEl.style.color = 'var(--accent-red)';
}
}
// ========== Analytics Charts ==========
const chartColors = ['#00ffff', '#3b82f6', '#8b5cf6', '#f59e0b', '#10b981', '#ef4444', '#ec4899'];
const severityColors = { critical: '#ef4444', high: '#f59e0b', medium: '#fbbf24', low: '#10b981' };
async function loadAnalytics() {
try {
const [summary, byType, bySeverity, byTime] = await Promise.all([
fetchJSON('/api/analytics/summary'),
fetchJSON('/api/analytics/violations/by-type'),
fetchJSON('/api/analytics/violations/by-severity'),
fetchJSON('/api/analytics/violations/by-time?days=7')
]);
// Render summary cards
document.getElementById('analytics-summary').innerHTML = \`
<div class="analytics-card">
<div class="analytics-card-value">\${summary.total_violations || 0}</div>
<div class="analytics-card-label">Total Violations</div>
</div>
<div class="analytics-card">
<div class="analytics-card-value" style="color: var(--accent-orange);">\${summary.last_24h || 0}</div>
<div class="analytics-card-label">Last 24 Hours</div>
</div>
\`;
// Render bar chart (by type)
renderBarChart('chart-svg-type', byType, 'type', 'count');
// Render pie chart (by severity)
renderPieChart('chart-svg-severity', bySeverity, 'severity', 'count');
// Render time series
renderTimeChart('chart-svg-time', byTime);
} catch (e) {
console.error('Error loading analytics:', e);
}
}
function renderBarChart(containerId, data, labelKey, valueKey) {
const svg = document.getElementById(containerId);
if (!data || data.length === 0) {
svg.innerHTML = '<text x="50%" y="50%" text-anchor="middle" fill="var(--text-muted)" font-size="11">No data</text>';
return;
}
const width = 300;
const height = 180;
const margin = { top: 10, right: 10, bottom: 40, left: 40 };
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
const maxValue = Math.max(...data.map(d => d[valueKey]));
const barWidth = chartWidth / data.length - 4;
let bars = '';
data.forEach((d, i) => {
const barHeight = (d[valueKey] / maxValue) * chartHeight;
const x = margin.left + i * (barWidth + 4);
const y = margin.top + chartHeight - barHeight;
bars += \`
<rect class="bar-chart-bar" x="\${x}" y="\${y}" width="\${barWidth}" height="\${barHeight}" fill="\${chartColors[i % chartColors.length]}"/>
<text class="bar-chart-value" x="\${x + barWidth/2}" y="\${y - 4}" text-anchor="middle">\${d[valueKey]}</text>
<text class="bar-chart-label" x="\${x + barWidth/2}" y="\${height - 5}" text-anchor="middle" transform="rotate(-45, \${x + barWidth/2}, \${height - 5})">\${(d[labelKey] || '').substring(0, 10)}</text>
\`;
});
svg.innerHTML = bars;
}
function renderPieChart(containerId, data, labelKey, valueKey) {
const svg = document.getElementById(containerId);
if (!data || data.length === 0) {
svg.innerHTML = '<text x="50%" y="50%" text-anchor="middle" fill="var(--text-muted)" font-size="11">No data</text>';
return;
}
const cx = 100;
const cy = 100;
const radius = 70;
const total = data.reduce((sum, d) => sum + d[valueKey], 0);
let startAngle = 0;
let paths = '';
let legend = '';
data.forEach((d, i) => {
const sliceAngle = (d[valueKey] / total) * 2 * Math.PI;
const endAngle = startAngle + sliceAngle;
const x1 = cx + radius * Math.cos(startAngle);
const y1 = cy + radius * Math.sin(startAngle);
const x2 = cx + radius * Math.cos(endAngle);
const y2 = cy + radius * Math.sin(endAngle);
const largeArc = sliceAngle > Math.PI ? 1 : 0;
const color = severityColors[d[labelKey]] || chartColors[i % chartColors.length];
paths += \`
<path class="pie-chart-segment" d="M \${cx} \${cy} L \${x1} \${y1} A \${radius} \${radius} 0 \${largeArc} 1 \${x2} \${y2} Z" fill="\${color}"/>
\`;
legend += \`
<div class="legend-item">
<div class="legend-dot" style="background: \${color}"></div>
<span>\${d[labelKey]}: \${d[valueKey]}</span>
</div>
\`;
startAngle = endAngle;
});
svg.innerHTML = paths;
// Add legend below
const container = svg.parentElement;
let legendEl = container.querySelector('.chart-legend');
if (!legendEl) {
legendEl = document.createElement('div');
legendEl.className = 'chart-legend';
container.appendChild(legendEl);
}
legendEl.innerHTML = legend;
}
function renderTimeChart(containerId, data) {
const svg = document.getElementById(containerId);
if (!data || data.length === 0) {
svg.innerHTML = '<text x="50%" y="50%" text-anchor="middle" fill="var(--text-muted)" font-size="11">No data</text>';
return;
}
const width = 600;
const height = 130;
const margin = { top: 10, right: 10, bottom: 20, left: 30 };
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
const maxValue = Math.max(...data.map(d => d.count), 1);
const barWidth = Math.min(chartWidth / data.length - 1, 20);
let bars = '';
data.forEach((d, i) => {
const barHeight = (d.count / maxValue) * chartHeight;
const x = margin.left + i * (barWidth + 1);
const y = margin.top + chartHeight - barHeight;
bars += \`<rect x="\${x}" y="\${y}" width="\${barWidth}" height="\${barHeight}" fill="var(--accent-cyan)" opacity="0.7"/>\`;
});
// Add axis labels
bars += \`<text x="\${margin.left}" y="\${height - 2}" fill="var(--text-muted)" font-size="9">\${data[0]?.hour || ''}</text>\`;
bars += \`<text x="\${width - margin.right}" y="\${height - 2}" fill="var(--text-muted)" font-size="9" text-anchor="end">\${data[data.length-1]?.hour || ''}</text>\`;
svg.innerHTML = bars;
}
// ========== Bug Tracking ==========
let selectedBugId = null;
async function loadBugsSummary() {
try {
const summary = await fetchJSON('/api/bugs/summary');
document.getElementById('bugs-total').textContent = summary.total || 0;
document.getElementById('bugs-open').textContent = summary.open || 0;
document.getElementById('bugs-in-progress').textContent = summary.in_progress || 0;
document.getElementById('bugs-resolved').textContent = summary.resolved || 0;
} catch (e) {
console.error('Error loading bugs summary:', e);
}
}
async function loadBugs() {
try {
const status = document.getElementById('bug-filter-status').value;
const severity = document.getElementById('bug-filter-severity').value;
let url = '/api/bugs?limit=100';
if (status) url += '&status=' + status;
if (severity) url += '&severity=' + severity;
const bugs = await fetchJSON(url);
const container = document.getElementById('bugs-list');
if (!bugs || bugs.length === 0) {
container.innerHTML = '<div style="color: var(--text-muted); text-align: center; padding: 40px;">No bugs found</div>';
return;
}
container.innerHTML = bugs.map(bug => \`
<div class="bug-item" onclick="showBugDetail('\${bug.id}')">
<div class="bug-severity \${bug.severity}"></div>
<span class="bug-status-badge \${bug.status}">\${bug.status.replace('_', ' ')}</span>
<span class="bug-message">\${escapeHtml(bug.message)}</span>
<span class="bug-meta">Phase \${bug.phase}</span>
<span class="bug-meta">\${formatTimeAgo(bug.detected_at)}</span>
</div>
\`).join('');
} catch (e) {
console.error('Error loading bugs:', e);
}
}
async function showBugDetail(bugId) {
try {
const bug = await fetchJSON('/api/bugs/' + bugId);
if (!bug) return;
selectedBugId = bugId;
document.getElementById('bug-detail-panel').style.display = 'flex';
document.getElementById('bug-detail-id').textContent = bug.id;
document.getElementById('bug-detail-content').innerHTML = \`
<div class="bug-detail-row">
<span class="bug-detail-label">Status:</span>
<span class="bug-status-badge \${bug.status}">\${bug.status.replace('_', ' ')}</span>
</div>
<div class="bug-detail-row">
<span class="bug-detail-label">Severity:</span>
<span class="bug-detail-value">\${bug.severity}</span>
</div>
<div class="bug-detail-row">
<span class="bug-detail-label">Type:</span>
<span class="bug-detail-value">\${bug.type}</span>
</div>
<div class="bug-detail-row">
<span class="bug-detail-label">Phase:</span>
<span class="bug-detail-value">\${bug.phase} - \${bug.phase_name}</span>
</div>
<div class="bug-detail-row">
<span class="bug-detail-label">Directory:</span>
<span class="bug-detail-value">\${bug.directory}</span>
</div>
<div class="bug-detail-row">
<span class="bug-detail-label">Detected:</span>
<span class="bug-detail-value">\${new Date(bug.detected_at).toLocaleString()}</span>
</div>
\${bug.assigned_to ? \`<div class="bug-detail-row"><span class="bug-detail-label">Assigned:</span><span class="bug-detail-value">\${bug.assigned_to}</span></div>\` : ''}
\${bug.resolution_notes ? \`<div class="bug-detail-row"><span class="bug-detail-label">Notes:</span><span class="bug-detail-value">\${escapeHtml(bug.resolution_notes)}</span></div>\` : ''}
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-color);">
<strong>Message:</strong><br/>
<span style="color: var(--text-primary);">\${escapeHtml(bug.message)}</span>
</div>
\`;
// Show appropriate action buttons based on status
let actions = '';
if (bug.status === 'open') {
actions = \`
<button onclick="updateBugStatus('\${bugId}', 'in_progress')">Start Working</button>
<button class="primary" onclick="updateBugStatus('\${bugId}', 'resolved')">Mark Resolved</button>
\`;
} else if (bug.status === 'in_progress') {
actions = \`
<button onclick="updateBugStatus('\${bugId}', 'open')">Reopen</button>
<button class="primary" onclick="updateBugStatus('\${bugId}', 'resolved')">Mark Resolved</button>
\`;
} else {
actions = \`
<button onclick="updateBugStatus('\${bugId}', 'open')">Reopen</button>
\`;
}
document.getElementById('bug-detail-actions').innerHTML = actions;
} catch (e) {
console.error('Error loading bug detail:', e);
}
}
function closeBugDetail() {
document.getElementById('bug-detail-panel').style.display = 'none';
selectedBugId = null;
}
async function updateBugStatus(bugId, newStatus) {
const notes = newStatus === 'resolved' ? prompt('Resolution notes (optional):') : null;
try {
const res = await fetch('/api/bugs/' + bugId, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus, notes: notes || undefined })
});
if (res.ok) {
await loadBugsSummary();
await loadBugs();
if (selectedBugId === bugId) {
await showBugDetail(bugId);
}
} else {
alert('Failed to update bug status');
}
} catch (e) {
console.error('Error updating bug:', e);
alert('Error updating bug status');
}
}
function showLogBugModal() {
const message = prompt('Bug description:');
if (!message) return;
const severity = prompt('Severity (critical/high/medium/low):', 'medium');
if (!['critical', 'high', 'medium', 'low'].includes(severity)) {
alert('Invalid severity');
return;
}
logNewBug(message, severity);
}
async function logNewBug(message, severity) {
try {
const res = await fetch('/api/bugs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, severity })
});
if (res.ok) {
await loadBugsSummary();
await loadBugs();
} else {
alert('Failed to log bug');
}
} catch (e) {
console.error('Error logging bug:', e);
alert('Error logging bug');
}
}
function formatTimeAgo(dateStr) {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 60) return diffMins + 'm ago';
if (diffHours < 24) return diffHours + 'h ago';
return diffDays + 'd ago';
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ========== Tier Progression ==========
async function loadTierProgression() {
try {
const [summary, promotions, definitions] = await Promise.all([
fetchJSON('/api/tiers/summary'),
fetchJSON('/api/tiers/promotions?limit=20'),
fetchJSON('/api/tiers/definitions')
]);
// Render tier cards
const cardsContainer = document.getElementById('tier-cards');
cardsContainer.innerHTML = definitions.map(def => {
const count = summary[\`T\${def.tier.replace('T', '')}\`] || summary[def.tier] || 0;
return \`
<div class="tier-card \${def.tier.toLowerCase()}">
<div class="tier-label">\${def.tier}</div>
<div class="tier-count">\${count}</div>
<div class="tier-name">\${def.name}</div>
</div>
\`;
}).join('');
// Render promotion history
const historyContainer = document.getElementById('tier-history-list');
if (!promotions || promotions.length === 0) {
historyContainer.innerHTML = '<div style="color: var(--text-muted); font-size: 11px;">No promotions yet</div>';
return;
}
historyContainer.innerHTML = promotions.map(p => \`
<div class="tier-history-item">
<span style="color: var(--text-primary); font-weight: 600;">\${p.agent_id}</span>
<span class="tier-badge t\${p.from_tier || 0}">T\${p.from_tier || 0}</span>
<span class="promotion-arrow">→</span>
<span class="tier-badge t\${p.to_tier || 0}">T\${p.to_tier || 0}</span>
<span style="color: var(--text-muted); margin-left: auto;">\${p.timestamp ? new Date(p.timestamp).toLocaleString() : ''}</span>
</div>
\`).join('');
} catch (e) {
console.error('Error loading tier progression:', e);
}
}
// Refresh All
async function refresh() {
await Promise.all([
loadPipelines(),
loadStats(),
loadHistory(),
loadApprovalQueue(),
loadOrchestration()
]);
if (selectedPipelineId) {
await loadLogs(selectedPipelineId);
}
// Refresh current tab data
if (currentTab !== 'pipelines' && tabDataLoaded[currentTab]) {
loadTabData(currentTab);
}
}
// Initialize
connectWebSocket();
setInterval(() => {
loadPipelines();
loadStats();
loadApprovalQueue();
loadOrchestration();
}, 5000);
</script>
</body>
</html>`;
}
// =============================================================================
// HTTP Server with WebSocket
// =============================================================================
const server = Bun.serve({
port: PORT,
async fetch(req, server) {
const url = new URL(req.url);
const path = url.pathname;
// WebSocket upgrade
if (path === "/ws") {
const upgraded = server.upgrade(req);
if (upgraded) return undefined;
return new Response("WebSocket upgrade failed", { status: 400 });
}
const headers = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
};
try {
// API Routes
if (path === "/api/agents") {
const agents = await getAgentStates();
return new Response(JSON.stringify(agents), { headers });
}
if (path === "/api/revocations") {
const limit = parseInt(url.searchParams.get("limit") || "50");
const revocations = await getRevocations(limit);
return new Response(JSON.stringify(revocations), { headers });
}
if (path === "/api/violations") {
const limit = parseInt(url.searchParams.get("limit") || "50");
const agentId = url.searchParams.get("agent_id");
let violations = await getViolations(limit);
if (agentId) {
violations = violations.filter((v: any) => v.agent_id === agentId);
}
return new Response(JSON.stringify(violations), { headers });
}
// Bug Tracking API
if (path === "/api/bugs" && req.method === "GET") {
const bugs = await getBugs(url.searchParams);
return new Response(JSON.stringify(bugs), { headers });
}
if (path === "/api/bugs/summary" && req.method === "GET") {
const summary = await getBugSummary();
return new Response(JSON.stringify(summary), { headers });
}
if (path.match(/^\/api\/bugs\/[^/]+$/) && req.method === "GET") {
const bugId = path.split("/").pop()!;
const bug = await getBug(bugId);
if (bug) {
return new Response(JSON.stringify(bug), { headers });
}
return new Response(JSON.stringify({ error: "Bug not found" }), { status: 404, headers });
}
if (path === "/api/bugs" && req.method === "POST") {
const body = await req.json() as {
message: string;
severity?: string;
type?: string;
phase?: number;
directory?: string;
details?: Record<string, any>;
};
if (!body.message) {
return new Response(JSON.stringify({ error: "message required" }), { status: 400, headers });
}
const bug = await logBug(body);
broadcastUpdate("bug_logged", bug);
return new Response(JSON.stringify(bug), { status: 201, headers });
}
if (path.match(/^\/api\/bugs\/[^/]+$/) && req.method === "PATCH") {
const bugId = path.split("/").pop()!;
const body = await req.json() as {
status?: string;
notes?: string;
assigned_to?: string;
};
const result = await updateBugStatus(bugId, body);
if (result.success) {
broadcastUpdate("bug_updated", { id: bugId, ...body });
return new Response(JSON.stringify(result), { headers });
}
return new Response(JSON.stringify(result), { status: 404, headers });
}
if (path === "/api/promotions") {
const limit = parseInt(url.searchParams.get("limit") || "20");
const promotions = await getPromotions(limit);
return new Response(JSON.stringify(promotions), { headers });
}
if (path === "/api/metrics") {
const metrics = await getAgentMetrics();
return new Response(JSON.stringify(metrics), { headers });
}
if (path === "/api/alerts") {
const limit = parseInt(url.searchParams.get("limit") || "20");
const alerts = await getAlerts(limit);
return new Response(JSON.stringify(alerts), { headers });
}
if (path === "/api/ledger") {
const limit = parseInt(url.searchParams.get("limit") || "50");
const actions = await getLedgerActions(limit);
return new Response(JSON.stringify(actions), { headers });
}
if (path === "/api/status") {
const status = await getSystemStatus();
return new Response(JSON.stringify(status), { headers });
}
// Orchestration APIs (Ledger Integration)
if (path === "/api/orchestration") {
const limit = parseInt(url.searchParams.get("limit") || "50");
const logs = await getOrchestrationLogs(limit);
return new Response(JSON.stringify(logs), { headers });
}
if (path === "/api/orchestration/summary") {
const summary = await getOrchestrationSummary();
return new Response(JSON.stringify(summary), { headers });
}
// Pipeline Control APIs
if (path === "/api/spawn" && req.method === "POST") {
const body = await req.json() as {
objective: string;
task_id?: string;
auto_continue?: boolean;
model?: string;
timeout?: number;
};
if (!body.objective) {
return new Response(JSON.stringify({ error: "objective required" }), { status: 400, headers });
}
const result = await spawnPipeline({
task_id: body.task_id || `task-${Date.now().toString(36)}`,
objective: body.objective,
spawn_diagnostic: true,
auto_continue: body.auto_continue ?? true, // Default to auto-continue enabled
model: body.model,
timeout: body.timeout,
});
return new Response(JSON.stringify(result), { headers });
}
// Continue pipeline to OpenRouter orchestration
if (path === "/api/pipeline/continue" && req.method === "POST") {
const body = await req.json() as {
pipeline_id: string;
model?: string;
timeout?: number;
};
if (!body.pipeline_id) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const result = await continueOrchestration(body.pipeline_id, body.model, body.timeout);
return new Response(JSON.stringify(result), { headers });
}
// Get orchestration status for a pipeline
if (path === "/api/pipeline/orchestration") {
const pipelineId = url.searchParams.get("pipeline_id");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const pipelineKey = `pipeline:${pipelineId}`;
const data = await redis.hGetAll(pipelineKey);
return new Response(JSON.stringify({
pipeline_id: pipelineId,
status: data.status,
orchestration_started_at: data.orchestration_started_at,
completed_at: data.completed_at,
model: data.model,
}), { headers });
}
if (path === "/api/active-pipelines") {
const pipelines = await getActivePipelines();
return new Response(JSON.stringify(pipelines), { headers });
}
if (path === "/api/pipeline/logs") {
const pipelineId = url.searchParams.get("pipeline_id");
const limit = parseInt(url.searchParams.get("limit") || "100");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const logs = await getPipelineLogs(pipelineId, limit);
return new Response(JSON.stringify(logs), { headers });
}
// Vault Token Management APIs
if (path === "/api/pipeline/token") {
const pipelineId = url.searchParams.get("pipeline_id");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const status = await getPipelineTokenStatus(pipelineId);
return new Response(JSON.stringify(status), { headers });
}
if (path === "/api/pipeline/token/revoke" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string; reason: string };
if (!body.pipeline_id || !body.reason) {
return new Response(JSON.stringify({ error: "pipeline_id and reason required" }), { status: 400, headers });
}
const success = await revokePipelineToken(body.pipeline_id, body.reason);
return new Response(JSON.stringify({ success, message: success ? "Token revoked" : "Failed to revoke token" }), { headers });
}
if (path === "/api/pipeline/token/renew" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string };
if (!body.pipeline_id) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const success = await renewPipelineToken(body.pipeline_id);
return new Response(JSON.stringify({ success, message: success ? "Token renewed" : "Failed to renew token" }), { headers });
}
// Error Budget & Observability APIs
if (path === "/api/pipeline/errors") {
const pipelineId = url.searchParams.get("pipeline_id");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const budget = await getErrorBudget(pipelineId);
return new Response(JSON.stringify(budget || { pipeline_id: pipelineId, total_errors: 0, errors_per_minute: 0, threshold_exceeded: false, error_types: {} }), { headers });
}
if (path === "/api/pipeline/errors/record" && req.method === "POST") {
const body = await req.json() as {
pipeline_id: string;
error_type: string;
severity: "low" | "medium" | "high" | "critical";
details: string;
};
if (!body.pipeline_id || !body.error_type || !body.severity) {
return new Response(JSON.stringify({ error: "pipeline_id, error_type, and severity required" }), { status: 400, headers });
}
const result = await recordError(body.pipeline_id, body.error_type, body.severity, body.details || "");
return new Response(JSON.stringify(result), { headers });
}
if (path === "/api/observability/handoff" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string };
if (!body.pipeline_id) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const report = await generateHandoffReport(body.pipeline_id);
return new Response(JSON.stringify(report), { headers });
}
if (path === "/api/observability/diagnostic" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string; error_type: string; details: string };
if (!body.pipeline_id || !body.error_type) {
return new Response(JSON.stringify({ error: "pipeline_id and error_type required" }), { status: 400, headers });
}
const diagnosticId = await spawnDiagnosticPipeline(body.pipeline_id, body.error_type, body.details || "");
return new Response(JSON.stringify({ success: true, diagnostic_pipeline_id: diagnosticId }), { headers });
}
if (path === "/api/pipeline/metrics") {
const pipelineId = url.searchParams.get("pipeline_id");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
// Get metrics from multi-agent coordination
const metricsKey = `metrics:${pipelineId}`;
const metricsData = await redis.hGetAll(metricsKey);
const errorBudget = await getErrorBudget(pipelineId);
const tokenStatus = await getPipelineTokenStatus(pipelineId);
return new Response(JSON.stringify({
pipeline_id: pipelineId,
coordination: metricsData,
error_budget: errorBudget,
token_status: tokenStatus
}), { headers });
}
// Agent Lifecycle Status API
if (path === "/api/agents/lifecycle") {
const pipelineId = url.searchParams.get("pipeline_id");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
// Get agents from pipeline
const pipelineKey = `pipeline:${pipelineId}`;
const agentsRaw = await redis.hGet(pipelineKey, "agents");
const agents = agentsRaw ? JSON.parse(agentsRaw) : [];
// Enrich with state from multi-agent coordination
const enrichedAgents = [];
for (const agent of agents) {
const stateKey = `agents:${pipelineId}`;
const stateData = await redis.hGet(stateKey, agent.type);
let state = null;
if (stateData) {
try { state = JSON.parse(stateData); } catch {}
}
enrichedAgents.push({
...agent,
lifecycle: determineAgentLifecycle(agent.status, state),
state: state
});
}
return new Response(JSON.stringify({ pipeline_id: pipelineId, agents: enrichedAgents }), { headers });
}
// Plan Execution APIs
if (path === "/api/plans") {
const pipelineId = url.searchParams.get("pipeline_id");
if (pipelineId) {
const plans = await getPlansForPipeline(pipelineId);
return new Response(JSON.stringify(plans), { headers });
}
// Get all plans
const keys = await redis.keys("plan:*");
const plans: StoredPlan[] = [];
for (const key of keys) {
const plan = await getPlan(key.replace("plan:", ""));
if (plan) plans.push(plan);
}
return new Response(JSON.stringify(plans), { headers });
}
if (path === "/api/plan" && req.method === "GET") {
const planId = url.searchParams.get("plan_id");
if (!planId) {
return new Response(JSON.stringify({ error: "plan_id required" }), { status: 400, headers });
}
const plan = await getPlan(planId);
if (!plan) {
return new Response(JSON.stringify({ error: "Plan not found" }), { status: 404, headers });
}
return new Response(JSON.stringify(plan), { headers });
}
if (path === "/api/plan/store" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string; plan: any };
if (!body.pipeline_id || !body.plan) {
return new Response(JSON.stringify({ error: "pipeline_id and plan required" }), { status: 400, headers });
}
const planId = await storePlan(body.pipeline_id, body.plan);
return new Response(JSON.stringify({ success: true, plan_id: planId }), { headers });
}
if (path === "/api/plan/execute" && req.method === "POST") {
try {
const body = await req.json() as { plan_id: string; dry_run?: boolean; tier?: number };
console.log("[API] /api/plan/execute body:", body);
if (!body.plan_id) {
return new Response(JSON.stringify({ error: "plan_id required" }), { status: 400, headers });
}
const result = await executePlan(body.plan_id, {
dryRun: body.dry_run ?? false,
tier: body.tier ?? 1,
});
return new Response(JSON.stringify(result), { headers });
} catch (e: any) {
console.error("[API] /api/plan/execute error:", e.message, e.stack);
return new Response(JSON.stringify({ error: e.message, stack: e.stack }), { status: 500, headers });
}
}
if (path === "/api/plan/verify" && req.method === "POST") {
try {
const body = await req.json() as { plan_id: string };
console.log("[API] /api/plan/verify body:", body);
if (!body.plan_id) {
return new Response(JSON.stringify({ error: "plan_id required" }), { status: 400, headers });
}
const result = await verifyPlan(body.plan_id);
return new Response(JSON.stringify(result), { headers });
} catch (e: any) {
console.error("[API] /api/plan/verify error:", e.message, e.stack);
return new Response(JSON.stringify({ error: e.message, stack: e.stack }), { status: 500, headers });
}
}
if (path === "/api/plan/package" && req.method === "POST") {
try {
const body = await req.json() as { plan_id: string };
console.log("[API] /api/plan/package body:", body);
if (!body.plan_id) {
return new Response(JSON.stringify({ error: "plan_id required" }), { status: 400, headers });
}
const result = await packagePlan(body.plan_id);
return new Response(JSON.stringify(result), { headers });
} catch (e: any) {
console.error("[API] /api/plan/package error:", e.message, e.stack);
return new Response(JSON.stringify({ error: e.message, stack: e.stack }), { status: 500, headers });
}
}
if (path === "/api/plan/report" && req.method === "POST") {
try {
const body = await req.json() as { plan_id: string };
console.log("[API] /api/plan/report body:", body);
if (!body.plan_id) {
return new Response(JSON.stringify({ error: "plan_id required" }), { status: 400, headers });
}
const result = await reportPlan(body.plan_id);
return new Response(JSON.stringify(result), { headers });
} catch (e: any) {
console.error("[API] /api/plan/report error:", e.message, e.stack);
return new Response(JSON.stringify({ error: e.message, stack: e.stack }), { status: 500, headers });
}
}
// Get report by plan_id or report_id
if (path === "/api/report/get") {
const planId = url.searchParams.get("plan_id");
const reportId = url.searchParams.get("report_id");
try {
let report = null;
if (reportId) {
// Fetch directly by report ID
const data = await redis.hGetAll(`report:${reportId}`);
if (data && data.report_id) {
report = {
...data,
phases_completed: JSON.parse(data.phases_completed || "[]"),
assumptions_validated: JSON.parse(data.assumptions_validated || "[]"),
dependencies_used: JSON.parse(data.dependencies_used || "[]"),
side_effects_produced: JSON.parse(data.side_effects_produced || "[]"),
next_actions: JSON.parse(data.next_actions || "[]"),
summary: {
title: data.plan_id,
outcome: data.outcome,
confidence: parseFloat(data.confidence || "0"),
execution_time_ms: parseInt(data.execution_time_ms || "0")
}
};
}
} else if (planId) {
// Get report_id from plan, then fetch report
const storedReportId = await redis.hGet(`plan:${planId}`, "report_id");
if (storedReportId) {
const data = await redis.hGetAll(`report:${storedReportId}`);
if (data && data.report_id) {
report = {
...data,
phases_completed: JSON.parse(data.phases_completed || "[]"),
assumptions_validated: JSON.parse(data.assumptions_validated || "[]"),
dependencies_used: JSON.parse(data.dependencies_used || "[]"),
side_effects_produced: JSON.parse(data.side_effects_produced || "[]"),
next_actions: JSON.parse(data.next_actions || "[]"),
summary: {
title: data.plan_id,
outcome: data.outcome,
confidence: parseFloat(data.confidence || "0"),
execution_time_ms: parseInt(data.execution_time_ms || "0")
}
};
}
}
}
if (report) {
return new Response(JSON.stringify(report), { headers });
} else {
return new Response(JSON.stringify({ error: "Report not found" }), { status: 404, headers });
}
} catch (e: any) {
return new Response(JSON.stringify({ error: e.message }), { status: 500, headers });
}
}
if (path === "/api/plan/execute-from-pipeline" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string; dry_run?: boolean; tier?: number };
if (!body.pipeline_id) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
// Get the plan associated with this pipeline
const pipelineKey = `pipeline:${body.pipeline_id}`;
const planId = await redis.hGet(pipelineKey, "plan_id");
if (!planId) {
return new Response(JSON.stringify({ error: "No plan found for this pipeline" }), { status: 404, headers });
}
const result = await executePlan(planId, {
dryRun: body.dry_run ?? false,
tier: body.tier ?? 1,
});
return new Response(JSON.stringify(result), { headers });
}
if (path === "/api/evidence") {
const evidenceId = url.searchParams.get("evidence_id");
if (!evidenceId) {
// List all evidence
const keys = await redis.keys("evidence:*");
const evidence: any[] = [];
for (const key of keys) {
const data = await redis.hGetAll(key);
if (data.evidence_id) {
evidence.push({
...data,
results: JSON.parse(data.results || "[]"),
});
}
}
return new Response(JSON.stringify(evidence), { headers });
}
const data = await redis.hGetAll(`evidence:${evidenceId}`);
if (!data.evidence_id) {
return new Response(JSON.stringify({ error: "Evidence not found" }), { status: 404, headers });
}
return new Response(JSON.stringify({
...data,
results: JSON.parse(data.results || "[]"),
}), { headers });
}
// Approval Workflow APIs
if (path === "/api/approval/queue") {
const queue = await getApprovalQueue();
return new Response(JSON.stringify(queue), { headers });
}
if (path === "/api/approval/approve" && req.method === "POST") {
const body = await req.json() as {
request_id: string;
reviewer: string;
notes?: string;
tier?: number;
};
if (!body.request_id || !body.reviewer) {
return new Response(JSON.stringify({ error: "request_id and reviewer required" }), { status: 400, headers });
}
const result = await approveRequest(body.request_id, body.reviewer, body.notes || "", body.tier || 1);
return new Response(JSON.stringify(result), { headers });
}
if (path === "/api/approval/reject" && req.method === "POST") {
const body = await req.json() as {
request_id: string;
reviewer: string;
reason: string;
};
if (!body.request_id || !body.reviewer || !body.reason) {
return new Response(JSON.stringify({ error: "request_id, reviewer, and reason required" }), { status: 400, headers });
}
const result = await rejectRequest(body.request_id, body.reviewer, body.reason);
return new Response(JSON.stringify(result), { headers });
}
// Auto-Execution Config APIs
if (path === "/api/config/auto-exec") {
if (req.method === "GET") {
return new Response(JSON.stringify(await getAutoExecConfig()), { headers });
}
if (req.method === "POST") {
const updates = await req.json();
const config = await updateAutoExecConfig(updates);
return new Response(JSON.stringify(config), { headers });
}
}
if (path === "/api/auto-exec/queue") {
const queue = await redis.lRange("auto_exec_queue", 0, -1);
return new Response(JSON.stringify(queue.map(q => JSON.parse(q))), { headers });
}
// Legacy Pipeline APIs
if (path === "/api/pipelines") {
const pipelines = await getPipelines();
return new Response(JSON.stringify(pipelines), { headers });
}
if (path === "/api/pipeline/messages") {
const taskId = url.searchParams.get("task_id");
const limit = parseInt(url.searchParams.get("limit") || "50");
if (!taskId) {
return new Response(JSON.stringify({ error: "task_id required" }), { status: 400, headers });
}
const messages = await getMessageLog(taskId, limit);
return new Response(JSON.stringify(messages), { headers });
}
if (path === "/api/pipeline/history") {
const taskId = url.searchParams.get("task_id");
if (!taskId) {
return new Response(JSON.stringify({ error: "task_id required" }), { status: 400, headers });
}
const history = await getTaskHistory(taskId);
return new Response(JSON.stringify(history), { headers });
}
if (path === "/api/pipeline/solutions") {
const taskId = url.searchParams.get("task_id");
if (!taskId) {
return new Response(JSON.stringify({ error: "task_id required" }), { status: 400, headers });
}
const solutions = await getBlackboardSolutions(taskId);
return new Response(JSON.stringify(solutions), { headers });
}
// =========================================================================
// New UI Tab APIs
// =========================================================================
// Checkpoint APIs
if (path === "/api/checkpoint/list") {
const limit = parseInt(url.searchParams.get("limit") || "20");
const checkpoints = await getCheckpointList(limit);
return new Response(JSON.stringify(checkpoints), { headers });
}
if (path === "/api/checkpoint/get") {
const id = url.searchParams.get("id");
const detail = await getCheckpointDetail(id || undefined);
return new Response(JSON.stringify(detail), { headers });
}
if (path === "/api/checkpoint/diff") {
const fromId = url.searchParams.get("from");
const toId = url.searchParams.get("to");
const diff = await getCheckpointDiff(fromId || undefined, toId || undefined);
return new Response(JSON.stringify(diff), { headers });
}
if (path === "/api/checkpoint/summary") {
const level = url.searchParams.get("level") || "compact";
const summary = await getCheckpointSummary(level);
return new Response(JSON.stringify({ summary }), { headers });
}
if (path === "/api/checkpoint/create" && req.method === "POST") {
const body = await req.json() as { notes?: string };
const result = await createCheckpointNow(body.notes);
broadcastUpdate("checkpoint_created", result);
return new Response(JSON.stringify(result), { headers });
}
if (path === "/api/checkpoint/report") {
const report = await getCheckpointReport();
return new Response(JSON.stringify(report), { headers });
}
if (path === "/api/checkpoint/timeline") {
const limit = parseInt(url.searchParams.get("limit") || "10");
const timeline = await getCheckpointTimeline(limit);
return new Response(JSON.stringify(timeline), { headers });
}
// Memory APIs
if (path === "/api/memory/list") {
const type = url.searchParams.get("type");
const limit = parseInt(url.searchParams.get("limit") || "50");
const entries = await getMemoryList(type || undefined, limit);
return new Response(JSON.stringify(entries), { headers });
}
if (path === "/api/memory/get") {
const id = url.searchParams.get("id");
if (!id) {
return new Response(JSON.stringify({ error: "id required" }), { status: 400, headers });
}
const entry = await getMemoryEntry(id);
return new Response(JSON.stringify(entry), { headers });
}
if (path === "/api/memory/search") {
const query = url.searchParams.get("q");
if (!query) {
return new Response(JSON.stringify({ error: "q required" }), { status: 400, headers });
}
const results = await searchMemory(query);
return new Response(JSON.stringify(results), { headers });
}
if (path === "/api/memory/stats") {
const stats = await getMemoryStats();
return new Response(JSON.stringify(stats), { headers });
}
// Status Grid API
if (path === "/api/status/grid") {
const grid = await getStatusGrid();
return new Response(JSON.stringify(grid), { headers });
}
// Integration APIs
if (path === "/api/integrations/status") {
const status = await getIntegrationStatus();
return new Response(JSON.stringify(status), { headers });
}
if (path === "/api/integrations/test" && req.method === "POST") {
const body = await req.json() as { name: string };
if (!body.name) {
return new Response(JSON.stringify({ error: "name required" }), { status: 400, headers });
}
const result = await testIntegration(body.name);
return new Response(JSON.stringify(result), { headers });
}
// Analytics APIs
if (path === "/api/analytics/violations/by-type") {
const data = await getViolationsByType();
return new Response(JSON.stringify(data), { headers });
}
if (path === "/api/analytics/violations/by-severity") {
const data = await getViolationsBySeverity();
return new Response(JSON.stringify(data), { headers });
}
if (path === "/api/analytics/violations/by-time") {
const days = parseInt(url.searchParams.get("days") || "7");
const data = await getViolationsByTime(days);
return new Response(JSON.stringify(data), { headers });
}
if (path === "/api/analytics/summary") {
const summary = await getAnalyticsSummary();
return new Response(JSON.stringify(summary), { headers });
}
// Tier APIs
if (path === "/api/tiers/summary") {
const summary = await getTierSummary();
return new Response(JSON.stringify(summary), { headers });
}
if (path === "/api/tiers/promotions") {
const limit = parseInt(url.searchParams.get("limit") || "20");
const promotions = await getTierPromotions(limit);
return new Response(JSON.stringify(promotions), { headers });
}
if (path === "/api/tiers/definitions") {
const definitions = await getTierDefinitions();
return new Response(JSON.stringify(definitions), { headers });
}
// HTML Dashboard
if (path === "/" || path === "/dashboard") {
return new Response(renderDashboard(), {
headers: { "Content-Type": "text/html" },
});
}
return new Response("Not Found", { status: 404 });
} catch (error: any) {
console.error("API Error:", error.message);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers,
});
}
},
websocket: {
open(ws) {
wsClients.add(ws);
console.log(`[WS] Client connected (${wsClients.size} total)`);
ws.send(JSON.stringify({ type: "connected", timestamp: new Date().toISOString() }));
},
message(ws, message) {
// Handle ping/pong
if (message === "ping") {
ws.send("pong");
}
},
close(ws) {
wsClients.delete(ws);
console.log(`[WS] Client disconnected (${wsClients.size} total)`);
},
},
});
// =============================================================================
// Main
// =============================================================================
async function main() {
console.log("\n" + "=".repeat(50));
console.log("Agent Governance Dashboard");
console.log("=".repeat(50));
await connectRedis();
// Start Vault token renewal loop for active pipelines
runTokenRenewalLoop();
console.log("[VAULT] Token renewal loop started");
console.log(`\n[SERVER] Dashboard running at http://localhost:${PORT}`);
console.log("[SERVER] WebSocket endpoint: ws://localhost:" + PORT + "/ws");
console.log("[SERVER] Press Ctrl+C to stop\n");
// Broadcast refresh periodically
setInterval(() => {
broadcastUpdate("refresh", {});
}, 3000);
}
main().catch(console.error);