loki-mode 6.31.0 → 6.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
@@ -10,7 +10,7 @@
10
10
  [![Autonomi](https://img.shields.io/badge/Autonomi-autonomi.dev-5B4EEA)](https://www.autonomi.dev/)
11
11
  [![Docker Pulls](https://img.shields.io/docker/pulls/asklokesh/loki-mode)](https://hub.docker.com/r/asklokesh/loki-mode)
12
12
 
13
- **Current Version: v6.31.0**
13
+ **Current Version: v6.32.0**
14
14
 
15
15
  ### Traction
16
16
 
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 minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v6.31.0
6
+ # Loki Mode v6.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 @@ The following features are documented in skill modules but not yet fully automat
267
267
  | Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
268
268
  | Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
269
269
 
270
- **v6.31.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
270
+ **v6.32.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.31.0
1
+ 6.32.1
package/autonomy/loki CHANGED
@@ -394,7 +394,7 @@ show_help() {
394
394
  echo " demo Run interactive demo (~60s simulated session)"
395
395
  echo " init [name] Project scaffolding with 22 PRD templates"
396
396
  echo " issue <url|num> [DEPRECATED] Use 'loki run' instead"
397
- echo " watch [sec] Live TUI session monitor (v6.0.0)"
397
+ echo " watch [prd] Auto-rerun on PRD file changes (v6.33.0)"
398
398
  echo " export <format> Export session data: json|markdown|csv|timeline (v6.0.0)"
399
399
  echo " stop Stop execution immediately"
400
400
  echo " cleanup Kill orphaned processes from crashed sessions"
@@ -424,7 +424,7 @@ show_help() {
424
424
  echo " review [opts] Standalone code review with quality gates (diff, staged, PR, files)"
425
425
  echo " optimize Optimize prompts based on session history"
426
426
  echo " enterprise Enterprise feature management (tokens, OIDC)"
427
- echo " metrics Prometheus/OpenMetrics metrics from dashboard"
427
+ echo " metrics [opts] Session productivity report (--json, --last N, --save, --share)"
428
428
  echo " dogfood Show self-development statistics"
429
429
  echo " secrets [cmd] API key status and validation (status|validate)"
430
430
  echo " reset [target] Reset session state (all|retries|failed)"
@@ -487,7 +487,7 @@ show_help() {
487
487
  echo " loki start ./prd.md # Start with PRD file"
488
488
  echo " loki start --bg # Start in background"
489
489
  echo " loki start --parallel # Start in parallel mode"
490
- echo " loki watch # Live session monitor"
490
+ echo " loki watch # Watch PRD for changes, auto-rerun"
491
491
  echo " loki export json # Export session data"
492
492
  echo " loki config set maxTier sonnet # Cap model cost"
493
493
  echo " loki status # Check current status"
@@ -4312,126 +4312,252 @@ EOF
4312
4312
 
4313
4313
  # Show configuration
4314
4314
  #===============================================================================
4315
- # loki watch - Live TUI session monitor (v6.0.0)
4315
+ # loki watch - Auto-rerun on PRD file changes (v6.33.0)
4316
4316
  #===============================================================================
4317
4317
 
4318
4318
  cmd_watch() {
4319
- local interval="${1:-2}"
4319
+ local prd_path=""
4320
+ local run_once=false
4321
+ local poll_interval=2
4322
+ local no_auto_start=false
4323
+ local debounce=3
4324
+ local _watch_child_pid=""
4320
4325
 
4321
- # Handle --help
4322
- if [[ "$interval" == "--help" || "$interval" == "-h" ]]; then
4323
- # fall through to help block
4324
- :
4325
- elif ! [[ "$interval" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
4326
- echo -e "${RED}Invalid interval: $interval (expected positive number)${NC}"
4327
- return 1
4328
- elif [[ "$interval" =~ ^0+(\.0+)?$ ]]; then
4329
- echo -e "${RED}Invalid interval: $interval (must be greater than 0)${NC}"
4326
+ while [[ $# -gt 0 ]]; do
4327
+ case "$1" in
4328
+ --help|-h)
4329
+ echo -e "${BOLD}loki watch${NC} - Auto-rerun on PRD file changes (v6.33.0)"
4330
+ echo ""
4331
+ echo "Usage: loki watch [prd-path] [options]"
4332
+ echo ""
4333
+ echo "Monitors a PRD file for changes and automatically re-runs Loki Mode"
4334
+ echo "when the file is saved. Enables a tight edit-PRD-see-results loop."
4335
+ echo ""
4336
+ echo "Arguments:"
4337
+ echo " prd-path Path to PRD file (default: auto-detect prd.md, PRD.md, or first *.md)"
4338
+ echo ""
4339
+ echo "Options:"
4340
+ echo " --once Run once immediately then exit"
4341
+ echo " --interval N Poll interval in seconds for fallback watcher (default: 2)"
4342
+ echo " --no-auto-start Watch but do not auto-start, just print change timestamps"
4343
+ echo " --debounce N Wait N seconds after change before triggering (default: 3)"
4344
+ echo " --help, -h Show this help"
4345
+ echo ""
4346
+ echo "File watcher priority:"
4347
+ echo " 1. fswatch (macOS) -- native filesystem events"
4348
+ echo " 2. inotifywait (Linux) -- inotify-based"
4349
+ echo " 3. stat polling -- universal fallback"
4350
+ echo ""
4351
+ echo "Examples:"
4352
+ echo " loki watch # Auto-detect PRD and watch"
4353
+ echo " loki watch ./my-prd.md # Watch specific file"
4354
+ echo " loki watch --once # Run once immediately then exit"
4355
+ echo " loki watch --no-auto-start # Just report changes, don't run"
4356
+ echo " loki watch --debounce 5 # Wait 5s after change before triggering"
4357
+ echo " loki watch --interval 1 # Poll every 1s (fallback mode)"
4358
+ return 0
4359
+ ;;
4360
+ --once) run_once=true; shift ;;
4361
+ --interval) poll_interval="${2:-2}"; shift 2 ;;
4362
+ --interval=*) poll_interval="${1#*=}"; shift ;;
4363
+ --no-auto-start) no_auto_start=true; shift ;;
4364
+ --debounce) debounce="${2:-3}"; shift 2 ;;
4365
+ --debounce=*) debounce="${1#*=}"; shift ;;
4366
+ -*)
4367
+ echo -e "${RED}Unknown option: $1${NC}"
4368
+ echo "Run 'loki watch --help' for usage."
4369
+ return 1
4370
+ ;;
4371
+ *)
4372
+ if [ -z "$prd_path" ]; then
4373
+ prd_path="$1"
4374
+ else
4375
+ echo -e "${RED}Unexpected argument: $1${NC}"
4376
+ return 1
4377
+ fi
4378
+ shift
4379
+ ;;
4380
+ esac
4381
+ done
4382
+
4383
+ # Validate numeric arguments
4384
+ if ! [[ "$poll_interval" =~ ^[0-9]+$ ]] || [ "$poll_interval" -lt 1 ]; then
4385
+ echo -e "${RED}Invalid --interval: $poll_interval (expected positive integer)${NC}"
4330
4386
  return 1
4331
4387
  fi
4332
- if [[ "$interval" == "--help" || "$interval" == "-h" ]]; then
4333
- echo -e "${BOLD}loki watch${NC} - Live session monitor (v6.0.0)"
4334
- echo ""
4335
- echo "Usage: loki watch [interval]"
4336
- echo ""
4337
- echo "Options:"
4338
- echo " interval Refresh interval in seconds (default: 2)"
4339
- echo ""
4340
- echo "Displays:"
4341
- echo " - Current iteration and RARV phase"
4342
- echo " - Provider and model tier"
4343
- echo " - Task queue status"
4344
- echo " - Recent log lines"
4345
- echo " - Cost tracking"
4346
- echo ""
4347
- echo "Press Ctrl+C to exit."
4348
- exit 0
4388
+ if ! [[ "$debounce" =~ ^[0-9]+$ ]] || [ "$debounce" -lt 0 ]; then
4389
+ echo -e "${RED}Invalid --debounce: $debounce (expected non-negative integer)${NC}"
4390
+ return 1
4349
4391
  fi
4350
4392
 
4351
- if [ ! -d "$LOKI_DIR" ]; then
4352
- echo -e "${RED}No active session found.${NC}"
4353
- echo "Start a session with: loki start <prd>"
4354
- exit 1
4393
+ # Auto-detect PRD file if not specified
4394
+ if [ -z "$prd_path" ]; then
4395
+ if [ -f "prd.md" ]; then
4396
+ prd_path="prd.md"
4397
+ elif [ -f "PRD.md" ]; then
4398
+ prd_path="PRD.md"
4399
+ else
4400
+ # Find first .md file in current directory
4401
+ local first_md
4402
+ first_md=$(ls -1 *.md 2>/dev/null | head -1 || true)
4403
+ if [ -n "$first_md" ]; then
4404
+ prd_path="$first_md"
4405
+ else
4406
+ echo -e "${RED}No PRD file found.${NC}"
4407
+ echo "Specify a file: loki watch ./my-prd.md"
4408
+ echo "Or create one: prd.md, PRD.md, or any .md file in current directory."
4409
+ return 1
4410
+ fi
4411
+ fi
4355
4412
  fi
4356
4413
 
4357
- echo -e "${BOLD}Loki Mode Watch${NC} (refresh: ${interval}s, Ctrl+C to exit)"
4358
- echo ""
4359
-
4360
- trap 'echo ""; echo "Watch stopped."; return 0' INT TERM
4414
+ if [ ! -f "$prd_path" ]; then
4415
+ echo -e "${RED}PRD file not found: $prd_path${NC}"
4416
+ return 1
4417
+ fi
4361
4418
 
4362
- while true; do
4363
- clear
4364
- echo -e "${BOLD}Loki Mode Watch${NC} $(date '+%H:%M:%S') (refresh: ${interval}s)"
4365
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
4419
+ # Resolve to absolute path
4420
+ prd_path="$(cd "$(dirname "$prd_path")" && pwd)/$(basename "$prd_path")"
4421
+ local prd_basename
4422
+ prd_basename="$(basename "$prd_path")"
4423
+
4424
+ # --once mode: run immediately and exit
4425
+ if [ "$run_once" = true ]; then
4426
+ echo -e "${BOLD}loki watch${NC} --once: running loki start ${prd_basename}"
4427
+ "$0" start "$prd_path"
4428
+ return $?
4429
+ fi
4366
4430
 
4367
- # Session status
4368
- if [ -f "$LOKI_DIR/PAUSE" ]; then
4369
- echo -e "Status: ${YELLOW}PAUSED${NC}"
4370
- elif [ -f "$LOKI_DIR/STOP" ]; then
4371
- echo -e "Status: ${RED}STOPPED${NC}"
4372
- elif [ -f "$LOKI_DIR/loki.pid" ] && kill -0 "$(cat "$LOKI_DIR/loki.pid" 2>/dev/null)" 2>/dev/null; then
4373
- echo -e "Status: ${GREEN}RUNNING${NC}"
4374
- else
4375
- echo -e "Status: ${DIM}IDLE${NC}"
4431
+ # Graceful shutdown handler
4432
+ _watch_cleanup() {
4433
+ echo ""
4434
+ echo "Watch stopped."
4435
+ if [ -n "$_watch_child_pid" ] && kill -0 "$_watch_child_pid" 2>/dev/null; then
4436
+ echo "Stopping running loki session (PID $_watch_child_pid)..."
4437
+ kill "$_watch_child_pid" 2>/dev/null
4438
+ wait "$_watch_child_pid" 2>/dev/null || true
4376
4439
  fi
4377
-
4378
- # Iteration and phase
4379
- if [ -f "$LOKI_DIR/state/iteration" ]; then
4380
- local iter
4381
- iter=$(cat "$LOKI_DIR/state/iteration" 2>/dev/null || echo "?")
4382
- echo -e "Iteration: $iter"
4440
+ # Kill any background watcher process
4441
+ if [ -n "${_watch_bg_pid:-}" ] && kill -0 "$_watch_bg_pid" 2>/dev/null; then
4442
+ kill "$_watch_bg_pid" 2>/dev/null
4443
+ wait "$_watch_bg_pid" 2>/dev/null || true
4383
4444
  fi
4445
+ exit 0
4446
+ }
4447
+ trap '_watch_cleanup' INT TERM
4384
4448
 
4385
- # Provider info
4386
- local prov="${LOKI_PROVIDER:-claude}"
4387
- if [ -f "$LOKI_DIR/state/provider" ]; then
4388
- prov=$(cat "$LOKI_DIR/state/provider" 2>/dev/null || echo "$prov")
4389
- fi
4390
- echo -e "Provider: $prov"
4449
+ local last_run="never"
4391
4450
 
4392
- # Queue status
4393
- echo ""
4394
- echo -e "${CYAN}Task Queue:${NC}"
4395
- for queue in pending in-progress completed failed; do
4396
- local queue_file="$LOKI_DIR/queue/${queue}.json"
4397
- if [ -f "$queue_file" ]; then
4398
- local count
4399
- count=$(_QUEUE_FILE="$queue_file" python3 -c "import json, os; print(len(json.load(open(os.environ['_QUEUE_FILE']))))" 2>/dev/null || echo "0")
4400
- echo " $queue: $count"
4401
- fi
4402
- done
4451
+ echo -e "${BOLD}loki watch${NC} -- monitoring ${prd_basename} for changes"
4452
+ echo "Watching: $prd_path"
4453
+ echo "Debounce: ${debounce}s | Ctrl+C to exit"
4454
+ if [ "$no_auto_start" = true ]; then
4455
+ echo "Mode: observe only (--no-auto-start)"
4456
+ fi
4457
+ echo ""
4403
4458
 
4404
- # Cost tracking
4405
- if [ -f "$LOKI_DIR/state/cost-tracker.json" ]; then
4406
- echo ""
4407
- echo -e "${CYAN}Cost:${NC}"
4408
- _LOKI_COST_FILE="$LOKI_DIR/state/cost-tracker.json" python3 -c "
4409
- import json, os
4410
- with open(os.environ['_LOKI_COST_FILE']) as f:
4411
- data = json.load(f)
4412
- total = data.get('total_cost_usd', 0)
4413
- budget = data.get('budget_limit', 0)
4414
- print(f' Total: \${total:.4f}')
4415
- if budget > 0:
4416
- print(f' Budget: \${budget:.2f} ({total/budget*100:.1f}% used)')
4417
- " 2>/dev/null || true
4459
+ _watch_status_line() {
4460
+ echo -e "\rWatching ${prd_basename} -- last run: ${last_run} -- next: on change"
4461
+ }
4462
+
4463
+ _watch_trigger() {
4464
+ local change_time
4465
+ change_time="$(date '+%H:%M:%S')"
4466
+
4467
+ if [ "$no_auto_start" = true ]; then
4468
+ echo "PRD changed: ${change_time}"
4469
+ return 0
4418
4470
  fi
4419
4471
 
4420
- # Recent log
4421
- echo ""
4422
- echo -e "${CYAN}Recent Activity:${NC}"
4423
- local log_file="$LOKI_DIR/logs/loki-run.log"
4424
- if [ -f "$log_file" ]; then
4425
- tail -5 "$log_file" 2>/dev/null | sed 's/^/ /'
4426
- else
4427
- echo " (no log file)"
4472
+ # Kill any currently running loki session
4473
+ if [ -n "$_watch_child_pid" ] && kill -0 "$_watch_child_pid" 2>/dev/null; then
4474
+ echo "Stopping previous loki session (PID $_watch_child_pid)..."
4475
+ kill "$_watch_child_pid" 2>/dev/null
4476
+ wait "$_watch_child_pid" 2>/dev/null || true
4477
+ _watch_child_pid=""
4428
4478
  fi
4429
4479
 
4430
4480
  echo ""
4431
4481
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
4482
+ echo "PRD changed -- starting loki... (${change_time})"
4483
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
4484
+ echo ""
4432
4485
 
4433
- sleep "$interval"
4434
- done
4486
+ "$0" start "$prd_path" &
4487
+ _watch_child_pid=$!
4488
+ last_run="$change_time"
4489
+ }
4490
+
4491
+ # Determine watcher backend
4492
+ local watcher="poll"
4493
+ if command -v fswatch &>/dev/null; then
4494
+ watcher="fswatch"
4495
+ elif command -v inotifywait &>/dev/null; then
4496
+ watcher="inotifywait"
4497
+ fi
4498
+
4499
+ echo "Watcher: $watcher"
4500
+ _watch_status_line
4501
+
4502
+ case "$watcher" in
4503
+ fswatch)
4504
+ # Use fswatch for native macOS filesystem events
4505
+ fswatch -1 --latency "$debounce" "$prd_path" | while read -r _event; do
4506
+ _watch_trigger
4507
+ _watch_status_line
4508
+ # Re-attach fswatch for next change
4509
+ fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1 || true
4510
+ done
4511
+ # Fallback: loop-based fswatch for continuous watching
4512
+ while true; do
4513
+ fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1
4514
+ _watch_trigger
4515
+ _watch_status_line
4516
+ done
4517
+ ;;
4518
+ inotifywait)
4519
+ while true; do
4520
+ inotifywait -qq -e modify "$prd_path" 2>/dev/null
4521
+ if [ "$debounce" -gt 0 ]; then
4522
+ sleep "$debounce"
4523
+ fi
4524
+ _watch_trigger
4525
+ _watch_status_line
4526
+ done
4527
+ ;;
4528
+ poll)
4529
+ # Universal fallback: stat-based polling
4530
+ local last_mtime
4531
+ last_mtime=$(stat -f '%m' "$prd_path" 2>/dev/null || stat -c '%Y' "$prd_path" 2>/dev/null || echo "0")
4532
+ local debounce_pending=false
4533
+ local debounce_deadline=0
4534
+
4535
+ while true; do
4536
+ sleep "$poll_interval"
4537
+ local current_mtime
4538
+ current_mtime=$(stat -f '%m' "$prd_path" 2>/dev/null || stat -c '%Y' "$prd_path" 2>/dev/null || echo "0")
4539
+
4540
+ if [ "$current_mtime" != "$last_mtime" ]; then
4541
+ last_mtime="$current_mtime"
4542
+ if [ "$debounce" -gt 0 ]; then
4543
+ # Start debounce window
4544
+ debounce_pending=true
4545
+ debounce_deadline=$(( $(date +%s) + debounce ))
4546
+ else
4547
+ _watch_trigger
4548
+ _watch_status_line
4549
+ fi
4550
+ fi
4551
+
4552
+ # Check debounce deadline
4553
+ if [ "$debounce_pending" = true ] && [ "$(date +%s)" -ge "$debounce_deadline" ]; then
4554
+ debounce_pending=false
4555
+ _watch_trigger
4556
+ _watch_status_line
4557
+ fi
4558
+ done
4559
+ ;;
4560
+ esac
4435
4561
  }
4436
4562
 
4437
4563
  #===============================================================================
@@ -13833,47 +13959,448 @@ cmd_syslog() {
13833
13959
 
13834
13960
  # Fetch and display Prometheus metrics from dashboard
13835
13961
  cmd_metrics() {
13836
- local subcommand="${1:-}"
13837
- local port="${LOKI_DASHBOARD_PORT:-57374}"
13838
- local host="127.0.0.1"
13962
+ local show_json=false
13963
+ local last_n=0
13964
+ local save_file=false
13965
+ local share_flag=false
13839
13966
 
13840
- case "$subcommand" in
13841
- help|--help|-h)
13842
- echo -e "${BOLD}loki metrics${NC} - Prometheus/OpenMetrics metrics"
13843
- echo ""
13844
- echo "Usage: loki metrics [options]"
13845
- echo ""
13846
- echo "Fetches metrics from the dashboard API in Prometheus/OpenMetrics format."
13847
- echo "The dashboard must be running (loki dashboard start or loki serve)."
13848
- echo ""
13849
- echo "Options:"
13850
- echo " help Show this help"
13851
- echo ""
13852
- echo "Environment:"
13853
- echo " LOKI_DASHBOARD_PORT Dashboard port (default: 57374)"
13854
- echo ""
13855
- echo "Examples:"
13856
- echo " loki metrics # Display all metrics"
13857
- echo " loki metrics | grep loki_cost_usd # Filter specific metric"
13858
- ;;
13859
- "")
13860
- # Fetch metrics from dashboard
13861
- local url="http://${host}:${port}/metrics"
13862
- local response
13863
- response=$(curl -sf "$url" 2>/dev/null) || true
13864
- if [ -z "$response" ]; then
13865
- echo -e "${RED}Error: Could not connect to dashboard at ${url}${NC}"
13866
- echo "Make sure the dashboard is running: loki serve"
13867
- exit 1
13868
- fi
13869
- echo "$response"
13870
- ;;
13871
- *)
13872
- echo -e "${RED}Unknown metrics command: $subcommand${NC}"
13873
- echo "Run 'loki metrics help' for usage."
13967
+ while [[ $# -gt 0 ]]; do
13968
+ case "$1" in
13969
+ --help|-h)
13970
+ echo -e "${BOLD}loki metrics${NC} - Session productivity reporter"
13971
+ echo ""
13972
+ echo "Usage: loki metrics [options]"
13973
+ echo ""
13974
+ echo "Analyzes past Loki Mode session data and generates a productivity"
13975
+ echo "stats report showing agents deployed, iterations completed, files"
13976
+ echo "changed, and estimated time saved."
13977
+ echo ""
13978
+ echo "Options:"
13979
+ echo " --json Machine-readable JSON output"
13980
+ echo " --last N Analyze only the last N sessions (default: all)"
13981
+ echo " --save Write report to METRICS.md in project root"
13982
+ echo " --share Upload report as GitHub Gist (via loki share)"
13983
+ echo " --help, -h Show this help"
13984
+ echo ""
13985
+ echo "Subcommands:"
13986
+ echo " prometheus Fetch Prometheus/OpenMetrics from dashboard"
13987
+ echo ""
13988
+ echo "Examples:"
13989
+ echo " loki metrics # Full productivity report"
13990
+ echo " loki metrics --json # Machine-readable output"
13991
+ echo " loki metrics --last 5 # Last 5 sessions only"
13992
+ echo " loki metrics --save # Write METRICS.md"
13993
+ echo " loki metrics --share # Share as GitHub Gist"
13994
+ echo " loki metrics prometheus # Prometheus metrics from dashboard"
13995
+ exit 0
13996
+ ;;
13997
+ --json) show_json=true; shift ;;
13998
+ --last) last_n="${2:-0}"; shift 2 ;;
13999
+ --last=*) last_n="${1#*=}"; shift ;;
14000
+ --save) save_file=true; shift ;;
14001
+ --share) share_flag=true; shift ;;
14002
+ prometheus)
14003
+ # Legacy Prometheus metrics subcommand
14004
+ shift
14005
+ local port="${LOKI_DASHBOARD_PORT:-57374}"
14006
+ local host="127.0.0.1"
14007
+ local url="http://${host}:${port}/metrics"
14008
+ local response
14009
+ response=$(curl -sf "$url" 2>/dev/null) || true
14010
+ if [ -z "$response" ]; then
14011
+ echo -e "${RED}Error: Could not connect to dashboard at ${url}${NC}"
14012
+ echo "Make sure the dashboard is running: loki serve"
14013
+ exit 1
14014
+ fi
14015
+ echo "$response"
14016
+ exit 0
14017
+ ;;
14018
+ *) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki metrics --help' for usage."; exit 1 ;;
14019
+ esac
14020
+ done
14021
+
14022
+ local loki_dir="${LOKI_DIR:-.loki}"
14023
+
14024
+ if ! command -v python3 &>/dev/null; then
14025
+ echo -e "${RED}python3 is required for metrics generation${NC}"
14026
+ exit 1
14027
+ fi
14028
+
14029
+ local metrics_output
14030
+ metrics_output=$(LOKI_DIR="$loki_dir" \
14031
+ METRICS_JSON="$show_json" \
14032
+ METRICS_LAST_N="$last_n" \
14033
+ METRICS_SAVE="$save_file" \
14034
+ python3 << 'METRICS_SCRIPT'
14035
+ import json
14036
+ import os
14037
+ import sys
14038
+ import glob
14039
+ import subprocess
14040
+ from datetime import datetime
14041
+
14042
+ loki_dir = os.environ.get("LOKI_DIR", ".loki")
14043
+ show_json = os.environ.get("METRICS_JSON", "false") == "true"
14044
+ last_n = int(os.environ.get("METRICS_LAST_N", "0"))
14045
+ save_flag = os.environ.get("METRICS_SAVE", "false") == "true"
14046
+
14047
+ def load_json(path):
14048
+ try:
14049
+ with open(path) as f:
14050
+ return json.load(f)
14051
+ except Exception:
14052
+ return None
14053
+
14054
+ def fmt_number(n):
14055
+ return f"{n:,}"
14056
+
14057
+ def load_queue(path):
14058
+ data = load_json(path)
14059
+ if isinstance(data, list):
14060
+ return data
14061
+ if isinstance(data, dict) and "tasks" in data:
14062
+ return data["tasks"]
14063
+ return []
14064
+
14065
+ # --- Gather project name ---
14066
+ project_name = os.path.basename(os.getcwd())
14067
+
14068
+ # --- Session data ---
14069
+ sessions_analyzed = 0
14070
+ total_iterations = 0
14071
+ total_tasks_completed = 0
14072
+ total_tasks_failed = 0
14073
+ agent_types_seen = set()
14074
+ total_duration_seconds = 0
14075
+
14076
+ # Count sessions from checkpoints index or autonomy-state
14077
+ checkpoint_index = os.path.join(loki_dir, "state", "checkpoints", "index.jsonl")
14078
+ session_entries = []
14079
+ if os.path.isfile(checkpoint_index):
14080
+ try:
14081
+ with open(checkpoint_index) as f:
14082
+ for line in f:
14083
+ line = line.strip()
14084
+ if line:
14085
+ try:
14086
+ session_entries.append(json.loads(line))
14087
+ except Exception:
14088
+ pass
14089
+ except Exception:
14090
+ pass
14091
+
14092
+ # Orchestrator state
14093
+ orch = load_json(os.path.join(loki_dir, "state", "orchestrator.json")) or {}
14094
+ current_phase = orch.get("currentPhase", "N/A")
14095
+ orch_metrics = orch.get("metrics", {})
14096
+ tasks_completed_orch = orch_metrics.get("tasksCompleted", 0)
14097
+ tasks_failed_orch = orch_metrics.get("tasksFailed", 0)
14098
+
14099
+ # Per-iteration efficiency files
14100
+ eff_dir = os.path.join(loki_dir, "metrics", "efficiency")
14101
+ iterations = []
14102
+ if os.path.isdir(eff_dir):
14103
+ for path in sorted(glob.glob(os.path.join(eff_dir, "iteration-*.json"))):
14104
+ data = load_json(path)
14105
+ if data:
14106
+ iterations.append(data)
14107
+
14108
+ # Apply --last N filter
14109
+ if last_n > 0 and len(iterations) > last_n:
14110
+ iterations = iterations[-last_n:]
14111
+
14112
+ total_iterations = len(iterations) if iterations else max(orch.get("currentIteration", 0), 0)
14113
+
14114
+ # Gather agent types from iterations and agents.json
14115
+ for it in iterations:
14116
+ model = it.get("model", "")
14117
+ if model:
14118
+ agent_types_seen.add(model)
14119
+ total_duration_seconds += it.get("duration_seconds", 0)
14120
+
14121
+ agents_file = os.path.join(loki_dir, "state", "agents.json")
14122
+ agents_data = load_json(agents_file)
14123
+ if isinstance(agents_data, list):
14124
+ for agent in agents_data:
14125
+ atype = agent.get("agent_type", "")
14126
+ if atype:
14127
+ agent_types_seen.add(atype)
14128
+
14129
+ # Queue data for task counts
14130
+ completed_tasks = load_queue(os.path.join(loki_dir, "queue", "completed.json"))
14131
+ failed_tasks = load_queue(os.path.join(loki_dir, "queue", "failed.json"))
14132
+ pending_tasks = load_queue(os.path.join(loki_dir, "queue", "pending.json"))
14133
+ in_progress_tasks = load_queue(os.path.join(loki_dir, "queue", "in-progress.json"))
14134
+
14135
+ total_tasks_completed = len(completed_tasks) if completed_tasks else tasks_completed_orch
14136
+ total_tasks_failed = len(failed_tasks) if failed_tasks else tasks_failed_orch
14137
+ total_tasks = total_tasks_completed + total_tasks_failed + len(pending_tasks) + len(in_progress_tasks)
14138
+ success_rate = (total_tasks_completed / total_tasks * 100) if total_tasks > 0 else 0.0
14139
+
14140
+ # Session count: count unique session directories or fall back to 1 if .loki exists
14141
+ sessions_dir = os.path.join(loki_dir, "state", "sessions")
14142
+ if os.path.isdir(sessions_dir):
14143
+ sessions_analyzed = len([d for d in os.listdir(sessions_dir) if os.path.isdir(os.path.join(sessions_dir, d))])
14144
+ if sessions_analyzed == 0:
14145
+ # Check autonomy-state.json retryCount as proxy for session attempts
14146
+ auto_state = load_json(os.path.join(loki_dir, "autonomy-state.json"))
14147
+ if auto_state:
14148
+ sessions_analyzed = max(1, auto_state.get("retryCount", 1))
14149
+ elif os.path.isdir(loki_dir):
14150
+ sessions_analyzed = 1
14151
+
14152
+ # Apply --last N to sessions
14153
+ if last_n > 0:
14154
+ sessions_analyzed = min(sessions_analyzed, last_n)
14155
+
14156
+ # --- Git stats ---
14157
+ lines_added = 0
14158
+ lines_removed = 0
14159
+ commits_made = 0
14160
+ files_changed_git = 0
14161
+ tests_written = 0
14162
+
14163
+ try:
14164
+ result = subprocess.run(
14165
+ ["git", "log", "--shortstat", "--format="],
14166
+ capture_output=True, text=True, timeout=10
14167
+ )
14168
+ if result.returncode == 0:
14169
+ for line in result.stdout.strip().split("\n"):
14170
+ line = line.strip()
14171
+ if not line:
14172
+ continue
14173
+ parts = line.split(",")
14174
+ for part in parts:
14175
+ part = part.strip()
14176
+ if "file" in part and "changed" in part:
14177
+ try:
14178
+ files_changed_git += int(part.split()[0])
14179
+ except (ValueError, IndexError):
14180
+ pass
14181
+ elif "insertion" in part:
14182
+ try:
14183
+ lines_added += int(part.split()[0])
14184
+ except (ValueError, IndexError):
14185
+ pass
14186
+ elif "deletion" in part:
14187
+ try:
14188
+ lines_removed += int(part.split()[0])
14189
+ except (ValueError, IndexError):
14190
+ pass
14191
+
14192
+ # Count commits
14193
+ result2 = subprocess.run(
14194
+ ["git", "rev-list", "--count", "HEAD"],
14195
+ capture_output=True, text=True, timeout=10
14196
+ )
14197
+ if result2.returncode == 0:
14198
+ commits_made = int(result2.stdout.strip())
14199
+ except Exception:
14200
+ pass
14201
+
14202
+ # Estimate tests written from git log (files matching test/spec patterns)
14203
+ try:
14204
+ result3 = subprocess.run(
14205
+ ["git", "log", "--diff-filter=A", "--name-only", "--format="],
14206
+ capture_output=True, text=True, timeout=10
14207
+ )
14208
+ if result3.returncode == 0:
14209
+ for fname in result3.stdout.strip().split("\n"):
14210
+ fname = fname.strip().lower()
14211
+ if any(kw in fname for kw in ["test", "spec", ".test.", ".spec.", "_test.", "_spec."]):
14212
+ tests_written += 1
14213
+ except Exception:
14214
+ pass
14215
+
14216
+ # --- Token/cost data ---
14217
+ total_cost = sum(it.get("cost_usd", 0) for it in iterations)
14218
+ total_input_tokens = sum(it.get("input_tokens", 0) for it in iterations)
14219
+ total_output_tokens = sum(it.get("output_tokens", 0) for it in iterations)
14220
+
14221
+ # Also check context tracking for cost data
14222
+ ctx_file = os.path.join(loki_dir, "context", "tracking.json")
14223
+ ctx = load_json(ctx_file)
14224
+ if ctx and total_cost == 0:
14225
+ totals = ctx.get("totals", {})
14226
+ total_cost = totals.get("total_cost_usd", 0)
14227
+ total_input_tokens = totals.get("total_input", 0)
14228
+ total_output_tokens = totals.get("total_output", 0)
14229
+
14230
+ # --- Time saved estimate ---
14231
+ # Each Loki iteration replaces ~15min of manual work
14232
+ time_saved_hours = round(total_iterations * 15 / 60, 2)
14233
+
14234
+ # --- Memory stats ---
14235
+ episodic_count = 0
14236
+ semantic_count = 0
14237
+ episodic_dir = os.path.join(loki_dir, "memory", "episodic")
14238
+ semantic_dir = os.path.join(loki_dir, "memory", "semantic")
14239
+ if os.path.isdir(episodic_dir):
14240
+ for root, dirs, files in os.walk(episodic_dir):
14241
+ episodic_count += len([f for f in files if f.endswith(".json")])
14242
+ if os.path.isdir(semantic_dir):
14243
+ for root, dirs, files in os.walk(semantic_dir):
14244
+ semantic_count += len([f for f in files if f.endswith(".json")])
14245
+
14246
+ # --- Build output ---
14247
+ version = "6.32.0"
14248
+ try:
14249
+ version_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "VERSION")
14250
+ # In heredoc context, __file__ is not reliable; try relative
14251
+ for vf in ["VERSION", "../VERSION", os.path.join(loki_dir, "..", "VERSION")]:
14252
+ if os.path.isfile(vf):
14253
+ with open(vf) as f:
14254
+ version = f.read().strip()
14255
+ break
14256
+ except Exception:
14257
+ pass
14258
+
14259
+ data = {
14260
+ "project": project_name,
14261
+ "sessions_analyzed": sessions_analyzed,
14262
+ "agent_activity": {
14263
+ "total_iterations": total_iterations,
14264
+ "agent_types": len(agent_types_seen),
14265
+ "tasks_completed": total_tasks_completed,
14266
+ "tasks_failed": total_tasks_failed,
14267
+ "success_rate": round(success_rate, 1)
14268
+ },
14269
+ "code_output": {
14270
+ "files_changed": files_changed_git,
14271
+ "lines_added": lines_added,
14272
+ "lines_removed": lines_removed,
14273
+ "commits": commits_made,
14274
+ "tests_written": tests_written
14275
+ },
14276
+ "tokens": {
14277
+ "input": total_input_tokens,
14278
+ "output": total_output_tokens,
14279
+ "total": total_input_tokens + total_output_tokens,
14280
+ "cost_usd": round(total_cost, 2)
14281
+ },
14282
+ "time_saved": {
14283
+ "hours": time_saved_hours,
14284
+ "iterations": total_iterations,
14285
+ "minutes_per_iteration": 15
14286
+ },
14287
+ "memory": {
14288
+ "episodic_entries": episodic_count,
14289
+ "semantic_patterns": semantic_count
14290
+ },
14291
+ "version": version
14292
+ }
14293
+
14294
+ if show_json:
14295
+ print(json.dumps(data, indent=2))
14296
+ else:
14297
+ # ASCII stats card
14298
+ lines = []
14299
+ lines.append("")
14300
+ lines.append("LOKI MODE PRODUCTIVITY REPORT")
14301
+ lines.append("=" * 40)
14302
+ lines.append(f"Project: {project_name}")
14303
+ lines.append(f"Sessions analyzed: {sessions_analyzed}")
14304
+ lines.append("")
14305
+ lines.append("AGENT ACTIVITY")
14306
+ lines.append(f" Total iterations: {fmt_number(total_iterations)}")
14307
+ lines.append(f" Agent types used: {len(agent_types_seen)}")
14308
+ lines.append(f" Tasks completed: {fmt_number(total_tasks_completed)}")
14309
+ if total_tasks_failed > 0:
14310
+ lines.append(f" Tasks failed: {fmt_number(total_tasks_failed)}")
14311
+ if total_tasks > 0:
14312
+ lines.append(f" Success rate: {success_rate:.1f}%")
14313
+ lines.append("")
14314
+ lines.append("CODE OUTPUT")
14315
+ lines.append(f" Files changed: {fmt_number(files_changed_git)}")
14316
+ lines.append(f" Lines added: {fmt_number(lines_added)}")
14317
+ lines.append(f" Lines removed: {fmt_number(lines_removed)}")
14318
+ lines.append(f" Commits: {fmt_number(commits_made)}")
14319
+ lines.append(f" Tests written: {fmt_number(tests_written)}")
14320
+ lines.append("")
14321
+ if total_cost > 0 or total_input_tokens > 0:
14322
+ lines.append("TOKEN USAGE")
14323
+ lines.append(f" Input tokens: {fmt_number(total_input_tokens)}")
14324
+ lines.append(f" Output tokens: {fmt_number(total_output_tokens)}")
14325
+ lines.append(f" Estimated cost: ${total_cost:.2f}")
14326
+ lines.append("")
14327
+ lines.append("TIME SAVED")
14328
+ lines.append(f" Estimated: {time_saved_hours} hours")
14329
+ lines.append(f" (based on {fmt_number(total_iterations)} iterations x 15min each)")
14330
+ lines.append("")
14331
+ if episodic_count > 0 or semantic_count > 0:
14332
+ lines.append("MEMORY")
14333
+ lines.append(f" Episodic entries: {fmt_number(episodic_count)}")
14334
+ lines.append(f" Semantic patterns: {fmt_number(semantic_count)}")
14335
+ lines.append("")
14336
+ lines.append(f"Generated by Loki Mode v{version} | autonomi.dev")
14337
+ lines.append("Share your stats: loki metrics --share")
14338
+ lines.append("")
14339
+
14340
+ output_text = "\n".join(lines)
14341
+ print(output_text)
14342
+
14343
+ # Save to METRICS.md if requested
14344
+ if save_flag:
14345
+ try:
14346
+ with open("METRICS.md", "w") as f:
14347
+ f.write("# Loki Mode Productivity Report\n\n")
14348
+ f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
14349
+ f.write("```\n")
14350
+ f.write(output_text.strip())
14351
+ f.write("\n```\n")
14352
+ print("Saved to METRICS.md")
14353
+ except Exception as e:
14354
+ print(f"Error saving METRICS.md: {e}", file=sys.stderr)
14355
+
14356
+ METRICS_SCRIPT
14357
+ )
14358
+ local metrics_exit=$?
14359
+
14360
+ if [ $metrics_exit -ne 0 ]; then
14361
+ echo -e "${RED}Failed to generate metrics report${NC}"
14362
+ echo "$metrics_output"
14363
+ exit 1
14364
+ fi
14365
+
14366
+ echo "$metrics_output"
14367
+
14368
+ # Handle --share flag
14369
+ if [ "$share_flag" = true ]; then
14370
+ local tmpfile
14371
+ tmpfile=$(mktemp "/tmp/loki-metrics-XXXXXX.md")
14372
+ echo "$metrics_output" > "$tmpfile"
14373
+
14374
+ if ! command -v gh &>/dev/null; then
14375
+ echo -e "${RED}gh CLI not found -- cannot share${NC}"
14376
+ echo "Install: brew install gh"
14377
+ rm -f "$tmpfile"
13874
14378
  exit 1
