loki-mode 5.31.0 → 5.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -426,6 +426,68 @@ Go get coffee. It'll be deployed when you get back.
426
426
 
427
427
  ---
428
428
 
429
+ ## Architecture
430
+
431
+ ```mermaid
432
+ graph TB
433
+ PRD["PRD Document"] --> REASON
434
+
435
+ subgraph RARVC["RARV+C Cycle"]
436
+ direction TB
437
+ REASON["1. Reason"] --> ACT["2. Act"]
438
+ ACT --> REFLECT["3. Reflect"]
439
+ REFLECT --> VERIFY["4. Verify"]
440
+ VERIFY -->|"pass"| COMPOUND["5. Compound"]
441
+ VERIFY -->|"fail"| REASON
442
+ COMPOUND --> REASON
443
+ end
444
+
445
+ subgraph PROVIDERS["Provider Layer"]
446
+ CLAUDE["Claude Code<br/>(full features)"]
447
+ CODEX["Codex CLI<br/>(degraded)"]
448
+ GEMINI["Gemini CLI<br/>(degraded)"]
449
+ end
450
+
451
+ ACT --> PROVIDERS
452
+
453
+ subgraph AGENTS["Agent Swarms (41 types)"]
454
+ ENG["Engineering (8)"]
455
+ OPS["Operations (8)"]
456
+ BIZ["Business (8)"]
457
+ DATA["Data (3)"]
458
+ PROD["Product (3)"]
459
+ GROWTH["Growth (4)"]
460
+ REVIEW["Review (3)"]
461
+ ORCH["Orchestration (4)"]
462
+ end
463
+
464
+ PROVIDERS --> AGENTS
465
+
466
+ subgraph INFRA["Infrastructure"]
467
+ DASHBOARD["Dashboard<br/>(FastAPI + Web UI)"]
468
+ MEMORY["Memory System<br/>(Episodic/Semantic/Procedural)"]
469
+ COUNCIL["Completion Council<br/>(3-member voting)"]
470
+ QUEUE["Task Queue<br/>(.loki/queue/)"]
471
+ end
472
+
473
+ AGENTS --> QUEUE
474
+ VERIFY --> COUNCIL
475
+ REFLECT --> MEMORY
476
+ COMPOUND --> MEMORY
477
+ DASHBOARD -.->|"reads"| QUEUE
478
+ DASHBOARD -.->|"reads"| MEMORY
479
+ ```
480
+
481
+ **Key components:**
482
+ - **RARV+C Cycle** -- Reason, Act, Reflect, Verify, Compound. Every iteration follows this loop. Failed verification triggers retry from Reason.
483
+ - **Provider Layer** -- Claude Code (full parallel agents, Task tool, MCP), Codex CLI and Gemini CLI (sequential, degraded mode).
484
+ - **Agent Swarms** -- 41 specialized agent types across 7 swarms, spawned on demand based on project complexity.
485
+ - **Completion Council** -- 3 members vote on whether the project is done. Anti-sycophancy devil's advocate on unanimous votes.
486
+ - **Memory System** -- Episodic traces, semantic patterns, procedural skills. Progressive disclosure reduces context usage by 60-80%.
487
+ - **Dashboard** -- FastAPI server reading `.loki/` flat files, with real-time web UI for task queue, agents, logs, and council state.
488
+
489
+ ---
490
+
429
491
  ## CLI Commands (v4.1.0)
430
492
 
431
493
  The `loki` CLI provides easy access to all Loki Mode features:
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v5.31.0
6
+ # Loki Mode v5.32.1
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -267,4 +267,4 @@ Auto-detected or force with `LOKI_COMPLEXITY`:
267
267
 
268
268
  ---
269
269
 
