loki-mode 6.18.0 → 6.19.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +223 -0
- package/autonomy/run.sh +285 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.
|
|
6
|
+
# Loki Mode v6.19.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -267,4 +267,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
267
267
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
268
268
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
269
269
|
|
|
270
|
-
**v6.
|
|
270
|
+
**v6.19.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.19.0
|
package/autonomy/loki
CHANGED
|
@@ -435,6 +435,7 @@ show_help() {
|
|
|
435
435
|
echo " agent [cmd] Agent type dispatch (list|info|run|start|review)"
|
|
436
436
|
echo " remote [PRD] Start remote session (connect from phone/browser, Claude Pro/Max)"
|
|
437
437
|
echo " trigger Event-driven autonomous execution (schedules, webhooks)"
|
|
438
|
+
echo " failover [cmd] Cross-provider auto-failover (status|--enable|--test|--chain)"
|
|
438
439
|
echo " plan <PRD> Dry-run PRD analysis: complexity, cost, and execution plan"
|
|
439
440
|
echo " version Show version"
|
|
440
441
|
echo " help Show this help"
|
|
@@ -8299,6 +8300,225 @@ for line in sys.stdin:
|
|
|
8299
8300
|
esac
|
|
8300
8301
|
}
|
|
8301
8302
|
|
|
8303
|
+
# Cross-provider auto-failover management (v6.19.0)
|
|
8304
|
+
cmd_failover() {
|
|
8305
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
8306
|
+
local failover_file="$loki_dir/state/failover.json"
|
|
8307
|
+
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8308
|
+
|
|
8309
|
+
if [ $# -eq 0 ]; then
|
|
8310
|
+
# No args: show current failover config and health
|
|
8311
|
+
if [ ! -f "$failover_file" ]; then
|
|
8312
|
+
echo -e "${BOLD}Cross-Provider Auto-Failover${NC}"
|
|
8313
|
+
echo ""
|
|
8314
|
+
echo "Status: DISABLED"
|
|
8315
|
+
echo ""
|
|
8316
|
+
echo "Enable with: loki failover --enable"
|
|
8317
|
+
echo "Or set: LOKI_FAILOVER=true loki start"
|
|
8318
|
+
return 0
|
|
8319
|
+
fi
|
|
8320
|
+
|
|
8321
|
+
echo -e "${BOLD}Cross-Provider Auto-Failover${NC}"
|
|
8322
|
+
echo ""
|
|
8323
|
+
python3 << 'PYEOF'
|
|
8324
|
+
import json, os
|
|
8325
|
+
fpath = os.path.join(os.environ.get('TARGET_DIR', '.'), '.loki/state/failover.json')
|
|
8326
|
+
try:
|
|
8327
|
+
with open(fpath) as f:
|
|
8328
|
+
d = json.load(f)
|
|
8329
|
+
status = "ENABLED" if d.get("enabled") else "DISABLED"
|
|
8330
|
+
print(f"Status: {status}")
|
|
8331
|
+
print(f"Chain: {' -> '.join(d.get('chain', []))}")
|
|
8332
|
+
print(f"Current provider: {d.get('currentProvider', 'unknown')}")
|
|
8333
|
+
print(f"Primary provider: {d.get('primaryProvider', 'unknown')}")
|
|
8334
|
+
print(f"Failover count: {d.get('failoverCount', 0)}")
|
|
8335
|
+
last = d.get('lastFailover')
|
|
8336
|
+
print(f"Last failover: {last if last else 'never'}")
|
|
8337
|
+
print()
|
|
8338
|
+
print("Health Status:")
|
|
8339
|
+
health = d.get('healthCheck', {})
|
|
8340
|
+
for provider in d.get('chain', []):
|
|
8341
|
+
h = health.get(provider, 'unknown')
|
|
8342
|
+
indicator = '[OK]' if h == 'healthy' else '[--]' if h == 'unknown' else '[!!]'
|
|
8343
|
+
print(f" {indicator} {provider}: {h}")
|
|
8344
|
+
except FileNotFoundError:
|
|
8345
|
+
print("Failover not initialized. Run: loki failover --enable")
|
|
8346
|
+
except Exception as e:
|
|
8347
|
+
print(f"Error reading failover state: {e}")
|
|
8348
|
+
PYEOF
|
|
8349
|
+
return 0
|
|
8350
|
+
fi
|
|
8351
|
+
|
|
8352
|
+
while [[ $# -gt 0 ]]; do
|
|
8353
|
+
case "$1" in
|
|
8354
|
+
--enable)
|
|
8355
|
+
mkdir -p "$loki_dir/state"
|
|
8356
|
+
local current_provider
|
|
8357
|
+
current_provider=$(cat "$loki_dir/state/provider" 2>/dev/null || echo "claude")
|
|
8358
|
+
local chain="${LOKI_FAILOVER_CHAIN:-claude,codex,gemini}"
|
|
8359
|
+
|
|
8360
|
+
cat > "$failover_file" << FEOF
|
|
8361
|
+
{
|
|
8362
|
+
"enabled": true,
|
|
8363
|
+
"chain": $(printf '%s' "$chain" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip().split(",")))' 2>/dev/null || echo '["claude","codex","gemini"]'),
|
|
8364
|
+
"currentProvider": "$current_provider",
|
|
8365
|
+
"primaryProvider": "$current_provider",
|
|
8366
|
+
"lastFailover": null,
|
|
8367
|
+
"failoverCount": 0,
|
|
8368
|
+
"healthCheck": {}
|
|
8369
|
+
}
|
|
8370
|
+
FEOF
|
|
8371
|
+
echo -e "${GREEN}Failover enabled${NC}"
|
|
8372
|
+
echo "Chain: $chain"
|
|
8373
|
+
echo "Primary: $current_provider"
|
|
8374
|
+
shift
|
|
8375
|
+
;;
|
|
8376
|
+
--disable)
|
|
8377
|
+
if [ -f "$failover_file" ]; then
|
|
8378
|
+
python3 -c "
|
|
8379
|
+
import json
|
|
8380
|
+
with open('$failover_file') as f: d = json.load(f)
|
|
8381
|
+
d['enabled'] = False
|
|
8382
|
+
with open('$failover_file', 'w') as f: json.dump(d, f, indent=2)
|
|
8383
|
+
" 2>/dev/null
|
|
8384
|
+
echo -e "${YELLOW}Failover disabled${NC}"
|
|
8385
|
+
else
|
|
8386
|
+
echo "Failover not initialized."
|
|
8387
|
+
fi
|
|
8388
|
+
shift
|
|
8389
|
+
;;
|
|
8390
|
+
--chain)
|
|
8391
|
+
shift
|
|
8392
|
+
local new_chain="$1"
|
|
8393
|
+
if [ -z "$new_chain" ]; then
|
|
8394
|
+
echo -e "${RED}Error: --chain requires a comma-separated list of providers${NC}"
|
|
8395
|
+
return 1
|
|
8396
|
+
fi
|
|
8397
|
+
# Validate each provider in chain
|
|
8398
|
+
local IFS=','
|
|
8399
|
+
for p in $new_chain; do
|
|
8400
|
+
case "$p" in
|
|
8401
|
+
claude|codex|gemini|cline|aider) ;;
|
|
8402
|
+
*) echo -e "${RED}Error: invalid provider '$p' in chain${NC}"; return 1 ;;
|
|
8403
|
+
esac
|
|
8404
|
+
done
|
|
8405
|
+
unset IFS
|
|
8406
|
+
|
|
8407
|
+
if [ ! -f "$failover_file" ]; then
|
|
8408
|
+
echo -e "${RED}Error: failover not initialized. Run: loki failover --enable${NC}"
|
|
8409
|
+
return 1
|
|
8410
|
+
fi
|
|
8411
|
+
|
|
8412
|
+
python3 -c "
|
|
8413
|
+
import json
|
|
8414
|
+
with open('$failover_file') as f: d = json.load(f)
|
|
8415
|
+
d['chain'] = '$new_chain'.split(',')
|
|
8416
|
+
with open('$failover_file', 'w') as f: json.dump(d, f, indent=2)
|
|
8417
|
+
" 2>/dev/null
|
|
8418
|
+
echo "Failover chain updated: $new_chain"
|
|
8419
|
+
shift
|
|
8420
|
+
;;
|
|
8421
|
+
--test)
|
|
8422
|
+
echo -e "${BOLD}Testing all providers in failover chain...${NC}"
|
|
8423
|
+
echo ""
|
|
8424
|
+
# Source provider loader for health checks
|
|
8425
|
+
if [ -f "$script_dir/../providers/loader.sh" ]; then
|
|
8426
|
+
source "$script_dir/../providers/loader.sh"
|
|
8427
|
+
fi
|
|
8428
|
+
|
|
8429
|
+
local chain_providers="claude,codex,gemini"
|
|
8430
|
+
if [ -f "$failover_file" ]; then
|
|
8431
|
+
chain_providers=$(python3 -c "import json; print(','.join(json.load(open('$failover_file')).get('chain', ['claude','codex','gemini'])))" 2>/dev/null || echo "claude,codex,gemini")
|
|
8432
|
+
fi
|
|
8433
|
+
|
|
8434
|
+
local IFS=','
|
|
8435
|
+
local all_pass=true
|
|
8436
|
+
for p in $chain_providers; do
|
|
8437
|
+
printf " %-10s " "$p:"
|
|
8438
|
+
|
|
8439
|
+
# Check CLI installed
|
|
8440
|
+
local cli_name="$p"
|
|
8441
|
+
if command -v "$cli_name" &>/dev/null; then
|
|
8442
|
+
local cli_version
|
|
8443
|
+
cli_version=$("$cli_name" --version 2>/dev/null | head -1 || echo "unknown")
|
|
8444
|
+
printf "CLI %-20s " "[$cli_version]"
|
|
8445
|
+
else
|
|
8446
|
+
printf "CLI %-20s " "[NOT INSTALLED]"
|
|
8447
|
+
echo -e "${RED}FAIL${NC}"
|
|
8448
|
+
all_pass=false
|
|
8449
|
+
continue
|
|
8450
|
+
fi
|
|
8451
|
+
|
|
8452
|
+
# Check API key
|
|
8453
|
+
local has_key=false
|
|
8454
|
+
case "$p" in
|
|
8455
|
+
claude) [ -n "${ANTHROPIC_API_KEY:-}" ] && has_key=true ;;
|
|
8456
|
+
codex) [ -n "${OPENAI_API_KEY:-}" ] && has_key=true ;;
|
|
8457
|
+
gemini) [ -n "${GOOGLE_API_KEY:-${GEMINI_API_KEY:-}}" ] && has_key=true ;;
|
|
8458
|
+
cline|aider) has_key=true ;; # Key check varies
|
|
8459
|
+
esac
|
|
8460
|
+
|
|
8461
|
+
if [ "$has_key" = "true" ]; then
|
|
8462
|
+
printf "Key [OK] "
|
|
8463
|
+
echo -e "${GREEN}PASS${NC}"
|
|
8464
|
+
else
|
|
8465
|
+
printf "Key [MISSING] "
|
|
8466
|
+
echo -e "${RED}FAIL${NC}"
|
|
8467
|
+
all_pass=false
|
|
8468
|
+
fi
|
|
8469
|
+
done
|
|
8470
|
+
unset IFS
|
|
8471
|
+
|
|
8472
|
+
echo ""
|
|
8473
|
+
if [ "$all_pass" = "true" ]; then
|
|
8474
|
+
echo -e "${GREEN}All providers healthy${NC}"
|
|
8475
|
+
else
|
|
8476
|
+
echo -e "${YELLOW}Some providers unavailable - failover chain may be limited${NC}"
|
|
8477
|
+
fi
|
|
8478
|
+
shift
|
|
8479
|
+
;;
|
|
8480
|
+
--reset)
|
|
8481
|
+
if [ -f "$failover_file" ]; then
|
|
8482
|
+
rm -f "$failover_file"
|
|
8483
|
+
echo "Failover state reset to defaults."
|
|
8484
|
+
else
|
|
8485
|
+
echo "No failover state to reset."
|
|
8486
|
+
fi
|
|
8487
|
+
shift
|
|
8488
|
+
;;
|
|
8489
|
+
--help|-h)
|
|
8490
|
+
echo -e "${BOLD}loki failover${NC} - Cross-provider auto-failover management (v6.19.0)"
|
|
8491
|
+
echo ""
|
|
8492
|
+
echo "Usage: loki failover [options]"
|
|
8493
|
+
echo ""
|
|
8494
|
+
echo "Options:"
|
|
8495
|
+
echo " (no args) Show failover status and health"
|
|
8496
|
+
echo " --enable Enable auto-failover"
|
|
8497
|
+
echo " --disable Disable auto-failover"
|
|
8498
|
+
echo " --chain X,Y,Z Set failover chain (e.g., claude,codex,gemini)"
|
|
8499
|
+
echo " --test Test all providers in chain"
|
|
8500
|
+
echo " --reset Reset failover state to defaults"
|
|
8501
|
+
echo " --help, -h Show this help"
|
|
8502
|
+
echo ""
|
|
8503
|
+
echo "Environment:"
|
|
8504
|
+
echo " LOKI_FAILOVER=true Enable failover at startup"
|
|
8505
|
+
echo " LOKI_FAILOVER_CHAIN=X,Y,Z Set default chain"
|
|
8506
|
+
echo ""
|
|
8507
|
+
echo "When a rate limit (429/529) is detected, Loki automatically switches"
|
|
8508
|
+
echo "to the next healthy provider in the chain. After each successful"
|
|
8509
|
+
echo "iteration on a fallback provider, the primary is health-checked and"
|
|
8510
|
+
echo "execution switches back when it recovers."
|
|
8511
|
+
return 0
|
|
8512
|
+
;;
|
|
8513
|
+
*)
|
|
8514
|
+
echo -e "${RED}Unknown option: $1${NC}"
|
|
8515
|
+
echo "Run 'loki failover --help' for usage."
|
|
8516
|
+
return 1
|
|
8517
|
+
;;
|
|
8518
|
+
esac
|
|
8519
|
+
done
|
|
8520
|
+
}
|
|
8521
|
+
|
|
8302
8522
|
# Dry-run PRD analysis and cost estimation (v6.18.0)
|
|
8303
8523
|
cmd_plan() {
|
|
8304
8524
|
local prd_file=""
|
|
@@ -8966,6 +9186,9 @@ main() {
|
|
|
8966
9186
|
plan)
|
|
8967
9187
|
cmd_plan "$@"
|
|
8968
9188
|
;;
|
|
9189
|
+
failover)
|
|
9190
|
+
cmd_failover "$@"
|
|
9191
|
+
;;
|
|
8969
9192
|
version|--version|-v)
|
|
8970
9193
|
cmd_version
|
|
8971
9194
|
;;
|
package/autonomy/run.sh
CHANGED
|
@@ -6525,6 +6525,278 @@ calculate_wait() {
|
|
|
6525
6525
|
echo $wait_time
|
|
6526
6526
|
}
|
|
6527
6527
|
|
|
6528
|
+
#===============================================================================
|
|
6529
|
+
# Cross-Provider Auto-Failover (v6.19.0)
|
|
6530
|
+
#===============================================================================
|
|
6531
|
+
|
|
6532
|
+
# Initialize failover state file on startup
|
|
6533
|
+
init_failover_state() {
|
|
6534
|
+
local failover_dir="${TARGET_DIR:-.}/.loki/state"
|
|
6535
|
+
local failover_file="$failover_dir/failover.json"
|
|
6536
|
+
|
|
6537
|
+
# Only create if failover is enabled via env or config
|
|
6538
|
+
if [ "${LOKI_FAILOVER:-false}" != "true" ]; then
|
|
6539
|
+
return
|
|
6540
|
+
fi
|
|
6541
|
+
|
|
6542
|
+
mkdir -p "$failover_dir"
|
|
6543
|
+
|
|
6544
|
+
if [ ! -f "$failover_file" ]; then
|
|
6545
|
+
local chain="${LOKI_FAILOVER_CHAIN:-claude,codex,gemini}"
|
|
6546
|
+
local primary="${PROVIDER_NAME:-claude}"
|
|
6547
|
+
cat > "$failover_file" << FEOF
|
|
6548
|
+
{
|
|
6549
|
+
"enabled": true,
|
|
6550
|
+
"chain": $(printf '%s' "$chain" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip().split(",")))' 2>/dev/null || echo '["claude","codex","gemini"]'),
|
|
6551
|
+
"currentProvider": "$primary",
|
|
6552
|
+
"primaryProvider": "$primary",
|
|
6553
|
+
"lastFailover": null,
|
|
6554
|
+
"failoverCount": 0,
|
|
6555
|
+
"healthCheck": {
|
|
6556
|
+
"$primary": "healthy"
|
|
6557
|
+
}
|
|
6558
|
+
}
|
|
6559
|
+
FEOF
|
|
6560
|
+
log_info "Failover initialized: chain=$chain, primary=$primary"
|
|
6561
|
+
fi
|
|
6562
|
+
}
|
|
6563
|
+
|
|
6564
|
+
# Read failover config from state file
|
|
6565
|
+
# Sets: FAILOVER_ENABLED, FAILOVER_CHAIN, FAILOVER_CURRENT, FAILOVER_PRIMARY
|
|
6566
|
+
read_failover_config() {
|
|
6567
|
+
local failover_file="${TARGET_DIR:-.}/.loki/state/failover.json"
|
|
6568
|
+
|
|
6569
|
+
if [ ! -f "$failover_file" ]; then
|
|
6570
|
+
FAILOVER_ENABLED="false"
|
|
6571
|
+
return 1
|
|
6572
|
+
fi
|
|
6573
|
+
|
|
6574
|
+
eval "$(python3 << 'PYEOF' 2>/dev/null || echo 'FAILOVER_ENABLED=false'
|
|
6575
|
+
import json, os
|
|
6576
|
+
try:
|
|
6577
|
+
with open(os.path.join(os.environ.get('TARGET_DIR', '.'), '.loki/state/failover.json')) as f:
|
|
6578
|
+
d = json.load(f)
|
|
6579
|
+
chain = ','.join(d.get('chain', ['claude','codex','gemini']))
|
|
6580
|
+
print(f'FAILOVER_ENABLED={str(d.get("enabled", False)).lower()}')
|
|
6581
|
+
print(f'FAILOVER_CHAIN="{chain}"')
|
|
6582
|
+
print(f'FAILOVER_CURRENT="{d.get("currentProvider", "claude")}"')
|
|
6583
|
+
print(f'FAILOVER_PRIMARY="{d.get("primaryProvider", "claude")}"')
|
|
6584
|
+
print(f'FAILOVER_COUNT={d.get("failoverCount", 0)}')
|
|
6585
|
+
except Exception:
|
|
6586
|
+
print('FAILOVER_ENABLED=false')
|
|
6587
|
+
PYEOF
|
|
6588
|
+
)"
|
|
6589
|
+
}
|
|
6590
|
+
|
|
6591
|
+
# Update failover state file
|
|
6592
|
+
update_failover_state() {
|
|
6593
|
+
local key="$1"
|
|
6594
|
+
local value="$2"
|
|
6595
|
+
local failover_file="${TARGET_DIR:-.}/.loki/state/failover.json"
|
|
6596
|
+
|
|
6597
|
+
[ ! -f "$failover_file" ] && return 1
|
|
6598
|
+
|
|
6599
|
+
python3 << PYEOF 2>/dev/null || true
|
|
6600
|
+
import json, os
|
|
6601
|
+
fpath = os.path.join(os.environ.get('TARGET_DIR', '.'), '.loki/state/failover.json')
|
|
6602
|
+
try:
|
|
6603
|
+
with open(fpath) as f:
|
|
6604
|
+
d = json.load(f)
|
|
6605
|
+
key = "$key"
|
|
6606
|
+
value = "$value"
|
|
6607
|
+
# Handle type conversion
|
|
6608
|
+
if value == "null":
|
|
6609
|
+
d[key] = None
|
|
6610
|
+
elif value == "true":
|
|
6611
|
+
d[key] = True
|
|
6612
|
+
elif value == "false":
|
|
6613
|
+
d[key] = False
|
|
6614
|
+
elif value.isdigit():
|
|
6615
|
+
d[key] = int(value)
|
|
6616
|
+
else:
|
|
6617
|
+
d[key] = value
|
|
6618
|
+
with open(fpath, 'w') as f:
|
|
6619
|
+
json.dump(d, f, indent=2)
|
|
6620
|
+
except Exception:
|
|
6621
|
+
pass
|
|
6622
|
+
PYEOF
|
|
6623
|
+
}
|
|
6624
|
+
|
|
6625
|
+
# Update health status for a specific provider in failover.json
|
|
6626
|
+
update_failover_health() {
|
|
6627
|
+
local provider="$1"
|
|
6628
|
+
local status="$2" # healthy, unhealthy, unknown
|
|
6629
|
+
local failover_file="${TARGET_DIR:-.}/.loki/state/failover.json"
|
|
6630
|
+
|
|
6631
|
+
[ ! -f "$failover_file" ] && return 1
|
|
6632
|
+
|
|
6633
|
+
python3 << PYEOF 2>/dev/null || true
|
|
6634
|
+
import json, os
|
|
6635
|
+
fpath = os.path.join(os.environ.get('TARGET_DIR', '.'), '.loki/state/failover.json')
|
|
6636
|
+
try:
|
|
6637
|
+
with open(fpath) as f:
|
|
6638
|
+
d = json.load(f)
|
|
6639
|
+
if 'healthCheck' not in d:
|
|
6640
|
+
d['healthCheck'] = {}
|
|
6641
|
+
d['healthCheck']["$provider"] = "$status"
|
|
6642
|
+
with open(fpath, 'w') as f:
|
|
6643
|
+
json.dump(d, f, indent=2)
|
|
6644
|
+
except Exception:
|
|
6645
|
+
pass
|
|
6646
|
+
PYEOF
|
|
6647
|
+
}
|
|
6648
|
+
|
|
6649
|
+
# Check provider health: API key exists + CLI installed
|
|
6650
|
+
# Returns: 0 if healthy, 1 if unhealthy
|
|
6651
|
+
check_provider_health() {
|
|
6652
|
+
local provider="$1"
|
|
6653
|
+
|
|
6654
|
+
# Check CLI is installed
|
|
6655
|
+
case "$provider" in
|
|
6656
|
+
claude)
|
|
6657
|
+
command -v claude &>/dev/null || return 1
|
|
6658
|
+
[ -n "${ANTHROPIC_API_KEY:-}" ] || return 1
|
|
6659
|
+
;;
|
|
6660
|
+
codex)
|
|
6661
|
+
command -v codex &>/dev/null || return 1
|
|
6662
|
+
[ -n "${OPENAI_API_KEY:-}" ] || return 1
|
|
6663
|
+
;;
|
|
6664
|
+
gemini)
|
|
6665
|
+
command -v gemini &>/dev/null || return 1
|
|
6666
|
+
[ -n "${GOOGLE_API_KEY:-${GEMINI_API_KEY:-}}" ] || return 1
|
|
6667
|
+
;;
|
|
6668
|
+
cline)
|
|
6669
|
+
command -v cline &>/dev/null || return 1
|
|
6670
|
+
;;
|
|
6671
|
+
aider)
|
|
6672
|
+
command -v aider &>/dev/null || return 1
|
|
6673
|
+
;;
|
|
6674
|
+
*)
|
|
6675
|
+
return 1
|
|
6676
|
+
;;
|
|
6677
|
+
esac
|
|
6678
|
+
|
|
6679
|
+
return 0
|
|
6680
|
+
}
|
|
6681
|
+
|
|
6682
|
+
# Attempt failover to next healthy provider in chain
|
|
6683
|
+
# Called when rate limit is detected on current provider
|
|
6684
|
+
# Returns: 0 if failover succeeded, 1 if all providers exhausted
|
|
6685
|
+
attempt_provider_failover() {
|
|
6686
|
+
read_failover_config || return 1
|
|
6687
|
+
|
|
6688
|
+
if [ "$FAILOVER_ENABLED" != "true" ]; then
|
|
6689
|
+
return 1
|
|
6690
|
+
fi
|
|
6691
|
+
|
|
6692
|
+
local current="${FAILOVER_CURRENT:-${PROVIDER_NAME:-claude}}"
|
|
6693
|
+
log_warn "Failover: rate limit on $current, checking chain: $FAILOVER_CHAIN"
|
|
6694
|
+
|
|
6695
|
+
# Mark current as unhealthy
|
|
6696
|
+
update_failover_health "$current" "unhealthy"
|
|
6697
|
+
|
|
6698
|
+
# Walk the chain looking for the next healthy provider
|
|
6699
|
+
local IFS=','
|
|
6700
|
+
local found_current=false
|
|
6701
|
+
local tried_wrap=false
|
|
6702
|
+
|
|
6703
|
+
# Two passes: first from current position to end, then from start to current
|
|
6704
|
+
for provider in $FAILOVER_CHAIN $FAILOVER_CHAIN; do
|
|
6705
|
+
if [ "$provider" = "$current" ]; then
|
|
6706
|
+
if [ "$found_current" = "true" ]; then
|
|
6707
|
+
# We've wrapped around, all exhausted
|
|
6708
|
+
break
|
|
6709
|
+
fi
|
|
6710
|
+
found_current=true
|
|
6711
|
+
continue
|
|
6712
|
+
fi
|
|
6713
|
+
|
|
6714
|
+
[ "$found_current" != "true" ] && continue
|
|
6715
|
+
|
|
6716
|
+
# Check if this provider is healthy
|
|
6717
|
+
if check_provider_health "$provider"; then
|
|
6718
|
+
log_info "Failover: switching from $current to $provider"
|
|
6719
|
+
|
|
6720
|
+
# Load the new provider config
|
|
6721
|
+
local provider_dir
|
|
6722
|
+
provider_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/providers"
|
|
6723
|
+
if [ -f "$provider_dir/$provider.sh" ]; then
|
|
6724
|
+
source "$provider_dir/$provider.sh"
|
|
6725
|
+
fi
|
|
6726
|
+
|
|
6727
|
+
# Update state
|
|
6728
|
+
update_failover_state "currentProvider" "$provider"
|
|
6729
|
+
update_failover_state "lastFailover" "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
6730
|
+
update_failover_state "failoverCount" "$((FAILOVER_COUNT + 1))"
|
|
6731
|
+
update_failover_health "$provider" "healthy"
|
|
6732
|
+
|
|
6733
|
+
# Update runtime provider vars
|
|
6734
|
+
PROVIDER_NAME="$provider"
|
|
6735
|
+
|
|
6736
|
+
emit_event_json "provider_failover" \
|
|
6737
|
+
"from=$current" \
|
|
6738
|
+
"to=$provider" \
|
|
6739
|
+
"reason=rate_limit" \
|
|
6740
|
+
"iteration=$ITERATION_COUNT" 2>/dev/null || true
|
|
6741
|
+
|
|
6742
|
+
log_info "Failover: now using $provider (failover #$((FAILOVER_COUNT + 1)))"
|
|
6743
|
+
return 0
|
|
6744
|
+
else
|
|
6745
|
+
log_debug "Failover: $provider is unhealthy, skipping"
|
|
6746
|
+
update_failover_health "$provider" "unhealthy"
|
|
6747
|
+
fi
|
|
6748
|
+
done
|
|
6749
|
+
|
|
6750
|
+
log_warn "Failover: all providers in chain exhausted, falling back to retry"
|
|
6751
|
+
return 1
|
|
6752
|
+
}
|
|
6753
|
+
|
|
6754
|
+
# Check if primary provider has recovered after running on a fallback
|
|
6755
|
+
# Called after each successful iteration when on a non-primary provider
|
|
6756
|
+
# Returns: 0 if switched back to primary, 1 if still on fallback
|
|
6757
|
+
check_primary_recovery() {
|
|
6758
|
+
read_failover_config || return 1
|
|
6759
|
+
|
|
6760
|
+
if [ "$FAILOVER_ENABLED" != "true" ]; then
|
|
6761
|
+
return 1
|
|
6762
|
+
fi
|
|
6763
|
+
|
|
6764
|
+
local current="${FAILOVER_CURRENT:-${PROVIDER_NAME:-claude}}"
|
|
6765
|
+
local primary="${FAILOVER_PRIMARY:-claude}"
|
|
6766
|
+
|
|
6767
|
+
# Already on primary
|
|
6768
|
+
if [ "$current" = "$primary" ]; then
|
|
6769
|
+
return 1
|
|
6770
|
+
fi
|
|
6771
|
+
|
|
6772
|
+
# Check if primary is healthy again
|
|
6773
|
+
if check_provider_health "$primary"; then
|
|
6774
|
+
log_info "Failover: primary provider $primary appears healthy, switching back"
|
|
6775
|
+
|
|
6776
|
+
# Load primary provider config
|
|
6777
|
+
local provider_dir
|
|
6778
|
+
provider_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/providers"
|
|
6779
|
+
if [ -f "$provider_dir/$primary.sh" ]; then
|
|
6780
|
+
source "$provider_dir/$primary.sh"
|
|
6781
|
+
fi
|
|
6782
|
+
|
|
6783
|
+
update_failover_state "currentProvider" "$primary"
|
|
6784
|
+
update_failover_health "$primary" "healthy"
|
|
6785
|
+
|
|
6786
|
+
PROVIDER_NAME="$primary"
|
|
6787
|
+
|
|
6788
|
+
emit_event_json "provider_recovery" \
|
|
6789
|
+
"from=$current" \
|
|
6790
|
+
"to=$primary" \
|
|
6791
|
+
"iteration=$ITERATION_COUNT" 2>/dev/null || true
|
|
6792
|
+
|
|
6793
|
+
log_info "Failover: recovered to primary provider $primary"
|
|
6794
|
+
return 0
|
|
6795
|
+
fi
|
|
6796
|
+
|
|
6797
|
+
return 1
|
|
6798
|
+
}
|
|
6799
|
+
|
|
6528
6800
|
#===============================================================================
|
|
6529
6801
|
# Rate Limit Detection
|
|
6530
6802
|
#===============================================================================
|
|
@@ -8145,6 +8417,9 @@ run_autonomous() {
|
|
|
8145
8417
|
load_state
|
|
8146
8418
|
local retry=$RETRY_COUNT
|
|
8147
8419
|
|
|
8420
|
+
# Initialize Cross-Provider Failover (v6.19.0)
|
|
8421
|
+
init_failover_state
|
|
8422
|
+
|
|
8148
8423
|
# Initialize Completion Council (v5.25.0)
|
|
8149
8424
|
if type council_init &>/dev/null; then
|
|
8150
8425
|
council_init "$prd_path"
|
|
@@ -8784,6 +9059,9 @@ if __name__ == "__main__":
|
|
|
8784
9059
|
log_warn "Council will evaluate at next check interval (every ${COUNCIL_CHECK_INTERVAL:-5} iterations)"
|
|
8785
9060
|
fi
|
|
8786
9061
|
|
|
9062
|
+
# Cross-provider failover: check if primary has recovered (v6.19.0)
|
|
9063
|
+
check_primary_recovery 2>/dev/null || true
|
|
9064
|
+
|
|
8787
9065
|
# SUCCESS exit - continue IMMEDIATELY to next iteration (no wait!)
|
|
8788
9066
|
log_step "Starting next iteration..."
|
|
8789
9067
|
((retry++))
|
|
@@ -8801,6 +9079,13 @@ if __name__ == "__main__":
|
|
|
8801
9079
|
local wait_time
|
|
8802
9080
|
|
|
8803
9081
|
if [ $rate_limit_wait -gt 0 ]; then
|
|
9082
|
+
# Cross-provider failover (v6.19.0): try switching provider before waiting
|
|
9083
|
+
if attempt_provider_failover 2>/dev/null; then
|
|
9084
|
+
log_info "Failover succeeded - retrying immediately with ${PROVIDER_NAME}"
|
|
9085
|
+
((retry++))
|
|
9086
|
+
continue
|
|
9087
|
+
fi
|
|
9088
|
+
|
|
8804
9089
|
wait_time=$rate_limit_wait
|
|
8805
9090
|
local human_time=$(format_duration $wait_time)
|
|
8806
9091
|
log_warn "Rate limit detected! Waiting until reset (~$human_time)..."
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED