loki-mode 7.5.17 → 7.5.27
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 +10 -9
- package/SKILL.md +14 -14
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +26 -3
- package/autonomy/lib/claude-flags.sh +132 -0
- package/autonomy/lib/mcp-config.sh +160 -0
- package/autonomy/lib/project-graph.sh +675 -0
- package/autonomy/lib/voter-agents.sh +356 -0
- package/autonomy/loki +61 -96
- package/autonomy/run.sh +95 -186
- package/bin/loki +10 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/requirements.txt +13 -8
- package/dashboard/server.py +33 -15
- package/dashboard/static/index.html +298 -299
- package/docs/INSTALLATION.md +54 -21
- package/docs/retrospectives/v7.5.15-fleet-postmortem.md +325 -0
- package/docs/retrospectives/v7.5.15-honesty-audit.md +136 -0
- package/docs/retrospectives/v7.5.15-llm-failure-modes.md +49 -0
- package/loki-ts/data/finding-schema.json +74 -0
- package/loki-ts/data/model-pricing.json +12 -0
- package/loki-ts/dist/loki.js +109 -108
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +713 -0
- package/mcp/requirements.txt +9 -3
- package/mcp/tests/__init__.py +0 -0
- package/mcp/tests/test_lsp_proxy.py +377 -0
- package/memory/app_graph.py +153 -0
- package/memory/storage.py +6 -1
- package/memory/tests/test_app_graph.py +134 -0
- package/package.json +4 -3
- package/providers/claude.sh +115 -4
- package/providers/codex.sh +2 -2
- package/providers/loader.sh +4 -4
- package/providers/model_catalog.json +0 -9
- package/providers/models.sh +1 -2
- package/references/multi-provider.md +26 -35
- package/references/prompt-repetition.md +1 -1
- package/references/quality-control.md +1 -1
- package/skills/00-index.md +3 -3
- package/skills/model-selection.md +11 -14
- package/skills/providers.md +17 -57
- package/skills/quality-gates.md +2 -2
- package/skills/troubleshooting.md +1 -1
- package/src/integrations/github/action-handler.js +3 -2
- package/src/protocols/tools/start-project.js +1 -1
- 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,
|
|
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,
|
|
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,
|
|
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.,
|
|
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,
|
|
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,
|
|
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
|
|
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"
|
|
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'
|
|
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|
|
|
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
|
|
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,
|
|
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
|
-
#
|
|
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/
|
|
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
|
|
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,
|
|
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,
|
|
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" "$@"
|
package/dashboard/__init__.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# Loki Mode Dashboard Dependencies
|
|
2
|
-
# Using >= instead of == to avoid build failures on different Python/platform
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
aiosqlite
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|