270
- **v5.31.0 | Shell completions, GitHub Action enhancements, auto-confirm | ~280 lines core**
270
+ **v5.32.1 | action.yml API key verification, fail-fast on missing key | ~280 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.31.0
1
+ 5.32.1
package/autonomy/loki CHANGED
@@ -306,7 +306,7 @@ show_help() {
306
306
  echo " stop Stop execution immediately"
307
307
  echo " pause Pause after current session"
308
308
  echo " resume Resume paused execution"
309
- echo " status Show current status"
309
+ echo " status [--json] Show current status (--json for machine-readable)"
310
310
  echo " logs Show recent log output"
311
311
  echo " dashboard [cmd] Dashboard server (start|stop|status|url|open)"
312
312
  echo " provider [cmd] Manage AI provider (show|set|list|info)"
@@ -323,6 +323,7 @@ show_help() {
323
323
  echo " council [cmd] Completion council (status|verdicts|convergence|force-review|report)"
324
324
  echo " dogfood Show self-development statistics"
325
325
  echo " reset [target] Reset session state (all|retries|failed)"
326
+ echo " doctor [--json] Check system prerequisites"
326
327
  echo " version Show version"
327
328
  echo " help Show this help"
328
329
  echo ""
@@ -394,8 +395,8 @@ cmd_start() {
394
395
  echo ""
395
396
  echo "Environment Variables:"
396
397
  echo " LOKI_PRD_FILE Path to PRD file (alternative to positional arg)"
397
- echo " LOKI_AUTO_CONFIRM Set to 'true' to skip confirmation prompts"
398
- echo " CI When 'true', auto-confirms prompts"
398
+ echo " LOKI_AUTO_CONFIRM Set to 'true'/'false' to control prompts (takes precedence over CI)"
399
+ echo " CI Fallback: auto-confirms when 'true' and LOKI_AUTO_CONFIRM is unset"
399
400
  echo " LOKI_MAX_ITERATIONS Max iteration count"
400
401
  echo " LOKI_BUDGET_LIMIT Cost budget limit in USD"
401
402
  echo ""
@@ -502,7 +503,10 @@ cmd_start() {
502
503
  else
503
504
  # No PRD file specified -- warn and confirm before consuming API credits
504
505
  # Auto-confirm in CI environments or when LOKI_AUTO_CONFIRM is set
505
- if [[ "${LOKI_AUTO_CONFIRM:-}" == "true" ]] || [[ "${CI:-}" == "true" ]]; then
506
+ # LOKI_AUTO_CONFIRM takes precedence when explicitly set;
507
+ # fall back to CI env var only when LOKI_AUTO_CONFIRM is unset
508
+ local _auto_confirm="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
509
+ if [[ "$_auto_confirm" == "true" ]]; then
506
510
  echo -e "${YELLOW}Warning: No PRD file specified. Auto-confirming (CI mode).${NC}"
507
511
  else
508
512
  echo -e "${YELLOW}Warning: No PRD file specified.${NC}"
@@ -886,6 +890,167 @@ cmd_status() {
886
890
  fi
887
891
  }
888
892
 
893
+ # JSON output for loki status --json
894
+ cmd_status_json() {
895
+ local skill_dir="$SKILL_DIR"
896
+ local loki_dir="$LOKI_DIR"
897
+ local dashboard_port="${LOKI_DASHBOARD_PORT:-57374}"
898
+ local env_provider="${LOKI_PROVIDER:-claude}"
899
+
900
+ python3 -c "
901
+ import json, os, sys, time
902
+
903
+ skill_dir = sys.argv[1]
904
+ loki_dir = sys.argv[2]
905
+ dashboard_port = sys.argv[3]
906
+ env_provider = sys.argv[4]
907
+ result = {}
908
+
909
+ # Version
910
+ version_file = os.path.join(skill_dir, 'VERSION')
911
+ if os.path.isfile(version_file):
912
+ with open(version_file) as f:
913
+ result['version'] = f.read().strip()
914
+ else:
915
+ result['version'] = 'unknown'
916
+
917
+ # Check if session exists
918
+ if not os.path.isdir(loki_dir):
919
+ result['status'] = 'inactive'
920
+ result['phase'] = None
921
+ result['iteration'] = 0
922
+ result['provider'] = env_provider
923
+ result['dashboard_url'] = None
924
+ result['pid'] = None
925
+ result['elapsed_time'] = 0
926
+ result['task_counts'] = {'total': 0, 'completed': 0, 'failed': 0, 'pending': 0}
927
+ print(json.dumps(result, indent=2))
928
+ sys.exit(0)
929
+
930
+ # Status from signals and session.json
931
+ if os.path.isfile(os.path.join(loki_dir, 'PAUSE')):
932
+ result['status'] = 'paused'
933
+ elif os.path.isfile(os.path.join(loki_dir, 'STOP')):
934
+ result['status'] = 'stopped'
935
+ else:
936
+ session_file = os.path.join(loki_dir, 'session.json')
937
+ if os.path.isfile(session_file):
938
+ try:
939
+ with open(session_file) as f:
940
+ session = json.load(f)
941
+ result['status'] = session.get('status', 'unknown')
942
+ except Exception:
943
+ result['status'] = 'unknown'
944
+ else:
945
+ result['status'] = 'unknown'
946
+
947
+ # Phase and iteration from dashboard-state.json
948
+ ds_file = os.path.join(loki_dir, 'dashboard-state.json')
949
+ if os.path.isfile(ds_file):
950
+ try:
951
+ with open(ds_file) as f:
952
+ ds = json.load(f)
953
+ result['phase'] = ds.get('phase', ds.get('currentPhase'))
954
+ result['iteration'] = ds.get('iteration', ds.get('currentIteration', 0))
955
+ except Exception:
956
+ result['phase'] = None
957
+ result['iteration'] = 0
958
+ else:
959
+ orch_file = os.path.join(loki_dir, 'state', 'orchestrator.json')
960
+ if os.path.isfile(orch_file):
961
+ try:
962
+ with open(orch_file) as f:
963
+ orch = json.load(f)
964
+ result['phase'] = orch.get('currentPhase')
965
+ result['iteration'] = orch.get('currentIteration', 0)
966
+ except Exception:
967
+ result['phase'] = None
968
+ result['iteration'] = 0
969
+ else:
970
+ result['phase'] = None
971
+ result['iteration'] = 0
972
+
973
+ # Provider
974
+ provider_file = os.path.join(loki_dir, 'state', 'provider')
975
+ if os.path.isfile(provider_file):
976
+ with open(provider_file) as f:
977
+ result['provider'] = f.read().strip()
978
+ else:
979
+ result['provider'] = env_provider
980
+
981
+ # PID
982
+ pid_file = os.path.join(loki_dir, 'loki.pid')
983
+ if os.path.isfile(pid_file):
984
+ try:
985
+ with open(pid_file) as f:
986
+ result['pid'] = int(f.read().strip())
987
+ except (ValueError, Exception):
988
+ result['pid'] = None
989
+ else:
990
+ result['pid'] = None
991
+
992
+ # Elapsed time from session.json
993
+ session_file = os.path.join(loki_dir, 'session.json')
994
+ if os.path.isfile(session_file):
995
+ try:
996
+ with open(session_file) as f:
997
+ session = json.load(f)
998
+ start_time = session.get('start_time', session.get('startTime'))
999
+ if start_time:
1000
+ if isinstance(start_time, (int, float)):
1001
+ result['elapsed_time'] = int(time.time() - start_time)
1002
+ else:
1003
+ from datetime import datetime
1004
+ dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
1005
+ result['elapsed_time'] = int(time.time() - dt.timestamp())
1006
+ else:
1007
+ result['elapsed_time'] = 0
1008
+ except Exception:
1009
+ result['elapsed_time'] = 0
1010
+ else:
1011
+ result['elapsed_time'] = 0
1012
+
1013
+ # Dashboard URL
1014
+ dashboard_pid_file = os.path.join(loki_dir, 'dashboard', 'dashboard.pid')
1015
+ dashboard_url = None
1016
+ if os.path.isfile(dashboard_pid_file):
1017
+ try:
1018
+ with open(dashboard_pid_file) as f:
1019
+ dpid = int(f.read().strip())
1020
+ os.kill(dpid, 0)
1021
+ dashboard_url = 'http://127.0.0.1:' + dashboard_port + '/'
1022
+ except (ProcessLookupError, PermissionError, ValueError, Exception):
1023
+ pass
1024
+ result['dashboard_url'] = dashboard_url
1025
+
1026
+ # Task counts from queue files
1027
+ task_counts = {'total': 0, 'completed': 0, 'failed': 0, 'pending': 0}
1028
+ queue_dir = os.path.join(loki_dir, 'queue')
1029
+ if os.path.isdir(queue_dir):
1030
+ for name, key in [('pending.json', 'pending'), ('completed.json', 'completed'), ('failed.json', 'failed')]:
1031
+ fpath = os.path.join(queue_dir, name)
1032
+ if os.path.isfile(fpath):
1033
+ try:
1034
+ with open(fpath) as f:
1035
+ data = json.load(f)
1036
+ if isinstance(data, list):
1037
+ task_counts[key] = len(data)
1038
+ elif isinstance(data, dict) and 'tasks' in data:
1039
+ task_counts[key] = len(data['tasks'])
1040
+ except Exception:
1041
+ pass
1042
+ task_counts['total'] = task_counts['pending'] + task_counts['completed'] + task_counts['failed']
1043
+ result['task_counts'] = task_counts
1044
+
1045
+ print(json.dumps(result, indent=2))
1046
+ " "$skill_dir" "$loki_dir" "$dashboard_port" "$env_provider"
1047
+
1048
+ if [ $? -ne 0 ]; then
1049
+ echo '{"error": "Failed to generate JSON status. Ensure python3 is available."}' >&2
1050
+ return 1
1051
+ fi
1052
+ }
1053
+
889
1054
  # Provider management
890
1055
  cmd_provider() {
891
1056
  local subcommand="${1:-show}"
@@ -2293,6 +2458,290 @@ cmd_config_path() {
2293
2458
  echo "Create a config file with: loki config init"
2294
2459
  }
2295
2460
 
2461
+ # Check system prerequisites
2462
+ cmd_doctor() {
2463
+ local json_output=false
2464
+
2465
+ while [[ $# -gt 0 ]]; do
2466
+ case "$1" in
2467
+ --json)
2468
+ json_output=true
2469
+ shift
2470
+ ;;
2471
+ --help|-h)
2472
+ echo -e "${BOLD}loki doctor${NC} - Check system prerequisites"
2473
+ echo ""
2474
+ echo "Usage: loki doctor [--json]"
2475
+ echo ""
2476
+ echo "Options:"
2477
+ echo " --json Output machine-readable JSON"
2478
+ echo ""
2479
+ echo "Checks: node, python3, jq, git, curl, bash version,"
2480
+ echo " claude/codex/gemini CLIs, and disk space."
2481
+ return 0
2482
+ ;;
2483
+ *)
2484
+ echo -e "${RED}Unknown option: $1${NC}"
2485
+ echo "Usage: loki doctor [--json]"
2486
+ return 1
2487
+ ;;
2488
+ esac
2489
+ done
2490
+
2491
+ if [ "$json_output" = true ]; then
2492
+ cmd_doctor_json
2493
+ return $?
2494
+ fi
2495
+
2496
+ echo -e "${BOLD}Loki Mode Doctor${NC}"
2497
+ echo ""
2498
+ echo "Checking system prerequisites..."
2499
+ echo ""
2500
+
2501
+ local pass_count=0
2502
+ local fail_count=0
2503
+ local warn_count=0
2504
+
2505
+ # Helper: check command exists and optionally check version
2506
+ doctor_check() {
2507
+ local name="$1"
2508
+ local cmd="$2"
2509
+ local required="$3" # required, recommended, optional
2510
+ local min_version="${4:-}"
2511
+
2512
+ if ! command -v "$cmd" &> /dev/null; then
2513
+ if [ "$required" = "required" ]; then
2514
+ echo -e " ${RED}FAIL${NC} $name - not found"
2515
+ fail_count=$((fail_count + 1))
2516
+ elif [ "$required" = "recommended" ]; then
2517
+ echo -e " ${YELLOW}WARN${NC} $name - not found (recommended)"
2518
+ warn_count=$((warn_count + 1))
2519
+ else
2520
+ echo -e " ${YELLOW}WARN${NC} $name - not found (optional)"
2521
+ warn_count=$((warn_count + 1))
2522
+ fi
2523
+ return 1
2524
+ fi
2525
+
2526
+ local version=""
2527
+ case "$cmd" in
2528
+ node)
2529
+ version=$(node --version 2>/dev/null | tr -d 'v')
2530
+ ;;
2531
+ python3)
2532
+ version=$(python3 --version 2>/dev/null | awk '{print $2}')
2533
+ ;;
2534
+ git)
2535
+ version=$(git --version 2>/dev/null | awk '{print $3}')
2536
+ ;;
2537
+ bash)
2538
+ version=$("$cmd" --version 2>/dev/null | head -1 | sed 's/.*version \([0-9.]*\).*/\1/')
2539
+ ;;
2540
+ jq)
2541
+ version=$(jq --version 2>/dev/null | tr -d 'jq-')
2542
+ ;;
2543
+ curl)
2544
+ version=$(curl --version 2>/dev/null | head -1 | awk '{print $2}')
2545
+ ;;
2546
+ claude)
2547
+ version=$(claude --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
2548
+ ;;
2549
+ codex)
2550
+ version=$(codex --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
2551
+ ;;
2552
+ gemini)
2553
+ version=$(gemini --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
2554
+ ;;
2555
+ esac
2556
+
2557
+ local version_display=""
2558
+ if [ -n "$version" ]; then
2559
+ version_display=" (v$version)"
2560
+ fi
2561
+
2562
+ # Simple major version check if min_version is specified
2563
+ if [ -n "$min_version" ] && [ -n "$version" ]; then
2564
+ local cur_major cur_minor min_major min_minor
2565
+ cur_major=$(echo "$version" | cut -d. -f1)
2566
+ min_major=$(echo "$min_version" | cut -d. -f1)
2567
+ cur_minor=$(echo "$version" | cut -d. -f2)
2568
+ min_minor=$(echo "$min_version" | cut -d. -f2)
2569
+
2570
+ if [ "$cur_major" -lt "$min_major" ] 2>/dev/null || \
2571
+ { [ "$cur_major" -eq "$min_major" ] 2>/dev/null && [ "$cur_minor" -lt "$min_minor" ] 2>/dev/null; }; then
2572
+ if [ "$required" = "required" ]; then
2573
+ echo -e " ${RED}FAIL${NC} $name$version_display - requires >= $min_version"
2574
+ fail_count=$((fail_count + 1))
2575
+ else
2576
+ echo -e " ${YELLOW}WARN${NC} $name$version_display - recommended >= $min_version"
2577
+ warn_count=$((warn_count + 1))
2578
+ fi
2579
+ return 1
2580
+ fi
2581
+ fi
2582
+
2583
+ echo -e " ${GREEN}PASS${NC} $name$version_display"
2584
+ pass_count=$((pass_count + 1))
2585
+ return 0
2586
+ }
2587
+
2588
+ echo -e "${CYAN}Required:${NC}"
2589
+ doctor_check "Node.js (>= 18)" node required 18.0
2590
+ doctor_check "Python 3 (>= 3.8)" python3 required 3.8
2591
+ doctor_check "jq" jq required
2592
+ doctor_check "git" git required
2593
+ doctor_check "curl" curl required
2594
+ echo ""
2595
+
2596
+ echo -e "${CYAN}AI Providers:${NC}"
2597
+ doctor_check "Claude CLI" claude optional
2598
+ doctor_check "Codex CLI" codex optional
2599
+ doctor_check "Gemini CLI" gemini optional
2600
+ echo ""
2601
+
2602
+ echo -e "${CYAN}System:${NC}"
2603
+ doctor_check "bash (>= 4.0)" bash recommended 4.0
2604
+
2605
+ # Disk space check
2606
+ local disk_avail
2607
+ if command -v df &> /dev/null; then
2608
+ # Get available space in GB (works on macOS and Linux)
2609
+ disk_avail=$(df -g "$HOME" 2>/dev/null | tail -1 | awk '{print $4}')
2610
+ if [ -z "$disk_avail" ]; then
2611
+ # Linux fallback (df -g may not work)
2612
+ disk_avail=$(df -BG "$HOME" 2>/dev/null | tail -1 | awk '{print $4}' | tr -d 'G')
2613
+ fi
2614
+ if [ -n "$disk_avail" ] && [ "$disk_avail" -gt 0 ] 2>/dev/null; then
2615
+ if [ "$disk_avail" -lt 1 ]; then
2616
+ echo -e " ${RED}FAIL${NC} Disk space: ${disk_avail}GB available (need >= 1GB)"
2617
+ fail_count=$((fail_count + 1))
2618
+ elif [ "$disk_avail" -lt 5 ]; then
2619
+ echo -e " ${YELLOW}WARN${NC} Disk space: ${disk_avail}GB available (low)"
2620
+ warn_count=$((warn_count + 1))
2621
+ else
2622
+ echo -e " ${GREEN}PASS${NC} Disk space: ${disk_avail}GB available"
2623
+ pass_count=$((pass_count + 1))
2624
+ fi
2625
+ else
2626
+ echo -e " ${YELLOW}WARN${NC} Disk space: unable to determine"
2627
+ warn_count=$((warn_count + 1))
2628
+ fi
2629
+ fi
2630
+ echo ""
2631
+
2632
+ # Summary
2633
+ echo -e "${BOLD}Summary:${NC} ${GREEN}$pass_count passed${NC}, ${RED}$fail_count failed${NC}, ${YELLOW}$warn_count warnings${NC}"
2634
+ echo ""
2635
+
2636
+ if [ "$fail_count" -gt 0 ]; then
2637
+ echo -e "${RED}Some required prerequisites are missing.${NC}"
2638
+ echo "Install missing dependencies and run 'loki doctor' again."
2639
+ return 1
2640
+ elif [ "$warn_count" -gt 0 ]; then
2641
+ echo -e "${YELLOW}All required checks passed with some warnings.${NC}"
2642
+ return 0
2643
+ else
2644
+ echo -e "${GREEN}All checks passed. System is ready for Loki Mode.${NC}"
2645
+ return 0
2646
+ fi
2647
+ }
2648
+
2649
+ # JSON output for loki doctor --json
2650
+ cmd_doctor_json() {
2651
+ python3 -c "
2652
+ import json, os, subprocess, sys, shutil
2653
+
2654
+ def get_version(cmd, args=None):
2655
+ try:
2656
+ if args is None:
2657
+ args = ['--version']
2658
+ result = subprocess.run([cmd] + args, capture_output=True, text=True, timeout=5)
2659
+ output = result.stdout.strip() or result.stderr.strip()
2660
+ # Extract version number
2661
+ import re
2662
+ match = re.search(r'(\d+\.\d+[\.\d]*)', output)
2663
+ return match.group(1) if match else None
2664
+ except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
2665
+ return None
2666
+
2667
+ def check_tool(name, cmd, required, min_version=None):
2668
+ found = shutil.which(cmd) is not None
2669
+ version = get_version(cmd) if found else None
2670
+ status = 'pass'
2671
+
2672
+ if not found:
2673
+ status = 'fail' if required == 'required' else 'warn'
2674
+ elif min_version and version:
2675
+ cur_parts = [int(x) for x in version.split('.')[:2]]
2676
+ min_parts = [int(x) for x in min_version.split('.')[:2]]
2677
+ while len(cur_parts) < 2: cur_parts.append(0)
2678
+ while len(min_parts) < 2: min_parts.append(0)
2679
+ if cur_parts < min_parts:
2680
+ status = 'fail' if required == 'required' else 'warn'
2681
+
2682
+ return {
2683
+ 'name': name,
2684
+ 'command': cmd,
2685
+ 'found': found,
2686
+ 'version': version,
2687
+ 'required': required,
2688
+ 'min_version': min_version,
2689
+ 'status': status,
2690
+ 'path': shutil.which(cmd)
2691
+ }
2692
+
2693
+ checks = []
2694
+ checks.append(check_tool('Node.js', 'node', 'required', '18.0'))
2695
+ checks.append(check_tool('Python 3', 'python3', 'required', '3.8'))
2696
+ checks.append(check_tool('jq', 'jq', 'required'))
2697
+ checks.append(check_tool('git', 'git', 'required'))
2698
+ checks.append(check_tool('curl', 'curl', 'required'))
2699
+ checks.append(check_tool('bash', 'bash', 'recommended', '4.0'))
2700
+ checks.append(check_tool('Claude CLI', 'claude', 'optional'))
2701
+ checks.append(check_tool('Codex CLI', 'codex', 'optional'))
2702
+ checks.append(check_tool('Gemini CLI', 'gemini', 'optional'))
2703
+
2704
+ # Disk space
2705
+ disk_gb = None
2706
+ try:
2707
+ stat = os.statvfs(os.path.expanduser('~'))
2708
+ disk_gb = round((stat.f_bavail * stat.f_frsize) / (1024**3), 1)
2709
+ except Exception:
2710
+ pass
2711
+
2712
+ disk_status = 'pass'
2713
+ if disk_gb is not None:
2714
+ if disk_gb < 1:
2715
+ disk_status = 'fail'
2716
+ elif disk_gb < 5:
2717
+ disk_status = 'warn'
2718
+
2719
+ pass_count = sum(1 for c in checks if c['status'] == 'pass')
2720
+ fail_count = sum(1 for c in checks if c['status'] == 'fail')
2721
+ warn_count = sum(1 for c in checks if c['status'] == 'warn')
2722
+
2723
+ if disk_status == 'pass': pass_count += 1
2724
+ elif disk_status == 'fail': fail_count += 1
2725
+ elif disk_status == 'warn': warn_count += 1
2726
+
2727
+ result = {
2728
+ 'checks': checks,
2729
+ 'disk': {
2730
+ 'available_gb': disk_gb,
2731
+ 'status': disk_status
2732
+ },
2733
+ 'summary': {
2734
+ 'passed': pass_count,
2735
+ 'failed': fail_count,
2736
+ 'warnings': warn_count,
2737
+ 'ok': fail_count == 0
2738
+ }
2739
+ }
2740
+
2741
+ print(json.dumps(result, indent=2))
2742
+ "
2743
+ }
2744
+
2296
2745
  # Show version
