loki-mode 5.33.0 → 5.35.0

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
@@ -304,14 +304,14 @@ Last Updated: 2026-01-04 20:45:32
304
304
 
305
305
  **Access the dashboard:**
306
306
  ```bash
307
- # Automatically opens when running autonomously
307
+ # Automatically starts when running autonomously
308
308
  ./autonomy/run.sh ./docs/requirements.md
309
309
 
310
310
  # Or open manually
311
- open .loki/dashboard/index.html
311
+ open http://localhost:57374
312
312
  ```
313
313
 
314
- Auto-refreshes every 3 seconds. Works with any modern browser.
314
+ The dashboard at `http://localhost:57374` auto-refreshes via WebSocket. Works with any modern browser.
315
315
 
316
316
  ---
317
317
 
@@ -488,7 +488,7 @@ graph TB
488
488
 
489
489
  ---
490
490
 
491
- ## CLI Commands (v4.1.0)
491
+ ## CLI Commands
492
492
 
493
493
  The `loki` CLI provides easy access to all Loki Mode features:
494
494
 
@@ -663,7 +663,7 @@ loki memory retrieve "query" # Test task-aware retrieval
663
663
  ```
664
664
 
665
665
  **API Endpoints:**
666
- - `GET /api/memory` - Memory summary
666
+ - `GET /api/memory/summary` - Memory summary
667
667
  - `POST /api/memory/retrieve` - Query memories
668
668
  - `POST /api/memory/consolidate` - Trigger consolidation
669
669
  - `GET /api/memory/economics` - Token economics
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.33.0
6
+ # Loki Mode v5.35.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -149,6 +149,7 @@ GROWTH ──[continuous improvement loop]──> GROWTH
149
149
  | `.loki/queue/dead-letter.json` | Session start | On task failure (5+ attempts) |
150
150
  | `.loki/signals/CONTEXT_CLEAR_REQUESTED` | Never | When context heavy |
151
151
  | `.loki/signals/HUMAN_REVIEW_NEEDED` | Never | When human decision required |
152
+ | `.loki/state/checkpoints/` | After task completion | Automatic + manual via `loki checkpoint` |
152
153
 
153
154
  ---
154
155
 
@@ -254,9 +255,9 @@ The following features are documented in skill modules but not yet fully automat
254
255
  | Feature | Status | Notes |
255
256
  |---------|--------|-------|
256
257
  | PRE-ACT goal drift detection | Planned | Agent-level attention check before each action; no automated enforcement yet |
257
- | CONTINUITY.md working memory | Planned | Referenced in run.sh prompts but not automatically managed |
258
+ | CONTINUITY.md working memory | Implemented (v5.35.0) | Auto-managed by run.sh, updated each iteration |
258
259
  | GitHub issue import | Planned | Config flags exist (`LOKI_GITHUB_IMPORT`); `gh` CLI integration partial |
259
- | Quality gates 3-reviewer system | Planned | Instructions in `skills/quality-gates.md`; not automated |
260
+ | Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
260
261
  | Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
261
262
 
262
- **v5.33.0 | audit cleanup, honest feature claims | ~260 lines core**
263
+ **v5.35.0 | checkpoint/restore, GitHub Action provider-agnostic | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.33.0
1
+ 5.35.0
package/api/README.md CHANGED
@@ -33,7 +33,7 @@ brew install deno
33
33
 
34
34
  | Variable | Default | Description |
35
35
  |----------|---------|-------------|
36
- | `LOKI_API_PORT` | `8420` | Server port |
36
+ | `LOKI_DASHBOARD_PORT` | `57374` | Server port |
37
37
  | `LOKI_API_HOST` | `localhost` | Server host |
38
38
  | `LOKI_API_TOKEN` | none | API token for remote access |
39
39
  | `LOKI_DIR` | auto | Loki installation directory |
package/api/server.js CHANGED
@@ -33,7 +33,7 @@ const { EventEmitter } = require('events');
33
33
  // Configuration
34
34
  //=============================================================================
35
35
 
36
- const DEFAULT_PORT = 9898;
36
+ const DEFAULT_PORT = 57374;
37
37
  const DEFAULT_HOST = '127.0.0.1';
38
38
  const PROJECT_DIR = process.env.LOKI_PROJECT_DIR || process.cwd();
39
39
 
package/api/server.ts CHANGED
@@ -84,8 +84,8 @@ interface ServerConfig {
84
84
  }
85
85
 
86
86
  const defaultConfig: ServerConfig = {
87
- port: parseInt(Deno.env.get("LOKI_API_PORT") || "8420", 10),
88
- host: Deno.env.get("LOKI_API_HOST") || "localhost",
87
+ port: parseInt(Deno.env.get("LOKI_DASHBOARD_PORT") || "57374", 10),
88
+ host: Deno.env.get("LOKI_DASHBOARD_HOST") || "localhost",
89
89
  cors: true,
90
90
  auth: true,
91
91
  };
@@ -365,15 +365,15 @@ Usage:
365
365
  deno run --allow-all api/server.ts [options]
366
366
 
367
367
  Options:
368
- --port, -p <port> Port to listen on (default: 8420)
368
+ --port, -p <port> Port to listen on (default: 57374)
369
369
  --host, -h <host> Host to bind to (default: localhost)
370
370
  --no-cors Disable CORS
371
371
  --no-auth Disable authentication
372
372
  --help Show this help message
373
373
 
374
374
  Environment Variables:
375
- LOKI_API_PORT Port (overridden by --port)
376
- LOKI_API_HOST Host (overridden by --host)
375
+ LOKI_DASHBOARD_PORT Port (overridden by --port)
376
+ LOKI_DASHBOARD_HOST Host (overridden by --host)
377
377
  LOKI_API_TOKEN API token for remote access
378
378
  LOKI_DIR Loki installation directory
379
379
  LOKI_VERSION Version string
package/api/test.js CHANGED
@@ -34,7 +34,7 @@ const fs = require('fs');
34
34
  // Test Configuration
35
35
  //=============================================================================
36
36
 
37
- const PORT = 19898; // Use a different port for testing (9898 + 10000)
37
+ const PORT = 67374; // Use a different port for testing (57374 + 10000)
38
38
  const HOST = '127.0.0.1';
39
39
  const BASE_URL = `http://${HOST}:${PORT}`;
