loki-mode 5.48.2 → 5.49.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/autonomy/run.sh CHANGED
@@ -667,6 +667,146 @@ log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
667
667
  log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
668
668
  log_debug() { [[ "${LOKI_DEBUG:-}" == "true" ]] && echo -e "${CYAN}[DEBUG]${NC} $*" || true; }
669
669
 
670
+ #===============================================================================
671
+ # Process Registry (PID Supervisor)
672
+ # Central registry of all spawned child processes for reliable cleanup
673
+ #===============================================================================
674
+
675
+ PID_REGISTRY_DIR=""
676
+
677
+ # Initialize the PID registry directory
678
+ init_pid_registry() {
679
+ PID_REGISTRY_DIR="${TARGET_DIR:-.}/.loki/pids"
680
+ mkdir -p "$PID_REGISTRY_DIR"
681
+ }
682
+
683
+ # Parse a field from a JSON registry entry (python3 with shell fallback)
684
+ # Usage: _parse_json_field <file> <field>
685
+ _parse_json_field() {
686
+ local file="$1" field="$2"
687
+ if command -v python3 >/dev/null 2>&1; then
688
+ python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2],''))" "$file" "$field" 2>/dev/null
689
+ else
690
+ # Shell fallback: extract value for simple flat JSON
691
+ sed 's/.*"'"$field"'":\s*//' "$file" 2>/dev/null | sed 's/[",}].*//' | head -1
692
+ fi
693
+ }
694
+
695
+ # Register a spawned process in the central registry
696
+ # Usage: register_pid <pid> <label> [<extra_info>]
697
+ # Example: register_pid $! "dashboard" "port=57374"
698
+ register_pid() {
699
+ local pid="$1"
700
+ # Sanitize label and extra for JSON safety (escape backslash first, then double-quote, strip newlines)
701
+ local label="${2//\\/\\\\}"
702
+ label="${label//\"/\\\"}"
703
+ label="$(printf '%s' "$label" | tr -d '\n\r')"
704
+ local extra="${3:-}"
705
+ extra="${extra//\\/\\\\}"
706
+ extra="${extra//\"/\\\"}"
707
+ extra="$(printf '%s' "$extra" | tr -d '\n\r')"
708
+ [ -z "$PID_REGISTRY_DIR" ] && init_pid_registry
709
+ local entry_file="$PID_REGISTRY_DIR/${pid}.json"
710
+ cat > "$entry_file" << EOF
711
+ {"pid":$pid,"label":"$label","started":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","ppid":$$,"extra":"$extra"}
712
+ EOF
713
+ }
714
+
715
+ # Unregister a process from the registry (called on clean shutdown)
716
+ # Usage: unregister_pid <pid>
717
+ unregister_pid() {
718
+ local pid="$1"
719
+ [ -z "$PID_REGISTRY_DIR" ] && init_pid_registry
720
+ rm -f "$PID_REGISTRY_DIR/${pid}.json" 2>/dev/null
721
+ }
722
+
723
+ # Kill a registered process with SIGTERM -> wait -> SIGKILL escalation
724
+ # Usage: kill_registered_pid <pid>
725
+ kill_registered_pid() {
726
+ local pid="$1"
727
+ if kill -0 "$pid" 2>/dev/null; then
728
+ kill "$pid" 2>/dev/null || true
729
+ # Wait up to 2 seconds for graceful exit
730
+ local waited=0
731
+ while [ $waited -lt 4 ] && kill -0 "$pid" 2>/dev/null; do
732
+ sleep 0.5
733
+ waited=$((waited + 1))
734
+ done
735
+ # Escalate to SIGKILL if still alive
736
+ if kill -0 "$pid" 2>/dev/null; then
737
+ kill -9 "$pid" 2>/dev/null || true
738
+ fi
739
+ fi
740
+ unregister_pid "$pid"
741
+ }
742
+
743
+ # Scan registry for orphaned processes and kill them
744
+ # Called on startup and by `loki cleanup`
745
+ # Returns: number of orphans killed
746
+ cleanup_orphan_pids() {
747
+ [ -z "$PID_REGISTRY_DIR" ] && init_pid_registry
748
+ local orphan_count=0
749
+
750
+ if [ ! -d "$PID_REGISTRY_DIR" ]; then
751
+ echo "0"
752
+ return 0
753
+ fi
754
+
755
+ for entry_file in "$PID_REGISTRY_DIR"/*.json; do
756
+ [ -f "$entry_file" ] || continue
757
+ local pid
758
+ pid=$(basename "$entry_file" .json)
759
+
760
+ # Skip non-numeric filenames
761
+ case "$pid" in
762
+ ''|*[!0-9]*) continue ;;
763
+ esac
764
+
765
+ if kill -0 "$pid" 2>/dev/null; then
766
+ # Process is alive -- check if its parent session is dead
767
+ local ppid_val=""
768
+ ppid_val=$(_parse_json_field "$entry_file" "ppid") || true
769
+
770
+ # Validate ppid_val is numeric before using with kill
771
+ case "$ppid_val" in ''|*[!0-9]*) ppid_val="" ;; esac
772
+ if [ -n "$ppid_val" ] && [ "$ppid_val" != "$$" ]; then
773
+ if ! kill -0 "$ppid_val" 2>/dev/null; then
774
+ # Parent is dead -- this is an orphan
775
+ local label=""
776
+ label=$(_parse_json_field "$entry_file" "label") || label="unknown"
777
+ log_warn "Killing orphaned process: PID=$pid label=$label (parent $ppid_val is dead)" >&2
778
+ kill_registered_pid "$pid"
779
+ orphan_count=$((orphan_count + 1))
780
+ fi
781
+ fi
782
+ else
783
+ # Process is dead -- clean up stale registry entry
784
+ rm -f "$entry_file" 2>/dev/null
785
+ fi
786
+ done
787
+
788
+ echo "$orphan_count"
789
+ }
790
+
791
+ # Kill ALL registered processes (used during full shutdown)
792
+ kill_all_registered() {
793
+ [ -z "$PID_REGISTRY_DIR" ] && init_pid_registry
794
+
795
+ if [ ! -d "$PID_REGISTRY_DIR" ]; then
796
+ return 0
797
+ fi
798
+
799
+ for entry_file in "$PID_REGISTRY_DIR"/*.json; do
800
+ [ -f "$entry_file" ] || continue
801
+ local pid
802
+ pid=$(basename "$entry_file" .json)
803
+ case "$pid" in
804
+ ''|*[!0-9]*) continue ;;
805
+ esac
806
+ kill_registered_pid "$pid"
807
+ done
808
+ }
809
+
670
810
  #===============================================================================
671
811
  # Event Emission (Dashboard Integration)
672
812
  # Writes events to .loki/events.jsonl for dashboard consumption
@@ -1688,6 +1828,7 @@ create_worktree() {
1688
1828
  ) &
1689
1829
  # Capture install PID for cleanup on exit
1690
1830
  WORKTREE_INSTALL_PIDS+=($!)
1831
+ register_pid "$!" "worktree-install" "stream=$stream_name"
1691
1832
 
1692
1833
  log_info "Created worktree: $worktree_path"
1693
1834
  return 0
@@ -1796,6 +1937,7 @@ spawn_worktree_session() {
1796
1937
 
1797
1938
  local pid=$!
1798
1939
  WORKTREE_PIDS[$stream_name]=$pid
1940
+ register_pid "$pid" "worktree-session" "stream=$stream_name"
1799
1941
 
1800
1942
  log_info "Session spawned: $stream_name (PID: $pid)"
1801
1943
  return 0
@@ -2002,6 +2144,7 @@ cleanup_parallel_streams() {
2002
2144
  if kill -0 "$pid" 2>/dev/null; then
2003
2145
  kill "$pid" 2>/dev/null || true
2004
2146
  fi
2147
+ unregister_pid "$pid"
2005
2148
  done
2006
2149
  WORKTREE_INSTALL_PIDS=()
2007
2150
 
@@ -2012,6 +2155,7 @@ cleanup_parallel_streams() {
2012
2155
  log_step "Stopping session: $stream"
2013
2156
  kill "$pid" 2>/dev/null || true
2014
2157
  fi
2158
+ unregister_pid "$pid"
2015
2159
  done
2016
2160
 
2017
2161
  # Wait for all to finish
@@ -2620,8 +2764,8 @@ write_dashboard_state() {
2620
2764
  # Get complexity tier
2621
2765
  local complexity="${DETECTED_COMPLEXITY:-standard}"
2622
2766
 
2623
- # Get RARV cycle step (approximate based on iteration)
2624
- local rarv_step=$((ITERATION_COUNT % 4))
2767
+ # Get RARV cycle step from actual phase tracking (falls back to iteration-based)
2768
+ local rarv_step=${RARV_CURRENT_STEP:-$((ITERATION_COUNT % 4))}
2625
2769
  local rarv_stages='["reason", "act", "reflect", "verify"]'
2626
2770
 
2627
2771
  # Get memory system stats (if available)
@@ -2634,9 +2778,9 @@ write_dashboard_state() {
2634
2778
  [ -d ".loki/memory/skills" ] && procedural_count=$(find ".loki/memory/skills" -type f -name "*.json" 2>/dev/null | wc -l | tr -d ' ')
2635
2779
 
2636
2780
  # Get quality gates status (if available)
2637
- local quality_gates='{"staticAnalysis":"pending","codeReview":"pending","antiSycophancy":"pending","testCoverage":"pending","securityScan":"pending","performance":"pending"}'
2781
+ local quality_gates='null'
2638
2782
  if [ -f ".loki/state/quality-gates.json" ]; then
2639
- quality_gates=$(cat ".loki/state/quality-gates.json" 2>/dev/null || echo "$quality_gates")
2783
+ quality_gates=$(cat ".loki/state/quality-gates.json" 2>/dev/null || echo 'null')
2640
2784
  fi
2641
2785
 
2642
2786
  # Get Completion Council state (v5.25.0)
@@ -3037,6 +3181,7 @@ start_status_monitor() {
3037
3181
  done
3038
3182
  ) &
3039
3183
  STATUS_MONITOR_PID=$!
3184
+ register_pid "$STATUS_MONITOR_PID" "status-monitor"
3040
3185
 
3041
3186
  log_info "Status monitor started"
3042
3187
  log_info "Monitor progress: ${CYAN}watch -n 2 cat .loki/STATUS.txt${NC}"
@@ -3046,6 +3191,7 @@ stop_status_monitor() {
3046
3191
  if [ -n "$STATUS_MONITOR_PID" ]; then
3047
3192
  kill "$STATUS_MONITOR_PID" 2>/dev/null || true
3048
3193
  wait "$STATUS_MONITOR_PID" 2>/dev/null || true
3194
+ unregister_pid "$STATUS_MONITOR_PID"
3049
3195
  fi
3050
3196
  stop_resource_monitor
3051
3197
  }
@@ -3621,6 +3767,7 @@ start_resource_monitor() {
3621
3767
  done
3622
3768
  ) &
3623
3769
  RESOURCE_MONITOR_PID=$!
3770
+ register_pid "$RESOURCE_MONITOR_PID" "resource-monitor"
3624
3771
 
3625
3772
  log_info "Resource monitor started (CPU threshold: ${RESOURCE_CPU_THRESHOLD}%, Memory threshold: ${RESOURCE_MEM_THRESHOLD}%)"
3626
3773
  log_info "Check status: ${CYAN}cat .loki/state/resources.json${NC}"
@@ -3630,6 +3777,7 @@ stop_resource_monitor() {
3630
3777
  if [ -n "$RESOURCE_MONITOR_PID" ]; then
3631
3778
  kill "$RESOURCE_MONITOR_PID" 2>/dev/null || true
3632
3779
  wait "$RESOURCE_MONITOR_PID" 2>/dev/null || true
3780
+ unregister_pid "$RESOURCE_MONITOR_PID"
3633
3781
  fi
3634
3782
  }
3635
3783
 
@@ -4720,12 +4868,14 @@ BUILD_PROMPT
4720
4868
  esac
4721
4869
  ) &
4722
4870
  pids+=($!)
4871
+ register_pid "$!" "code-reviewer" "name=$reviewer_name"
4723
4872
  done
4724
4873
 
4725
4874
  # Wait for all reviewers to complete
4726
4875
  log_info "Waiting for $reviewer_count reviewers to complete (blind review)..."
4727
4876
  for pid in "${pids[@]}"; do
4728
4877
  wait "$pid" || true
4878
+ unregister_pid "$pid"
4729
4879
  done
4730
4880
 
4731
4881
  log_info "All reviewers complete. Aggregating verdicts..."
@@ -5191,6 +5341,7 @@ start_dashboard() {
5191
5341
  LOKI_TLS_CERT="${LOKI_TLS_CERT:-}" LOKI_TLS_KEY="${LOKI_TLS_KEY:-}" \
5192
5342
  LOKI_SKILL_DIR="${skill_dir}" PYTHONPATH="${skill_dir}" nohup "$python_cmd" -m dashboard.server > "$log_file" 2>&1 &
5193
5343
  DASHBOARD_PID=$!
5344
+ register_pid "$DASHBOARD_PID" "dashboard" "port=${DASHBOARD_PORT:-57374}"
5194
5345
 
5195
5346
  # Save PID for later cleanup
5196
5347
  mkdir -p .loki/dashboard
@@ -5224,6 +5375,7 @@ stop_dashboard() {
5224
5375
  if [ -n "$DASHBOARD_PID" ]; then
5225
5376
  kill "$DASHBOARD_PID" 2>/dev/null || true
5226
5377
  wait "$DASHBOARD_PID" 2>/dev/null || true
5378
+ unregister_pid "$DASHBOARD_PID"
5227
5379
  fi
5228
5380
 
5229
5381
  # Also try PID file
@@ -5231,6 +5383,7 @@ stop_dashboard() {
5231
5383
  local saved_pid=$(cat ".loki/dashboard/dashboard.pid" 2>/dev/null)
5232
5384
  if [ -n "$saved_pid" ]; then
5233
5385
  kill "$saved_pid" 2>/dev/null || true
5386
+ unregister_pid "$saved_pid"
5234
5387
  fi
5235
5388
  rm -f ".loki/dashboard/dashboard.pid"
5236
5389
  fi
@@ -5681,6 +5834,132 @@ load_handoff_context() {
5681
5834
  fi
5682
5835
  }
5683
5836
 
5837
+ # Write structured handoff document (v5.49.0)
5838
+ # Produces both JSON (machine-readable) and markdown (human-readable) handoffs
5839
+ # Called at end of session or before context clear
5840
+ write_structured_handoff() {
5841
+ local reason="${1:-session_end}"
5842
+ local handoff_dir=".loki/memory/handoffs"
5843
+ mkdir -p "$handoff_dir"
5844
+
5845
+ local timestamp
5846
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
5847
+ local file_ts
5848
+ file_ts=$(date +"%Y%m%d-%H%M%S")
5849
+ local handoff_json="$handoff_dir/${file_ts}.json"
5850
+ local handoff_md="$handoff_dir/${file_ts}.md"
5851
+
5852
+ # Gather structured data
5853
+ local files_modified=""
5854
+ files_modified=$(git diff --name-only HEAD 2>/dev/null | head -20 | tr '\n' ',' | sed 's/,$//')
5855
+ local recent_commits=""
5856
+ recent_commits=$(git log --oneline -5 2>/dev/null | tr '\n' '|' | sed 's/|$//')
5857
+ local pending_tasks=0
5858
+ local completed_tasks=0
5859
+ if [ -f ".loki/queue/pending.json" ]; then
5860
+ pending_tasks=$(_QF=".loki/queue/pending.json" python3 -c "import json,os;print(len(json.load(open(os.environ['_QF']))))" 2>/dev/null || echo "0")
5861
+ fi
5862
+ if [ -f ".loki/queue/completed.json" ]; then
5863
+ completed_tasks=$(_QF=".loki/queue/completed.json" python3 -c "import json,os;print(len(json.load(open(os.environ['_QF']))))" 2>/dev/null || echo "0")
5864
+ fi
5865
+
5866
+ # Write JSON handoff
5867
+ _H_TS="$timestamp" \
5868
+ _H_REASON="$reason" \
5869
+ _H_ITER="${ITERATION_COUNT:-0}" \
5870
+ _H_FILES="$files_modified" \
5871
+ _H_COMMITS="$recent_commits" \
5872
+ _H_PENDING="$pending_tasks" \
5873
+ _H_COMPLETED="$completed_tasks" \
5874
+ _H_JSON="$handoff_json" \
5875
+ python3 -c "
5876
+ import json, os
5877
+ handoff = {
5878
+ 'schema_version': '1.0.0',
5879
+ 'timestamp': os.environ['_H_TS'],
5880
+ 'reason': os.environ['_H_REASON'],
5881
+ 'iteration': int(os.environ['_H_ITER']),
5882
+ 'files_modified': [f for f in os.environ['_H_FILES'].split(',') if f],
5883
+ 'recent_commits': [c for c in os.environ['_H_COMMITS'].split('|') if c],
5884
+ 'task_status': {
5885
+ 'pending': int(os.environ['_H_PENDING']),
5886
+ 'completed': int(os.environ['_H_COMPLETED'])
5887
+ },
5888
+ 'open_questions': [],
5889
+ 'key_decisions': [],
5890
+ 'blockers': []
5891
+ }
5892
+ with open(os.environ['_H_JSON'], 'w') as f:
5893
+ json.dump(handoff, f, indent=2)
5894
+ " 2>/dev/null || log_warn "Failed to write structured handoff JSON"
5895
+
5896
+ # Write markdown companion
5897
+ cat > "$handoff_md" << HANDOFF_EOF
5898
+ # Session Handoff - $timestamp
5899
+
5900
+ **Reason:** $reason
5901
+ **Iteration:** ${ITERATION_COUNT:-0}
5902
+
5903
+ ## Files Modified
5904
+ $files_modified
5905
+
5906
+ ## Recent Commits
5907
+ $(git log --oneline -5 2>/dev/null || echo "none")
5908
+
5909
+ ## Task Status
5910
+ - Pending: $pending_tasks
5911
+ - Completed: $completed_tasks
5912
+
5913
+ ## Notes
5914
+ Session handoff generated automatically.
5915
+ HANDOFF_EOF
5916
+
5917
+ log_info "Structured handoff written to $handoff_json"
5918
+ }
5919
+
5920
+ # Load recent handoffs for context (reads both JSON and markdown)
5921
+ load_handoff_context() {
5922
+ local handoff_content=""
5923
+
5924
+ # Prefer JSON handoffs (structured, v5.49.0+)
5925
+ local recent_json
5926
+ recent_json=$(find .loki/memory/handoffs -name "*.json" -mtime -1 2>/dev/null | sort -r | head -1)
5927
+
5928
+ if [ -n "$recent_json" ] && [ -f "$recent_json" ]; then
5929
+ handoff_content=$(_HF="$recent_json" python3 -c "
5930
+ import json, os
5931
+ try:
5932
+ h = json.load(open(os.environ['_HF']))
5933
+ parts = []
5934
+ parts.append(f\"Handoff from {h.get('timestamp','unknown')} (reason: {h.get('reason','unknown')})\")
5935
+ parts.append(f\"Iteration: {h.get('iteration',0)}\")
5936
+ files = h.get('files_modified', [])
5937
+ if files:
5938
+ parts.append(f\"Modified files: {', '.join(files[:10])}\")
5939
+ tasks = h.get('task_status', {})
5940
+ parts.append(f\"Tasks - pending: {tasks.get('pending',0)}, completed: {tasks.get('completed',0)}\")
5941
+ for q in h.get('open_questions', []):
5942
+ parts.append(f\"Open question: {q}\")
5943
+ for b in h.get('blockers', []):
5944
+ parts.append(f\"Blocker: {b}\")
5945
+ print(' | '.join(parts))
5946
+ except Exception as e:
5947
+ print(f'Error reading handoff: {e}')
5948
+ " 2>/dev/null)
5949
+ echo "$handoff_content"
5950
+ return
5951
+ fi
5952
+
5953
+ # Fallback to markdown handoffs (pre-v5.49.0)
5954
+ local recent_handoff
5955
+ recent_handoff=$(find .loki/memory/handoffs -name "*.md" -mtime -1 2>/dev/null | sort -r | head -1)
5956
+
5957
+ if [ -n "$recent_handoff" ] && [ -f "$recent_handoff" ]; then
5958
+ handoff_content=$(cat "$recent_handoff" | head -80)
5959
+ echo "$handoff_content"
5960
+ fi
5961
+ }
5962
+
5684
5963
  # Load relevant learnings
5685
5964
  load_learnings_context() {
5686
5965
  local learnings=""
@@ -6995,6 +7274,7 @@ cleanup() {
6995
7274
  fi
6996
7275
  stop_dashboard
6997
7276
  stop_status_monitor
7277
+ kill_all_registered
6998
7278
  rm -f "$loki_dir/loki.pid" 2>/dev/null
6999
7279
  if [ -f "$loki_dir/session.json" ]; then
7000
7280
  _LOKI_SESSION_FILE="$loki_dir/session.json" python3 -c "
@@ -7022,6 +7302,7 @@ except (json.JSONDecodeError, OSError): pass
7022
7302
  fi
7023
7303
  stop_dashboard
7024
7304
  stop_status_monitor
7305
+ kill_all_registered
7025
7306
  rm -f .loki/loki.pid .loki/PAUSE 2>/dev/null
7026
7307
  # Mark session.json as stopped
7027
7308
  if [ -f ".loki/session.json" ]; then
@@ -7209,6 +7490,7 @@ main() {
7209
7490
  LOKI_RUNNING_FROM_TEMP='' nohup "$original_script" "${cmd_args[@]}" > "$log_file" 2>&1 &
7210
7491
  local bg_pid=$!
7211
7492
  echo "$bg_pid" > "$pid_file"
7493
+ register_pid "$bg_pid" "background-session" "log=$log_file"
7212
7494
 
7213
7495
  echo ""
7214
7496
  echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
@@ -7331,6 +7613,14 @@ main() {
7331
7613
  # Write PID file for ALL modes (foreground + background)
7332
7614
  echo "$$" > "$pid_file"
7333
7615
 
7616
+ # Initialize PID registry and clean up orphans from previous sessions
7617
+ init_pid_registry
7618
+ local orphan_count
7619
+ orphan_count=$(cleanup_orphan_pids)
7620
+ if [ "$orphan_count" -gt 0 ]; then
7621
+ log_warn "Killed $orphan_count orphaned process(es) from previous session"
7622
+ fi
7623
+
7334
7624
  # Copy skill files to .loki/skills/ - makes CLI self-contained
7335
7625
  # No need to install Claude Code skill separately
7336
7626
  copy_skill_files
@@ -7410,10 +7700,12 @@ main() {
7410
7700
  run_autonomous "$PRD_PATH"
7411
7701
  ) &
7412
7702
  local main_pid=$!
7703
+ register_pid "$main_pid" "parallel-main" ""
7413
7704
 
7414
7705
  # Run parallel orchestrator
7415
7706
  run_parallel_orchestrator &
7416
7707
  local orchestrator_pid=$!
7708
+ register_pid "$orchestrator_pid" "parallel-orchestrator" ""
7417
7709
 
7418
7710
  # Wait for main session (orchestrator continues watching)
7419
7711
  wait $main_pid || result=$?
@@ -7489,6 +7781,9 @@ main() {
7489
7781
  --context "{\"provider\":\"${PROVIDER_NAME:-claude}\",\"iterations\":$ITERATION_COUNT,\"exit_code\":$result}"
7490
7782
  fi
7491
7783
 
7784
+ # Write structured handoff for future sessions (v5.49.0)
7785
+ write_structured_handoff "session_end_result_${result}" 2>/dev/null || true
7786
+
7492
7787
  # Create PR from agent branch if branch protection was enabled
7493
7788
  create_session_pr
7494
7789
  audit_agent_action "session_stop" "Session ended" "result=$result,iterations=$ITERATION_COUNT"
@@ -843,6 +843,15 @@ start_sandbox() {
843
843
  docker_args+=("--volume" "$PROJECT_DIR:/workspace:rw")
844
844
  fi
845
845
 
846
+ # Config self-protection: mount critical .loki/ paths as read-only
847
+ # Prevents agents from corrupting council state, config, or audit trail
848
+ if [[ -d "$PROJECT_DIR/.loki/council" ]]; then
849
+ docker_args+=("--volume" "$PROJECT_DIR/.loki/council:/workspace/.loki/council:ro")
850
+ fi
851
+ if [[ -f "$PROJECT_DIR/.loki/config.yaml" ]]; then
852
+ docker_args+=("--volume" "$PROJECT_DIR/.loki/config.yaml:/workspace/.loki/config.yaml:ro")
853
+ fi
854
+
846
855
  # Mount git config (read-only) - mount to /home/loki since container runs as user loki
847
856
  if [[ -f "$HOME/.gitconfig" ]]; then
848
857
  docker_args+=("--volume" "$HOME/.gitconfig:/home/loki/.gitconfig:ro")
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "5.48.2"
10
+ __version__ = "5.49.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -404,7 +404,20 @@ async def get_status() -> StatusResponse:
404
404
  iteration = state.get("iteration", 0)
405
405
  complexity = state.get("complexity", "standard")
406
406
  mode = state.get("mode", "")
407
- running_agents = len(state.get("agents", []))
407
+ # Count only agents with alive PIDs (not raw array length)
408
+ agents_list = state.get("agents", [])
409
+ running_agents = 0
410
+ for agent in agents_list:
411
+ agent_pid = agent.get("pid")
412
+ if agent_pid:
413
+ try:
414
+ os.kill(int(agent_pid), 0)
415
+ running_agents += 1
416
+ except (OSError, ValueError, TypeError):
417
+ pass
418
+ else:
419
+ # No PID field -- count as running (legacy data)
420
+ running_agents += 1
408
421
 
409
422
  tasks = state.get("tasks", {})
410
423
  pending_tasks = len(tasks.get("pending", []))
@@ -3194,6 +3207,31 @@ async def get_process_health(token: Optional[dict] = Depends(auth.get_current_to
3194
3207
  except Exception:
3195
3208
  pass
3196
3209
 
3210
+ # PID registry (central process supervisor)
3211
+ pids_dir = loki_dir / "pids"
3212
+ registered: list[dict[str, Any]] = []
3213
+ if pids_dir.exists():
3214
+ for entry_file in sorted(pids_dir.glob("*.json")):
3215
+ try:
3216
+ pid_str = entry_file.stem
3217
+ pid = int(pid_str)
3218
+ entry = json.loads(entry_file.read_text())
3219
+ try:
3220
+ os.kill(pid, 0)
3221
+ status = "alive"
3222
+ except OSError:
3223
+ status = "dead"
3224
+ registered.append({
3225
+ "pid": pid,
3226
+ "label": entry.get("label", "unknown"),
3227
+ "started": entry.get("started", ""),
3228
+ "ppid": entry.get("ppid"),
3229
+ "status": status,
3230
+ })
3231
+ except (ValueError, json.JSONDecodeError, OSError):
3232
+ continue
3233
+ result["registered_processes"] = registered
3234
+
3197
3235
  watchdog_enabled = os.environ.get("LOKI_WATCHDOG", "false").lower() == "true"
3198
3236
  result["watchdog_enabled"] = watchdog_enabled
3199
3237
 
@@ -12,7 +12,7 @@
12
12
  | Feature | **Loki Mode** | **Zencoder** | **Devin** | **OpenAI Codex** | **Cursor** | **Claude Code** | **Kiro** | **Antigravity** | **Amazon Q** | **OpenCode** |
13
13
  |---------|--------------|--------------|-----------|-----------------|------------|-----------------|----------|-----------------|--------------|--------------|
14
14
  | **Type** | Skill/Framework | Enterprise Platform | Standalone Agent | Cloud Agent | AI IDE | CLI Agent | AI IDE | AI IDE | Cloud Agent | AI IDE (OSS) |
15
- | **Autonomy Level** | Full (zero human) | High | Full | High | Medium-High | High | High | High | High | High |
15
+ | **Autonomy Level** | High (minimal human) | High | Full | High | Medium-High | High | High | High | High | High |
16
16
  | **Max Runtime** | Unlimited | Async/Scheduled | Hours | Per-task | Session | Session | Days | Async | Per-task | Session |
17
17
  | **Pricing** | Free (OSS) | Enterprise | $20/mo | ChatGPT Plus | $20/mo | API costs | Free preview | Free preview | $19/mo | Free (OSS) |
18
18
  | **Open Source** | Yes | No | No | No | No | No | No | No | No | Yes |
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Feature | **Loki Mode** | **Devin** | **Codex** | **Cursor** | **Kiro** | **Antigravity** | **Amazon Q** | **OpenCode** |
26
26
  |---------|--------------|-----------|-----------|------------|----------|-----------------|--------------|--------------|
27
- | **Multi-Agent** | 41 agents in 7 swarms | Single | Single | Up to 8 parallel | Background | Manager Surface | Multiple types | 4 built-in |
27
+ | **Multi-Agent** | 41 agents in 8 swarms | Single | Single | Up to 8 parallel | Background | Manager Surface | Multiple types | 4 built-in |
28
28
  | **Orchestration** | Full orchestrator | N/A | N/A | Git worktree | Hooks | Manager view | Workflow | Subagents |
29
29
  | **Parallel Exec** | 10+ Haiku, 4 impl (worktree) | No | No | 8 max | Yes | Yes | Yes | Yes |
30
30
  | **Agent Swarms** | Eng, Ops, Business, Data, Product, Growth, Review | N/A | N/A | N/A | N/A | N/A | 3 types | N/A |
@@ -180,9 +180,9 @@
180
180
 
181
181
  1. **Quality Control**: 7 explicit gates + blind review + devil's advocate vs built-in loops
182
182
  2. **Memory System**: 3-tier (episodic/semantic/procedural) with cross-project learning
183
- 3. **Agent Specialization**: 41 pre-defined specialized agents across 7 swarms
183
+ 3. **Agent Specialization**: 41 pre-defined specialized agents across 8 swarms
184
184
  4. **Anti-Sycophancy**: CONSENSAGENT patterns prevent reviewer groupthink
185
- 5. **Autonomy Design**: Zero human intervention from PRD to production
185
+ 5. **Autonomy Design**: Minimal human intervention from PRD to production
186
186
  6. **Research Foundation**: 10+ academic papers integrated vs proprietary
187
187
 
188
188
  ### Where Zencoder EXCEEDS Loki Mode
@@ -203,13 +203,13 @@
203
203
  |---------|--------------|---------|-----------------|------------|-----------------|---------------------|----------------|
204
204
  | **Stars** | 594 | 11,903 | 35K+ | 26K+ | 13.7K | N/A | N/A |
205
205
  | **npm/wk** | 6.1K | 21.4K | N/A | N/A | N/A | N/A | N/A |
206
- | **Agents** | 41 in 7 swarms | 11 agents | Fresh per task | 108 agents | Swarm-based | 32 agents | N/A |
206
+ | **Agents** | 41 in 8 swarms | 11 agents | Fresh per task | 108 agents | Swarm-based | 32 agents | N/A |
207
207
  | **Skills** | Progressive disclosure | 6 slash commands | N/A | 129 skills | N/A | 35 skills | Memory focus |
208
208
  | **Multi-Provider** | Yes (Claude/Codex/Gemini) | 3 CLIs (separate) | No | No | No | No | No |
209
209
  | **Memory System** | 3-tier (episodic/semantic/procedural) | None | N/A | N/A | Hybrid | N/A | SQLite+FTS5 |
210
210
  | **Quality Gates** | 7 gates + Completion Council | User verify only | Two-Stage Review | N/A | Consensus | Tiered | N/A |
211
211
  | **Context Mgmt** | Standard | Fresh per task (core innovation) | Fresh per task | N/A | N/A | N/A | Progressive |
212
- | **Autonomy** | Full (zero human) | Semi (checkpoints) | Human-guided | Human-guided | Orchestrated | Human-guided | N/A |
212
+ | **Autonomy** | High (minimal human) | Semi (checkpoints) | Human-guided | Human-guided | Orchestrated | Human-guided | N/A |
213
213
 
214
214
  ### What Loki Mode LACKS (Honest Assessment)
215
215
 
@@ -236,7 +236,7 @@ These are patterns from competing projects that are **practically and scientific
236
236
  | **Constitutional AI Integration** | Principles-based self-critique from Anthropic research | None have this |
237
237
  | **Anti-Sycophancy (CONSENSAGENT)** | Blind review + devil's advocate prevents groupthink | None have this |
238
238
  | **Provider Abstraction Layer** | Clean degradation from full-featured to sequential-only | Claude-only projects can't degrade |
239
- | **41 Specialized Agents** | Purpose-built agents in 7 swarms vs generic | agents (108) has more but less organized |
239
+ | **41 Specialized Agents** | Purpose-built agents in 8 swarms vs generic | agents (108) has more but less organized |
240
240
  | **Research Foundation** | 10+ academic papers integrated with citations | Most have no research backing |
241
241
 
242
242
  ### Superpowers Deep-Dive (35K+ Stars)
@@ -342,7 +342,7 @@ Tiered agent architecture with explicit escalation:
342
342
 
343
343
  | Agent | Killer Feature |
344
344
  |-------|---------------|
345
- | **Loki Mode** | Zero-human-intervention full SDLC, 41 agents in 7 swarms, Constitutional AI, anti-sycophancy, cross-project learning, code transformation, property-based testing |
345
+ | **Loki Mode** | Minimal-human-intervention full SDLC, 41 agents in 8 swarms, Constitutional AI, anti-sycophancy, cross-project learning, code transformation, property-based testing |
346
346
  | **Devin** | Full software engineer persona, Slack integration, 67% PR merge rate |
347
347
  | **OpenAI Codex** | Skills marketplace, $skill-creator, GPT-5.2-Codex, secure sandbox |
348
348
  | **Cursor** | 8 parallel agents, BugBot, Memories, $10B valuation, Composer model (250 tok/s) |
@@ -357,8 +357,8 @@ Tiered agent architecture with explicit escalation:
357
357
 
358
358
  | Dimension | Loki Mode Advantage |
359
359
  |-----------|-------------------|
360
- | **Autonomy** | Only agent designed for TRUE zero human intervention |
361
- | **Multi-Agent** | 41 specialized agents in 7 swarms vs 1-8 in competitors |
360
+ | **Autonomy** | Designed for high autonomy with minimal human intervention |
361
+ | **Multi-Agent** | 41 specialized agents in 8 swarms vs 1-8 in competitors |
362
362
  | **Quality** | 7 gates + blind review + devil's advocate + property-based testing |
363
363
  | **Research** | 10+ academic papers integrated vs proprietary/undisclosed |
364
364
  | **Anti-Sycophancy** | Only agent with CONSENSAGENT-based blind review |
@@ -37,9 +37,9 @@ GSD is the closest competitor -- a context engineering system that spawns fresh
37
37
  |---------|-----------|-------------|---------|--------|--------------|-------|
38
38
  | **GitHub Stars** | 594 | 13,700 | 62,400 | 25,000+ | N/A (Commercial) | N/A (Commercial) |
39
39
  | **Agent Count** | 41 types | 64+ agents | 5 roles | Unlimited | 8 parallel | 1 autonomous |
40
- | **Parallel Execution** | Yes (100+) | Yes (swarms) | Sequential | Yes (crews) | Yes (8 worktrees) | Yes (fleet) |
41
- | **Published Benchmarks** | **98.78% HumanEval (multi-agent)** | None | 85.9-87.7% HumanEval | None | ~250 tok/s | 15% complex tasks |
42
- | **SWE-bench Score** | **99.67% patch gen (299/300)** | Unknown | Unknown | Unknown | Unknown | 15% complex |
40
+ | **Parallel Execution** | Yes (multi-agent) | Yes (swarms) | Sequential | Yes (crews) | Yes (8 worktrees) | Yes (fleet) |
41
+ | **Published Benchmarks** | 98.78% HumanEval (self-reported, max 3 retries) | None | 85.9-87.7% HumanEval | None | ~250 tok/s | 15% complex tasks |
42
+ | **SWE-bench Score** | 99.67% patch gen (unevaluated, 299/300) | Unknown | Unknown | Unknown | Unknown | 15% complex |
43
43
  | **Full SDLC** | Yes (8 phases) | Yes | Partial | Partial | No | Partial |
44
44
  | **Business Ops** | **Yes (8 agents)** | No | No | No | No | No |
45
45
  | **Enterprise Security** | `--dangerously-skip-permissions` | MCP sandboxed | Sandboxed | Audit logs, RBAC | Staged autonomy | Sandboxed |