13875
- ;;
13876
- esac
14379
+ fi
14380
+
14381
+ if ! gh auth status &>/dev/null 2>&1; then
14382
+ echo -e "${RED}GitHub CLI not authenticated${NC}"
14383
+ echo "Run 'gh auth login' to authenticate."
14384
+ rm -f "$tmpfile"
14385
+ exit 1
14386
+ fi
14387
+
14388
+ echo "Uploading metrics report..."
14389
+ local gist_desc="Loki Mode productivity report - ${project_name:-project} ($(date +%Y-%m-%d))"
14390
+ local project_name
14391
+ project_name=$(basename "$(pwd)")
14392
+ local gist_url
14393
+ gist_url=$(gh gist create "$tmpfile" --desc "$gist_desc" --public 2>&1)
14394
+ local gist_exit=$?
14395
+ rm -f "$tmpfile"
14396
+
14397
+ if [ $gist_exit -ne 0 ]; then
14398
+ echo -e "${RED}Failed to create gist${NC}"
14399
+ echo "$gist_url"
14400
+ exit 1
14401
+ fi
14402
+ echo -e "${GREEN}Shared: ${gist_url}${NC}"
14403
+ fi
13877
14404
  }
13878
14405
 
13879
14406
  # Output shell completion scripts
@@ -85,6 +85,30 @@ if (results.some(r => !r.ok)) {
85
85
  console.log(` loki setup-skill`);
86
86
  }
