loki-mode 7.5.17 → 7.5.28

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.
Files changed (47) hide show
  1. package/README.md +10 -9
  2. package/SKILL.md +14 -14
  3. package/VERSION +1 -1
  4. package/autonomy/completion-council.sh +26 -3
  5. package/autonomy/lib/claude-flags.sh +132 -0
  6. package/autonomy/lib/mcp-config.sh +160 -0
  7. package/autonomy/lib/project-graph.sh +685 -0
  8. package/autonomy/lib/voter-agents.sh +356 -0
  9. package/autonomy/loki +108 -111
  10. package/autonomy/run.sh +95 -186
  11. package/bin/loki +12 -1
  12. package/dashboard/__init__.py +1 -1
  13. package/dashboard/requirements.txt +13 -8
  14. package/dashboard/server.py +33 -15
  15. package/dashboard/static/index.html +298 -299
  16. package/docs/INSTALLATION.md +54 -21
  17. package/docs/retrospectives/v7.5.15-fleet-postmortem.md +325 -0
  18. package/docs/retrospectives/v7.5.15-honesty-audit.md +136 -0
  19. package/docs/retrospectives/v7.5.15-llm-failure-modes.md +49 -0
  20. package/loki-ts/data/finding-schema.json +74 -0
  21. package/loki-ts/data/model-pricing.json +12 -0
  22. package/loki-ts/dist/loki.js +198 -172
  23. package/mcp/__init__.py +1 -1
  24. package/mcp/lsp_proxy.py +713 -0
  25. package/mcp/requirements.txt +9 -3
  26. package/mcp/tests/__init__.py +0 -0
  27. package/mcp/tests/test_lsp_proxy.py +377 -0
  28. package/memory/app_graph.py +153 -0
  29. package/memory/storage.py +6 -1
  30. package/memory/tests/test_app_graph.py +134 -0
  31. package/package.json +4 -3
  32. package/providers/claude.sh +115 -4
  33. package/providers/codex.sh +2 -2
  34. package/providers/loader.sh +4 -4
  35. package/providers/model_catalog.json +0 -9
  36. package/providers/models.sh +1 -2
  37. package/references/multi-provider.md +26 -35
  38. package/references/prompt-repetition.md +1 -1
  39. package/references/quality-control.md +1 -1
  40. package/skills/00-index.md +3 -3
  41. package/skills/model-selection.md +11 -14
  42. package/skills/providers.md +17 -57
  43. package/skills/quality-gates.md +2 -2
  44. package/skills/troubleshooting.md +1 -1
  45. package/src/integrations/github/action-handler.js +3 -2
  46. package/src/protocols/tools/start-project.js +1 -1
  47. package/providers/gemini.sh +0 -343
package/autonomy/run.sh CHANGED
@@ -15,7 +15,7 @@
15
15
  # ./autonomy/run.sh --parallel ./prd.md # Parallel mode with PRD
16
16
  #
17
17
  # Environment Variables:
18
- # LOKI_PROVIDER - AI provider: claude (default), codex, gemini
18
+ # LOKI_PROVIDER - AI provider: claude (default), codex, cline, aider
19
19
  # LOKI_MAX_RETRIES - Max retry attempts (default: 50)
20
20
  # LOKI_BASE_WAIT - Base wait time in seconds (default: 60)
21
21
  # LOKI_MAX_WAIT - Max wait time in seconds (default: 3600)
@@ -758,7 +758,7 @@ COMPLEXITY_TIER=${LOKI_COMPLEXITY:-auto}
758
758
  DETECTED_COMPLEXITY=""
759
759
 
760
760
  # Multi-Provider Support (v5.0.0)
761
- # Provider: claude (default), codex, gemini
761
+ # Provider: claude (default), codex, cline, aider
762
762
  LOKI_PROVIDER=${LOKI_PROVIDER:-claude}
763
763
 
764
764
  # Source provider configuration
@@ -1291,7 +1291,7 @@ get_iteration_duration_ms() {
1291
1291
  validate_api_keys() {
1292
1292
  local provider="${LOKI_PROVIDER:-claude}"
1293
1293
 
1294
- # CLI tools (claude, codex, gemini) use their own login sessions.
1294
+ # CLI tools (claude, codex, cline, aider) use their own login sessions.
1295
1295
  # Only require API keys inside Docker/K8s where CLI login isn't available.
1296
1296
  if [[ ! -f "/.dockerenv" ]] && [[ -z "${KUBERNETES_SERVICE_HOST:-}" ]]; then
1297
1297
  return 0
@@ -1301,7 +1301,6 @@ validate_api_keys() {
1301
1301
  case "$provider" in
1302
1302
  claude) key_var="ANTHROPIC_API_KEY" ;;
1303
1303
  codex) key_var="OPENAI_API_KEY" ;;
1304
- gemini) key_var="GOOGLE_API_KEY" ;;
1305
1304
  cline) # Cline manages its own keys via `cline auth`
1306
1305
  if ! command -v cline &>/dev/null; then
1307
1306
  log_error "Cline CLI not found. Install: npm install -g cline"
@@ -1497,7 +1496,7 @@ get_phase_names() {
1497
1496
 
1498
1497
  # Global tier for current iteration (set by get_rarv_tier)
1499
1498
  CURRENT_TIER="development"
1500
- # Export for provider helper functions (e.g., gemini.sh:provider_get_current_model)
1499
+ # Export for provider helper functions (e.g., provider_get_current_model)
1501
1500
  LOKI_CURRENT_TIER="$CURRENT_TIER"
1502
1501
  export LOKI_CURRENT_TIER
1503
1502
 
@@ -1573,14 +1572,6 @@ get_provider_tier_param() {
1573
1572
  *) echo "high" ;;
1574
1573
  esac
1575
1574
  ;;
1576
- gemini)
1577
- case "$tier" in
1578
- planning) echo "${PROVIDER_THINKING_PLANNING:-high}" ;;
1579
- development) echo "${PROVIDER_THINKING_DEVELOPMENT:-medium}" ;;
1580
- fast) echo "${PROVIDER_THINKING_FAST:-low}" ;;
1581
- *) echo "medium" ;;
1582
- esac
1583
- ;;
1584
1575
  cline)
1585
1576
  echo "${CLINE_DEFAULT_MODEL:-${LOKI_CLINE_MODEL:-default}}"
1586
1577
  ;;
@@ -2402,10 +2393,6 @@ spawn_worktree_session() {
2402
2393
  "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
2403
2394
  >> "$log_file" 2>&1 || _wt_exit=$?
2404
2395
  ;;
2405
- gemini)
2406
- invoke_gemini "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
2407
- >> "$log_file" 2>&1 || _wt_exit=$?
2408
- ;;
2409
2396
  cline)
2410
2397
  invoke_cline "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
2411
2398
  >> "$log_file" 2>&1 || _wt_exit=$?
@@ -2605,10 +2592,6 @@ Output ONLY the resolved file content with no conflict markers. No explanations.
2605
2592
  codex)
2606
2593
  resolution=$(codex exec --full-auto "$conflict_prompt" 2>/dev/null)
2607
2594
  ;;
2608
- gemini)
2609
- # Uses invoke_gemini_capture for rate limit fallback to flash model
2610
- resolution=$(invoke_gemini_capture "$conflict_prompt" 2>/dev/null)
2611
- ;;
2612
2595
  cline)
2613
2596
  resolution=$(invoke_cline_capture "$conflict_prompt" 2>/dev/null)
2614
2597
  ;;
@@ -2895,10 +2878,6 @@ check_prerequisites() {
2895
2878
  codex)
2896
2879
  log_info "Install: npm install -g @openai/codex"
2897
2880
  ;;
2898
- gemini)
2899
- # TODO: Verify official Gemini CLI package name when available
2900
- log_info "Install: npm install -g @google/gemini-cli (or visit https://ai.google.dev/)"
2901
- ;;
2902
2881
  cline)
2903
2882
  log_info "Install: npm install -g cline"
2904
2883
  ;;
@@ -3003,7 +2982,7 @@ check_skill_installed() {
3003
2982
  fi
3004
2983
  done
3005
2984
 
3006
- # For providers without skill system (Codex, Gemini), this is expected
2985
+ # For providers without skill system (Codex, Aider), this is expected
3007
2986
  if [ -z "${PROVIDER_SKILL_DIR:-}" ]; then
3008
2987
  log_info "Provider ${PROVIDER_NAME:-unknown} has no native skill directory"
3009
2988
  log_info "Skill will be passed via prompt injection"
@@ -3186,88 +3165,13 @@ _write_pricing_json() {
3186
3165
  "opus": {"input": 5.00, "output": 25.00, "label": "Opus (latest)", "provider": "claude"},
3187
3166
  "sonnet": {"input": 3.00, "output": 15.00, "label": "Sonnet (latest)", "provider": "claude"},
3188
3167
  "haiku": {"input": 1.00, "output": 5.00, "label": "Haiku (latest)", "provider": "claude"},
3189
- "gpt-5.3-codex": {"input": 1.50, "output": 12.00, "label": "GPT-5.3 Codex", "provider": "codex"},
3190
- "gemini-3-pro": {"input": 1.25, "output": 10.00, "label": "Gemini 3 Pro", "provider": "gemini"},
3191
- "gemini-3-flash": {"input": 0.10, "output": 0.40, "label": "Gemini 3 Flash", "provider": "gemini"}
3168
+ "gpt-5.3-codex": {"input": 1.50, "output": 12.00, "label": "GPT-5.3 Codex", "provider": "codex"}
3192
3169
  }
3193
3170
  }
3194
3171
  PRICING_EOF
3195
3172
  log_info "Pricing data written: .loki/pricing.json (provider: ${provider})"
3196
3173
  }
3197
3174
 
3198
- #===============================================================================
3199
- # Gemini Invocation with Rate Limit Fallback
3200
- #===============================================================================
3201
-
3202
- # Invoke Gemini with automatic fallback to flash model on rate limit
3203
- # Usage: invoke_gemini "prompt" [additional args...]
3204
- # Returns: exit code from gemini CLI
3205
- invoke_gemini() {
3206
- local prompt="$1"
3207
- shift
3208
-
3209
- # BUG-PROV-001/006 fix: Use dynamic model resolution instead of frozen PROVIDER_MODEL.
3210
- # provider_get_current_model() resolves based on LOKI_CURRENT_TIER at runtime.
3211
- # Falls back to provider_get_tier_param if available, then to GEMINI_DEFAULT_PRO.
3212
- local model
3213
- if type provider_get_current_model &>/dev/null; then
3214
- model=$(provider_get_current_model)
3215
- else
3216
- model="${GEMINI_DEFAULT_PRO:-gemini-3-pro-preview}"
3217
- fi
3218
- local fallback="${PROVIDER_MODEL_FALLBACK:-${GEMINI_DEFAULT_FLASH:-gemini-3-flash-preview}}"
3219
-
3220
- # Create temp file for output to preserve streaming while checking for rate limit
3221
- local tmp_output
3222
- tmp_output=$(mktemp)
3223
-
3224
- # Try primary model first
3225
- gemini --approval-mode=yolo --model "$model" "$prompt" "$@" < /dev/null 2>&1 | tee "$tmp_output"
3226
- local exit_code=${PIPESTATUS[0]}
3227
-
3228
- # Check for rate limit in output
3229
- if [[ $exit_code -ne 0 ]] && grep -qiE "(rate.?limit|429|quota|resource.?exhausted)" "$tmp_output"; then
3230
- log_warn "Rate limit hit on $model, falling back to $fallback"
3231
- rm -f "$tmp_output"
3232
- gemini --approval-mode=yolo --model "$fallback" "$prompt" "$@" < /dev/null
3233
- exit_code=$?
3234
- else
3235
- rm -f "$tmp_output"
3236
- fi
3237
-
3238
- return $exit_code
3239
- }
3240
-
3241
- # Invoke Gemini and capture output (for variable assignment)
3242
- # Usage: result=$(invoke_gemini_capture "prompt")
3243
- # Falls back to flash model on rate limit
3244
- invoke_gemini_capture() {
3245
- local prompt="$1"
3246
- shift
3247
-
3248
- # BUG-PROV-001/006 fix: Use dynamic model resolution instead of frozen PROVIDER_MODEL
3249
- local model
3250
- if type provider_get_current_model &>/dev/null; then
3251
- model=$(provider_get_current_model)
3252
- else
3253
- model="${GEMINI_DEFAULT_PRO:-gemini-3-pro-preview}"
3254
- fi
3255
- local fallback="${PROVIDER_MODEL_FALLBACK:-${GEMINI_DEFAULT_FLASH:-gemini-3-flash-preview}}"
3256
- local output
3257
-
3258
- # Try primary model first
3259
- output=$(gemini --approval-mode=yolo --model "$model" "$prompt" "$@" < /dev/null 2>&1)
3260
- local exit_code=$?
3261
-
3262
- # Check for rate limit in output
3263
- if [[ $exit_code -ne 0 ]] && echo "$output" | grep -qiE "(rate.?limit|429|quota|resource.?exhausted)"; then
3264
- log_warn "Rate limit hit on $model, falling back to $fallback" >&2
3265
- output=$(gemini --approval-mode=yolo --model "$fallback" "$prompt" "$@" < /dev/null 2>&1)
3266
- fi
3267
-
3268
- echo "$output"
3269
- }
3270
-
3271
3175
  #===============================================================================
3272
3176
  # Cline Invocation (Tier 2 - Near-Full)
3273
3177
  #===============================================================================
@@ -3334,7 +3238,7 @@ invoke_aider_capture() {
3334
3238
  copy_skill_files() {
3335
3239
  # Copy skill files from the CLI package to the project's .loki/ directory.
3336
3240
  # This makes the CLI self-contained - no need to install Claude Code skill separately.
3337
- # All providers (Claude, Gemini, Codex) use the same .loki/skills/ location.
3241
+ # All providers (Claude, Codex, Cline, Aider) use the same .loki/skills/ location.
3338
3242
 
3339
3243
  local skills_src="$PROJECT_DIR/skills"
3340
3244
  local skills_dst=".loki/skills"
@@ -4020,8 +3924,6 @@ track_iteration_complete() {
4020
3924
  local model_tier="${PROVIDER_MODEL_DEVELOPMENT:-sonnet}"
4021
3925
  if [ "${PROVIDER_NAME:-claude}" = "codex" ]; then
4022
3926
  model_tier="${PROVIDER_MODEL_DEVELOPMENT:-${CODEX_DEFAULT_MODEL:-gpt-5.3-codex}}"
4023
- elif [ "${PROVIDER_NAME:-claude}" = "gemini" ]; then
4024
- model_tier="${PROVIDER_MODEL_DEVELOPMENT:-${GEMINI_DEFAULT_PRO:-gemini-3-pro}}"
4025
3927
  elif [ "${PROVIDER_NAME:-claude}" = "cline" ]; then
4026
3928
  model_tier="${CLINE_DEFAULT_MODEL:-${LOKI_CLINE_MODEL:-sonnet}}"
4027
3929
  elif [ "${PROVIDER_NAME:-claude}" = "aider" ]; then
@@ -4973,7 +4875,6 @@ check_command_allowed() {
4973
4875
  # input. Command execution is handled by the AI CLI's own permission model:
4974
4876
  # - Claude Code: --dangerously-skip-permissions (with its own allowlist)
4975
4877
  # - Codex CLI: --full-auto or exec --dangerously-bypass-approvals-and-sandbox
4976
- # - Gemini CLI: --approval-mode=yolo
4977
4878
  #
4978
4879
  # HUMAN_INPUT.md content is injected as a text prompt to the AI agent (not
4979
4880
  # executed as a shell command), and is already guarded by:
@@ -6821,10 +6722,6 @@ BUILD_PROMPT
6821
6722
  codex exec --full-auto "$prompt_text" \
6822
6723
  > "$review_output" 2>/dev/null
6823
6724
  ;;
6824
- gemini)
6825
- invoke_gemini_capture "$prompt_text" \
6826
- > "$review_output" 2>/dev/null
6827
- ;;
6828
6725
  cline)
6829
6726
  invoke_cline_capture "$prompt_text" \
6830
6727
  > "$review_output" 2>/dev/null
@@ -7041,12 +6938,6 @@ ADVERSARIAL_EOF
7041
6938
  > "$result_file" 2>/dev/null || true
7042
6939
  fi
7043
6940
  ;;
7044
- gemini)
7045
- if command -v gemini &>/dev/null; then
7046
- invoke_gemini_capture "$adversarial_prompt" \
7047
- > "$result_file" 2>/dev/null || true
7048
- fi
7049
- ;;
7050
6941
  cline)
7051
6942
  if command -v cline &>/dev/null; then
7052
6943
  invoke_cline_capture "$adversarial_prompt" \
@@ -7683,12 +7574,12 @@ init_failover_state() {
7683
7574
  mkdir -p "$failover_dir"
7684
7575
 
7685
7576
  if [ ! -f "$failover_file" ]; then
7686
- local chain="${LOKI_FAILOVER_CHAIN:-claude,codex,gemini}"
7577
+ local chain="${LOKI_FAILOVER_CHAIN:-claude,codex}"
7687
7578
  local primary="${PROVIDER_NAME:-claude}"
7688
7579
  cat > "$failover_file" << FEOF
7689
7580
  {
7690
7581
  "enabled": true,
7691
- "chain": $(printf '%s' "$chain" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip().split(",")))' 2>/dev/null || echo '["claude","codex","gemini"]'),
7582
+ "chain": $(printf '%s' "$chain" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip().split(",")))' 2>/dev/null || echo '["claude","codex"]'),
7692
7583
  "currentProvider": "$primary",
7693
7584
  "primaryProvider": "$primary",
7694
7585
  "lastFailover": null,
@@ -7717,7 +7608,7 @@ import json, os
7717
7608
  try:
7718
7609
  with open(os.path.join(os.environ.get('TARGET_DIR', '.'), '.loki/state/failover.json')) as f:
7719
7610
  d = json.load(f)
7720
- chain = ','.join(d.get('chain', ['claude','codex','gemini']))
7611
+ chain = ','.join(d.get('chain', ['claude','codex']))
7721
7612
  print(f'FAILOVER_ENABLED={str(d.get("enabled", False)).lower()}')
7722
7613
  print(f'FAILOVER_CHAIN="{chain}"')
7723
7614
  print(f'FAILOVER_CURRENT="{d.get("currentProvider", "claude")}"')
@@ -7820,18 +7711,6 @@ check_provider_health() {
7820
7711
  command -v codex &>/dev/null || return 1
7821
7712
  [ -n "${OPENAI_API_KEY:-}" ] || return 1
7822
7713
  ;;
7823
- gemini)
7824
- command -v gemini &>/dev/null || return 1
7825
- # BUG-PROV-003: Also accept GEMINI_API_KEY and gcloud ADC
7826
- if [ -n "${GOOGLE_API_KEY:-}" ] || [ -n "${GEMINI_API_KEY:-}" ]; then
7827
- return 0
7828
- fi
7829
- # Check for gcloud Application Default Credentials
7830
- if [ -f "${HOME}/.config/gcloud/application_default_credentials.json" ]; then
7831
- return 0
7832
- fi
7833
- return 1
7834
- ;;
7835
7714
  cline)
7836
7715
  command -v cline &>/dev/null || return 1
7837
7716
  ;;
@@ -8105,7 +7984,7 @@ detect_rate_limit() {
8105
7984
  claude)
8106
7985
  wait_secs=$(parse_claude_reset_time "$log_file")
8107
7986
  ;;
8108
- codex|gemini|cline|aider|*)
7987
+ codex|cline|aider|*)
8109
7988
  # No provider-specific reset time format known
8110
7989
  # Fall through to generic parsing
8111
7990
  ;;
@@ -8186,8 +8065,6 @@ pricing = {
8186
8065
  'sonnet': {'input': 3.00, 'output': 15.00},
8187
8066
  'haiku': {'input': 1.00, 'output': 5.00},
8188
8067
  'gpt-5.3-codex': {'input': 1.50, 'output': 12.00},
8189
- 'gemini-3-pro': {'input': 1.25, 'output': 10.00},
8190
- 'gemini-3-flash': {'input': 0.10, 'output': 0.40},
8191
8068
  }
8192
8069
  for f in glob.glob('${efficiency_dir}/*.json'):
8193
8070
  try:
@@ -8349,7 +8226,7 @@ except Exception:
8349
8226
  # v7.4.17: also accepts a file-based fallback at .loki/signals/
8350
8227
  # COMPLETION_REQUESTED -- the LLM can `touch` this file directly when the
8351
8228
  # MCP tool isn't surfaced in its environment (e.g., harness limitations,
8352
- # Codex CLI, Gemini CLI). User reproduction: the LLM said "the
8229
+ # Codex CLI). User reproduction: the LLM said "the
8353
8230
  # loki_complete_task MCP tool isn't loaded in this environment" and
8354
8231
  # tried to signal completion via state files; we now honor that.
8355
8232
  #
