1992 lines
74 KiB
Bash
Executable File
1992 lines
74 KiB
Bash
Executable File
#!/bin/bash
|
|
#===============================================================================
|
|
# Loki Mode - Autonomous Runner
|
|
# Single script that handles prerequisites, setup, and autonomous execution
|
|
#
|
|
# Usage:
|
|
# ./autonomy/run.sh [PRD_PATH]
|
|
# ./autonomy/run.sh ./docs/requirements.md
|
|
# ./autonomy/run.sh # Interactive mode
|
|
#
|
|
# Environment Variables:
|
|
# LOKI_MAX_RETRIES - Max retry attempts (default: 50)
|
|
# LOKI_BASE_WAIT - Base wait time in seconds (default: 60)
|
|
# LOKI_MAX_WAIT - Max wait time in seconds (default: 3600)
|
|
# LOKI_SKIP_PREREQS - Skip prerequisite checks (default: false)
|
|
# LOKI_DASHBOARD - Enable web dashboard (default: true)
|
|
# LOKI_DASHBOARD_PORT - Dashboard port (default: 57374)
|
|
#
|
|
# Resource Monitoring (prevents system overload):
|
|
# LOKI_RESOURCE_CHECK_INTERVAL - Check resources every N seconds (default: 300 = 5min)
|
|
# LOKI_RESOURCE_CPU_THRESHOLD - CPU % threshold to warn (default: 80)
|
|
# LOKI_RESOURCE_MEM_THRESHOLD - Memory % threshold to warn (default: 80)
|
|
#
|
|
# Security & Autonomy Controls (Enterprise):
|
|
# LOKI_STAGED_AUTONOMY - Require approval before execution (default: false)
|
|
# LOKI_AUDIT_LOG - Enable audit logging (default: false)
|
|
# LOKI_MAX_PARALLEL_AGENTS - Limit concurrent agent spawning (default: 10)
|
|
# LOKI_SANDBOX_MODE - Run in sandboxed container (default: false, requires Docker)
|
|
# LOKI_ALLOWED_PATHS - Comma-separated paths agents can modify (default: all)
|
|
# LOKI_BLOCKED_COMMANDS - Comma-separated blocked shell commands (default: rm -rf /)
|
|
#
|
|
# SDLC Phase Controls (all enabled by default, set to 'false' to skip):
|
|
# LOKI_PHASE_UNIT_TESTS - Run unit tests (default: true)
|
|
# LOKI_PHASE_API_TESTS - Functional API testing (default: true)
|
|
# LOKI_PHASE_E2E_TESTS - E2E/UI testing with Playwright (default: true)
|
|
# LOKI_PHASE_SECURITY - Security scanning OWASP/auth (default: true)
|
|
# LOKI_PHASE_INTEGRATION - Integration tests SAML/OIDC/SSO (default: true)
|
|
# LOKI_PHASE_CODE_REVIEW - 3-reviewer parallel code review (default: true)
|
|
# LOKI_PHASE_WEB_RESEARCH - Competitor/feature gap research (default: true)
|
|
# LOKI_PHASE_PERFORMANCE - Load/performance testing (default: true)
|
|
# LOKI_PHASE_ACCESSIBILITY - WCAG compliance testing (default: true)
|
|
# LOKI_PHASE_REGRESSION - Regression testing (default: true)
|
|
# LOKI_PHASE_UAT - UAT simulation (default: true)
|
|
#
|
|
# Autonomous Loop Controls (Ralph Wiggum Mode):
|
|
# LOKI_COMPLETION_PROMISE - EXPLICIT stop condition text (default: none - runs forever)
|
|
# Example: "ALL TESTS PASSING 100%"
|
|
# Only stops when Claude outputs this EXACT text
|
|
# LOKI_MAX_ITERATIONS - Max loop iterations before exit (default: 1000)
|
|
# LOKI_PERPETUAL_MODE - Ignore ALL completion signals (default: false)
|
|
# Set to 'true' for truly infinite operation
|
|
#===============================================================================
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
#===============================================================================
|
|
# Self-Copy Protection
|
|
# Bash reads scripts incrementally, so editing a running script corrupts execution.
|
|
# Solution: Copy ourselves to /tmp and run from there. The original can be safely edited.
|
|
#===============================================================================
|
|
if [[ -z "${LOKI_RUNNING_FROM_TEMP:-}" ]]; then
|
|
TEMP_SCRIPT="/tmp/loki-run-$$.sh"
|
|
cp "${BASH_SOURCE[0]}" "$TEMP_SCRIPT"
|
|
chmod +x "$TEMP_SCRIPT"
|
|
export LOKI_RUNNING_FROM_TEMP=1
|
|
export LOKI_ORIGINAL_SCRIPT_DIR="$SCRIPT_DIR"
|
|
export LOKI_ORIGINAL_PROJECT_DIR="$PROJECT_DIR"
|
|
exec "$TEMP_SCRIPT" "$@"
|
|
fi
|
|
|
|
# Restore original paths when running from temp
|
|
SCRIPT_DIR="${LOKI_ORIGINAL_SCRIPT_DIR:-$SCRIPT_DIR}"
|
|
PROJECT_DIR="${LOKI_ORIGINAL_PROJECT_DIR:-$PROJECT_DIR}"
|
|
|
|
# Clean up temp script on exit
|
|
trap 'rm -f "${BASH_SOURCE[0]}" 2>/dev/null' EXIT
|
|
|
|
# Configuration
|
|
MAX_RETRIES=${LOKI_MAX_RETRIES:-50}
|
|
BASE_WAIT=${LOKI_BASE_WAIT:-60}
|
|
MAX_WAIT=${LOKI_MAX_WAIT:-3600}
|
|
SKIP_PREREQS=${LOKI_SKIP_PREREQS:-false}
|
|
ENABLE_DASHBOARD=${LOKI_DASHBOARD:-true}
|
|
DASHBOARD_PORT=${LOKI_DASHBOARD_PORT:-57374}
|
|
RESOURCE_CHECK_INTERVAL=${LOKI_RESOURCE_CHECK_INTERVAL:-300} # Check every 5 minutes
|
|
RESOURCE_CPU_THRESHOLD=${LOKI_RESOURCE_CPU_THRESHOLD:-80} # CPU % threshold
|
|
RESOURCE_MEM_THRESHOLD=${LOKI_RESOURCE_MEM_THRESHOLD:-80} # Memory % threshold
|
|
|
|
# Security & Autonomy Controls
|
|
STAGED_AUTONOMY=${LOKI_STAGED_AUTONOMY:-false} # Require plan approval
|
|
AUDIT_LOG_ENABLED=${LOKI_AUDIT_LOG:-false} # Enable audit logging
|
|
MAX_PARALLEL_AGENTS=${LOKI_MAX_PARALLEL_AGENTS:-10} # Limit concurrent agents
|
|
SANDBOX_MODE=${LOKI_SANDBOX_MODE:-false} # Docker sandbox mode
|
|
ALLOWED_PATHS=${LOKI_ALLOWED_PATHS:-""} # Empty = all paths allowed
|
|
BLOCKED_COMMANDS=${LOKI_BLOCKED_COMMANDS:-"rm -rf /,dd if=,mkfs,:(){ :|:& };:"}
|
|
|
|
STATUS_MONITOR_PID=""
|
|
DASHBOARD_PID=""
|
|
RESOURCE_MONITOR_PID=""
|
|
|
|
# SDLC Phase Controls (all enabled by default)
|
|
PHASE_UNIT_TESTS=${LOKI_PHASE_UNIT_TESTS:-true}
|
|
PHASE_API_TESTS=${LOKI_PHASE_API_TESTS:-true}
|
|
PHASE_E2E_TESTS=${LOKI_PHASE_E2E_TESTS:-true}
|
|
PHASE_SECURITY=${LOKI_PHASE_SECURITY:-true}
|
|
PHASE_INTEGRATION=${LOKI_PHASE_INTEGRATION:-true}
|
|
PHASE_CODE_REVIEW=${LOKI_PHASE_CODE_REVIEW:-true}
|
|
PHASE_WEB_RESEARCH=${LOKI_PHASE_WEB_RESEARCH:-true}
|
|
PHASE_PERFORMANCE=${LOKI_PHASE_PERFORMANCE:-true}
|
|
PHASE_ACCESSIBILITY=${LOKI_PHASE_ACCESSIBILITY:-true}
|
|
PHASE_REGRESSION=${LOKI_PHASE_REGRESSION:-true}
|
|
PHASE_UAT=${LOKI_PHASE_UAT:-true}
|
|
|
|
# Autonomous Loop Controls (Ralph Wiggum Mode)
|
|
# Default: No auto-completion - runs until max iterations or explicit promise
|
|
COMPLETION_PROMISE=${LOKI_COMPLETION_PROMISE:-""}
|
|
MAX_ITERATIONS=${LOKI_MAX_ITERATIONS:-1000}
|
|
ITERATION_COUNT=0
|
|
# Perpetual mode: never stop unless max iterations (ignores all completion signals)
|
|
PERPETUAL_MODE=${LOKI_PERPETUAL_MODE:-false}
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m'
|
|
|
|
#===============================================================================
|
|
# Logging Functions
|
|
#===============================================================================
|
|
|
|
log_header() {
|
|
echo ""
|
|
echo -e "${BLUE}╔════════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${BLUE}║${NC} ${BOLD}$1${NC}"
|
|
echo -e "${BLUE}╚════════════════════════════════════════════════════════════════╝${NC}"
|
|
}
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
log_warning() { log_warn "$@"; } # Alias for backwards compatibility
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
|
log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
|
|
|
#===============================================================================
|
|
# Prerequisites Check
|
|
#===============================================================================
|
|
|
|
check_prerequisites() {
|
|
log_header "Checking Prerequisites"
|
|
|
|
local missing=()
|
|
|
|
# Check Claude Code CLI
|
|
log_step "Checking Claude Code CLI..."
|
|
if command -v claude &> /dev/null; then
|
|
local version=$(claude --version 2>/dev/null | head -1 || echo "unknown")
|
|
log_info "Claude Code CLI: $version"
|
|
else
|
|
missing+=("claude")
|
|
log_error "Claude Code CLI not found"
|
|
log_info "Install: https://claude.ai/code or npm install -g @anthropic-ai/claude-code"
|
|
fi
|
|
|
|
# Check Python 3
|
|
log_step "Checking Python 3..."
|
|
if command -v python3 &> /dev/null; then
|
|
local py_version=$(python3 --version 2>&1)
|
|
log_info "Python: $py_version"
|
|
else
|
|
missing+=("python3")
|
|
log_error "Python 3 not found"
|
|
fi
|
|
|
|
# Check Git
|
|
log_step "Checking Git..."
|
|
if command -v git &> /dev/null; then
|
|
local git_version=$(git --version)
|
|
log_info "Git: $git_version"
|
|
else
|
|
missing+=("git")
|
|
log_error "Git not found"
|
|
fi
|
|
|
|
# Check Node.js (optional but recommended)
|
|
log_step "Checking Node.js (optional)..."
|
|
if command -v node &> /dev/null; then
|
|
local node_version=$(node --version)
|
|
log_info "Node.js: $node_version"
|
|
else
|
|
log_warn "Node.js not found (optional, needed for some builds)"
|
|
fi
|
|
|
|
# Check npm (optional)
|
|
if command -v npm &> /dev/null; then
|
|
local npm_version=$(npm --version)
|
|
log_info "npm: $npm_version"
|
|
fi
|
|
|
|
# Check curl (for web fetches)
|
|
log_step "Checking curl..."
|
|
if command -v curl &> /dev/null; then
|
|
log_info "curl: available"
|
|
else
|
|
missing+=("curl")
|
|
log_error "curl not found"
|
|
fi
|
|
|
|
# Check jq (optional but helpful)
|
|
log_step "Checking jq (optional)..."
|
|
if command -v jq &> /dev/null; then
|
|
log_info "jq: available"
|
|
else
|
|
log_warn "jq not found (optional, for JSON parsing)"
|
|
fi
|
|
|
|
# Summary
|
|
echo ""
|
|
if [ ${#missing[@]} -gt 0 ]; then
|
|
log_error "Missing required tools: ${missing[*]}"
|
|
log_info "Please install the missing tools and try again."
|
|
return 1
|
|
else
|
|
log_info "All required prerequisites are installed!"
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
#===============================================================================
|
|
# Skill Installation Check
|
|
#===============================================================================
|
|
|
|
check_skill_installed() {
|
|
log_header "Checking Loki Mode Skill"
|
|
|
|
local skill_locations=(
|
|
"$HOME/.claude/skills/loki-mode/SKILL.md"
|
|
".claude/skills/loki-mode/SKILL.md"
|
|
"$PROJECT_DIR/SKILL.md"
|
|
)
|
|
|
|
for loc in "${skill_locations[@]}"; do
|
|
if [ -f "$loc" ]; then
|
|
log_info "Skill found: $loc"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
log_warn "Loki Mode skill not found in standard locations"
|
|
log_info "The skill will be used from: $PROJECT_DIR/SKILL.md"
|
|
|
|
if [ -f "$PROJECT_DIR/SKILL.md" ]; then
|
|
log_info "Using skill from project directory"
|
|
return 0
|
|
else
|
|
log_error "SKILL.md not found!"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
#===============================================================================
|
|
# Initialize Loki Directory
|
|
#===============================================================================
|
|
|
|
init_loki_dir() {
|
|
log_header "Initializing Loki Mode Directory"
|
|
|
|
mkdir -p .loki/{state,queue,messages,logs,config,prompts,artifacts,scripts}
|
|
mkdir -p .loki/queue
|
|
mkdir -p .loki/state/checkpoints
|
|
mkdir -p .loki/artifacts/{releases,reports,backups}
|
|
mkdir -p .loki/memory/{ledgers,handoffs,learnings,episodic,semantic,skills}
|
|
mkdir -p .loki/metrics/{efficiency,rewards}
|
|
mkdir -p .loki/rules
|
|
mkdir -p .loki/signals
|
|
|
|
# Initialize queue files if they don't exist
|
|
for queue in pending in-progress completed failed dead-letter; do
|
|
if [ ! -f ".loki/queue/${queue}.json" ]; then
|
|
echo "[]" > ".loki/queue/${queue}.json"
|
|
fi
|
|
done
|
|
|
|
# Initialize orchestrator state if it doesn't exist
|
|
if [ ! -f ".loki/state/orchestrator.json" ]; then
|
|
cat > ".loki/state/orchestrator.json" << EOF
|
|
{
|
|
"version": "$(cat "$PROJECT_DIR/VERSION" 2>/dev/null || echo "2.2.0")",
|
|
"currentPhase": "BOOTSTRAP",
|
|
"startedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"agents": {},
|
|
"metrics": {
|
|
"tasksCompleted": 0,
|
|
"tasksFailed": 0,
|
|
"retries": 0
|
|
}
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
log_info "Loki directory initialized: .loki/"
|
|
}
|
|
|
|
#===============================================================================
|
|
# Task Status Monitor
|
|
#===============================================================================
|
|
|
|
update_status_file() {
|
|
# Create a human-readable status file
|
|
local status_file=".loki/STATUS.txt"
|
|
|
|
# Get current phase
|
|
local current_phase="UNKNOWN"
|
|
if [ -f ".loki/state/orchestrator.json" ]; then
|
|
current_phase=$(python3 -c "import json; print(json.load(open('.loki/state/orchestrator.json')).get('currentPhase', 'UNKNOWN'))" 2>/dev/null || echo "UNKNOWN")
|
|
fi
|
|
|
|
# Count tasks in each queue
|
|
local pending=0 in_progress=0 completed=0 failed=0
|
|
[ -f ".loki/queue/pending.json" ] && pending=$(python3 -c "import json; print(len(json.load(open('.loki/queue/pending.json'))))" 2>/dev/null || echo "0")
|
|
[ -f ".loki/queue/in-progress.json" ] && in_progress=$(python3 -c "import json; print(len(json.load(open('.loki/queue/in-progress.json'))))" 2>/dev/null || echo "0")
|
|
[ -f ".loki/queue/completed.json" ] && completed=$(python3 -c "import json; print(len(json.load(open('.loki/queue/completed.json'))))" 2>/dev/null || echo "0")
|
|
[ -f ".loki/queue/failed.json" ] && failed=$(python3 -c "import json; print(len(json.load(open('.loki/queue/failed.json'))))" 2>/dev/null || echo "0")
|
|
|
|
cat > "$status_file" << EOF
|
|
╔════════════════════════════════════════════════════════════════╗
|
|
║ LOKI MODE STATUS ║
|
|
╚════════════════════════════════════════════════════════════════╝
|
|
|
|
Updated: $(date)
|
|
|
|
Phase: $current_phase
|
|
|
|
Tasks:
|
|
├─ Pending: $pending
|
|
├─ In Progress: $in_progress
|
|
├─ Completed: $completed
|
|
└─ Failed: $failed
|
|
|
|
Monitor: watch -n 2 cat .loki/STATUS.txt
|
|
EOF
|
|
}
|
|
|
|
start_status_monitor() {
|
|
log_step "Starting status monitor..."
|
|
|
|
# Initial update
|
|
update_status_file
|
|
update_agents_state
|
|
|
|
# Background update loop
|
|
(
|
|
while true; do
|
|
update_status_file
|
|
update_agents_state
|
|
sleep 5
|
|
done
|
|
) &
|
|
STATUS_MONITOR_PID=$!
|
|
|
|
log_info "Status monitor started"
|
|
log_info "Monitor progress: ${CYAN}watch -n 2 cat .loki/STATUS.txt${NC}"
|
|
}
|
|
|
|
stop_status_monitor() {
|
|
if [ -n "$STATUS_MONITOR_PID" ]; then
|
|
kill "$STATUS_MONITOR_PID" 2>/dev/null || true
|
|
wait "$STATUS_MONITOR_PID" 2>/dev/null || true
|
|
fi
|
|
stop_resource_monitor
|
|
}
|
|
|
|
#===============================================================================
|
|
# Web Dashboard
|
|
#===============================================================================
|
|
|
|
generate_dashboard() {
|
|
# Generate HTML dashboard with Anthropic design language + Agent Monitoring
|
|
cat > .loki/dashboard/index.html << 'DASHBOARD_HTML'
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Loki Mode Dashboard</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: 'Söhne', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #FAF9F6;
|
|
color: #1A1A1A;
|
|
padding: 24px;
|
|
min-height: 100vh;
|
|
}
|
|
.header {
|
|
text-align: center;
|
|
padding: 32px 20px;
|
|
margin-bottom: 32px;
|
|
}
|
|
.header h1 {
|
|
color: #D97757;
|
|
font-size: 28px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.5px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.header .subtitle {
|
|
color: #666;
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
}
|
|
.header .phase {
|
|
display: inline-block;
|
|
margin-top: 16px;
|
|
padding: 8px 16px;
|
|
background: #FFF;
|
|
border: 1px solid #E5E3DE;
|
|
border-radius: 20px;
|
|
font-size: 13px;
|
|
color: #1A1A1A;
|
|
font-weight: 500;
|
|
}
|
|
.stats {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
margin-bottom: 40px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.stat {
|
|
background: #FFF;
|
|
border: 1px solid #E5E3DE;
|
|
border-radius: 12px;
|
|
padding: 20px 32px;
|
|
text-align: center;
|
|
min-width: 140px;
|
|
transition: box-shadow 0.2s ease;
|
|
}
|
|
.stat:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
|
|
.stat .number { font-size: 36px; font-weight: 600; margin-bottom: 4px; }
|
|
.stat .label { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.stat.pending .number { color: #D97757; }
|
|
.stat.progress .number { color: #5B8DEF; }
|
|
.stat.completed .number { color: #2E9E6E; }
|
|
.stat.failed .number { color: #D44F4F; }
|
|
.stat.agents .number { color: #9B6DD6; }
|
|
.section-header {
|
|
text-align: center;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #666;
|
|
margin: 40px 0 20px 0;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
.agents-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 16px;
|
|
max-width: 1400px;
|
|
margin: 0 auto 40px auto;
|
|
}
|
|
.agent-card {
|
|
background: #FFF;
|
|
border: 1px solid #E5E3DE;
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
}
|
|
.agent-card:hover {
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
|
border-color: #9B6DD6;
|
|
}
|
|
.agent-card .agent-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 12px;
|
|
}
|
|
.agent-card .agent-id {
|
|
font-size: 11px;
|
|
color: #999;
|
|
font-family: monospace;
|
|
}
|
|
.agent-card .model-badge {
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.agent-card .model-badge.sonnet {
|
|
background: #E8F0FD;
|
|
color: #5B8DEF;
|
|
}
|
|
.agent-card .model-badge.haiku {
|
|
background: #FFF4E6;
|
|
color: #F59E0B;
|
|
}
|
|
.agent-card .model-badge.opus {
|
|
background: #F3E8FF;
|
|
color: #9B6DD6;
|
|
}
|
|
.agent-card .agent-type {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #1A1A1A;
|
|
margin-bottom: 8px;
|
|
}
|
|
.agent-card .agent-status {
|
|
display: inline-block;
|
|
padding: 3px 8px;
|
|
border-radius: 4px;
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
margin-bottom: 12px;
|
|
}
|
|
.agent-card .agent-status.active {
|
|
background: #E6F5EE;
|
|
color: #2E9E6E;
|
|
}
|
|
.agent-card .agent-status.completed {
|
|
background: #F0EFEA;
|
|
color: #666;
|
|
}
|
|
.agent-card .agent-work {
|
|
font-size: 12px;
|
|
color: #666;
|
|
line-height: 1.5;
|
|
margin-bottom: 8px;
|
|
}
|
|
.agent-card .agent-meta {
|
|
display: flex;
|
|
gap: 12px;
|
|
font-size: 11px;
|
|
color: #999;
|
|
margin-top: 8px;
|
|
padding-top: 8px;
|
|
border-top: 1px solid #F0EFEA;
|
|
}
|
|
.agent-card .agent-meta span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.columns {
|
|
display: flex;
|
|
gap: 20px;
|
|
overflow-x: auto;
|
|
padding-bottom: 24px;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
.column {
|
|
flex: 1;
|
|
min-width: 300px;
|
|
max-width: 350px;
|
|
background: #FFF;
|
|
border: 1px solid #E5E3DE;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
.column h2 {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #666;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.column h2 .count {
|
|
background: #F0EFEA;
|
|
padding: 3px 10px;
|
|
border-radius: 12px;
|
|
font-size: 11px;
|
|
color: #1A1A1A;
|
|
}
|
|
.column.pending h2 .count { background: #FCEEE8; color: #D97757; }
|
|
.column.progress h2 .count { background: #E8F0FD; color: #5B8DEF; }
|
|
.column.completed h2 .count { background: #E6F5EE; color: #2E9E6E; }
|
|
.column.failed h2 .count { background: #FCE8E8; color: #D44F4F; }
|
|
.task {
|
|
background: #FAF9F6;
|
|
border: 1px solid #E5E3DE;
|
|
border-radius: 8px;
|
|
padding: 14px;
|
|
margin-bottom: 12px;
|
|
transition: border-color 0.2s ease;
|
|
}
|
|
.task:hover { border-color: #D97757; }
|
|
.task .id { font-size: 10px; color: #999; margin-bottom: 6px; font-family: monospace; }
|
|
.task .type {
|
|
display: inline-block;
|
|
background: #FCEEE8;
|
|
color: #D97757;
|
|
padding: 3px 10px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
margin-bottom: 8px;
|
|
}
|
|
.task .title { font-size: 13px; color: #1A1A1A; line-height: 1.5; }
|
|
.task .error {
|
|
font-size: 11px;
|
|
color: #D44F4F;
|
|
margin-top: 10px;
|
|
padding: 10px;
|
|
background: #FCE8E8;
|
|
border-radius: 6px;
|
|
font-family: monospace;
|
|
}
|
|
.refresh {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
background: #D97757;
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: background 0.2s ease;
|
|
box-shadow: 0 4px 12px rgba(217, 119, 87, 0.3);
|
|
}
|
|
.refresh:hover { background: #C56747; }
|
|
.updated {
|
|
text-align: center;
|
|
color: #999;
|
|
font-size: 12px;
|
|
margin-top: 24px;
|
|
}
|
|
.empty {
|
|
color: #999;
|
|
font-size: 13px;
|
|
text-align: center;
|
|
padding: 24px;
|
|
font-style: italic;
|
|
}
|
|
.powered-by {
|
|
text-align: center;
|
|
margin-top: 40px;
|
|
padding-top: 24px;
|
|
border-top: 1px solid #E5E3DE;
|
|
color: #999;
|
|
font-size: 12px;
|
|
}
|
|
.powered-by span { color: #D97757; font-weight: 500; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>LOKI MODE</h1>
|
|
<div class="subtitle">Autonomous Multi-Agent Startup System</div>
|
|
<div class="phase" id="phase">Loading...</div>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat agents"><div class="number" id="agents-count">-</div><div class="label">Active Agents</div></div>
|
|
<div class="stat pending"><div class="number" id="pending-count">-</div><div class="label">Pending</div></div>
|
|
<div class="stat progress"><div class="number" id="progress-count">-</div><div class="label">In Progress</div></div>
|
|
<div class="stat completed"><div class="number" id="completed-count">-</div><div class="label">Completed</div></div>
|
|
<div class="stat failed"><div class="number" id="failed-count">-</div><div class="label">Failed</div></div>
|
|
</div>
|
|
<div class="section-header">Active Agents</div>
|
|
<div class="agents-grid" id="agents-grid"></div>
|
|
<div class="section-header">Task Queue</div>
|
|
<div class="columns">
|
|
<div class="column pending"><h2>Pending <span class="count" id="pending-badge">0</span></h2><div id="pending-tasks"></div></div>
|
|
<div class="column progress"><h2>In Progress <span class="count" id="progress-badge">0</span></h2><div id="progress-tasks"></div></div>
|
|
<div class="column completed"><h2>Completed <span class="count" id="completed-badge">0</span></h2><div id="completed-tasks"></div></div>
|
|
<div class="column failed"><h2>Failed <span class="count" id="failed-badge">0</span></h2><div id="failed-tasks"></div></div>
|
|
</div>
|
|
<div class="updated" id="updated">Last updated: -</div>
|
|
<div class="powered-by">Powered by <span>Claude</span></div>
|
|
<button class="refresh" onclick="loadData()">Refresh</button>
|
|
<script>
|
|
async function loadJSON(path) {
|
|
try {
|
|
const res = await fetch(path + '?t=' + Date.now());
|
|
if (!res.ok) return [];
|
|
const text = await res.text();
|
|
if (!text.trim()) return [];
|
|
const data = JSON.parse(text);
|
|
return Array.isArray(data) ? data : (data.tasks || data.agents || []);
|
|
} catch { return []; }
|
|
}
|
|
function getModelClass(model) {
|
|
if (!model) return 'sonnet';
|
|
const m = model.toLowerCase();
|
|
if (m.includes('haiku')) return 'haiku';
|
|
if (m.includes('opus')) return 'opus';
|
|
return 'sonnet';
|
|
}
|
|
function formatDuration(isoDate) {
|
|
if (!isoDate) return 'Unknown';
|
|
const start = new Date(isoDate);
|
|
const now = new Date();
|
|
const seconds = Math.floor((now - start) / 1000);
|
|
if (seconds < 60) return seconds + 's';
|
|
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
|
|
return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
|
|
}
|
|
function renderAgent(agent) {
|
|
const modelClass = getModelClass(agent.model);
|
|
const modelName = agent.model || 'Sonnet 4.5';
|
|
const agentType = agent.agent_type || 'general-purpose';
|
|
const status = agent.status === 'completed' ? 'completed' : 'active';
|
|
const currentTask = agent.current_task || (agent.tasks_completed && agent.tasks_completed.length > 0
|
|
? 'Completed: ' + agent.tasks_completed.join(', ')
|
|
: 'Initializing...');
|
|
const duration = formatDuration(agent.spawned_at);
|
|
const tasksCount = agent.tasks_completed ? agent.tasks_completed.length : 0;
|
|
|
|
return `
|
|
<div class="agent-card">
|
|
<div class="agent-header">
|
|
<div class="agent-id">${agent.agent_id || 'Unknown'}</div>
|
|
<div class="model-badge ${modelClass}">${modelName}</div>
|
|
</div>
|
|
<div class="agent-type">${agentType}</div>
|
|
<div class="agent-status ${status}">${status}</div>
|
|
<div class="agent-work">${currentTask}</div>
|
|
<div class="agent-meta">
|
|
<span>⏱ ${duration}</span>
|
|
<span>✓ ${tasksCount} tasks</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
function renderTask(task) {
|
|
const payload = task.payload || {};
|
|
const title = payload.description || payload.action || task.type || 'Task';
|
|
const error = task.lastError ? `<div class="error">${task.lastError}</div>` : '';
|
|
return `<div class="task"><div class="id">${task.id}</div><span class="type">${task.type || 'general'}</span><div class="title">${title}</div>${error}</div>`;
|
|
}
|
|
async function loadData() {
|
|
const [pending, progress, completed, failed, agents] = await Promise.all([
|
|
loadJSON('../queue/pending.json'),
|
|
loadJSON('../queue/in-progress.json'),
|
|
loadJSON('../queue/completed.json'),
|
|
loadJSON('../queue/failed.json'),
|
|
loadJSON('../state/agents.json')
|
|
]);
|
|
|
|
// Agent stats
|
|
document.getElementById('agents-count').textContent = agents.length;
|
|
document.getElementById('agents-grid').innerHTML = agents.length
|
|
? agents.map(renderAgent).join('')
|
|
: '<div class="empty">No active agents</div>';
|
|
|
|
// Task stats
|
|
document.getElementById('pending-count').textContent = pending.length;
|
|
document.getElementById('progress-count').textContent = progress.length;
|
|
document.getElementById('completed-count').textContent = completed.length;
|
|
document.getElementById('failed-count').textContent = failed.length;
|
|
document.getElementById('pending-badge').textContent = pending.length;
|
|
document.getElementById('progress-badge').textContent = progress.length;
|
|
document.getElementById('completed-badge').textContent = completed.length;
|
|
document.getElementById('failed-badge').textContent = failed.length;
|
|
document.getElementById('pending-tasks').innerHTML = pending.length ? pending.map(renderTask).join('') : '<div class="empty">No pending tasks</div>';
|
|
document.getElementById('progress-tasks').innerHTML = progress.length ? progress.map(renderTask).join('') : '<div class="empty">No tasks in progress</div>';
|
|
document.getElementById('completed-tasks').innerHTML = completed.length ? completed.slice(-10).reverse().map(renderTask).join('') : '<div class="empty">No completed tasks</div>';
|
|
document.getElementById('failed-tasks').innerHTML = failed.length ? failed.map(renderTask).join('') : '<div class="empty">No failed tasks</div>';
|
|
|
|
try {
|
|
const state = await fetch('../state/orchestrator.json?t=' + Date.now()).then(r => r.json());
|
|
document.getElementById('phase').textContent = 'Phase: ' + (state.currentPhase || 'UNKNOWN');
|
|
} catch { document.getElementById('phase').textContent = 'Phase: UNKNOWN'; }
|
|
document.getElementById('updated').textContent = 'Last updated: ' + new Date().toLocaleTimeString();
|
|
}
|
|
loadData();
|
|
setInterval(loadData, 3000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
DASHBOARD_HTML
|
|
}
|
|
|
|
update_agents_state() {
|
|
# Aggregate agent information from .agent/sub-agents/*.json into .loki/state/agents.json
|
|
local agents_dir=".agent/sub-agents"
|
|
local output_file=".loki/state/agents.json"
|
|
|
|
# Initialize empty array if no agents directory
|
|
if [ ! -d "$agents_dir" ]; then
|
|
echo "[]" > "$output_file"
|
|
return
|
|
fi
|
|
|
|
# Find all agent JSON files and aggregate them
|
|
local agents_json="["
|
|
local first=true
|
|
|
|
for agent_file in "$agents_dir"/*.json; do
|
|
# Skip if no JSON files exist
|
|
[ -e "$agent_file" ] || continue
|
|
|
|
# Read agent JSON
|
|
local agent_data=$(cat "$agent_file" 2>/dev/null)
|
|
if [ -n "$agent_data" ]; then
|
|
# Add comma separator for all but first entry
|
|
if [ "$first" = true ]; then
|
|
first=false
|
|
else
|
|
agents_json="${agents_json},"
|
|
fi
|
|
agents_json="${agents_json}${agent_data}"
|
|
fi
|
|
done
|
|
|
|
agents_json="${agents_json}]"
|
|
|
|
# Write aggregated data
|
|
echo "$agents_json" > "$output_file"
|
|
}
|
|
|
|
#===============================================================================
|
|
# Resource Monitoring
|
|
#===============================================================================
|
|
|
|
check_system_resources() {
|
|
# Check CPU and memory usage and write status to .loki/state/resources.json
|
|
local output_file=".loki/state/resources.json"
|
|
|
|
# Get CPU usage (average across all cores)
|
|
local cpu_usage=0
|
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
# macOS: get CPU idle from top header, calculate usage = 100 - idle
|
|
local idle=$(top -l 2 -n 0 | grep "CPU usage" | tail -1 | awk -F'[:,]' '{for(i=1;i<=NF;i++) if($i ~ /idle/) print $(i)}' | awk '{print int($1)}')
|
|
cpu_usage=$((100 - ${idle:-0}))
|
|
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
|
# Linux: use top or mpstat
|
|
cpu_usage=$(top -bn2 | grep "Cpu(s)" | tail -1 | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print int(100 - $1)}')
|
|
else
|
|
cpu_usage=0
|
|
fi
|
|
|
|
# Get memory usage
|
|
local mem_usage=0
|
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
# macOS: use vm_stat
|
|
local page_size=$(pagesize)
|
|
local vm_stat=$(vm_stat)
|
|
local pages_free=$(echo "$vm_stat" | awk '/Pages free/ {print $3}' | tr -d '.')
|
|
local pages_active=$(echo "$vm_stat" | awk '/Pages active/ {print $3}' | tr -d '.')
|
|
local pages_inactive=$(echo "$vm_stat" | awk '/Pages inactive/ {print $3}' | tr -d '.')
|
|
local pages_speculative=$(echo "$vm_stat" | awk '/Pages speculative/ {print $3}' | tr -d '.')
|
|
local pages_wired=$(echo "$vm_stat" | awk '/Pages wired down/ {print $4}' | tr -d '.')
|
|
|
|
local total_pages=$((pages_free + pages_active + pages_inactive + pages_speculative + pages_wired))
|
|
local used_pages=$((pages_active + pages_wired))
|
|
mem_usage=$((used_pages * 100 / total_pages))
|
|
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
|
# Linux: use free
|
|
mem_usage=$(free | grep Mem | awk '{print int($3/$2 * 100)}')
|
|
else
|
|
mem_usage=0
|
|
fi
|
|
|
|
# Determine status
|
|
local cpu_status="ok"
|
|
local mem_status="ok"
|
|
local overall_status="ok"
|
|
local warning_message=""
|
|
|
|
if [ "$cpu_usage" -ge "$RESOURCE_CPU_THRESHOLD" ]; then
|
|
cpu_status="high"
|
|
overall_status="warning"
|
|
warning_message="CPU usage is ${cpu_usage}% (threshold: ${RESOURCE_CPU_THRESHOLD}%). Consider reducing parallel agent count or pausing non-critical tasks."
|
|
fi
|
|
|
|
if [ "$mem_usage" -ge "$RESOURCE_MEM_THRESHOLD" ]; then
|
|
mem_status="high"
|
|
overall_status="warning"
|
|
if [ -n "$warning_message" ]; then
|
|
warning_message="${warning_message} Memory usage is ${mem_usage}% (threshold: ${RESOURCE_MEM_THRESHOLD}%)."
|
|
else
|
|
warning_message="Memory usage is ${mem_usage}% (threshold: ${RESOURCE_MEM_THRESHOLD}%). Consider reducing parallel agent count or cleaning up resources."
|
|
fi
|
|
fi
|
|
|
|
# Write JSON status
|
|
cat > "$output_file" << EOF
|
|
{
|
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"cpu": {
|
|
"usage_percent": $cpu_usage,
|
|
"threshold_percent": $RESOURCE_CPU_THRESHOLD,
|
|
"status": "$cpu_status"
|
|
},
|
|
"memory": {
|
|
"usage_percent": $mem_usage,
|
|
"threshold_percent": $RESOURCE_MEM_THRESHOLD,
|
|
"status": "$mem_status"
|
|
},
|
|
"overall_status": "$overall_status",
|
|
"warning_message": "$warning_message"
|
|
}
|
|
EOF
|
|
|
|
# Log warning if resources are high
|
|
if [ "$overall_status" = "warning" ]; then
|
|
log_warn "RESOURCE WARNING: $warning_message"
|
|
fi
|
|
}
|
|
|
|
start_resource_monitor() {
|
|
log_step "Starting resource monitor (checks every ${RESOURCE_CHECK_INTERVAL}s)..."
|
|
|
|
# Initial check
|
|
check_system_resources
|
|
|
|
# Background monitoring loop
|
|
(
|
|
while true; do
|
|
sleep "$RESOURCE_CHECK_INTERVAL"
|
|
check_system_resources
|
|
done
|
|
) &
|
|
RESOURCE_MONITOR_PID=$!
|
|
|
|
log_info "Resource monitor started (CPU threshold: ${RESOURCE_CPU_THRESHOLD}%, Memory threshold: ${RESOURCE_MEM_THRESHOLD}%)"
|
|
log_info "Check status: ${CYAN}cat .loki/state/resources.json${NC}"
|
|
}
|
|
|
|
stop_resource_monitor() {
|
|
if [ -n "$RESOURCE_MONITOR_PID" ]; then
|
|
kill "$RESOURCE_MONITOR_PID" 2>/dev/null || true
|
|
wait "$RESOURCE_MONITOR_PID" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
#===============================================================================
|
|
# Audit Logging (Enterprise Security)
|
|
#===============================================================================
|
|
|
|
audit_log() {
|
|
# Log security-relevant events for enterprise compliance
|
|
local event_type="$1"
|
|
local event_data="$2"
|
|
local audit_file=".loki/logs/audit-$(date +%Y%m%d).jsonl"
|
|
|
|
if [ "$AUDIT_LOG_ENABLED" != "true" ]; then
|
|
return
|
|
fi
|
|
|
|
mkdir -p .loki/logs
|
|
|
|
local log_entry=$(cat << EOF
|
|
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","event":"$event_type","data":"$event_data","user":"$(whoami)","pid":$$}
|
|
EOF
|
|
)
|
|
echo "$log_entry" >> "$audit_file"
|
|
}
|
|
|
|
check_staged_autonomy() {
|
|
# In staged autonomy mode, write plan and wait for approval
|
|
local plan_file="$1"
|
|
|
|
if [ "$STAGED_AUTONOMY" != "true" ]; then
|
|
return 0
|
|
fi
|
|
|
|
log_info "STAGED AUTONOMY: Waiting for plan approval..."
|
|
log_info "Review plan at: $plan_file"
|
|
log_info "Create .loki/signals/PLAN_APPROVED to continue"
|
|
|
|
audit_log "STAGED_AUTONOMY_WAIT" "plan=$plan_file"
|
|
|
|
# Wait for approval signal
|
|
while [ ! -f ".loki/signals/PLAN_APPROVED" ]; do
|
|
sleep 5
|
|
done
|
|
|
|
rm -f ".loki/signals/PLAN_APPROVED"
|
|
audit_log "STAGED_AUTONOMY_APPROVED" "plan=$plan_file"
|
|
log_success "Plan approved, continuing execution..."
|
|
}
|
|
|
|
check_command_allowed() {
|
|
# Check if a command is in the blocked list
|
|
local command="$1"
|
|
|
|
IFS=',' read -ra BLOCKED_ARRAY <<< "$BLOCKED_COMMANDS"
|
|
for blocked in "${BLOCKED_ARRAY[@]}"; do
|
|
if [[ "$command" == *"$blocked"* ]]; then
|
|
audit_log "BLOCKED_COMMAND" "command=$command,pattern=$blocked"
|
|
log_error "SECURITY: Blocked dangerous command: $command"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
#===============================================================================
|
|
# Cross-Project Learnings Database
|
|
#===============================================================================
|
|
|
|
init_learnings_db() {
|
|
# Initialize the cross-project learnings database
|
|
local learnings_dir="${HOME}/.loki/learnings"
|
|
mkdir -p "$learnings_dir"
|
|
|
|
# Create database files if they don't exist
|
|
if [ ! -f "$learnings_dir/patterns.jsonl" ]; then
|
|
echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/patterns.jsonl"
|
|
fi
|
|
|
|
if [ ! -f "$learnings_dir/mistakes.jsonl" ]; then
|
|
echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/mistakes.jsonl"
|
|
fi
|
|
|
|
if [ ! -f "$learnings_dir/successes.jsonl" ]; then
|
|
echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/successes.jsonl"
|
|
fi
|
|
|
|
log_info "Learnings database initialized at: $learnings_dir"
|
|
}
|
|
|
|
save_learning() {
|
|
# Save a learning to the cross-project database
|
|
local learning_type="$1" # pattern, mistake, success
|
|
local category="$2"
|
|
local description="$3"
|
|
local project="${4:-$(basename "$(pwd)")}"
|
|
|
|
local learnings_dir="${HOME}/.loki/learnings"
|
|
local target_file="$learnings_dir/${learning_type}s.jsonl"
|
|
|
|
if [ ! -d "$learnings_dir" ]; then
|
|
init_learnings_db
|
|
fi
|
|
|
|
local learning_entry=$(cat << EOF
|
|
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","project":"$project","category":"$category","description":"$description"}
|
|
EOF
|
|
)
|
|
echo "$learning_entry" >> "$target_file"
|
|
log_info "Saved $learning_type: $category"
|
|
}
|
|
|
|
get_relevant_learnings() {
|
|
# Get learnings relevant to the current context
|
|
local context="$1"
|
|
local learnings_dir="${HOME}/.loki/learnings"
|
|
local output_file=".loki/state/relevant-learnings.json"
|
|
|
|
if [ ! -d "$learnings_dir" ]; then
|
|
echo '{"patterns":[],"mistakes":[],"successes":[]}' > "$output_file"
|
|
return
|
|
fi
|
|
|
|
# Simple grep-based relevance (can be enhanced with embeddings)
|
|
# Pass context via environment variable to avoid quote escaping issues
|
|
export LOKI_CONTEXT="$context"
|
|
python3 << 'LEARNINGS_SCRIPT'
|
|
import json
|
|
import os
|
|
|
|
learnings_dir = os.path.expanduser("~/.loki/learnings")
|
|
context = os.environ.get("LOKI_CONTEXT", "").lower()
|
|
|
|
def load_jsonl(filepath):
|
|
entries = []
|
|
try:
|
|
with open(filepath, 'r') as f:
|
|
for line in f:
|
|
try:
|
|
entry = json.loads(line)
|
|
if 'description' in entry:
|
|
entries.append(entry)
|
|
except:
|
|
continue
|
|
except:
|
|
pass
|
|
return entries
|
|
|
|
def filter_relevant(entries, context, limit=5):
|
|
scored = []
|
|
for e in entries:
|
|
desc = e.get('description', '').lower()
|
|
cat = e.get('category', '').lower()
|
|
score = sum(1 for word in context.split() if word in desc or word in cat)
|
|
if score > 0:
|
|
scored.append((score, e))
|
|
scored.sort(reverse=True, key=lambda x: x[0])
|
|
return [e for _, e in scored[:limit]]
|
|
|
|
patterns = load_jsonl(f"{learnings_dir}/patterns.jsonl")
|
|
mistakes = load_jsonl(f"{learnings_dir}/mistakes.jsonl")
|
|
successes = load_jsonl(f"{learnings_dir}/successes.jsonl")
|
|
|
|
result = {
|
|
"patterns": filter_relevant(patterns, context),
|
|
"mistakes": filter_relevant(mistakes, context),
|
|
"successes": filter_relevant(successes, context)
|
|
}
|
|
|
|
with open(".loki/state/relevant-learnings.json", 'w') as f:
|
|
json.dump(result, f, indent=2)
|
|
LEARNINGS_SCRIPT
|
|
|
|
log_info "Loaded relevant learnings to: $output_file"
|
|
}
|
|
|
|
extract_learnings_from_session() {
|
|
# Extract learnings from completed session
|
|
local continuity_file=".loki/CONTINUITY.md"
|
|
|
|
if [ ! -f "$continuity_file" ]; then
|
|
return
|
|
fi
|
|
|
|
log_info "Extracting learnings from session..."
|
|
|
|
# Parse CONTINUITY.md for Mistakes & Learnings section
|
|
python3 << EXTRACT_SCRIPT
|
|
import re
|
|
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
|
|
continuity_file = ".loki/CONTINUITY.md"
|
|
learnings_dir = os.path.expanduser("~/.loki/learnings")
|
|
|
|
if not os.path.exists(continuity_file):
|
|
exit(0)
|
|
|
|
with open(continuity_file, 'r') as f:
|
|
content = f.read()
|
|
|
|
# Find Mistakes & Learnings section
|
|
mistakes_match = re.search(r'## Mistakes & Learnings\n(.*?)(?=\n## |\Z)', content, re.DOTALL)
|
|
if mistakes_match:
|
|
mistakes_text = mistakes_match.group(1)
|
|
# Extract bullet points
|
|
bullets = re.findall(r'[-*]\s+(.+)', mistakes_text)
|
|
for bullet in bullets:
|
|
entry = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
"project": os.path.basename(os.getcwd()),
|
|
"category": "session",
|
|
"description": bullet.strip()
|
|
}
|
|
with open(f"{learnings_dir}/mistakes.jsonl", 'a') as f:
|
|
f.write(json.dumps(entry) + "\n")
|
|
print(f"Extracted: {bullet[:50]}...")
|
|
|
|
print("Learning extraction complete")
|
|
EXTRACT_SCRIPT
|
|
}
|
|
|
|
start_dashboard() {
|
|
log_header "Starting Loki Dashboard"
|
|
|
|
# Create dashboard directory
|
|
mkdir -p .loki/dashboard
|
|
|
|
# Generate HTML
|
|
generate_dashboard
|
|
|
|
# Kill any existing process on the dashboard port
|
|
if lsof -i :$DASHBOARD_PORT &>/dev/null; then
|
|
log_step "Killing existing process on port $DASHBOARD_PORT..."
|
|
lsof -ti :$DASHBOARD_PORT | xargs kill -9 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
|
|
# Start Python HTTP server from .loki/ root so it can serve queue/ and state/
|
|
log_step "Starting dashboard server..."
|
|
(
|
|
cd .loki
|
|
python3 -m http.server $DASHBOARD_PORT --bind 127.0.0.1 2>&1 | while read line; do
|
|
echo "[dashboard] $line" >> logs/dashboard.log
|
|
done
|
|
) &
|
|
DASHBOARD_PID=$!
|
|
|
|
sleep 1
|
|
|
|
if kill -0 $DASHBOARD_PID 2>/dev/null; then
|
|
log_info "Dashboard started (PID: $DASHBOARD_PID)"
|
|
log_info "Dashboard: ${CYAN}http://127.0.0.1:$DASHBOARD_PORT/dashboard/index.html${NC}"
|
|
|
|
# Open in browser (macOS)
|
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
open "http://127.0.0.1:$DASHBOARD_PORT/dashboard/index.html" 2>/dev/null || true
|
|
fi
|
|
return 0
|
|
else
|
|
log_warn "Dashboard failed to start"
|
|
DASHBOARD_PID=""
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
stop_dashboard() {
|
|
if [ -n "$DASHBOARD_PID" ]; then
|
|
kill "$DASHBOARD_PID" 2>/dev/null || true
|
|
wait "$DASHBOARD_PID" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
#===============================================================================
|
|
# Calculate Exponential Backoff
|
|
#===============================================================================
|
|
|
|
calculate_wait() {
|
|
local retry="$1"
|
|
local wait_time=$((BASE_WAIT * (2 ** retry)))
|
|
|
|
# Add jitter (0-30 seconds)
|
|
local jitter=$((RANDOM % 30))
|
|
wait_time=$((wait_time + jitter))
|
|
|
|
# Cap at max wait
|
|
if [ $wait_time -gt $MAX_WAIT ]; then
|
|
wait_time=$MAX_WAIT
|
|
fi
|
|
|
|
echo $wait_time
|
|
}
|
|
|
|
#===============================================================================
|
|
# Rate Limit Detection
|
|
#===============================================================================
|
|
|
|
# Detect rate limit from log and calculate wait time until reset
|
|
# Returns: seconds to wait, or 0 if no rate limit detected
|
|
detect_rate_limit() {
|
|
local log_file="$1"
|
|
|
|
# Look for rate limit message like "resets 4am" or "resets 10pm"
|
|
local reset_time=$(grep -o "resets [0-9]\+[ap]m" "$log_file" 2>/dev/null | tail -1 | grep -o "[0-9]\+[ap]m")
|
|
|
|
if [ -z "$reset_time" ]; then
|
|
echo 0
|
|
return
|
|
fi
|
|
|
|
# Parse the reset time
|
|
local hour=$(echo "$reset_time" | grep -o "[0-9]\+")
|
|
local ampm=$(echo "$reset_time" | grep -o "[ap]m")
|
|
|
|
# Convert to 24-hour format
|
|
if [ "$ampm" = "pm" ] && [ "$hour" -ne 12 ]; then
|
|
hour=$((hour + 12))
|
|
elif [ "$ampm" = "am" ] && [ "$hour" -eq 12 ]; then
|
|
hour=0
|
|
fi
|
|
|
|
# Get current time
|
|
local current_hour=$(date +%H)
|
|
local current_min=$(date +%M)
|
|
local current_sec=$(date +%S)
|
|
|
|
# Calculate seconds until reset
|
|
local current_secs=$((current_hour * 3600 + current_min * 60 + current_sec))
|
|
local reset_secs=$((hour * 3600))
|
|
|
|
local wait_secs=$((reset_secs - current_secs))
|
|
|
|
# If reset time is in the past, it means tomorrow
|
|
if [ $wait_secs -le 0 ]; then
|
|
wait_secs=$((wait_secs + 86400)) # Add 24 hours
|
|
fi
|
|
|
|
# Add 2 minute buffer to ensure limit is actually reset
|
|
wait_secs=$((wait_secs + 120))
|
|
|
|
echo $wait_secs
|
|
}
|
|
|
|
# Format seconds into human-readable time
|
|
format_duration() {
|
|
local secs="$1"
|
|
local hours=$((secs / 3600))
|
|
local mins=$(((secs % 3600) / 60))
|
|
|
|
if [ $hours -gt 0 ]; then
|
|
echo "${hours}h ${mins}m"
|
|
else
|
|
echo "${mins}m"
|
|
fi
|
|
}
|
|
|
|
#===============================================================================
|
|
# Check Completion
|
|
#===============================================================================
|
|
|
|
is_completed() {
|
|
# Check orchestrator state
|
|
if [ -f ".loki/state/orchestrator.json" ]; then
|
|
if command -v python3 &> /dev/null; then
|
|
local phase=$(python3 -c "import json; print(json.load(open('.loki/state/orchestrator.json')).get('currentPhase', ''))" 2>/dev/null || echo "")
|
|
# Accept various completion states
|
|
if [ "$phase" = "COMPLETED" ] || [ "$phase" = "complete" ] || [ "$phase" = "finalized" ] || [ "$phase" = "growth-loop" ]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Check for completion marker
|
|
if [ -f ".loki/COMPLETED" ]; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Check if completion promise is fulfilled in log output
|
|
check_completion_promise() {
|
|
local log_file="$1"
|
|
|
|
# Check for the completion promise phrase in recent log output
|
|
if grep -q "COMPLETION PROMISE FULFILLED" "$log_file" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
# Check for custom completion promise text
|
|
if [ -n "$COMPLETION_PROMISE" ] && grep -qF "$COMPLETION_PROMISE" "$log_file" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Check if max iterations reached
|
|
check_max_iterations() {
|
|
if [ $ITERATION_COUNT -ge $MAX_ITERATIONS ]; then
|
|
log_warn "Max iterations ($MAX_ITERATIONS) reached. Stopping."
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Check if context clear was requested by agent
|
|
check_context_clear_signal() {
|
|
if [ -f ".loki/signals/CONTEXT_CLEAR_REQUESTED" ]; then
|
|
log_info "Context clear signal detected from agent"
|
|
rm -f ".loki/signals/CONTEXT_CLEAR_REQUESTED"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Load latest ledger content for context injection
|
|
load_ledger_context() {
|
|
local ledger_content=""
|
|
|
|
# Find most recent ledger
|
|
local latest_ledger=$(ls -t .loki/memory/ledgers/LEDGER-*.md 2>/dev/null | head -1)
|
|
|
|
if [ -n "$latest_ledger" ] && [ -f "$latest_ledger" ]; then
|
|
ledger_content=$(cat "$latest_ledger" | head -100)
|
|
echo "$ledger_content"
|
|
fi
|
|
}
|
|
|
|
# Load recent handoffs for context
|
|
load_handoff_context() {
|
|
local handoff_content=""
|
|
|
|
# Find most recent handoff (last 24 hours)
|
|
local recent_handoff=$(find .loki/memory/handoffs -name "*.md" -mtime -1 2>/dev/null | head -1)
|
|
|
|
if [ -n "$recent_handoff" ] && [ -f "$recent_handoff" ]; then
|
|
handoff_content=$(cat "$recent_handoff" | head -80)
|
|
echo "$handoff_content"
|
|
fi
|
|
}
|
|
|
|
# Load relevant learnings
|
|
load_learnings_context() {
|
|
local learnings=""
|
|
|
|
# Get recent learnings (last 7 days)
|
|
for learning in $(find .loki/memory/learnings -name "*.md" -mtime -7 2>/dev/null | head -5); do
|
|
learnings+="$(head -30 "$learning")\n---\n"
|
|
done
|
|
|
|
echo -e "$learnings"
|
|
}
|
|
|
|
#===============================================================================
|
|
# Save/Load Wrapper State
|
|
#===============================================================================
|
|
|
|
save_state() {
|
|
local retry_count="$1"
|
|
local status="$2"
|
|
local exit_code="$3"
|
|
|
|
cat > ".loki/autonomy-state.json" << EOF
|
|
{
|
|
"retryCount": $retry_count,
|
|
"status": "$status",
|
|
"lastExitCode": $exit_code,
|
|
"lastRun": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"prdPath": "${PRD_PATH:-}",
|
|
"pid": $$,
|
|
"maxRetries": $MAX_RETRIES,
|
|
"baseWait": $BASE_WAIT
|
|
}
|
|
EOF
|
|
}
|
|
|
|
load_state() {
|
|
if [ -f ".loki/autonomy-state.json" ]; then
|
|
if command -v python3 &> /dev/null; then
|
|
RETRY_COUNT=$(python3 -c "import json; print(json.load(open('.loki/autonomy-state.json')).get('retryCount', 0))" 2>/dev/null || echo "0")
|
|
else
|
|
RETRY_COUNT=0
|
|
fi
|
|
else
|
|
RETRY_COUNT=0
|
|
fi
|
|
}
|
|
|
|
#===============================================================================
|
|
# Build Resume Prompt
|
|
#===============================================================================
|
|
|
|
build_prompt() {
|
|
local retry="$1"
|
|
local prd="$2"
|
|
local iteration="$3"
|
|
|
|
# Build SDLC phases configuration
|
|
local phases=""
|
|
[ "$PHASE_UNIT_TESTS" = "true" ] && phases="${phases}UNIT_TESTS,"
|
|
[ "$PHASE_API_TESTS" = "true" ] && phases="${phases}API_TESTS,"
|
|
[ "$PHASE_E2E_TESTS" = "true" ] && phases="${phases}E2E_TESTS,"
|
|
[ "$PHASE_SECURITY" = "true" ] && phases="${phases}SECURITY,"
|
|
[ "$PHASE_INTEGRATION" = "true" ] && phases="${phases}INTEGRATION,"
|
|
[ "$PHASE_CODE_REVIEW" = "true" ] && phases="${phases}CODE_REVIEW,"
|
|
[ "$PHASE_WEB_RESEARCH" = "true" ] && phases="${phases}WEB_RESEARCH,"
|
|
[ "$PHASE_PERFORMANCE" = "true" ] && phases="${phases}PERFORMANCE,"
|
|
[ "$PHASE_ACCESSIBILITY" = "true" ] && phases="${phases}ACCESSIBILITY,"
|
|
[ "$PHASE_REGRESSION" = "true" ] && phases="${phases}REGRESSION,"
|
|
[ "$PHASE_UAT" = "true" ] && phases="${phases}UAT,"
|
|
phases="${phases%,}" # Remove trailing comma
|
|
|
|
# Ralph Wiggum Mode - Reason-Act-Reflect-VERIFY cycle with self-verification loop (Boris Cherny pattern)
|
|
local rarv_instruction="RALPH WIGGUM MODE ACTIVE. Use Reason-Act-Reflect-VERIFY cycle: 1) REASON - READ .loki/CONTINUITY.md including 'Mistakes & Learnings' section to avoid past errors. CHECK .loki/state/relevant-learnings.json for cross-project learnings from previous projects (mistakes to avoid, patterns to apply). Check .loki/state/ and .loki/queue/, identify next task. CHECK .loki/state/resources.json for system resource warnings - if CPU or memory is high, reduce parallel agent spawning or pause non-critical tasks. Limit to MAX_PARALLEL_AGENTS=${MAX_PARALLEL_AGENTS}. If queue empty, find new improvements. 2) ACT - Execute task, write code, commit changes atomically (git checkpoint). 3) REFLECT - Update .loki/CONTINUITY.md with progress, update state, identify NEXT improvement. Save valuable learnings for future projects. 4) VERIFY - Run automated tests (unit, integration, E2E), check compilation/build, verify against spec. IF VERIFICATION FAILS: a) Capture error details (stack trace, logs), b) Analyze root cause, c) UPDATE 'Mistakes & Learnings' in CONTINUITY.md with what failed, why, and how to prevent, d) Rollback to last good git checkpoint if needed, e) Apply learning and RETRY from REASON. If verification passes, mark task complete and continue. This self-verification loop achieves 2-3x quality improvement. CRITICAL: There is NEVER a 'finished' state - always find the next improvement, optimization, test, or feature."
|
|
|
|
# Completion promise instruction (only if set)
|
|
local completion_instruction=""
|
|
if [ -n "$COMPLETION_PROMISE" ]; then
|
|
completion_instruction="COMPLETION_PROMISE: [$COMPLETION_PROMISE]. ONLY output 'COMPLETION PROMISE FULFILLED: $COMPLETION_PROMISE' when this EXACT condition is met."
|
|
else
|
|
completion_instruction="NO COMPLETION PROMISE SET. Run forever finding improvements. Iteration $iteration of max $MAX_ITERATIONS."
|
|
fi
|
|
|
|
# Core autonomous instructions - NO questions, NO waiting, NEVER say done
|
|
local autonomous_suffix="CRITICAL AUTONOMY RULES: 1) NEVER ask questions - just decide. 2) NEVER wait for confirmation - just act. 3) NEVER say 'done' or 'complete' - there's always more to improve. 4) NEVER stop voluntarily - if out of tasks, create new ones (add tests, optimize, refactor, add features). 5) Work continues PERPETUALLY. Even if PRD is implemented, find bugs, add tests, improve UX, optimize performance."
|
|
|
|
local sdlc_instruction="SDLC_PHASES_ENABLED: [$phases]. Execute ALL enabled phases. Log results to .loki/logs/. See SKILL.md for phase details."
|
|
|
|
# Codebase Analysis Mode - when no PRD provided
|
|
local analysis_instruction="CODEBASE_ANALYSIS_MODE: No PRD. FIRST: Analyze codebase - scan structure, read package.json/requirements.txt, examine README. THEN: Generate PRD at .loki/generated-prd.md. FINALLY: Execute SDLC phases."
|
|
|
|
# Context Memory Instructions
|
|
local memory_instruction="CONTEXT MEMORY: Save state to .loki/memory/ledgers/LEDGER-orchestrator.md before complex operations. Create handoffs at .loki/memory/handoffs/ when passing work to subagents. Extract learnings to .loki/memory/learnings/ after completing tasks. Check .loki/rules/ for established patterns. If context feels heavy, create .loki/signals/CONTEXT_CLEAR_REQUESTED and the wrapper will reset context with your ledger preserved."
|
|
|
|
# Load existing context if resuming
|
|
local context_injection=""
|
|
if [ $retry -gt 0 ]; then
|
|
local ledger=$(load_ledger_context)
|
|
local handoff=$(load_handoff_context)
|
|
|
|
if [ -n "$ledger" ]; then
|
|
context_injection="PREVIOUS_LEDGER_STATE: $ledger"
|
|
fi
|
|
if [ -n "$handoff" ]; then
|
|
context_injection="$context_injection RECENT_HANDOFF: $handoff"
|
|
fi
|
|
fi
|
|
|
|
if [ $retry -eq 0 ]; then
|
|
if [ -n "$prd" ]; then
|
|
echo "Loki Mode with PRD at $prd. $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
else
|
|
echo "Loki Mode. $analysis_instruction $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
fi
|
|
else
|
|
if [ -n "$prd" ]; then
|
|
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $context_injection $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
else
|
|
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $context_injection Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
#===============================================================================
|
|
# Main Autonomous Loop
|
|
#===============================================================================
|
|
|
|
run_autonomous() {
|
|
local prd_path="$1"
|
|
|
|
log_header "Starting Autonomous Execution"
|
|
|
|
# Auto-detect PRD if not provided
|
|
if [ -z "$prd_path" ]; then
|
|
log_step "No PRD provided, searching for existing PRD files..."
|
|
local found_prd=""
|
|
|
|
# Search common PRD file patterns
|
|
for pattern in "PRD.md" "prd.md" "REQUIREMENTS.md" "requirements.md" "SPEC.md" "spec.md" \
|
|
"docs/PRD.md" "docs/prd.md" "docs/REQUIREMENTS.md" "docs/requirements.md" \
|
|
"docs/SPEC.md" "docs/spec.md" ".github/PRD.md" "PROJECT.md" "project.md"; do
|
|
if [ -f "$pattern" ]; then
|
|
found_prd="$pattern"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -n "$found_prd" ]; then
|
|
log_info "Found existing PRD: $found_prd"
|
|
prd_path="$found_prd"
|
|
elif [ -f ".loki/generated-prd.md" ]; then
|
|
log_info "Using previously generated PRD: .loki/generated-prd.md"
|
|
prd_path=".loki/generated-prd.md"
|
|
else
|
|
log_info "No PRD found - will analyze codebase and generate one"
|
|
fi
|
|
fi
|
|
|
|
log_info "PRD: ${prd_path:-Codebase Analysis Mode}"
|
|
log_info "Max retries: $MAX_RETRIES"
|
|
log_info "Max iterations: $MAX_ITERATIONS"
|
|
log_info "Completion promise: $COMPLETION_PROMISE"
|
|
log_info "Base wait: ${BASE_WAIT}s"
|
|
log_info "Max wait: ${MAX_WAIT}s"
|
|
echo ""
|
|
|
|
load_state
|
|
local retry=$RETRY_COUNT
|
|
|
|
# Check max iterations before starting
|
|
if check_max_iterations; then
|
|
log_error "Max iterations already reached. Reset with: rm .loki/autonomy-state.json"
|
|
return 1
|
|
fi
|
|
|
|
while [ $retry -lt $MAX_RETRIES ]; do
|
|
# Increment iteration count
|
|
((ITERATION_COUNT++))
|
|
|
|
# Check max iterations
|
|
if check_max_iterations; then
|
|
save_state $retry "max_iterations_reached" 0
|
|
return 0
|
|
fi
|
|
|
|
local prompt=$(build_prompt $retry "$prd_path" $ITERATION_COUNT)
|
|
|
|
echo ""
|
|
log_header "Attempt $((retry + 1)) of $MAX_RETRIES"
|
|
log_info "Prompt: $prompt"
|
|
echo ""
|
|
|
|
save_state $retry "running" 0
|
|
|
|
# Run Claude Code with live output
|
|
local start_time=$(date +%s)
|
|
local log_file=".loki/logs/autonomy-$(date +%Y%m%d).log"
|
|
|
|
echo ""
|
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
echo -e "${CYAN} CLAUDE CODE OUTPUT (live)${NC}"
|
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
echo ""
|
|
|
|
# Log start time
|
|
echo "=== Session started at $(date) ===" >> "$log_file"
|
|
echo "=== Prompt: $prompt ===" >> "$log_file"
|
|
|
|
set +e
|
|
# Run Claude with stream-json for real-time output
|
|
# Parse JSON stream, display formatted output, and track agents
|
|
claude --dangerously-skip-permissions -p "$prompt" \
|
|
--output-format stream-json --verbose 2>&1 | \
|
|
tee -a "$log_file" | \
|
|
python3 -u -c '
|
|
import sys
|
|
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
|
|
# ANSI colors
|
|
CYAN = "\033[0;36m"
|
|
GREEN = "\033[0;32m"
|
|
YELLOW = "\033[1;33m"
|
|
MAGENTA = "\033[0;35m"
|
|
DIM = "\033[2m"
|
|
NC = "\033[0m"
|
|
|
|
# Agent tracking
|
|
AGENTS_FILE = ".loki/state/agents.json"
|
|
QUEUE_IN_PROGRESS = ".loki/queue/in-progress.json"
|
|
active_agents = {} # tool_id -> agent_info
|
|
orchestrator_id = "orchestrator-main"
|
|
session_start = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
def init_orchestrator():
|
|
"""Initialize the main orchestrator agent (always visible)."""
|
|
active_agents[orchestrator_id] = {
|
|
"agent_id": orchestrator_id,
|
|
"tool_id": orchestrator_id,
|
|
"agent_type": "orchestrator",
|
|
"model": "sonnet",
|
|
"current_task": "Initializing...",
|
|
"status": "active",
|
|
"spawned_at": session_start,
|
|
"tasks_completed": [],
|
|
"tool_count": 0
|
|
}
|
|
save_agents()
|
|
|
|
def update_orchestrator_task(tool_name, description=""):
|
|
"""Update orchestrator current task based on tool usage."""
|
|
if orchestrator_id in active_agents:
|
|
active_agents[orchestrator_id]["tool_count"] = active_agents[orchestrator_id].get("tool_count", 0) + 1
|
|
if description:
|
|
active_agents[orchestrator_id]["current_task"] = f"{tool_name}: {description[:80]}"
|
|
else:
|
|
active_agents[orchestrator_id]["current_task"] = f"Using {tool_name}..."
|
|
save_agents()
|
|
|
|
def load_agents():
|
|
"""Load existing agents from file."""
|
|
try:
|
|
if os.path.exists(AGENTS_FILE):
|
|
with open(AGENTS_FILE, "r") as f:
|
|
data = json.load(f)
|
|
return {a.get("tool_id", a.get("agent_id")): a for a in data if isinstance(a, dict)}
|
|
except:
|
|
pass
|
|
return {}
|
|
|
|
def save_agents():
|
|
"""Save agents to file for dashboard."""
|
|
try:
|
|
os.makedirs(os.path.dirname(AGENTS_FILE), exist_ok=True)
|
|
agents_list = list(active_agents.values())
|
|
with open(AGENTS_FILE, "w") as f:
|
|
json.dump(agents_list, f, indent=2)
|
|
except Exception as e:
|
|
print(f"{YELLOW}[Agent save error: {e}]{NC}", file=sys.stderr)
|
|
|
|
def save_in_progress(tasks):
|
|
"""Save in-progress tasks to queue file."""
|
|
try:
|
|
os.makedirs(os.path.dirname(QUEUE_IN_PROGRESS), exist_ok=True)
|
|
with open(QUEUE_IN_PROGRESS, "w") as f:
|
|
json.dump(tasks, f, indent=2)
|
|
except:
|
|
pass
|
|
|
|
def process_stream():
|
|
global active_agents
|
|
active_agents = load_agents()
|
|
|
|
# Always show the main orchestrator
|
|
init_orchestrator()
|
|
print(f"{MAGENTA}[Orchestrator Active]{NC} Main agent started", flush=True)
|
|
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
data = json.loads(line)
|
|
msg_type = data.get("type", "")
|
|
|
|
if msg_type == "assistant":
|
|
# Extract and print assistant text
|
|
message = data.get("message", {})
|
|
content = message.get("content", [])
|
|
for item in content:
|
|
if item.get("type") == "text":
|
|
text = item.get("text", "")
|
|
if text:
|
|
print(text, end="", flush=True)
|
|
elif item.get("type") == "tool_use":
|
|
tool = item.get("name", "unknown")
|
|
tool_id = item.get("id", "")
|
|
tool_input = item.get("input", {})
|
|
|
|
# Extract description based on tool type
|
|
tool_desc = ""
|
|
if tool == "Read":
|
|
tool_desc = tool_input.get("file_path", "")
|
|
elif tool == "Edit" or tool == "Write":
|
|
tool_desc = tool_input.get("file_path", "")
|
|
elif tool == "Bash":
|
|
tool_desc = tool_input.get("description", tool_input.get("command", "")[:60])
|
|
elif tool == "Grep":
|
|
tool_desc = f"pattern: {tool_input.get('pattern', '')}"
|
|
elif tool == "Glob":
|
|
tool_desc = tool_input.get("pattern", "")
|
|
|
|
# Update orchestrator with current tool activity
|
|
update_orchestrator_task(tool, tool_desc)
|
|
|
|
# Track Task tool calls (agent spawning)
|
|
if tool == "Task":
|
|
agent_type = tool_input.get("subagent_type", "general-purpose")
|
|
description = tool_input.get("description", "")
|
|
model = tool_input.get("model", "sonnet")
|
|
|
|
agent_info = {
|
|
"agent_id": f"agent-{tool_id[:8]}",
|
|
"tool_id": tool_id,
|
|
"agent_type": agent_type,
|
|
"model": model,
|
|
"current_task": description,
|
|
"status": "active",
|
|
"spawned_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
"tasks_completed": []
|
|
}
|
|
active_agents[tool_id] = agent_info
|
|
save_agents()
|
|
print(f"\n{MAGENTA}[Agent Spawned: {agent_type}]{NC} {description}", flush=True)
|
|
|
|
# Track TodoWrite for task updates
|
|
elif tool == "TodoWrite":
|
|
todos = tool_input.get("todos", [])
|
|
in_progress = [t for t in todos if t.get("status") == "in_progress"]
|
|
save_in_progress([{"id": f"todo-{i}", "type": "todo", "payload": {"action": t.get("content", "")}} for i, t in enumerate(in_progress)])
|
|
print(f"\n{CYAN}[Tool: {tool}]{NC} {len(todos)} items", flush=True)
|
|
|
|
else:
|
|
print(f"\n{CYAN}[Tool: {tool}]{NC}", flush=True)
|
|
|
|
elif msg_type == "user":
|
|
# Tool results - check for agent completion
|
|
content = data.get("message", {}).get("content", [])
|
|
for item in content:
|
|
if item.get("type") == "tool_result":
|
|
tool_id = item.get("tool_use_id", "")
|
|
|
|
# Mark agent as completed if it was a Task
|
|
if tool_id in active_agents:
|
|
active_agents[tool_id]["status"] = "completed"
|
|
active_agents[tool_id]["completed_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
save_agents()
|
|
print(f"{DIM}[Agent Complete]{NC} ", end="", flush=True)
|
|
else:
|
|
print(f"{DIM}[Result]{NC} ", end="", flush=True)
|
|
|
|
elif msg_type == "result":
|
|
# Session complete - mark all agents as completed
|
|
completed_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
for agent_id in active_agents:
|
|
if active_agents[agent_id].get("status") == "active":
|
|
active_agents[agent_id]["status"] = "completed"
|
|
active_agents[agent_id]["completed_at"] = completed_at
|
|
active_agents[agent_id]["current_task"] = "Session complete"
|
|
|
|
# Add session stats to orchestrator
|
|
if orchestrator_id in active_agents:
|
|
tool_count = active_agents[orchestrator_id].get("tool_count", 0)
|
|
active_agents[orchestrator_id]["tasks_completed"].append(f"{tool_count} tools used")
|
|
|
|
save_agents()
|
|
print(f"\n{GREEN}[Session complete]{NC}", flush=True)
|
|
is_error = data.get("is_error", False)
|
|
sys.exit(1 if is_error else 0)
|
|
|
|
except json.JSONDecodeError:
|
|
# Not JSON, print as-is
|
|
print(line, flush=True)
|
|
except Exception as e:
|
|
print(f"{YELLOW}[Parse error: {e}]{NC}", file=sys.stderr)
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
process_stream()
|
|
except KeyboardInterrupt:
|
|
sys.exit(130)
|
|
except BrokenPipeError:
|
|
sys.exit(0)
|
|
'
|
|
local exit_code=${PIPESTATUS[0]}
|
|
set -e
|
|
|
|
echo ""
|
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
echo ""
|
|
|
|
# Log end time
|
|
echo "=== Session ended at $(date) with exit code $exit_code ===" >> "$log_file"
|
|
|
|
local end_time=$(date +%s)
|
|
local duration=$((end_time - start_time))
|
|
|
|
log_info "Claude exited with code $exit_code after ${duration}s"
|
|
save_state $retry "exited" $exit_code
|
|
|
|
# Check for success - ONLY stop on explicit completion promise
|
|
# There's never a "complete" product - always improvements, bugs, features
|
|
if [ $exit_code -eq 0 ]; then
|
|
# Perpetual mode: NEVER stop, always continue
|
|
if [ "$PERPETUAL_MODE" = "true" ]; then
|
|
log_info "Perpetual mode: Ignoring exit, continuing immediately..."
|
|
((retry++))
|
|
continue # Immediately start next iteration, no wait
|
|
fi
|
|
|
|
# Only stop if EXPLICIT completion promise text was output
|
|
if [ -n "$COMPLETION_PROMISE" ] && check_completion_promise "$log_file"; then
|
|
echo ""
|
|
log_header "COMPLETION PROMISE FULFILLED: $COMPLETION_PROMISE"
|
|
log_info "Explicit completion promise detected in output."
|
|
save_state $retry "completion_promise_fulfilled" 0
|
|
return 0
|
|
fi
|
|
|
|
# Warn if Claude says it's "done" but no explicit promise
|
|
if is_completed; then
|
|
log_warn "Claude claims completion, but no explicit promise fulfilled."
|
|
log_warn "Projects are never truly complete - there are always improvements!"
|
|
fi
|
|
|
|
# SUCCESS exit - continue IMMEDIATELY to next iteration (no wait!)
|
|
log_info "Iteration complete. Continuing to next iteration..."
|
|
((retry++))
|
|
continue # Immediately start next iteration, no exponential backoff
|
|
fi
|
|
|
|
# Only apply retry logic for ERRORS (non-zero exit code)
|
|
# Handle retry - check for rate limit first
|
|
local rate_limit_wait=$(detect_rate_limit "$log_file")
|
|
local wait_time
|
|
|
|
if [ $rate_limit_wait -gt 0 ]; then
|
|
wait_time=$rate_limit_wait
|
|
local human_time=$(format_duration $wait_time)
|
|
log_warn "Rate limit detected! Waiting until reset (~$human_time)..."
|
|
log_info "Rate limit resets at approximately $(date -v+${wait_time}S '+%I:%M %p' 2>/dev/null || date -d "+${wait_time} seconds" '+%I:%M %p' 2>/dev/null || echo 'soon')"
|
|
else
|
|
wait_time=$(calculate_wait $retry)
|
|
log_warn "Will retry in ${wait_time}s..."
|
|
fi
|
|
|
|
log_info "Press Ctrl+C to cancel"
|
|
|
|
# Countdown with progress
|
|
local remaining=$wait_time
|
|
local interval=10
|
|
# Use longer interval for long waits
|
|
if [ $wait_time -gt 1800 ]; then
|
|
interval=60
|
|
fi
|
|
|
|
while [ $remaining -gt 0 ]; do
|
|
local human_remaining=$(format_duration $remaining)
|
|
printf "\r${YELLOW}Resuming in ${human_remaining}...${NC} "
|
|
sleep $interval
|
|
remaining=$((remaining - interval))
|
|
done
|
|
echo ""
|
|
|
|
((retry++))
|
|
done
|
|
|
|
log_error "Max retries ($MAX_RETRIES) exceeded"
|
|
save_state $retry "failed" 1
|
|
return 1
|
|
}
|
|
|
|
#===============================================================================
|
|
# Cleanup Handler
|
|
#===============================================================================
|
|
|
|
cleanup() {
|
|
echo ""
|
|
log_warn "Received interrupt signal"
|
|
stop_dashboard
|
|
stop_status_monitor
|
|
save_state ${RETRY_COUNT:-0} "interrupted" 130
|
|
log_info "State saved. Run again to resume."
|
|
exit 130
|
|
}
|
|
|
|
#===============================================================================
|
|
# Main Entry Point
|
|
#===============================================================================
|
|
|
|
main() {
|
|
trap cleanup INT TERM
|
|
|
|
echo ""
|
|
echo -e "${BOLD}${BLUE}"
|
|
echo " ██╗ ██████╗ ██╗ ██╗██╗ ███╗ ███╗ ██████╗ ██████╗ ███████╗"
|
|
echo " ██║ ██╔═══██╗██║ ██╔╝██║ ████╗ ████║██╔═══██╗██╔══██╗██╔════╝"
|
|
echo " ██║ ██║ ██║█████╔╝ ██║ ██╔████╔██║██║ ██║██║ ██║█████╗ "
|
|
echo " ██║ ██║ ██║██╔═██╗ ██║ ██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ "
|
|
echo " ███████╗╚██████╔╝██║ ██╗██║ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗"
|
|
echo " ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝"
|
|
echo -e "${NC}"
|
|
echo -e " ${CYAN}Autonomous Multi-Agent Startup System${NC}"
|
|
echo -e " ${CYAN}Version: $(cat "$PROJECT_DIR/VERSION" 2>/dev/null || echo "2.x.x")${NC}"
|
|
echo ""
|
|
|
|
# Parse arguments
|
|
PRD_PATH="${1:-}"
|
|
|
|
# Validate PRD if provided
|
|
if [ -n "$PRD_PATH" ] && [ ! -f "$PRD_PATH" ]; then
|
|
log_error "PRD file not found: $PRD_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
# Check prerequisites (unless skipped)
|
|
if [ "$SKIP_PREREQS" != "true" ]; then
|
|
if ! check_prerequisites; then
|
|
exit 1
|
|
fi
|
|
else
|
|
log_warn "Skipping prerequisite checks (LOKI_SKIP_PREREQS=true)"
|
|
fi
|
|
|
|
# Check skill installation
|
|
if ! check_skill_installed; then
|
|
exit 1
|
|
fi
|
|
|
|
# Initialize .loki directory
|
|
init_loki_dir
|
|
|
|
# Start web dashboard (if enabled)
|
|
if [ "$ENABLE_DASHBOARD" = "true" ]; then
|
|
start_dashboard
|
|
else
|
|
log_info "Dashboard disabled (LOKI_DASHBOARD=false)"
|
|
fi
|
|
|
|
# Start status monitor (background updates to .loki/STATUS.txt)
|
|
start_status_monitor
|
|
|
|
# Start resource monitor (background CPU/memory checks)
|
|
start_resource_monitor
|
|
|
|
# Initialize cross-project learnings database
|
|
init_learnings_db
|
|
|
|
# Load relevant learnings for this project context
|
|
if [ -n "$PRD_PATH" ] && [ -f "$PRD_PATH" ]; then
|
|
get_relevant_learnings "$(cat "$PRD_PATH" | head -100)"
|
|
else
|
|
get_relevant_learnings "general development"
|
|
fi
|
|
|
|
# Log session start for audit
|
|
audit_log "SESSION_START" "prd=$PRD_PATH,dashboard=$ENABLE_DASHBOARD,staged_autonomy=$STAGED_AUTONOMY"
|
|
|
|
# Run autonomous loop
|
|
local result=0
|
|
run_autonomous "$PRD_PATH" || result=$?
|
|
|
|
# Extract and save learnings from this session
|
|
extract_learnings_from_session
|
|
|
|
# Log session end for audit
|
|
audit_log "SESSION_END" "result=$result,prd=$PRD_PATH"
|
|
|
|
# Cleanup
|
|
stop_dashboard
|
|
stop_status_monitor
|
|
|
|
exit $result
|
|
}
|
|
|
|
# Run main
|
|
main "$@"
|