87
87
 
88
+ // PATH check: warn if npm global bin is not in PATH
89
+ try {
90
+ const { execSync } = require('child_process');
91
+ const npmBin = execSync('npm bin -g 2>/dev/null || npm prefix -g', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
92
+ const npmBinDir = npmBin.endsWith('/bin') ? npmBin : npmBin + '/bin';
93
+ const pathDirs = (process.env.PATH || '').split(':');
94
+ const lokiBinInPath = pathDirs.some(d => d === npmBinDir || d === npmBin);
95
+ if (!lokiBinInPath) {
96
+ console.log('');
97
+ console.log('[IMPORTANT] The `loki` command may not be in your PATH.');
98
+ console.log('');
99
+ console.log('Add the npm global bin directory to your PATH:');
100
+ console.log(` export PATH="${npmBinDir}:$PATH"`);
101
+ console.log('');
102
+ console.log('To make this permanent, add it to your shell config (~/.zshrc or ~/.bashrc):');
103
+ console.log(` echo 'export PATH="${npmBinDir}:$PATH"' >> ~/.zshrc && source ~/.zshrc`);
104
+ console.log('');
105
+ console.log('Or use the Homebrew tap (sets PATH automatically):');
106
+ console.log(' brew tap asklokesh/tap && brew install loki-mode');
107
+ }
108
+ } catch {
109
+ // If npm bin check fails, skip PATH warning silently
110
+ }
111
+
88
112
  console.log('');
89
113
  console.log('CLI commands:');
90
114
  console.log(' loki start ./prd.md Start with Claude (default)');
package/completions/_loki CHANGED
@@ -70,6 +70,15 @@ function _loki {
70
70
  issue)
71
71
  _loki_issue
72
72
  ;;
73
+ metrics)
74
+ _arguments \
75
+ '--json[Machine-readable JSON output]' \
76
+ '--last[Analyze last N sessions]:number:' \
77
+ '--save[Write report to METRICS.md]' \
78
+ '--share[Upload report as GitHub Gist]' \
79
+ '--help[Show help]' \
80
+ '1:subcommand:(prometheus)'
81
+ ;;
73
82
  share)