@@ -9284,7 +9161,7 @@ build_prompt() {
9284
9161
  # instead of emitting a prose completion string.
9285
9162
  local completion_instruction=""
9286
9163
  # v7.4.17: explicit fallback path. The loki_complete_task MCP tool is
9287
- # not always surfaced in the LLM's environment (Codex CLI, Gemini CLI,
9164
+ # not always surfaced in the LLM's environment (Codex CLI,
9288
9165
  # certain Claude Code harness configs). When unavailable, the LLM
9289
9166
  # should `touch .loki/signals/COMPLETION_REQUESTED` instead -- the
9290
9167
  # runner's check_task_completion_signal honors that file as a
@@ -9354,6 +9231,22 @@ build_prompt() {
9354
9231
  context_injection="$context_injection $memory_context"
9355
9232
  fi
9356
9233
 
9234
+ # Phase F (v7.5.23): inject layered CLAUDE.md context from sibling repos
9235
+ # when this target is part of a cross-project graph. Silent no-op when
9236
+ # LOKI_PROJECT_GRAPH_ROOT is unset (single-project workflows untouched).
9237
+ local _pg_helper_rs="${PROJECT_DIR}/autonomy/lib/project-graph.sh"
9238
+ if [ -f "$_pg_helper_rs" ]; then
9239
+ # shellcheck disable=SC1090
9240
+ . "$_pg_helper_rs" 2>/dev/null || true
9241
+ if [ -n "${LOKI_PROJECT_GRAPH_ROOT:-}" ] && declare -f load_app_graph_context >/dev/null 2>&1; then
9242
+ local app_graph_context=""
9243
+ app_graph_context=$(load_app_graph_context 2>/dev/null || true)
9244
+ if [ -n "$app_graph_context" ]; then
9245
+ context_injection="$context_injection APP_GRAPH_CONTEXT: $app_graph_context"
9246
+ fi
9247
+ fi
9248
+ fi
9249
+
9357
9250
  # Gate failure injection (v6.7.0) - tells LLM what to fix
9358
9251
  local gate_failure_context=""
9359
9252
  if [ -f "${TARGET_DIR:-.}/.loki/quality/gate-failures.txt" ]; then
@@ -10849,7 +10742,7 @@ except Exception as exc:
10849
10742
  esac
10850
10743
  fi
10851
10744
  # Export LOKI_CURRENT_TIER so provider helper functions
10852
- # (e.g., gemini.sh:provider_get_current_model) can resolve the correct model.
10745
+ # can resolve the correct model.
10853
10746
  # Without this, LOKI_CURRENT_TIER is always empty and defaults to "planning".
10854
10747
  LOKI_CURRENT_TIER="$CURRENT_TIER"
10855
10748
  export LOKI_CURRENT_TIER
@@ -10972,6 +10865,53 @@ def save_in_progress(tasks):
10972
10865
  except:
10973
10866
  pass
10974
10867
 
10868
+ # Phase D (v7.5.22): hook-event emission.
10869
+ # Mirror events/emit.sh::safe_append_event_jsonl semantics from inside
10870
+ # python by holding an fcntl.flock on .loki/events.jsonl.lock for the
10871
+ # duration of the append. Bash function is not callable from this
10872
+ # embedded process; fcntl matches the flock(1) path one-to-one.
10873
+ EVENTS_JSONL = ".loki/events.jsonl"
10874
+ HOOK_EVENTS_ENABLED = os.environ.get("LOKI_HOOK_EVENTS", "on") != "off"
10875
+
10876
+ def append_hook_event(event_name, payload):
10877
+ """Append a claude_hook_<event_name> record to .loki/events.jsonl."""
10878
+ if not HOOK_EVENTS_ENABLED:
10879
+ return
10880
+ try:
10881
+ import fcntl
10882
+ except ImportError:
10883
+ fcntl = None
10884
+ try:
10885
+ events_dir = os.path.dirname(EVENTS_JSONL)
10886
+ if events_dir:
10887
+ os.makedirs(events_dir, exist_ok=True)
10888
+ record = {
10889
+ "type": "claude_hook_" + str(event_name).lower(),
10890
+ "source": "claude_cli",
10891
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
10892
+ "payload": payload,
10893
+ }
10894
+ line = json.dumps(record, default=str)
10895
+ lock_path = EVENTS_JSONL + ".lock"
10896
+ if fcntl is not None:
10897
+ # flock path: serialize across processes.
10898
+ with open(lock_path, "a") as lf:
10899
+ try:
10900
+ fcntl.flock(lf.fileno(), fcntl.LOCK_EX)
10901
+ with open(EVENTS_JSONL, "a") as ef:
10902
+ ef.write(line + "\n")
10903
+ finally:
10904
+ try:
10905
+ fcntl.flock(lf.fileno(), fcntl.LOCK_UN)
10906
+ except Exception:
10907
+ pass
10908
+ else:
10909
+ # No fcntl available (extremely rare on POSIX). Best-effort.
10910
+ with open(EVENTS_JSONL, "a") as ef:
10911
+ ef.write(line + "\n")
10912
+ except Exception as e:
10913
+ print(f"{YELLOW}[Hook event append error: {e}]{NC}", file=sys.stderr)
10914
+
10975
10915
  def process_stream():
10976
10916
  global active_agents
10977
10917
  active_agents = load_agents()
@@ -11095,6 +11035,20 @@ def process_stream():
11095
11035
  else:
11096
11036
  print(f"{DIM}[Result]{NC} ", end="", flush=True)
11097
11037
 
11038
+ elif msg_type == "hook_event":
11039
+ # Phase D (v7.5.22): forward Claude hook lifecycle events
11040
+ # into .loki/events.jsonl as claude_hook_<eventname>.
11041
+ # Schema not fully specified upstream; probe common field
11042
+ # names for the event identifier and lowercase it.
11043
+ event_name = (
11044
+ data.get("hook_event")
11045
+ or data.get("event")
11046
+ or data.get("name")
11047
+ or data.get("hook")
11048
+ or "unknown"
11049
+ )
11050
+ append_hook_event(event_name, data)
11051
+
11098
11052
  elif msg_type == "result":
11099
11053
  # Session complete - mark all agents as completed
11100
11054
  completed_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
@@ -11143,51 +11097,6 @@ if __name__ == "__main__":
11143
11097
  } && exit_code=0 || exit_code=$?
11144
11098
  ;;
11145
11099
 
11146
- gemini)
11147
- # Gemini: Degraded mode - no stream-json, no agent tracking
11148
- # Uses invoke_gemini helper for rate limit fallback to flash model
11149
- # BUG-PROV-001 fix: Use tier_param (resolved model) instead of frozen PROVIDER_MODEL
11150
- # tier_param is computed above via get_provider_tier_param() -> resolve_model_for_tier()
11151
- # which returns the correct model name for the current RARV tier
11152
- local model="$tier_param"
11153
- local fallback="${PROVIDER_MODEL_FALLBACK:-${GEMINI_DEFAULT_FLASH:-gemini-3-flash-preview}}"
11154
- echo "[loki] Gemini model: $model (fallback: $fallback), tier: $CURRENT_TIER" >> "$log_file"
11155
- echo "[loki] Gemini model: $model (fallback: $fallback), tier: $CURRENT_TIER" >> "$agent_log"
11156
-
11157
- # BUG-PROV-003: Resolve API key (supports GEMINI_API_KEY alias and ADC)
11158
- if type _gemini_resolve_api_key &>/dev/null; then
11159
- _gemini_resolve_api_key || true
11160
- fi
11161
-
11162
- # Try primary model, fallback on rate limit or auth error
11163
- local tmp_output tmp_stderr
11164
- tmp_output=$(mktemp)
11165
- tmp_stderr=$(mktemp)
11166
- # BUG-RUN-011/RUN-013: Use PIPESTATUS[0] for primary invocation too
11167
- gemini --approval-mode=yolo --model "$model" "$prompt" < /dev/null 2>"$tmp_stderr" | tee "$tmp_output" | tee -a "$log_file" "$agent_log" "$iter_output"
11168
- exit_code=${PIPESTATUS[0]}
11169
-
11170
- # BUG-PROV-003: Handle auth errors with API key rotation
11171
- if [[ $exit_code -ne 0 ]] && grep -qiE "(401|403|unauthorized|forbidden|invalid.?api.?key|permission.?denied)" "$tmp_stderr" 2>/dev/null; then
11172
- if type _gemini_rotate_api_key &>/dev/null && _gemini_rotate_api_key; then
11173
- log_warn "Auth error on Gemini, rotated to next API key"
11174
- rm -f "$tmp_output" "$tmp_stderr"
11175
- tmp_output=$(mktemp)
11176
- tmp_stderr=$(mktemp)
11177
- gemini --approval-mode=yolo --model "$model" "$prompt" < /dev/null 2>"$tmp_stderr" | tee "$tmp_output" | tee -a "$log_file" "$agent_log" "$iter_output"
11178
- exit_code=${PIPESTATUS[0]}
11179
- fi
11180
- fi
11181
-
11182
- if [[ $exit_code -ne 0 ]] && grep -qiE "(rate.?limit|429|quota|resource.?exhausted)" "$tmp_stderr" "$tmp_output" 2>/dev/null; then
11183
- log_warn "Rate limit hit on $model, falling back to $fallback"
11184
- echo "[loki] Fallback to $fallback due to rate limit" >> "$log_file"
11185
- gemini --approval-mode=yolo --model "$fallback" "$prompt" < /dev/null 2>&1 | tee -a "$log_file" "$agent_log" "$iter_output"
11186
- exit_code=${PIPESTATUS[0]}
11187
- fi
11188
- rm -f "$tmp_output" "$tmp_stderr"
11189
- ;;
11190
-
11191
11100
  cline)
11192
11101
  # Cline: Tier 2 - near-full mode with subagents and MCP
11193
11102
  echo "[loki] Cline model: ${LOKI_CLINE_MODEL:-default}, tier: $tier_param" >> "$log_file"
@@ -11626,7 +11535,7 @@ INTERRUPT_LAST_TIME=0
11626
11535
  PAUSED=false
11627
11536
 
11628
11537
  # v7.5.12: Track active provider invocation for SIGINT propagation.
11629
- # When non-zero, indicates a provider pipeline (claude/codex/gemini/cline/aider)
11538
+ # When non-zero, indicates a provider pipeline (claude/codex/cline/aider)
11630
11539
  # is currently running and should be killed on Ctrl+C.
11631
11540
  LOKI_PROVIDER_ACTIVE=0
11632
11541
 
@@ -11642,7 +11551,7 @@ kill_provider_child() {
11642
11551
  fi
11643
11552
  # Also kill provider leaf processes by name in case they were reparented.
11644
11553
  local proc
11645
- for proc in claude codex gemini aider cline; do
11554
+ for proc in claude codex aider cline; do
11646
11555
  pkill -TERM -f "^${proc}( |$)" 2>/dev/null && killed=1
11647
11556
  done
11648
11557
 
@@ -12102,7 +12011,7 @@ main() {
12102
12011
  fi
12103
12012
  shift 2
12104
12013
  else
12105
- log_error "--provider requires a value (claude, codex, gemini, cline, aider)"
12014
+ log_error "--provider requires a value (claude, codex, cline, aider)"
12106
12015
  exit 1
12107
12016
  fi
12108
12017
  ;;
@@ -12136,7 +12045,7 @@ main() {
12136
12045
  echo "Options:"
12137
12046
  echo " --parallel Enable git worktree-based parallel workflows"
12138
12047
  echo " --allow-haiku Enable Haiku model for fast tier (default: disabled)"
12139
- echo " --provider <name> Provider: claude (default), codex, gemini, cline, aider"
12048
+ echo " --provider <name> Provider: claude (default), codex, cline, aider"
12140
12049
  echo " --bg, --background Run in background mode"
12141
12050
  echo " --interactive-prd Interactive PRD pre-flight analysis"
12142
12051
  echo " --help, -h Show this help message"
package/bin/loki CHANGED
@@ -88,6 +88,16 @@ if [ -z "${LOKI_TELEMETRY_DISABLED:-}" ] && [ "${DO_NOT_TRACK:-}" != "1" ] && [
88
88
  fi
89
89
  fi
90
90
 
91
+ # v7.5.18: Guard against deprecated LOKI_PROVIDER=gemini before routing.
92
+ # Fires for all commands (Bun-routed and bash-routed) so users get a clear
93
+ # error regardless of which subcommand they invoke.
94
+ if [ "${LOKI_PROVIDER:-}" = "gemini" ]; then
95
+ echo "Error: Provider 'gemini' is deprecated as of v7.5.18 and has been removed." >&2
96
+ echo "Active providers: claude, codex, cline, aider" >&2
97
+ echo "Unset LOKI_PROVIDER or use: LOKI_PROVIDER=claude" >&2
98
+ exit 1
99
+ fi
100
+
91
101
  # Force-fall-through to bash when the rollback flag is set.
92
102
  if [ "${LOKI_LEGACY_BASH:-0}" = "1" ] || [ "${LOKI_LEGACY_BASH:-}" = "true" ]; then
93
103
  exec "$BASH_CLI" "$@"
@@ -103,9 +113,10 @@ fi
103
113
  # Two-token routes (provider show/list, memory list/index) match on the first
104
114
  # token only; the Bun dispatcher handles subcommand routing internally.
105
115
  case "${1:-}" in
106
- version|--version|-v|status|stats|doctor|provider|memory|rollback|internal)
116
+ version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis)
107
117
  # v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
108
118
  # v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
119
+ # v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
109
120
  exec bun "$BUN_CLI" "$@"
110
121
  ;;
111
122
  *)
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.5.17"
10
+ __version__ = "7.5.28"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -1,9 +1,14 @@
1
1
  # Loki Mode Dashboard Dependencies
2
- # Using >= instead of == to avoid build failures on different Python/platform combos
3
- fastapi>=0.100.0
4
- uvicorn>=0.20.0
5
- sqlalchemy>=2.0.0
6
- aiosqlite>=0.19.0
7
- greenlet>=3.0.0
8
- pydantic>=2.0.0
9
- websockets>=12.0
2
+ # Using >= instead of == to avoid build failures on different Python/platform
3
+ # combos. Phase N (v7.5.27) added upper bounds to cap the risk of a future
4
+ # major-version break installing on a fresh `pip install -r requirements.txt`.
5
+ # Tested locally on this Mac at: fastapi 0.128.0, uvicorn 0.40.0,
6
+ # sqlalchemy 2.0.46, aiosqlite 0.22.1, greenlet 3.3.1, pydantic 2.12.5,
7
+ # websockets 15.0.1. Update upper bounds when next major is tested.
8
+ fastapi>=0.100.0,<1.0.0
9
+ uvicorn>=0.20.0,<1.0.0
10
+ sqlalchemy>=2.0.0,<3.0.0
11
+ aiosqlite>=0.19.0,<1.0.0
12
+ greenlet>=3.0.0,<4.0.0
13
+ pydantic>=2.0.0,<3.0.0
14
+ websockets>=12.0,<16.0