2297
2746
  cmd_version() {
2298
2747
  echo "Loki Mode v$(get_version)"
@@ -3534,7 +3983,7 @@ main() {
3534
3983
  cmd_resume
3535
3984
  ;;
3536
3985
  status)
3537
- cmd_status
3986
+ cmd_status "$@"
3538
3987
  ;;
3539
3988
  dashboard)
3540
3989
  cmd_dashboard "$@"
@@ -3590,6 +4039,9 @@ main() {
3590
4039
  voice)
3591
4040
  cmd_voice "$@"
3592
4041
  ;;
4042
+ doctor)
4043
+ cmd_doctor "$@"
4044
+ ;;
3593
4045
  version|--version|-v)
3594
4046
  cmd_version
3595
4047
  ;;
@@ -3871,9 +4323,13 @@ for filename in ['patterns.jsonl', 'mistakes.jsonl', 'successes.jsonl']:
3871
4323
 
3872
4324
  clear)
3873
4325
  local type="${2:-}"
4326
+ local confirm=""
3874
4327
 
3875
4328
  if [ -z "$type" ]; then
3876
- if [[ "${LOKI_AUTO_CONFIRM:-}" == "true" ]] || [[ "${CI:-}" == "true" ]]; then
4329
+ # LOKI_AUTO_CONFIRM takes precedence when explicitly set;
4330
+ # fall back to CI env var only when LOKI_AUTO_CONFIRM is unset
4331
+ local _auto_confirm="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
4332
+ if [[ "$_auto_confirm" == "true" ]]; then
3877
4333
  confirm="yes"
