#!/usr/bin/env bash # shellcheck disable=SC2034 # Many variables are used by sourced scripts # shellcheck disable=SC2155 # Declare and assign separately (acceptable in this codebase) # shellcheck disable=SC2329 # Functions may be invoked indirectly or via dynamic dispatch # shellcheck disable=SC2086 # Word splitting is intentional in some contexts #=============================================================================== # Loki Mode - Autonomous Runner # Single script that handles prerequisites, setup, and autonomous execution # # Usage: # ./autonomy/run.sh [OPTIONS] [PRD_PATH] # ./autonomy/run.sh ./docs/requirements.md # ./autonomy/run.sh # Interactive mode # ./autonomy/run.sh --parallel # Parallel mode with git worktrees # ./autonomy/run.sh --parallel ./prd.md # Parallel mode with PRD # # Environment Variables: # LOKI_PROVIDER - AI provider: claude (default), codex, gemini # 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) # LOKI_TLS_CERT - Path to PEM certificate (enables HTTPS for dashboard) # LOKI_TLS_KEY - Path to PEM private key (enables HTTPS for dashboard) # # 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) # # Budget / Cost Limits (opt-in): # LOKI_BUDGET_LIMIT - Max USD spend before auto-pause (default: empty = unlimited) # Example: "50.00" pauses session when estimated cost >= $50 # # Security & Autonomy Controls (Enterprise): # LOKI_STAGED_AUTONOMY - Require approval before execution (default: false) # LOKI_AUDIT_LOG - Enable audit logging (default: true) # LOKI_AUDIT_DISABLED - Disable 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 /) # # OIDC / SSO Authentication (optional, works alongside token auth): # LOKI_OIDC_ISSUER - OIDC issuer URL (e.g., https://accounts.google.com) # LOKI_OIDC_CLIENT_ID - OIDC client/application ID # LOKI_OIDC_AUDIENCE - Expected JWT audience (default: same as client_id) # # 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 the AI provider 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 # # Completion Council (v5.25.0) - Multi-agent completion verification: # LOKI_COUNCIL_ENABLED - Enable completion council (default: true) # LOKI_COUNCIL_SIZE - Number of council members (default: 3) # LOKI_COUNCIL_THRESHOLD - Votes needed for completion (default: 2) # LOKI_COUNCIL_CHECK_INTERVAL - Check every N iterations (default: 5) # LOKI_COUNCIL_MIN_ITERATIONS - Min iterations before council runs (default: 3) # LOKI_COUNCIL_STAGNATION_LIMIT - Max iterations with no git changes (default: 5) # # Model Selection: # LOKI_ALLOW_HAIKU - Enable Haiku model for fast tier (default: false) # When false: Opus for dev/bugfix, Sonnet for tests/docs # When true: Sonnet for dev, Haiku for tests/docs (original) # Use --allow-haiku flag or set to 'true' # # 2026 Research Enhancements: # LOKI_PROMPT_REPETITION - Enable prompt repetition for Haiku agents (default: true) # arXiv 2512.14982v1: Improves accuracy 4-5x on structured tasks # LOKI_CONFIDENCE_ROUTING - Enable confidence-based routing (default: true) # HN Production: 4-tier routing (auto-approve, direct, supervisor, escalate) # LOKI_AUTONOMY_MODE - Autonomy level (default: perpetual) # Options: perpetual, checkpoint, supervised # Tim Dettmers: "Shorter bursts of autonomy with feedback loops" # # Parallel Workflows (Git Worktrees): # LOKI_PARALLEL_MODE - Enable git worktree-based parallelism (default: false) # Use --parallel flag or set to 'true' # LOKI_MAX_WORKTREES - Maximum parallel worktrees (default: 5) # LOKI_MAX_PARALLEL_SESSIONS - Maximum concurrent AI sessions (default: 3) # LOKI_PARALLEL_TESTING - Run testing stream in parallel (default: true) # LOKI_PARALLEL_DOCS - Run documentation stream in parallel (default: true) # LOKI_PARALLEL_BLOG - Run blog stream if site has blog (default: false) # LOKI_AUTO_MERGE - Auto-merge completed features (default: true) # # Complexity Tiers (Auto-Claude pattern): # LOKI_COMPLEXITY - Force complexity tier (default: auto) # Options: auto, simple, standard, complex # Simple (3 phases): 1-2 files, single service, UI fixes, text changes # Standard (6 phases): 3-10 files, 1-2 services, features, bug fixes # Complex (8 phases): 10+ files, multiple services, external integrations # # GitHub Integration (v4.1.0): # LOKI_GITHUB_IMPORT - Import open issues as tasks (default: false) # LOKI_GITHUB_PR - Create PR when feature complete (default: false) # LOKI_GITHUB_SYNC - Sync status back to issues (default: false) # LOKI_GITHUB_REPO - Override repo detection (default: from git remote) # LOKI_GITHUB_LABELS - Filter by labels (comma-separated) # LOKI_GITHUB_MILESTONE - Filter by milestone # LOKI_GITHUB_ASSIGNEE - Filter by assignee # LOKI_GITHUB_LIMIT - Max issues to import (default: 100) # LOKI_GITHUB_PR_LABEL - Label for PRs (default: none, avoids error if label missing) # # Desktop Notifications (v4.1.0): # LOKI_NOTIFICATIONS - Enable desktop notifications (default: true) # LOKI_NOTIFICATION_SOUND - Play sound with notifications (default: true) # # Human Intervention (Auto-Claude pattern): # PAUSE file: touch .loki/PAUSE - pauses after current session # HUMAN_INPUT.md: echo "instructions" > .loki/HUMAN_INPUT.md # STOP file: touch .loki/STOP - stops immediately # Ctrl+C (once): Pauses execution, shows options # Ctrl+C (twice): Exits immediately # # Security (Enterprise): # LOKI_PROMPT_INJECTION - Enable HUMAN_INPUT.md processing (default: false) # Set to "true" only in trusted environments # # Branch Protection (agent isolation): # LOKI_BRANCH_PROTECTION - Create feature branch for agent changes (default: false) # Agent works on loki/session-- branch # Creates PR on session end if gh CLI is available # # Process Supervision (opt-in): # LOKI_WATCHDOG - Enable process health monitoring (default: false) # LOKI_WATCHDOG_INTERVAL - Check interval in seconds (default: 30) #=============================================================================== # # Compatibility: bash 3.2+ (macOS default), bash 4+ (Linux), WSL # Parallel mode (--parallel) requires bash 4.0+ for associative arrays #=============================================================================== set -uo pipefail # Compatibility check: Ensure we're running in bash (not sh, dash, zsh) if [ -z "${BASH_VERSION:-}" ]; then echo "[ERROR] This script requires bash. Please run with: bash $0" >&2 exit 1 fi # Extract major version for feature checks BASH_VERSION_MAJOR="${BASH_VERSION%%.*}" BASH_VERSION_MINOR="${BASH_VERSION#*.}" BASH_VERSION_MINOR="${BASH_VERSION_MINOR%%.*}" # Warn if bash version is very old (< 3.2) if [ "$BASH_VERSION_MAJOR" -lt 3 ] || { [ "$BASH_VERSION_MAJOR" -eq 3 ] && [ "$BASH_VERSION_MINOR" -lt 2 ]; }; then echo "[WARN] Bash version $BASH_VERSION is old. Recommend bash 3.2+ for full compatibility." >&2 echo "[WARN] Some features may not work correctly." >&2 fi 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:-}" ]] && [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then TEMP_SCRIPT=$(mktemp /tmp/loki-run-XXXXXX.sh) cp "${BASH_SOURCE[0]}" "$TEMP_SCRIPT" chmod 700 "$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 (only when running from temp copy) if [[ "${LOKI_RUNNING_FROM_TEMP:-}" == "1" ]]; then trap 'rm -f "${BASH_SOURCE[0]}" 2>/dev/null' EXIT fi #=============================================================================== # Configuration File Support (v4.1.0) # Loads settings from config file, environment variables take precedence #=============================================================================== load_config_file() { local config_file="" # Search for config file in order of priority # Security: Reject symlinks to prevent path traversal attacks # 1. Project-local config if [ -f ".loki/config.yaml" ] && [ ! -L ".loki/config.yaml" ]; then config_file=".loki/config.yaml" elif [ -f ".loki/config.yml" ] && [ ! -L ".loki/config.yml" ]; then config_file=".loki/config.yml" # 2. User-global config (symlinks allowed in home dir - user controls it) elif [ -f "${HOME}/.config/loki-mode/config.yaml" ]; then config_file="${HOME}/.config/loki-mode/config.yaml" elif [ -f "${HOME}/.config/loki-mode/config.yml" ]; then config_file="${HOME}/.config/loki-mode/config.yml" fi # If no config file found, return silently if [ -z "$config_file" ]; then return 0 fi # Check for yq (YAML parser) if ! command -v yq &> /dev/null; then # Fallback: parse simple YAML with sed/grep parse_simple_yaml "$config_file" return 0 fi # Use yq for proper YAML parsing parse_yaml_with_yq "$config_file" } # Fallback YAML parser for simple key: value format parse_simple_yaml() { local file="$1" # Parse core settings set_from_yaml "$file" "core.max_retries" "LOKI_MAX_RETRIES" set_from_yaml "$file" "core.base_wait" "LOKI_BASE_WAIT" set_from_yaml "$file" "core.max_wait" "LOKI_MAX_WAIT" set_from_yaml "$file" "core.skip_prereqs" "LOKI_SKIP_PREREQS" # Dashboard set_from_yaml "$file" "dashboard.enabled" "LOKI_DASHBOARD" set_from_yaml "$file" "dashboard.port" "LOKI_DASHBOARD_PORT" # Resources set_from_yaml "$file" "resources.check_interval" "LOKI_RESOURCE_CHECK_INTERVAL" set_from_yaml "$file" "resources.cpu_threshold" "LOKI_RESOURCE_CPU_THRESHOLD" set_from_yaml "$file" "resources.mem_threshold" "LOKI_RESOURCE_MEM_THRESHOLD" # Security set_from_yaml "$file" "security.staged_autonomy" "LOKI_STAGED_AUTONOMY" set_from_yaml "$file" "security.audit_log" "LOKI_AUDIT_LOG" set_from_yaml "$file" "security.max_parallel_agents" "LOKI_MAX_PARALLEL_AGENTS" set_from_yaml "$file" "security.sandbox_mode" "LOKI_SANDBOX_MODE" set_from_yaml "$file" "security.allowed_paths" "LOKI_ALLOWED_PATHS" set_from_yaml "$file" "security.blocked_commands" "LOKI_BLOCKED_COMMANDS" # Phases set_from_yaml "$file" "phases.unit_tests" "LOKI_PHASE_UNIT_TESTS" set_from_yaml "$file" "phases.api_tests" "LOKI_PHASE_API_TESTS" set_from_yaml "$file" "phases.e2e_tests" "LOKI_PHASE_E2E_TESTS" set_from_yaml "$file" "phases.security" "LOKI_PHASE_SECURITY" set_from_yaml "$file" "phases.integration" "LOKI_PHASE_INTEGRATION" set_from_yaml "$file" "phases.code_review" "LOKI_PHASE_CODE_REVIEW" set_from_yaml "$file" "phases.web_research" "LOKI_PHASE_WEB_RESEARCH" set_from_yaml "$file" "phases.performance" "LOKI_PHASE_PERFORMANCE" set_from_yaml "$file" "phases.accessibility" "LOKI_PHASE_ACCESSIBILITY" set_from_yaml "$file" "phases.regression" "LOKI_PHASE_REGRESSION" set_from_yaml "$file" "phases.uat" "LOKI_PHASE_UAT" # Completion set_from_yaml "$file" "completion.promise" "LOKI_COMPLETION_PROMISE" set_from_yaml "$file" "completion.max_iterations" "LOKI_MAX_ITERATIONS" set_from_yaml "$file" "completion.perpetual_mode" "LOKI_PERPETUAL_MODE" set_from_yaml "$file" "completion.council.enabled" "LOKI_COUNCIL_ENABLED" set_from_yaml "$file" "completion.council.size" "LOKI_COUNCIL_SIZE" set_from_yaml "$file" "completion.council.threshold" "LOKI_COUNCIL_THRESHOLD" set_from_yaml "$file" "completion.council.check_interval" "LOKI_COUNCIL_CHECK_INTERVAL" set_from_yaml "$file" "completion.council.min_iterations" "LOKI_COUNCIL_MIN_ITERATIONS" set_from_yaml "$file" "completion.council.stagnation_limit" "LOKI_COUNCIL_STAGNATION_LIMIT" # Model set_from_yaml "$file" "model.prompt_repetition" "LOKI_PROMPT_REPETITION" set_from_yaml "$file" "model.confidence_routing" "LOKI_CONFIDENCE_ROUTING" set_from_yaml "$file" "model.autonomy_mode" "LOKI_AUTONOMY_MODE" set_from_yaml "$file" "model.planning" "LOKI_MODEL_PLANNING" set_from_yaml "$file" "model.development" "LOKI_MODEL_DEVELOPMENT" set_from_yaml "$file" "model.fast" "LOKI_MODEL_FAST" set_from_yaml "$file" "model.compaction_interval" "LOKI_COMPACTION_INTERVAL" # Parallel set_from_yaml "$file" "parallel.enabled" "LOKI_PARALLEL_MODE" set_from_yaml "$file" "parallel.max_worktrees" "LOKI_MAX_WORKTREES" set_from_yaml "$file" "parallel.max_sessions" "LOKI_MAX_PARALLEL_SESSIONS" set_from_yaml "$file" "parallel.testing" "LOKI_PARALLEL_TESTING" set_from_yaml "$file" "parallel.docs" "LOKI_PARALLEL_DOCS" set_from_yaml "$file" "parallel.blog" "LOKI_PARALLEL_BLOG" set_from_yaml "$file" "parallel.auto_merge" "LOKI_AUTO_MERGE" # Complexity set_from_yaml "$file" "complexity.tier" "LOKI_COMPLEXITY" # GitHub set_from_yaml "$file" "github.import" "LOKI_GITHUB_IMPORT" set_from_yaml "$file" "github.pr" "LOKI_GITHUB_PR" set_from_yaml "$file" "github.sync" "LOKI_GITHUB_SYNC" set_from_yaml "$file" "github.repo" "LOKI_GITHUB_REPO" set_from_yaml "$file" "github.labels" "LOKI_GITHUB_LABELS" set_from_yaml "$file" "github.milestone" "LOKI_GITHUB_MILESTONE" set_from_yaml "$file" "github.assignee" "LOKI_GITHUB_ASSIGNEE" set_from_yaml "$file" "github.limit" "LOKI_GITHUB_LIMIT" set_from_yaml "$file" "github.pr_label" "LOKI_GITHUB_PR_LABEL" # Notifications set_from_yaml "$file" "notifications.enabled" "LOKI_NOTIFICATIONS" set_from_yaml "$file" "notifications.sound" "LOKI_NOTIFICATION_SOUND" } # Validate YAML value to prevent injection attacks validate_yaml_value() { local value="$1" local max_length="${2:-1000}" # Reject empty values if [ -z "$value" ]; then return 1 fi # Reject values with dangerous shell metacharacters # Allow alphanumeric, spaces, dots, dashes, underscores, slashes, colons, commas, @ if [[ "$value" =~ [\$\`\|\;\&\>\<\(\)\{\}\[\]\\] ]]; then return 1 fi # Reject values that are too long (DoS protection) if [ "${#value}" -gt "$max_length" ]; then return 1 fi # Reject values with newlines (could corrupt variables) if [[ "$value" == *$'\n'* ]]; then return 1 fi return 0 } # Escape regex metacharacters for safe grep usage escape_regex() { local input="$1" # Escape: . * ? + [ ] ^ $ { } | ( ) \ printf '%s' "$input" | sed 's/[.[\*?+^${}|()\\]/\\&/g' } # Helper: Extract value from YAML and set env var if not already set set_from_yaml() { local file="$1" local yaml_path="$2" local env_var="$3" # Skip if env var is already set if [ -n "${!env_var:-}" ]; then return 0 fi # Extract value using grep and sed (handles simple YAML) # Convert yaml path like "core.max_retries" to search pattern local value="" local key="${yaml_path##*.}" # Get last part of path # Escape regex metacharacters in key for safe grep local escaped_key escaped_key=$(escape_regex "$key") # Simple grep for the key (works for flat or indented YAML) # Use read to avoid xargs command execution risks value=$(grep -E "^\s*${escaped_key}:" "$file" 2>/dev/null | head -1 | sed -E 's/.*:\s*//' | sed 's/#.*//' | sed 's/^["\x27]//;s/["\x27]$//' | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # Validate value before export (security check) if [ -n "$value" ] && [ "$value" != "null" ] && validate_yaml_value "$value"; then export "$env_var=$value" fi } # Parse YAML using yq (proper parser) parse_yaml_with_yq() { local file="$1" local mappings=( "core.max_retries:LOKI_MAX_RETRIES" "core.base_wait:LOKI_BASE_WAIT" "core.max_wait:LOKI_MAX_WAIT" "core.skip_prereqs:LOKI_SKIP_PREREQS" "dashboard.enabled:LOKI_DASHBOARD" "dashboard.port:LOKI_DASHBOARD_PORT" "resources.check_interval:LOKI_RESOURCE_CHECK_INTERVAL" "resources.cpu_threshold:LOKI_RESOURCE_CPU_THRESHOLD" "resources.mem_threshold:LOKI_RESOURCE_MEM_THRESHOLD" "security.staged_autonomy:LOKI_STAGED_AUTONOMY" "security.audit_log:LOKI_AUDIT_LOG" "security.max_parallel_agents:LOKI_MAX_PARALLEL_AGENTS" "security.sandbox_mode:LOKI_SANDBOX_MODE" "security.allowed_paths:LOKI_ALLOWED_PATHS" "security.blocked_commands:LOKI_BLOCKED_COMMANDS" "phases.unit_tests:LOKI_PHASE_UNIT_TESTS" "phases.api_tests:LOKI_PHASE_API_TESTS" "phases.e2e_tests:LOKI_PHASE_E2E_TESTS" "phases.security:LOKI_PHASE_SECURITY" "phases.integration:LOKI_PHASE_INTEGRATION" "phases.code_review:LOKI_PHASE_CODE_REVIEW" "phases.web_research:LOKI_PHASE_WEB_RESEARCH" "phases.performance:LOKI_PHASE_PERFORMANCE" "phases.accessibility:LOKI_PHASE_ACCESSIBILITY" "phases.regression:LOKI_PHASE_REGRESSION" "phases.uat:LOKI_PHASE_UAT" "completion.promise:LOKI_COMPLETION_PROMISE" "completion.max_iterations:LOKI_MAX_ITERATIONS" "completion.perpetual_mode:LOKI_PERPETUAL_MODE" "completion.council.enabled:LOKI_COUNCIL_ENABLED" "completion.council.size:LOKI_COUNCIL_SIZE" "completion.council.threshold:LOKI_COUNCIL_THRESHOLD" "completion.council.check_interval:LOKI_COUNCIL_CHECK_INTERVAL" "completion.council.min_iterations:LOKI_COUNCIL_MIN_ITERATIONS" "completion.council.stagnation_limit:LOKI_COUNCIL_STAGNATION_LIMIT" "model.prompt_repetition:LOKI_PROMPT_REPETITION" "model.confidence_routing:LOKI_CONFIDENCE_ROUTING" "model.autonomy_mode:LOKI_AUTONOMY_MODE" "model.compaction_interval:LOKI_COMPACTION_INTERVAL" "parallel.enabled:LOKI_PARALLEL_MODE" "parallel.max_worktrees:LOKI_MAX_WORKTREES" "parallel.max_sessions:LOKI_MAX_PARALLEL_SESSIONS" "parallel.testing:LOKI_PARALLEL_TESTING" "parallel.docs:LOKI_PARALLEL_DOCS" "parallel.blog:LOKI_PARALLEL_BLOG" "parallel.auto_merge:LOKI_AUTO_MERGE" "complexity.tier:LOKI_COMPLEXITY" "github.import:LOKI_GITHUB_IMPORT" "github.pr:LOKI_GITHUB_PR" "github.sync:LOKI_GITHUB_SYNC" "github.repo:LOKI_GITHUB_REPO" "github.labels:LOKI_GITHUB_LABELS" "github.milestone:LOKI_GITHUB_MILESTONE" "github.assignee:LOKI_GITHUB_ASSIGNEE" "github.limit:LOKI_GITHUB_LIMIT" "github.pr_label:LOKI_GITHUB_PR_LABEL" "notifications.enabled:LOKI_NOTIFICATIONS" "notifications.sound:LOKI_NOTIFICATION_SOUND" ) for mapping in "${mappings[@]}"; do local yaml_path="${mapping%%:*}" local env_var="${mapping##*:}" # Skip if env var is already set if [ -n "${!env_var:-}" ]; then continue fi # Extract value using yq local value value=$(yq eval ".$yaml_path // \"\"" "$file" 2>/dev/null) # Set env var if value found and not empty/null # Also validate for security (prevent injection) if [ -n "$value" ] && [ "$value" != "null" ] && [ "$value" != "" ] && validate_yaml_value "$value"; then export "$env_var=$value" fi done } # Load config file before setting defaults load_config_file # Load JSON settings from loki config set (v6.0.0) _load_json_settings() { local settings_file="${TARGET_DIR:-.}/.loki/config/settings.json" [ -f "$settings_file" ] || return 0 eval "$(_LOKI_SETTINGS_FILE="$settings_file" python3 -c " import json, sys, os, shlex def get_nested(d, key): \"\"\"Resolve dotted keys through nested dicts (model.planning -> data['model']['planning'])\"\"\" parts = key.split('.') cur = d for p in parts: if isinstance(cur, dict): cur = cur.get(p) else: return None return cur try: with open(os.environ['_LOKI_SETTINGS_FILE']) as f: data = json.load(f) except Exception: sys.exit(0) mapping = { 'maxTier': 'LOKI_MAX_TIER', 'model.planning': 'LOKI_MODEL_PLANNING', 'model.development': 'LOKI_MODEL_DEVELOPMENT', 'model.fast': 'LOKI_MODEL_FAST', 'notify.slack': 'LOKI_SLACK_WEBHOOK', 'notify.discord': 'LOKI_DISCORD_WEBHOOK', } for key, env_var in mapping.items(): # Try nested dict lookup first, then flat key, then underscore variant val = get_nested(data, key) or data.get(key) or data.get(key.replace('.', '_')) if val and isinstance(val, str): safe_val = shlex.quote(val) print(f'[ -z \"\${{{env_var}:-}}\" ] && export {env_var}={safe_val}') " 2>/dev/null)" 2>/dev/null || true } _LOKI_SETTINGS_FILE="${TARGET_DIR:-.}/.loki/config/settings.json" _load_json_settings # 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 # Budget / Cost Limit (opt-in, empty = unlimited) BUDGET_LIMIT=${LOKI_BUDGET_LIMIT:-""} # USD amount, e.g., "50.00" # Background Mode BACKGROUND_MODE=${LOKI_BACKGROUND:-false} # Run in background # Security & Autonomy Controls STAGED_AUTONOMY=${LOKI_STAGED_AUTONOMY:-false} # Require plan approval AUDIT_LOG_ENABLED=${LOKI_AUDIT_LOG:-true} # Enable audit logging (on by default) 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,:(){ :|:& };:"} # Process Supervision (opt-in) WATCHDOG_ENABLED=${LOKI_WATCHDOG:-"false"} # Enable process health monitoring WATCHDOG_INTERVAL=${LOKI_WATCHDOG_INTERVAL:-30} # Check interval in seconds LAST_WATCHDOG_CHECK=0 STATUS_MONITOR_PID="" DASHBOARD_PID="" DASHBOARD_LAST_ALIVE=0 _DASHBOARD_RESTARTING=false 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} # Enterprise background service PIDs (OTEL bridge, audit subscriber, integration sync) ENTERPRISE_PIDS=() # Completion Council (v5.25.0) - Multi-agent completion verification # Source completion council module COUNCIL_SCRIPT="$SCRIPT_DIR/completion-council.sh" if [ -f "$COUNCIL_SCRIPT" ]; then # shellcheck source=completion-council.sh source "$COUNCIL_SCRIPT" fi # PRD Checklist module (v5.44.0) if [ -f "${SCRIPT_DIR}/prd-checklist.sh" ]; then # shellcheck source=prd-checklist.sh source "${SCRIPT_DIR}/prd-checklist.sh" fi # App Runner module (v5.45.0) if [ -f "${SCRIPT_DIR}/app-runner.sh" ]; then # shellcheck source=app-runner.sh source "${SCRIPT_DIR}/app-runner.sh" fi # Playwright Smoke Test module (v5.46.0) if [ -f "${SCRIPT_DIR}/playwright-verify.sh" ]; then # shellcheck source=playwright-verify.sh source "${SCRIPT_DIR}/playwright-verify.sh" fi # Anonymous usage telemetry (opt-out: LOKI_TELEMETRY_DISABLED=true or DO_NOT_TRACK=1) TELEMETRY_SCRIPT="$SCRIPT_DIR/telemetry.sh" if [ -f "$TELEMETRY_SCRIPT" ]; then # shellcheck source=telemetry.sh source "$TELEMETRY_SCRIPT" fi # 2026 Research Enhancements (minimal additions) PROMPT_REPETITION=${LOKI_PROMPT_REPETITION:-true} CONFIDENCE_ROUTING=${LOKI_CONFIDENCE_ROUTING:-true} AUTONOMY_MODE=${LOKI_AUTONOMY_MODE:-perpetual} # perpetual|checkpoint|supervised # Proactive Context Management (OpenCode/Sisyphus pattern, validated by Opus) COMPACTION_INTERVAL=${LOKI_COMPACTION_INTERVAL:-25} # Suggest compaction every N iterations # Parallel Workflows (Git Worktrees) PARALLEL_MODE=${LOKI_PARALLEL_MODE:-false} MAX_WORKTREES=${LOKI_MAX_WORKTREES:-5} MAX_PARALLEL_SESSIONS=${LOKI_MAX_PARALLEL_SESSIONS:-3} PARALLEL_TESTING=${LOKI_PARALLEL_TESTING:-true} PARALLEL_DOCS=${LOKI_PARALLEL_DOCS:-true} # Gate Escalation Ladder (v6.10.0) GATE_CLEAR_LIMIT=${LOKI_GATE_CLEAR_LIMIT:-3} GATE_ESCALATE_LIMIT=${LOKI_GATE_ESCALATE_LIMIT:-5} GATE_PAUSE_LIMIT=${LOKI_GATE_PAUSE_LIMIT:-10} TARGET_DIR="${LOKI_TARGET_DIR:-$(pwd)}" PARALLEL_BLOG=${LOKI_PARALLEL_BLOG:-false} AUTO_MERGE=${LOKI_AUTO_MERGE:-true} # Complexity Tiers (Auto-Claude pattern) # auto = detect from PRD/codebase, simple = 3 phases, standard = 6 phases, complex = 8 phases COMPLEXITY_TIER=${LOKI_COMPLEXITY:-auto} DETECTED_COMPLEXITY="" # Multi-Provider Support (v5.0.0) # Provider: claude (default), codex, gemini LOKI_PROVIDER=${LOKI_PROVIDER:-claude} # Source provider configuration PROVIDERS_DIR="$PROJECT_DIR/providers" if [ -f "$PROVIDERS_DIR/loader.sh" ]; then # shellcheck source=/dev/null source "$PROVIDERS_DIR/loader.sh" # Validate provider if ! validate_provider "$LOKI_PROVIDER"; then echo "ERROR: Unknown provider: $LOKI_PROVIDER" >&2 echo "Supported providers: ${SUPPORTED_PROVIDERS[*]}" >&2 exit 1 fi # Load provider config if ! load_provider "$LOKI_PROVIDER"; then echo "ERROR: Failed to load provider config: $LOKI_PROVIDER" >&2 exit 1 fi # Save provider for future runs (if .loki dir exists or will be created) if [ -d ".loki/state" ] || mkdir -p ".loki/state" 2>/dev/null; then echo "$LOKI_PROVIDER" > ".loki/state/provider" fi else # Fallback: Claude-only mode (backwards compatibility) PROVIDER_NAME="claude" PROVIDER_CLI="claude" PROVIDER_AUTONOMOUS_FLAG="--dangerously-skip-permissions" PROVIDER_PROMPT_FLAG="-p" PROVIDER_DEGRADED=false PROVIDER_DISPLAY_NAME="Claude Code" PROVIDER_HAS_PARALLEL=true PROVIDER_HAS_SUBAGENTS=true PROVIDER_HAS_TASK_TOOL=true PROVIDER_HAS_MCP=true PROVIDER_PROMPT_POSITIONAL=false fi # Track worktree PIDs for cleanup (requires bash 4+ for associative arrays) # BASH_VERSION_MAJOR is defined at script startup if [ "$BASH_VERSION_MAJOR" -ge 4 ] 2>/dev/null; then declare -A WORKTREE_PIDS=() declare -A WORKTREE_PATHS=() else # Fallback: parallel mode will check and warn # shellcheck disable=SC2178 WORKTREE_PIDS="" # shellcheck disable=SC2178 WORKTREE_PATHS="" fi # Track background install PIDs for cleanup (indexed array, works on all bash versions) WORKTREE_INSTALL_PIDS=() # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' 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} $*"; } log_debug() { [[ "${LOKI_DEBUG:-}" == "true" ]] && echo -e "${CYAN}[DEBUG]${NC} $*" || true; } #=============================================================================== # Process Registry (PID Supervisor) # Central registry of all spawned child processes for reliable cleanup #=============================================================================== PID_REGISTRY_DIR="" # Initialize the PID registry directory init_pid_registry() { PID_REGISTRY_DIR="${TARGET_DIR:-.}/.loki/pids" mkdir -p "$PID_REGISTRY_DIR" } # Parse a field from a JSON registry entry (python3 with shell fallback) # Usage: _parse_json_field _parse_json_field() { local file="$1" field="$2" if command -v python3 >/dev/null 2>&1; then python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2],''))" "$file" "$field" 2>/dev/null else # Shell fallback: extract value for simple flat JSON sed 's/.*"'"$field"'":\s*//' "$file" 2>/dev/null | sed 's/[",}].*//' | head -1 fi } # Register a spawned process in the central registry # Usage: register_pid