40
40
  const SERVER_PATH = path.join(__dirname, 'server.js');
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Loki Mode HTTP API Server (v1.2.0)
3
+ * Loki Mode HTTP API Server (v1.2.0) - LEGACY
4
+ * NOTE: The production API is now the unified FastAPI server at dashboard/server.py (port 57374)
5
+ * This file is kept for backward compatibility only.
4
6
  * Zero npm dependencies - uses only Node.js built-ins
5
7
  *
6
8
  * Usage:
7
- * node autonomy/api-server.js [--port 9898]
8
- * LOKI_API_PORT=9898 node autonomy/api-server.js
9
+ * node autonomy/api-server.js [--port 57374]
10
+ * LOKI_DASHBOARD_PORT=57374 node autonomy/api-server.js
9
11
  * loki api start
10
12
  *
11
13
  * Endpoints:
@@ -69,7 +71,7 @@ function parseArgs() {
69
71
  const cliArgs = parseArgs();
70
72
 
71
73
  // Configuration
72
- const PORT = cliArgs.port || parseInt(process.env.LOKI_API_PORT || '9898');
74
+ const PORT = cliArgs.port || parseInt(process.env.LOKI_DASHBOARD_PORT || '57374');
73
75
  const MAX_BODY_SIZE = parseInt(process.env.LOKI_API_MAX_BODY || '1048576'); // 1MB default
74
76
  const LOKI_DIR = process.env.LOKI_DIR || path.join(process.cwd(), '.loki');
75
77
  const STATE_DIR = path.join(LOKI_DIR, 'state');
@@ -823,6 +823,7 @@ council_aggregate_votes() {
823
823
  _THRESHOLD="$threshold" \
824
824
  _VERDICT="$verdict" \
825
825
  _VOTES="$votes_json" \
826
+ _ROUND_FILE="$round_file" \
826
827
  python3 -c "
827
828
  import json, os
828
829
  from datetime import datetime, timezone
@@ -836,7 +837,7 @@ round_data = {
836
837
  'verdict': os.environ['_VERDICT'],
837
838
  'votes': json.loads(os.environ['_VOTES'])
838
839
  }
839
- with open('$round_file', 'w') as f:
840
+ with open(os.environ['_ROUND_FILE'], 'w') as f:
840
841
  json.dump(round_data, f, indent=2)
841
842
  " || log_warn "Failed to write round vote file"
842
843
 
@@ -926,6 +927,7 @@ council_devils_advocate_review() {
926
927
  _ROUND="$round" \
927
928
  _ISSUES="$issues_found" \
928
929
  _DETAILS="${issue_details:-none}" \
930
+ _DA_FILE="$da_file" \
929
931
  python3 -c "
930
932
  import json, os
931
933
  from datetime import datetime, timezone
@@ -936,7 +938,7 @@ da_result = {
936
938
  'details': os.environ['_DETAILS'],
937
939
  'override': int(os.environ['_ISSUES']) > 0
938
940
  }
939
- with open('$da_file', 'w') as f:
941
+ with open(os.environ['_DA_FILE'], 'w') as f:
940
942
  json.dump(da_result, f, indent=2)
941
943
  " || log_warn "Failed to write devil's advocate result"
942
944
 
@@ -21,7 +21,7 @@ sys.path.insert(0, cwd)
21
21
  try:
22
22
  from memory.engine import MemoryEngine
23
23
  from memory.schemas import EpisodeTrace
24
- from datetime import datetime
24
+ from datetime import datetime, timezone
25
25
  import json
26
26
 
27
27
  engine = MemoryEngine(os.path.join(cwd, '.loki/memory'))
@@ -30,7 +30,7 @@ try:
30
30
  episode = EpisodeTrace(
31
31
  id=session_id,
32
32
  task_id=f'session-{session_id}',
33
- timestamp=datetime.utcnow(),
33
+ timestamp=datetime.now(timezone.utc),
34
34
  duration_seconds=0,
35
35
  agent='loki-mode',
36
36
  phase='session',
package/autonomy/loki CHANGED
@@ -310,8 +310,8 @@ show_help() {
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)"
313
- echo " serve Start HTTP API server (alias for api start)"
314
- echo " api [cmd] HTTP API server (start|stop|status)"
313
+ echo " serve Start dashboard/API server (alias for api start)"
314
+ echo " api [cmd] Dashboard/API server (start|stop|status)"
315
315
  echo " sandbox [cmd] Docker sandbox (start|stop|status|logs|shell|build)"
316
316
  echo " notify [cmd] Send notifications (test|slack|discord|webhook|status)"
317
317
  echo " voice [cmd] Voice input for PRD creation (status|listen|dictate|speak|start)"
@@ -2762,18 +2762,17 @@ cmd_logs() {
2762
2762
  tail -n "$lines" "$log_file"
2763
2763
  }
2764
2764
 
2765
- # API server management
2765
+ # API server management (delegates to unified FastAPI dashboard server)
2766
2766
  cmd_api() {
2767
2767
  local subcommand="${1:-help}"
2768
- local port="${LOKI_API_PORT:-9898}"
2769
- local pid_file="$LOKI_DIR/api.pid"
2770
- local api_server="$SKILL_DIR/api/server.js"
2768
+ local port="${LOKI_DASHBOARD_PORT:-57374}"
2769
+ local pid_file="$LOKI_DIR/dashboard/dashboard.pid"
2771
2770
 
2772
2771
  case "$subcommand" in
2773
2772
  start)
2774
- # Check if Node.js is available
2775
- if ! command -v node &> /dev/null; then
2776
- echo -e "${RED}Error: Node.js not found. Install with: brew install node${NC}"
2773
+ # Check if Python is available
2774
+ if ! command -v python3 &> /dev/null; then
2775
+ echo -e "${RED}Error: Python 3 not found. Install with: brew install python3${NC}"
2777
2776
  exit 1
2778
2777
  fi
2779
2778
 
@@ -2781,19 +2780,19 @@ cmd_api() {
2781
2780
  if [ -f "$pid_file" ]; then
2782
2781
  local existing_pid=$(cat "$pid_file")
2783
2782
  if kill -0 "$existing_pid" 2>/dev/null; then
2784
- echo -e "${YELLOW}API server already running (PID: $existing_pid)${NC}"
2783
+ echo -e "${YELLOW}Dashboard server already running (PID: $existing_pid)${NC}"
2785
2784
  echo "URL: http://localhost:$port"
2786
2785
  exit 0
2787
2786
  fi
2788
2787
  fi
2789
2788
 
2790
2789
  # Start server
2791
- mkdir -p "$LOKI_DIR/logs"
2792
- nohup node "$api_server" --port "$port" > "$LOKI_DIR/logs/api.log" 2>&1 &
2790
+ mkdir -p "$LOKI_DIR/logs" "$LOKI_DIR/dashboard"
2791
+ LOKI_DIR="$LOKI_DIR" nohup python3 -m uvicorn dashboard.server:app --host 0.0.0.0 --port "$port" > "$LOKI_DIR/logs/api.log" 2>&1 &
2793
2792
  local new_pid=$!
2794
2793
  echo "$new_pid" > "$pid_file"
2795
2794
 
2796
- echo -e "${GREEN}API server started${NC}"
2795
+ echo -e "${GREEN}Dashboard server started${NC}"
2797
2796
  echo " PID: $new_pid"
2798
2797
  echo " URL: http://localhost:$port"
2799
2798
  echo " Logs: $LOKI_DIR/logs/api.log"
@@ -2805,13 +2804,13 @@ cmd_api() {
2805
2804
  if kill -0 "$pid" 2>/dev/null; then
2806
2805
  kill "$pid"
2807
2806
  rm -f "$pid_file"
2808
- echo -e "${GREEN}API server stopped${NC}"
2807
+ echo -e "${GREEN}Dashboard server stopped${NC}"
2809
2808
  else
2810
2809
  rm -f "$pid_file"
2811
- echo -e "${YELLOW}API server was not running${NC}"
2810
+ echo -e "${YELLOW}Dashboard server was not running${NC}"
2812
2811
  fi
2813
2812
  else
2814
- echo -e "${YELLOW}No API server PID file found${NC}"
2813
+ echo -e "${YELLOW}No dashboard server PID file found${NC}"
2815
2814
  fi
2816
2815
  ;;
2817
2816
 
@@ -2819,7 +2818,7 @@ cmd_api() {
2819
2818
  if [ -f "$pid_file" ]; then
2820
2819
  local pid=$(cat "$pid_file")
2821
2820
  if kill -0 "$pid" 2>/dev/null; then
2822
- echo -e "${GREEN}API server running${NC}"
2821
+ echo -e "${GREEN}Dashboard server running${NC}"
2823
2822
  echo " PID: $pid"
2824
2823
  echo " URL: http://localhost:$port"
2825
2824
 
@@ -2827,39 +2826,39 @@ cmd_api() {
2827
2826
  if command -v curl &> /dev/null; then
2828
2827
  echo ""
2829
2828
  echo -e "${CYAN}Status:${NC}"
2830
- curl -s "http://localhost:$port/status" 2>/dev/null | jq . 2>/dev/null || true
2829
+ curl -s "http://localhost:$port/api/status" 2>/dev/null | jq . 2>/dev/null || true
2831
2830
  fi
2832
2831
  else
2833
- echo -e "${YELLOW}API server not running (stale PID file)${NC}"
2832
+ echo -e "${YELLOW}Dashboard server not running (stale PID file)${NC}"
2834
2833
  rm -f "$pid_file"
2835
2834
  fi
2836
2835
  else
2837
- echo -e "${YELLOW}API server not running${NC}"
2836
+ echo -e "${YELLOW}Dashboard server not running${NC}"
2838
2837
  fi
2839
2838
  ;;
2840
2839
 
2841
2840
  *)
2842
- echo -e "${BOLD}Loki Mode API Server${NC}"
2841
+ echo -e "${BOLD}Loki Mode Dashboard/API Server${NC}"
2843
2842
  echo ""
2844
2843
  echo "Usage: loki api <command>"
2845
2844
  echo ""
2846
2845
  echo "Commands:"
2847
- echo " start Start the HTTP API server"
2848
- echo " stop Stop the API server"
2849
- echo " status Check if API server is running"
2846
+ echo " start Start the unified FastAPI dashboard server"
2847
+ echo " stop Stop the dashboard server"
2848
+ echo " status Check if dashboard server is running"
2850
2849
  echo ""
2851
2850
  echo "Environment:"
2852
- echo " LOKI_API_PORT Port to listen on (default: 9898)"
2851
+ echo " LOKI_DASHBOARD_PORT Port to listen on (default: 57374)"
2853
2852
  echo ""
2854
2853
  echo "Endpoints:"
2855
- echo " GET /health - Health check"
2856
- echo " GET /status - Session status"
2857
- echo " GET /events - SSE stream"
2858
- echo " GET /logs - Recent logs"
2859
- echo " POST /start - Start session"
2860
- echo " POST /stop - Stop session"
2861
- echo " POST /pause - Pause session"
2862
- echo " POST /resume - Resume session"
2854
+ echo " GET /api/health - Health check"
2855
+ echo " GET /api/status - Session status"
2856
+ echo " GET /api/events - SSE stream"
2857
+ echo " GET /api/logs - Recent logs"
2858
+ echo " POST /api/start - Start session"
2859
+ echo " POST /api/stop - Stop session"
2860
+ echo " POST /api/pause - Pause session"
2861
+ echo " POST /api/resume - Resume session"
2863
2862
  ;;
2864
2863
  esac
2865
2864
  }
@@ -4032,6 +4031,9 @@ main() {
4032
4031
  compound)
4033
4032
  cmd_compound "$@"
4034
4033
  ;;
4034
+ checkpoint|cp)
4035
+ cmd_checkpoint "$@"
4036
+ ;;
4035
4037
  council)
4036
4038
  cmd_council "$@"
4037
4039
  ;;
@@ -5318,6 +5320,294 @@ COMPOUND_RUN_SCRIPT
5318
5320
  esac
5319
5321
  }
5320
5322
 
5323
+ # Checkpoint management - save and restore session state (v5.34.0)
5324
+ cmd_checkpoint() {
5325
+ local subcommand="${1:-list}"
5326
+ shift 2>/dev/null || true
5327
+
5328
+ local checkpoints_dir=".loki/state/checkpoints"
5329
+ local index_file="$checkpoints_dir/index.jsonl"
5330
+
5331
+ case "$subcommand" in
5332
+ list|ls)
5333
+ echo -e "${BOLD}Session Checkpoints${NC}"
5334
+ echo ""
5335
+
5336
+ if [ ! -f "$index_file" ]; then
5337
+ echo " No checkpoints yet."
5338
+ echo ""
5339
+ echo " Create one with: loki checkpoint create [message]"
5340
+ return 0
5341
+ fi
5342
+
5343
+ local count=0
5344
+ local lines=()
5345
+ while IFS= read -r line; do
5346
+ lines+=("$line")
5347
+ done < "$index_file"
5348
+
5349
+ # Show last 10 entries (most recent last)
5350
+ local total=${#lines[@]}
5351
+ local start=0
5352
+ if [ "$total" -gt 10 ]; then
5353
+ start=$((total - 10))
5354
+ fi
5355
+
5356
+ printf " ${DIM}%-14s %-20s %-10s %s${NC}\n" "ID" "TIMESTAMP" "GIT SHA" "MESSAGE"
5357
+
5358
+ local i
5359
+ for (( i=start; i<total; i++ )); do
5360
+ local entry="${lines[$i]}"
5361
+ local cp_id=$(echo "$entry" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null)
5362
+ local cp_ts=$(echo "$entry" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('timestamp','') or d.get('ts',''))" 2>/dev/null)
5363
+ local cp_sha=$(echo "$entry" | python3 -c "import sys,json; d=json.load(sys.stdin); print((d.get('git_sha','') or d.get('sha',''))[:8])" 2>/dev/null)
5364
+ local cp_msg=$(echo "$entry" | python3 -c "import sys,json; d=json.load(sys.stdin); print((d.get('message','') or d.get('task',''))[:50])" 2>/dev/null)
5365
+
5366
+ if [ -n "$cp_id" ]; then
5367
+ printf " ${GREEN}%-14s${NC} %-20s ${CYAN}%-10s${NC} %s\n" "$cp_id" "$cp_ts" "$cp_sha" "$cp_msg"
5368
+ count=$((count + 1))
5369
+ fi
5370
+ done
5371
+
5372
+ if [ "$count" -eq 0 ]; then
5373
+ echo " No valid checkpoints found."
5374
+ else
5375
+ echo ""
5376
+ echo " Showing ${count} of ${total} checkpoints"
5377
+ fi
5378
+ ;;
5379
+
5380
+ create)
5381
+ local raw_message="${*:-manual checkpoint}"
5382
+ # Escape backslashes and double quotes for JSON safety
5383
+ local message
5384
+ message=$(printf '%s' "$raw_message" | sed 's/\\/\\\\/g; s/"/\\"/g' | head -c 200)
5385
+
5386
+ echo -e "${BOLD}Creating checkpoint...${NC}"
5387
+ echo ""
5388
+
5389
+ # Ensure .loki exists
5390
+ if [ ! -d ".loki" ]; then
5391
+ echo -e "${RED}Error: No .loki directory found. Are you in a Loki project?${NC}"
5392
+ return 1
5393
+ fi
5394
+
5395
+ # Capture git info
5396
+ local git_sha=""
5397
+ local git_branch=""
5398
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
5399
+ git_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
5400
+ git_branch=$(git branch --show-current 2>/dev/null || echo "unknown")
5401
+ else
5402
+ git_sha="not-a-git-repo"
5403
+ git_branch="none"
5404
+ fi
5405
+
5406
+ # Generate checkpoint ID with timestamp
5407
+ local ts=$(date -u '+%Y%m%d-%H%M%S')
5408
+ local cp_id="cp-${ts}"
5409
+ local cp_dir="$checkpoints_dir/$cp_id"
5410
+
5411
+ # Create checkpoint directory
5412
+ mkdir -p "$cp_dir"
5413
+
5414
+ # Copy state files
5415
+ local copied=0
5416
+ for item in .loki/session.json .loki/dashboard-state.json .loki/queue .loki/memory .loki/metrics .loki/council; do
5417
+ if [ -e "$item" ]; then
5418
+ cp -r "$item" "$cp_dir/" 2>/dev/null && copied=$((copied + 1))
5419
+ fi
5420
+ done
5421
+
5422
+ # Write metadata (use python3 json.dumps for safe serialization)
5423
+ local iso_ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
5424
+ # Truncate message to 500 chars to prevent abuse
5425
+ local safe_message="${message:0:500}"
5426
+ _CP_ID="$cp_id" _CP_TS="$iso_ts" _CP_SHA="$git_sha" _CP_BRANCH="$git_branch" \
5427
+ _CP_MSG="$safe_message" _CP_FILES="$copied" _CP_DIR="$cp_dir" _CP_INDEX="$index_file" \
5428
+ _CP_CHKDIR="$checkpoints_dir" python3 << 'WRITE_META_EOF'
5429
+ import json, os
5430
+ metadata = {
5431
+ "id": os.environ["_CP_ID"],
5432
+ "timestamp": os.environ["_CP_TS"],
5433
+ "git_sha": os.environ["_CP_SHA"],
5434
+ "git_branch": os.environ["_CP_BRANCH"],
5435
+ "message": os.environ["_CP_MSG"],
5436
+ "files_copied": int(os.environ["_CP_FILES"]),
5437
+ "created_by": "loki checkpoint create"
5438
+ }
5439
+ cp_dir = os.environ["_CP_DIR"]
5440
+ os.makedirs(cp_dir, exist_ok=True)
5441
+ with open(os.path.join(cp_dir, "metadata.json"), "w") as f:
5442
+ json.dump(metadata, f, indent=4)
5443
+ index_file = os.environ["_CP_INDEX"]
5444
+ os.makedirs(os.environ["_CP_CHKDIR"], exist_ok=True)
5445
+ with open(index_file, "a") as f:
5446
+ index_entry = {
5447
+ "id": metadata["id"],
5448
+ "timestamp": metadata["timestamp"],
5449
+ "git_sha": metadata["git_sha"],
5450
+ "git_branch": metadata["git_branch"],
5451
+ "message": metadata["message"],
5452
+ }
5453
+ f.write(json.dumps(index_entry) + "\n")
5454
+ WRITE_META_EOF
5455
+
5456
+ echo -e " Checkpoint: ${GREEN}$cp_id${NC}"
5457
+ echo -e " Git SHA: ${CYAN}${git_sha:0:8}${NC} ($git_branch)"
5458
+ echo -e " Message: $message"
5459
+ echo -e " Files: $copied state items copied"
5460
+ echo -e " Location: $cp_dir"
5461
+ echo ""
5462
+ echo " Restore with: loki checkpoint rollback $cp_id"
5463
+ ;;
5464
+
5465
+ show)
5466
+ local cp_id="${1:-}"
5467
+ if [ -z "$cp_id" ]; then
5468
+ echo -e "${RED}Error: Specify a checkpoint ID${NC}"
5469
+ echo "Usage: loki checkpoint show <id>"
5470
+ echo "Run 'loki checkpoint list' to see available checkpoints."
5471
+ return 1
5472
+ fi
5473
+
5474
+ # Validate checkpoint ID (prevent path traversal)
5475
+ if [[ ! "$cp_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
5476
+ echo -e "${RED}Error: Invalid checkpoint ID (must be alphanumeric, hyphens, underscores only)${NC}"
5477
+ return 1
5478
+ fi
5479
+
5480
+ local cp_dir="$checkpoints_dir/$cp_id"
5481
+ local metadata="$cp_dir/metadata.json"
5482
+
5483
+ if [ ! -f "$metadata" ]; then
5484
+ echo -e "${RED}Error: Checkpoint not found: $cp_id${NC}"
5485
+ echo "Run 'loki checkpoint list' to see available checkpoints."
5486
+ return 1
5487
+ fi
5488
+
5489
+ echo -e "${BOLD}Checkpoint: $cp_id${NC}"
5490
+ echo ""
5491
+
5492
+ _CP_METADATA="$metadata" python3 << 'SHOW_EOF'
5493
+ import json, os
5494
+ with open(os.environ["_CP_METADATA"], "r") as f:
5495
+ d = json.load(f)
5496
+ print(f" ID: {d.get('id', 'unknown')}")
5497
+ print(f" Timestamp: {d.get('timestamp', 'unknown')}")
5498
+ print(f" Git SHA: {d.get('git_sha', 'unknown')}")
5499
+ print(f" Git Branch: {d.get('git_branch', 'unknown')}")
5500
+ print(f" Message: {d.get('message', 'none')}")
5501
+ print(f" Files: {d.get('files_copied', 0)} state items")
5502
+ print(f" Created By: {d.get('created_by', 'unknown')}")
5503
+ SHOW_EOF
5504
+
5505
+ echo ""
5506
+ echo " Contents:"
5507
+ for item in "$cp_dir"/*; do
5508
+ [ -e "$item" ] || continue
5509
+ local name=$(basename "$item")
5510
+ [ "$name" = "metadata.json" ] && continue
5511
+ if [ -d "$item" ]; then
5512
+ local fcount=$(find "$item" -type f 2>/dev/null | wc -l | tr -d ' ')
5513
+ echo -e " ${DIM}[dir]${NC} $name/ ($fcount files)"
5514
+ else
5515
+ local fsize=$(wc -c < "$item" 2>/dev/null | tr -d ' ')
5516
+ echo -e " ${DIM}[file]${NC} $name (${fsize} bytes)"
5517
+ fi
5518
+ done
5519
+ ;;
5520
+
5521
+ rollback)
5522
+ local cp_id="${1:-}"
5523
+ if [ -z "$cp_id" ]; then
5524
+ echo -e "${RED}Error: Specify a checkpoint ID${NC}"
5525
+ echo "Usage: loki checkpoint rollback <id>"
5526
+ echo "Run 'loki checkpoint list' to see available checkpoints."
5527
+ return 1
5528
+ fi
5529
+
5530
+ # Validate checkpoint ID (prevent path traversal)
5531
+ if [[ ! "$cp_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
5532
+ echo -e "${RED}Error: Invalid checkpoint ID (must be alphanumeric, hyphens, underscores only)${NC}"
5533
+ return 1
5534
+ fi
5535
+
5536
+ local cp_dir="$checkpoints_dir/$cp_id"
5537
+ local metadata="$cp_dir/metadata.json"
5538
+
5539
+ if [ ! -f "$metadata" ]; then
5540
+ echo -e "${RED}Error: Checkpoint not found: $cp_id${NC}"
5541
+ echo "Run 'loki checkpoint list' to see available checkpoints."
5542
+ return 1
5543
+ fi
5544
+
5545
+ echo -e "${BOLD}Rolling back to checkpoint: $cp_id${NC}"
5546
+ echo ""
5547
+
5548
+ # Restore state files
5549
+ local restored=0
5550
+ for item in "$cp_dir"/*; do
5551
+ [ -e "$item" ] || continue
5552
+ local name=$(basename "$item")
5553
+ [ "$name" = "metadata.json" ] && continue
5554
+
5555
+ if [ -d "$item" ]; then
5556
+ rm -rf ".loki/$name"
5557
+ cp -r "$item" ".loki/$name" 2>/dev/null && restored=$((restored + 1))
5558
+ else
5559
+ cp "$item" ".loki/$name" 2>/dev/null && restored=$((restored + 1))
5560
+ fi
5561
+ done
5562
+
5563
+ echo -e " Restored: ${GREEN}$restored${NC} state items from $cp_id"
5564
+
5565
+ # Show git info for manual code rollback
5566
+ local cp_sha=$(_CP_METADATA="$metadata" python3 -c "import json, os; d=json.load(open(os.environ['_CP_METADATA'])); print(d.get('git_sha','unknown'))" 2>/dev/null)
5567
+ if [ -n "$cp_sha" ] && [ "$cp_sha" != "unknown" ] && [ "$cp_sha" != "not-a-git-repo" ]; then
5568
+ echo ""
5569
+ echo -e " ${YELLOW}Note:${NC} Session state has been restored, but code is unchanged."
5570
+ echo " To also roll back code to the checkpoint's git state:"
5571
+ echo ""
5572
+ echo -e " ${DIM}git reset --hard ${cp_sha:0:8}${NC}"
5573
+ echo ""
5574
+ echo -e " ${RED}Warning:${NC} git reset --hard will discard uncommitted changes."
5575
+ echo " Consider 'git stash' first to preserve current work."
5576
+ fi
5577
+ ;;
5578
+
5579
+ help|--help|-h)
5580
+ echo -e "${BOLD}loki checkpoint${NC} - Session state checkpoints"
5581
+ echo ""
5582
+ echo "Save and restore session state snapshots during autonomous runs."
5583
+ echo "Checkpoints capture .loki/ state files and record the git SHA"
5584
+ echo "at the time of creation."
5585
+ echo ""
5586
+ echo "Usage: loki checkpoint <command> [args]"
5587
+ echo " loki cp <command> [args]"
5588
+ echo ""
5589
+ echo "Commands:"
5590
+ echo " list List recent checkpoints (default)"
5591
+ echo " create [message] Create a new checkpoint"
5592
+ echo " show <id> Show checkpoint details"
5593
+ echo " rollback <id> Restore state from a checkpoint"
5594
+ echo " help Show this help"
5595
+ echo ""
5596
+ echo "Examples:"
5597
+ echo " loki checkpoint create 'before refactor'"
5598
+ echo " loki cp list"
5599
+ echo " loki cp show cp-20260212-143022"
5600
+ echo " loki cp rollback cp-20260212-143022"
5601
+ ;;
5602
+
5603
+ *)
5604
+ echo -e "${RED}Unknown checkpoint command: $subcommand${NC}"
5605
+ echo "Run 'loki checkpoint help' for usage."
5606
+ return 1
5607
+ ;;
5608
+ esac
5609
+ }
5610
+
5321
5611
  # Completion Council management
5322
5612
  cmd_council() {
5323
5613
  local subcommand="${1:-status}"