74
83
  _arguments \
75
84
  '--private[Create a secret gist]' \
@@ -115,6 +124,7 @@ function _loki_commands {
115
124
  'voice:Voice input commands'
116
125
  'doctor:Check system prerequisites'
117
126
  'onboard:Analyze repo and generate CLAUDE.md'
127
+ 'metrics:Session productivity report'
118
128
  'share:Share session report as GitHub Gist'
119
129
  'version:Show version'
120
130
  'completions:Output shell completions'
@@ -5,7 +5,7 @@ _loki_completion() {
5
5
  _init_completion || return
6
6
 
7
7
  # Main subcommands (must match autonomy/loki main case statement)
8
- local main_commands="start quick demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise voice secrets doctor watchdog audit metrics syslog onboard share version completions help"
8
+ local main_commands="start quick demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise voice secrets doctor watchdog audit metrics syslog onboard share explain plan report test ci watch version completions help"
9
9
 
10
10
  # 1. If we are on the first argument (subcommand)
11
11
  if [[ $cword -eq 1 ]]; then
@@ -144,6 +144,24 @@ _loki_completion() {
144
144
  _filedir -d
145
145
  ;;
146
146
 
147
+ metrics)
148
+ if [[ "$cur" == -* ]]; then
149
+ COMPREPLY=( $(compgen -W "--json --last --save --share --help" -- "$cur") )
150
+ return 0
151
+ fi
152
+ local metrics_cmds="prometheus"
153
+ COMPREPLY=( $(compgen -W "${metrics_cmds}" -- "$cur") )
154
+ ;;
155
+
156
+ watch)
157
+ if [[ "$cur" == -* ]]; then
158
+ COMPREPLY=( $(compgen -W "--once --interval --no-auto-start --debounce --help" -- "$cur") )
159
+ return 0
160
+ fi
161
+ # Default to file completion for PRD files
162
+ COMPREPLY=( $(compgen -f -- "$cur") )
163
+ ;;
164
+
147
165
  share)
148
166
  if [[ "$cur" == -* ]]; then
149
167
  COMPREPLY=( $(compgen -W "--private --format --help" -- "$cur") )
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.31.0"
10
+ __version__ = "6.32.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v6.31.0
5
+ **Version:** v6.32.1
6
6
 
7
7
  ---
8
8
 
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.31.0'
60
+ __version__ = '6.32.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "6.31.0",
3
+ "version": "6.32.1",
4
4
  "description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "agent",