3878
4334
  else
3879
4335
  echo -e "${YELLOW}This will delete ALL cross-project learnings.${NC}"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "5.31.0"
10
+ __version__ = "5.32.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2200,7 +2200,7 @@ except ImportError as e:
2200
2200
  # =============================================================================
2201
2201
  # Must be configured AFTER all API routes to avoid conflicts
2202
2202
 
2203
- from fastapi.responses import FileResponse, HTMLResponse
2203
+ from fastapi.responses import FileResponse, HTMLResponse, Response
2204
2204
 
2205
2205
  # Find static files in multiple possible locations
2206
2206
  DASHBOARD_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -2240,6 +2240,17 @@ if STATIC_DIR:
2240
2240
  if os.path.isdir(ASSETS_DIR):
2241
2241
  app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets")
2242
2242
 
2243
+ # Serve favicon.svg from static directory
2244
+ @app.get("/favicon.svg", include_in_schema=False)
2245
+ async def serve_favicon():
2246
+ """Serve the dashboard favicon."""
2247
+ if STATIC_DIR:
2248
+ favicon_path = os.path.join(STATIC_DIR, "favicon.svg")
2249
+ if os.path.isfile(favicon_path):
2250
+ return FileResponse(favicon_path, media_type="image/svg+xml")
2251
+ return Response(status_code=404)
2252
+
2253
+
2243
2254
  # Serve index.html or standalone HTML for root
2244
2255
  @app.get("/", include_in_schema=False)
2245
2256
  async def serve_index():
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2
+ <path d="M16 6C8 6 2 16 2 16s6 10 14 10 14-10 14-10S24 6 16 6z" fill="none" stroke="#7c3aed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
3
+ <circle cx="16" cy="16" r="5" fill="#7c3aed"/>
4
+ <circle cx="16" cy="16" r="2" fill="#fff"/>
5
+ </svg>