loki-mode 6.18.0 → 6.20.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 +660 -184
- 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.20.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.20.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.20.0
|
package/autonomy/loki
CHANGED
|
@@ -420,7 +420,7 @@ show_help() {
|
|
|
420
420
|
echo " checkpoint|cp Save/restore session checkpoints"
|
|
421
421
|
echo " projects Multi-project registry management"
|
|
422
422
|
echo " audit [cmd] Agent audit log and quality scanning (log|scan)"
|
|
423
|
-
echo " review [
|
|
423
|
+
echo " review [opts] Standalone code review with quality gates (diff, staged, PR, files)"
|
|
424
424
|
echo " optimize Optimize prompts based on session history"
|
|
425
425
|
echo " enterprise Enterprise feature management (tokens, OIDC)"
|
|
426
426
|
echo " metrics Prometheus/OpenMetrics metrics from dashboard"
|
|
@@ -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
|
;;
|
|
@@ -8982,224 +9205,477 @@ main() {
|
|
|
8982
9205
|
;;
|
|
8983
9206
|
esac
|
|
8984
9207
|
}
|
|
8985
|
-
|
|
8986
|
-
# Standalone project review - run quality gates on any codebase (v6.14.0)
|
|
9208
|
+
# Standalone code review - diff-based quality gates (v6.20.0)
|
|
8987
9209
|
cmd_review() {
|
|
8988
|
-
local
|
|
8989
|
-
local
|
|
8990
|
-
local
|
|
9210
|
+
local review_staged=false
|
|
9211
|
+
local review_pr=""
|
|
9212
|
+
local review_since=""
|
|
9213
|
+
local review_target=""
|
|
9214
|
+
local review_format="text"
|
|
9215
|
+
local review_severity="all"
|
|
8991
9216
|
|
|
8992
9217
|
while [[ $# -gt 0 ]]; do
|
|
8993
9218
|
case "$1" in
|
|
8994
|
-
--json) json_output="true"; shift ;;
|
|
8995
|
-
--verbose|-v) verbose="true"; shift ;;
|
|
8996
9219
|
--help|-h)
|
|
8997
|
-
echo -e "${BOLD}loki review${NC} -
|
|
9220
|
+
echo -e "${BOLD}loki review${NC} - Standalone code review with quality gates"
|
|
8998
9221
|
echo ""
|
|
8999
|
-
echo "Usage: loki review [
|
|
9222
|
+
echo "Usage: loki review [options] [file-or-dir]"
|
|
9000
9223
|
echo ""
|
|
9001
|
-
echo "
|
|
9002
|
-
echo "
|
|
9224
|
+
echo "Reviews code changes using static analysis, security scanning,"
|
|
9225
|
+
echo "style checks, and anti-pattern detection. No AI needed."
|
|
9226
|
+
echo ""
|
|
9227
|
+
echo "Sources (default: uncommitted changes via git diff):"
|
|
9228
|
+
echo " --staged Review staged changes only"
|
|
9229
|
+
echo " --pr <number> Review a GitHub PR"
|
|
9230
|
+
echo " --since <commit> Review changes since a commit"
|
|
9231
|
+
echo " <file-or-dir> Review specific files or directory"
|
|
9003
9232
|
echo ""
|
|
9004
9233
|
echo "Options:"
|
|
9005
|
-
echo " --json
|
|
9006
|
-
echo " --
|
|
9007
|
-
echo " --help, -h
|
|
9234
|
+
echo " --format json Output as JSON (for CI integration)"
|
|
9235
|
+
echo " --severity <level> Filter: critical, high, medium, low, info (shows level+above)"
|
|
9236
|
+
echo " --help, -h Show this help"
|
|
9008
9237
|
echo ""
|
|
9009
|
-
echo "
|
|
9010
|
-
echo "
|
|
9238
|
+
echo "Exit codes:"
|
|
9239
|
+
echo " 0 No HIGH or CRITICAL findings"
|
|
9240
|
+
echo " 1 HIGH severity findings present"
|
|
9241
|
+
echo " 2 CRITICAL severity findings present"
|
|
9242
|
+
echo ""
|
|
9243
|
+
echo "Examples:"
|
|
9244
|
+
echo " loki review # Review uncommitted changes"
|
|
9245
|
+
echo " loki review --staged # Review staged changes"
|
|
9246
|
+
echo " loki review --pr 42 # Review GitHub PR #42"
|
|
9247
|
+
echo " loki review src/ # Review all files in src/"
|
|
9248
|
+
echo " loki review --since HEAD~5 # Changes in last 5 commits"
|
|
9249
|
+
echo " loki review --format json # JSON output for CI"
|
|
9250
|
+
echo " loki review --severity high # Only HIGH+ findings"
|
|
9011
9251
|
return 0
|
|
9012
9252
|
;;
|
|
9253
|
+
--staged) review_staged=true; shift ;;
|
|
9254
|
+
--pr)
|
|
9255
|
+
shift
|
|
9256
|
+
review_pr="${1:-}"
|
|
9257
|
+
if [ -z "$review_pr" ]; then
|
|
9258
|
+
echo -e "${RED}Error: --pr requires a PR number${NC}"
|
|
9259
|
+
return 1
|
|
9260
|
+
fi
|
|
9261
|
+
shift
|
|
9262
|
+
;;
|
|
9263
|
+
--since)
|
|
9264
|
+
shift
|
|
9265
|
+
review_since="${1:-}"
|
|
9266
|
+
if [ -z "$review_since" ]; then
|
|
9267
|
+
echo -e "${RED}Error: --since requires a commit reference${NC}"
|
|
9268
|
+
return 1
|
|
9269
|
+
fi
|
|
9270
|
+
shift
|
|
9271
|
+
;;
|
|
9272
|
+
--format)
|
|
9273
|
+
shift
|
|
9274
|
+
review_format="${1:-text}"
|
|
9275
|
+
shift
|
|
9276
|
+
;;
|
|
9277
|
+
--severity)
|
|
9278
|
+
shift
|
|
9279
|
+
review_severity="${1:-all}"
|
|
9280
|
+
review_severity="$(echo "$review_severity" | tr '[:upper:]' '[:lower:]')"
|
|
9281
|
+
shift
|
|
9282
|
+
;;
|
|
9013
9283
|
-*) echo -e "${RED}Unknown option: $1${NC}"; return 1 ;;
|
|
9014
|
-
*)
|
|
9284
|
+
*) review_target="$1"; shift ;;
|
|
9015
9285
|
esac
|
|
9016
9286
|
done
|
|
9017
9287
|
|
|
9018
|
-
#
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
# Helper: record gate result
|
|
9029
|
-
_review_gate() {
|
|
9030
|
-
local gate="$1" status="$2" detail="${3:-}"
|
|
9031
|
-
results+=("$gate:$status")
|
|
9032
|
-
gate_details+=("$gate:$detail")
|
|
9033
|
-
case "$status" in
|
|
9034
|
-
PASS) total_pass=$((total_pass + 1)) ;;
|
|
9035
|
-
WARN) total_warn=$((total_warn + 1)) ;;
|
|
9036
|
-
FAIL) total_fail=$((total_fail + 1)) ;;
|
|
9288
|
+
# Severity level ordering (higher number = more severe)
|
|
9289
|
+
_review_sev_level() {
|
|
9290
|
+
case "$1" in
|
|
9291
|
+
INFO) echo 1 ;;
|
|
9292
|
+
LOW) echo 2 ;;
|
|
9293
|
+
MEDIUM) echo 3 ;;
|
|
9294
|
+
HIGH) echo 4 ;;
|
|
9295
|
+
CRITICAL) echo 5 ;;
|
|
9296
|
+
*) echo 0 ;;
|
|
9037
9297
|
esac
|
|
9038
9298
|
}
|
|
9039
9299
|
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9300
|
+
local min_sev=0
|
|
9301
|
+
case "$review_severity" in
|
|
9302
|
+
info) min_sev=1 ;;
|
|
9303
|
+
low) min_sev=2 ;;
|
|
9304
|
+
medium) min_sev=3 ;;
|
|
9305
|
+
high) min_sev=4 ;;
|
|
9306
|
+
critical) min_sev=5 ;;
|
|
9307
|
+
all) min_sev=0 ;;
|
|
9308
|
+
esac
|
|
9309
|
+
|
|
9310
|
+
# Gather code to review
|
|
9311
|
+
local diff_content=""
|
|
9312
|
+
local review_files=""
|
|
9313
|
+
local source_desc=""
|
|
9314
|
+
|
|
9315
|
+
if [ -n "$review_pr" ]; then
|
|
9316
|
+
# PR review via gh CLI
|
|
9317
|
+
if ! command -v gh &>/dev/null; then
|
|
9318
|
+
echo -e "${RED}Error: gh CLI required for PR review. Install: https://cli.github.com${NC}"
|
|
9319
|
+
return 1
|
|
9320
|
+
fi
|
|
9321
|
+
diff_content=$(gh pr diff "$review_pr" 2>&1) || {
|
|
9322
|
+
echo -e "${RED}Error: Could not fetch PR #${review_pr}: $diff_content${NC}"
|
|
9323
|
+
return 1
|
|
9324
|
+
}
|
|
9325
|
+
source_desc="PR #${review_pr}"
|
|
9326
|
+
elif [ -n "$review_since" ]; then
|
|
9327
|
+
diff_content=$(git diff "$review_since" 2>&1) || {
|
|
9328
|
+
echo -e "${RED}Error: Invalid commit reference: $review_since${NC}"
|
|
9329
|
+
return 1
|
|
9330
|
+
}
|
|
9331
|
+
source_desc="changes since $review_since"
|
|
9332
|
+
elif [ -n "$review_target" ]; then
|
|
9333
|
+
# File or directory target - get full contents
|
|
9334
|
+
if [ -d "$review_target" ]; then
|
|
9335
|
+
review_files=$(find "$review_target" -type f \
|
|
9336
|
+
\( -name "*.sh" -o -name "*.bash" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \
|
|
9337
|
+
-o -name "*.jsx" -o -name "*.tsx" -o -name "*.go" -o -name "*.rs" -o -name "*.rb" \
|
|
9338
|
+
-o -name "*.java" -o -name "*.c" -o -name "*.cpp" -o -name "*.h" \) \
|
|
9339
|
+
-not -path "*/node_modules/*" -not -path "*/.git/*" \
|
|
9340
|
+
-not -path "*/vendor/*" -not -path "*/__pycache__/*" \
|
|
9341
|
+
-not -path "*/.venv/*" -not -path "*/target/*" 2>/dev/null)
|
|
9342
|
+
source_desc="directory $review_target"
|
|
9343
|
+
elif [ -f "$review_target" ]; then
|
|
9344
|
+
review_files="$review_target"
|
|
9345
|
+
source_desc="file $review_target"
|
|
9346
|
+
else
|
|
9347
|
+
echo -e "${RED}Error: Not found: $review_target${NC}"
|
|
9348
|
+
return 1
|
|
9349
|
+
fi
|
|
9350
|
+
elif [ "$review_staged" = true ]; then
|
|
9351
|
+
diff_content=$(git diff --cached 2>/dev/null)
|
|
9352
|
+
source_desc="staged changes"
|
|
9052
9353
|
else
|
|
9053
|
-
|
|
9354
|
+
diff_content=$(git diff 2>/dev/null)
|
|
9355
|
+
source_desc="uncommitted changes"
|
|
9054
9356
|
fi
|
|
9055
9357
|
|
|
9056
|
-
#
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
fi ;;
|
|
9065
|
-
python)
|
|
9066
|
-
if command -v ruff &>/dev/null; then
|
|
9067
|
-
lint_output=$(cd "$target_dir" && ruff check . 2>&1) || lint_status="FAIL"
|
|
9068
|
-
elif command -v pylint &>/dev/null; then
|
|
9069
|
-
lint_output=$(cd "$target_dir" && pylint --recursive=y . 2>&1) || lint_status="FAIL"
|
|
9070
|
-
else
|
|
9071
|
-
lint_output="No linter available (install ruff or pylint)"; lint_status="WARN"
|
|
9072
|
-
fi ;;
|
|
9073
|
-
go)
|
|
9074
|
-
if command -v golangci-lint &>/dev/null; then
|
|
9075
|
-
lint_output=$(cd "$target_dir" && golangci-lint run 2>&1) || lint_status="FAIL"
|
|
9076
|
-
else
|
|
9077
|
-
lint_output=$(cd "$target_dir" && go vet ./... 2>&1) || lint_status="FAIL"
|
|
9078
|
-
fi ;;
|
|
9079
|
-
rust)
|
|
9080
|
-
lint_output=$(cd "$target_dir" && cargo clippy -- -D warnings 2>&1) || lint_status="FAIL"
|
|
9081
|
-
;;
|
|
9082
|
-
*) lint_output="Skipped (unknown project type)"; lint_status="WARN" ;;
|
|
9083
|
-
esac
|
|
9084
|
-
_review_gate "lint" "$lint_status" "$lint_output"
|
|
9085
|
-
|
|
9086
|
-
# Gate 3: Tests
|
|
9087
|
-
local test_output="" test_status="PASS"
|
|
9088
|
-
case "$project_type" in
|
|
9089
|
-
node)
|
|
9090
|
-
if grep -q '"test"' "$target_dir/package.json" 2>/dev/null; then
|
|
9091
|
-
test_output=$(cd "$target_dir" && npm test 2>&1) || test_status="FAIL"
|
|
9092
|
-
else
|
|
9093
|
-
test_output="No test script in package.json"; test_status="WARN"
|
|
9094
|
-
fi ;;
|
|
9095
|
-
python)
|
|
9096
|
-
if command -v pytest &>/dev/null; then
|
|
9097
|
-
test_output=$(cd "$target_dir" && pytest --tb=short 2>&1) || test_status="FAIL"
|
|
9098
|
-
else
|
|
9099
|
-
test_output="pytest not available"; test_status="WARN"
|
|
9100
|
-
fi ;;
|
|
9101
|
-
go) test_output=$(cd "$target_dir" && go test ./... 2>&1) || test_status="FAIL" ;;
|
|
9102
|
-
rust) test_output=$(cd "$target_dir" && cargo test 2>&1) || test_status="FAIL" ;;
|
|
9103
|
-
*) test_output="Skipped (unknown project type)"; test_status="WARN" ;;
|
|
9104
|
-
esac
|
|
9105
|
-
_review_gate "tests" "$test_status" "$test_output"
|
|
9106
|
-
|
|
9107
|
-
# Gate 4: Security - grep for hardcoded secrets
|
|
9108
|
-
local secret_output="" secret_status="PASS"
|
|
9109
|
-
local secret_patterns='(API_KEY|SECRET_KEY|PASSWORD|TOKEN|PRIVATE_KEY)\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
|
|
9110
|
-
secret_output=$(grep -rEn "$secret_patterns" "$target_dir" \
|
|
9111
|
-
--include="*.js" --include="*.ts" --include="*.py" --include="*.go" --include="*.rs" \
|
|
9112
|
-
--include="*.jsx" --include="*.tsx" --include="*.java" --include="*.rb" \
|
|
9113
|
-
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=vendor \
|
|
9114
|
-
--exclude-dir=__pycache__ --exclude-dir=.venv --exclude-dir=target 2>/dev/null) || true
|
|
9115
|
-
if [ -n "$secret_output" ]; then
|
|
9116
|
-
secret_status="FAIL"
|
|
9117
|
-
secret_output="Potential hardcoded secrets found:
|
|
9118
|
-
$secret_output"
|
|
9119
|
-
else
|
|
9120
|
-
secret_output="No hardcoded secrets detected"
|
|
9358
|
+
# Check if there is anything to review
|
|
9359
|
+
if [ -z "$diff_content" ] && [ -z "$review_files" ]; then
|
|
9360
|
+
if [ "$review_format" = "json" ]; then
|
|
9361
|
+
echo '{"source":"'"$source_desc"'","findings":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"total":0},"exit_code":0}'
|
|
9362
|
+
else
|
|
9363
|
+
echo -e "${GREEN}No changes to review ($source_desc).${NC}"
|
|
9364
|
+
fi
|
|
9365
|
+
return 0
|
|
9121
9366
|
fi
|
|
9122
|
-
_review_gate "security" "$secret_status" "$secret_output"
|
|
9123
9367
|
|
|
9124
|
-
#
|
|
9125
|
-
local
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
python)
|
|
9134
|
-
if command -v pip-audit &>/dev/null; then
|
|
9135
|
-
dep_output=$(cd "$target_dir" && pip-audit 2>&1) || dep_status="WARN"
|
|
9136
|
-
else
|
|
9137
|
-
dep_output="pip-audit not available"; dep_status="WARN"
|
|
9138
|
-
fi ;;
|
|
9139
|
-
*) dep_output="No dependency audit available for $project_type"; dep_status="WARN" ;;
|
|
9140
|
-
esac
|
|
9141
|
-
_review_gate "dependencies" "$dep_status" "$dep_output"
|
|
9142
|
-
|
|
9143
|
-
# Gate 6: Structure check
|
|
9144
|
-
local struct_output="" struct_status="PASS" struct_issues=()
|
|
9145
|
-
[ ! -f "$target_dir/README.md" ] && [ ! -f "$target_dir/README.rst" ] && [ ! -f "$target_dir/README" ] && struct_issues+=("Missing README")
|
|
9146
|
-
local file_count
|
|
9147
|
-
file_count=$(find "$target_dir" -type f -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*/__pycache__/*' -not -path '*/.venv/*' -not -path '*/target/*' 2>/dev/null | wc -l | tr -d ' ')
|
|
9148
|
-
[ "$file_count" -gt 5000 ] && struct_issues+=("Large project: $file_count files")
|
|
9149
|
-
local huge_files
|
|
9150
|
-
huge_files=$(find "$target_dir" -type f -size +1M -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*/target/*' 2>/dev/null | head -5)
|
|
9151
|
-
[ -n "$huge_files" ] && struct_issues+=("Files >1MB found: $(echo "$huge_files" | wc -l | tr -d ' ')")
|
|
9152
|
-
if [ ${#struct_issues[@]} -gt 0 ]; then
|
|
9153
|
-
struct_status="WARN"
|
|
9154
|
-
struct_output=$(printf '%s\n' "${struct_issues[@]}")
|
|
9368
|
+
# Build file content for non-diff reviews
|
|
9369
|
+
local all_content=""
|
|
9370
|
+
if [ -n "$review_files" ]; then
|
|
9371
|
+
while IFS= read -r f; do
|
|
9372
|
+
[ -z "$f" ] && continue
|
|
9373
|
+
all_content+="=== FILE: $f ===
|
|
9374
|
+
$(cat "$f" 2>/dev/null)
|
|
9375
|
+
"
|
|
9376
|
+
done <<< "$review_files"
|
|
9155
9377
|
else
|
|
9156
|
-
|
|
9157
|
-
fi
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9378
|
+
all_content="$diff_content"
|
|
9379
|
+
fi
|
|
9380
|
+
|
|
9381
|
+
# Findings array: "file|line|severity|category|finding|suggestion"
|
|
9382
|
+
local findings=()
|
|
9383
|
+
local has_critical=false
|
|
9384
|
+
local has_high=false
|
|
9385
|
+
|
|
9386
|
+
# Helper to add a finding
|
|
9387
|
+
_add_finding() {
|
|
9388
|
+
local file="$1" line="$2" sev="$3" cat="$4" finding="$5" suggestion="$6"
|
|
9389
|
+
local sev_num
|
|
9390
|
+
sev_num=$(_review_sev_level "$sev")
|
|
9391
|
+
if [ "$sev_num" -ge "$min_sev" ]; then
|
|
9392
|
+
findings+=("${file}|${line}|${sev}|${cat}|${finding}|${suggestion}")
|
|
9393
|
+
[ "$sev" = "CRITICAL" ] && has_critical=true || true
|
|
9394
|
+
[ "$sev" = "HIGH" ] && has_high=true || true
|
|
9395
|
+
fi
|
|
9396
|
+
}
|
|
9397
|
+
|
|
9398
|
+
# --- Gate 1: Static Analysis ---
|
|
9399
|
+
# Run shellcheck on shell files if available
|
|
9400
|
+
if command -v shellcheck &>/dev/null; then
|
|
9401
|
+
local shell_files=""
|
|
9402
|
+
if [ -n "$review_files" ]; then
|
|
9403
|
+
shell_files=$(echo "$review_files" | grep -E '\.(sh|bash)$' || true)
|
|
9404
|
+
elif [ -n "$diff_content" ]; then
|
|
9405
|
+
shell_files=$(echo "$diff_content" | grep -E '^\+\+\+ b/' | sed 's|^+++ b/||' | grep -E '\.(sh|bash)$' || true)
|
|
9406
|
+
fi
|
|
9407
|
+
if [ -n "$shell_files" ]; then
|
|
9408
|
+
while IFS= read -r sf; do
|
|
9409
|
+
[ -z "$sf" ] && continue
|
|
9410
|
+
[ ! -f "$sf" ] && continue
|
|
9411
|
+
local sc_out
|
|
9412
|
+
sc_out=$(shellcheck -f gcc "$sf" 2>/dev/null || true)
|
|
9413
|
+
while IFS= read -r sc_line; do
|
|
9414
|
+
[ -z "$sc_line" ] && continue
|
|
9415
|
+
local sc_file sc_lineno sc_sev sc_msg
|
|
9416
|
+
sc_file=$(echo "$sc_line" | cut -d: -f1)
|
|
9417
|
+
sc_lineno=$(echo "$sc_line" | cut -d: -f2)
|
|
9418
|
+
sc_sev=$(echo "$sc_line" | sed -n 's/.*: \(warning\|error\|note\|info\):.*/\1/p')
|
|
9419
|
+
sc_msg=$(echo "$sc_line" | sed 's/.*: \(warning\|error\|note\|info\): //')
|
|
9420
|
+
local mapped_sev="LOW"
|
|
9421
|
+
case "$sc_sev" in
|
|
9422
|
+
error) mapped_sev="HIGH" ;;
|
|
9423
|
+
warning) mapped_sev="MEDIUM" ;;
|
|
9424
|
+
*) mapped_sev="LOW" ;;
|
|
9425
|
+
esac
|
|
9426
|
+
_add_finding "$sc_file" "$sc_lineno" "$mapped_sev" "static-analysis" "$sc_msg" "Fix shellcheck finding"
|
|
9427
|
+
done <<< "$sc_out"
|
|
9428
|
+
done <<< "$shell_files"
|
|
9429
|
+
fi
|
|
9430
|
+
fi
|
|
9431
|
+
|
|
9432
|
+
# Run eslint if available on JS/TS files
|
|
9433
|
+
if command -v npx &>/dev/null; then
|
|
9434
|
+
local js_files=""
|
|
9435
|
+
if [ -n "$review_files" ]; then
|
|
9436
|
+
js_files=$(echo "$review_files" | grep -E '\.(js|ts|jsx|tsx)$' || true)
|
|
9437
|
+
elif [ -n "$diff_content" ]; then
|
|
9438
|
+
js_files=$(echo "$diff_content" | grep -E '^\+\+\+ b/' | sed 's|^+++ b/||' | grep -E '\.(js|ts|jsx|tsx)$' || true)
|
|
9439
|
+
fi
|
|
9440
|
+
if [ -n "$js_files" ]; then
|
|
9441
|
+
while IFS= read -r jsf; do
|
|
9442
|
+
[ -z "$jsf" ] && continue
|
|
9443
|
+
[ ! -f "$jsf" ] && continue
|
|
9444
|
+
if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then
|
|
9445
|
+
local eslint_out
|
|
9446
|
+
eslint_out=$(npx eslint --format compact "$jsf" 2>/dev/null || true)
|
|
9447
|
+
while IFS= read -r el; do
|
|
9448
|
+
[ -z "$el" ] && continue
|
|
9449
|
+
[[ "$el" == *"problem"* ]] && continue
|
|
9450
|
+
local el_file el_line el_msg
|
|
9451
|
+
el_file=$(echo "$el" | cut -d: -f1)
|
|
9452
|
+
el_line=$(echo "$el" | cut -d: -f2)
|
|
9453
|
+
el_msg=$(echo "$el" | sed 's/^[^)]*) //')
|
|
9454
|
+
local el_sev="LOW"
|
|
9455
|
+
[[ "$el" == *"Error"* ]] && el_sev="MEDIUM"
|
|
9456
|
+
_add_finding "$el_file" "$el_line" "$el_sev" "static-analysis" "$el_msg" "Fix eslint finding"
|
|
9457
|
+
done <<< "$eslint_out"
|
|
9458
|
+
fi
|
|
9459
|
+
done <<< "$js_files"
|
|
9460
|
+
fi
|
|
9461
|
+
fi
|
|
9462
|
+
|
|
9463
|
+
# --- Gate 2: Security Scan ---
|
|
9464
|
+
# Helper: scan for a pattern across files or diff content
|
|
9465
|
+
_review_scan() {
|
|
9466
|
+
local pattern="$1" sev="$2" cat="$3" finding="$4" suggestion="$5"
|
|
9467
|
+
if [ -n "$review_files" ]; then
|
|
9468
|
+
# Scan actual files with grep -Hn (file:line:content)
|
|
9469
|
+
local scan_files_arr=()
|
|
9470
|
+
while IFS= read -r sf; do
|
|
9471
|
+
[ -z "$sf" ] && continue
|
|
9472
|
+
[ -f "$sf" ] && scan_files_arr+=("$sf")
|
|
9473
|
+
done <<< "$review_files"
|
|
9474
|
+
[ ${#scan_files_arr[@]} -eq 0 ] && return
|
|
9475
|
+
while IFS= read -r match; do
|
|
9476
|
+
[ -z "$match" ] && continue
|
|
9477
|
+
local mf ml
|
|
9478
|
+
mf=$(echo "$match" | cut -d: -f1)
|
|
9479
|
+
ml=$(echo "$match" | cut -d: -f2)
|
|
9480
|
+
_add_finding "$mf" "$ml" "$sev" "$cat" "$finding" "$suggestion"
|
|
9481
|
+
done < <(grep -HnE "$pattern" "${scan_files_arr[@]}" 2>/dev/null || true)
|
|
9482
|
+
else
|
|
9483
|
+
# Scan diff content (line numbers are from the text blob)
|
|
9484
|
+
while IFS= read -r match; do
|
|
9485
|
+
[ -z "$match" ] && continue
|
|
9486
|
+
local ml
|
|
9487
|
+
ml=$(echo "$match" | cut -d: -f1)
|
|
9488
|
+
_add_finding "diff" "$ml" "$sev" "$cat" "$finding" "$suggestion"
|
|
9489
|
+
done < <(echo "$all_content" | grep -nE "$pattern" 2>/dev/null || true)
|
|
9490
|
+
fi
|
|
9491
|
+
}
|
|
9492
|
+
|
|
9493
|
+
# Hardcoded secrets
|
|
9494
|
+
local secret_patterns=(
|
|
9495
|
+
'API_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
|
|
9496
|
+
'SECRET_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
|
|
9497
|
+
'PASSWORD\s*[=:]\s*["\x27][^\x27"]{4,}'
|
|
9498
|
+
'PRIVATE_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
|
|
9499
|
+
'AWS_ACCESS_KEY_ID\s*[=:]\s*["\x27]AK[A-Z0-9]{18}'
|
|
9500
|
+
'ghp_[A-Za-z0-9]{36}'
|
|
9501
|
+
'sk-[A-Za-z0-9]{32,}'
|
|
9502
|
+
'Bearer\s+[A-Za-z0-9._-]{20,}'
|
|
9503
|
+
)
|
|
9504
|
+
for pattern in "${secret_patterns[@]}"; do
|
|
9505
|
+
_review_scan "$pattern" "CRITICAL" "security" \
|
|
9506
|
+
"Potential hardcoded secret detected" \
|
|
9507
|
+
"Use environment variables or a secrets manager instead"
|
|
9508
|
+
done
|
|
9509
|
+
|
|
9510
|
+
# SQL injection patterns
|
|
9511
|
+
_review_scan '(SELECT|INSERT|UPDATE|DELETE|DROP)\s.*\+\s*(req\.|request\.|params\.|user)' \
|
|
9512
|
+
"HIGH" "security" \
|
|
9513
|
+
"Potential SQL injection: string concatenation in query" \
|
|
9514
|
+
"Use parameterized queries or prepared statements"
|
|
9515
|
+
|
|
9516
|
+
# eval/exec usage
|
|
9517
|
+
_review_scan '(^|\s)(eval|exec)\s*\(' \
|
|
9518
|
+
"HIGH" "security" \
|
|
9519
|
+
"Dangerous eval/exec usage detected" \
|
|
9520
|
+
"Avoid eval/exec with dynamic input; use safer alternatives"
|
|
9521
|
+
|
|
9522
|
+
# Unsafe deserialization
|
|
9523
|
+
_review_scan '(pickle\.loads?|yaml\.load\s*\()' \
|
|
9524
|
+
"HIGH" "security" \
|
|
9525
|
+
"Unsafe deserialization detected (pickle/yaml.load)" \
|
|
9526
|
+
"Use yaml.safe_load or avoid pickle with untrusted data"
|
|
9527
|
+
|
|
9528
|
+
# --- Gate 3: Code Style ---
|
|
9529
|
+
# Long functions (>100 lines heuristic from diff)
|
|
9530
|
+
if [ -n "$review_files" ]; then
|
|
9531
|
+
while IFS= read -r f; do
|
|
9532
|
+
[ -z "$f" ] && continue
|
|
9533
|
+
[ ! -f "$f" ] && continue
|
|
9534
|
+
local line_count
|
|
9535
|
+
line_count=$(wc -l < "$f" | tr -d ' ')
|
|
9536
|
+
if [ "$line_count" -gt 500 ]; then
|
|
9537
|
+
_add_finding "$f" "1" "MEDIUM" "style" \
|
|
9538
|
+
"File has $line_count lines (>500)" \
|
|
9539
|
+
"Consider breaking into smaller modules"
|
|
9540
|
+
fi
|
|
9541
|
+
# Check for very long lines
|
|
9542
|
+
local long_lines
|
|
9543
|
+
long_lines=$(awk 'length > 200 {count++} END {print count+0}' "$f" 2>/dev/null)
|
|
9544
|
+
if [ "$long_lines" -gt 5 ]; then
|
|
9545
|
+
_add_finding "$f" "1" "LOW" "style" \
|
|
9546
|
+
"$long_lines lines exceed 200 characters" \
|
|
9547
|
+
"Break long lines for readability"
|
|
9548
|
+
fi
|
|
9549
|
+
done <<< "$review_files"
|
|
9550
|
+
fi
|
|
9551
|
+
|
|
9552
|
+
# TODO/FIXME/HACK/XXX detection
|
|
9553
|
+
_review_scan '(TODO|FIXME|HACK|XXX):' "INFO" "style" \
|
|
9554
|
+
"TODO/FIXME/HACK/XXX comment marker found" \
|
|
9555
|
+
"Track in issue tracker instead of code comments"
|
|
9556
|
+
|
|
9557
|
+
# --- Gate 4: Anti-pattern Detection ---
|
|
9558
|
+
# console.log left in code (JS/TS)
|
|
9559
|
+
_review_scan 'console\.(log|debug|warn)\(' "LOW" "anti-pattern" \
|
|
9560
|
+
"console.log statement (debug artifact?)" \
|
|
9561
|
+
"Remove debug logging or use a proper logger"
|
|
9562
|
+
|
|
9563
|
+
# Catch bare except in Python
|
|
9564
|
+
_review_scan 'except\s*:' "MEDIUM" "anti-pattern" \
|
|
9565
|
+
"Bare except clause catches all exceptions" \
|
|
9566
|
+
"Catch specific exceptions (e.g., except ValueError)"
|
|
9567
|
+
|
|
9568
|
+
# Disabled SSL verification
|
|
9569
|
+
_review_scan '(verify\s*=\s*False|VERIFY_SSL\s*=\s*False|NODE_TLS_REJECT_UNAUTHORIZED.*0|rejectUnauthorized.*false)' \
|
|
9570
|
+
"HIGH" "anti-pattern" \
|
|
9571
|
+
"SSL verification disabled" \
|
|
9572
|
+
"Enable SSL verification in production"
|
|
9573
|
+
|
|
9574
|
+
# Hardcoded IP addresses (skip localhost/0.0.0.0/version patterns)
|
|
9575
|
+
_review_scan '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' \
|
|
9576
|
+
"LOW" "anti-pattern" \
|
|
9577
|
+
"Hardcoded IP address detected" \
|
|
9578
|
+
"Use configuration or DNS names instead"
|
|
9579
|
+
|
|
9580
|
+
# --- Tally results ---
|
|
9581
|
+
local count_critical=0 count_high=0 count_medium=0 count_low=0 count_info=0
|
|
9582
|
+
for f in "${findings[@]}"; do
|
|
9583
|
+
local sev
|
|
9584
|
+
sev=$(echo "$f" | cut -d'|' -f3)
|
|
9585
|
+
case "$sev" in
|
|
9586
|
+
CRITICAL) count_critical=$((count_critical + 1)) ;;
|
|
9587
|
+
HIGH) count_high=$((count_high + 1)) ;;
|
|
9588
|
+
MEDIUM) count_medium=$((count_medium + 1)) ;;
|
|
9589
|
+
LOW) count_low=$((count_low + 1)) ;;
|
|
9590
|
+
INFO) count_info=$((count_info + 1)) ;;
|
|
9591
|
+
esac
|
|
9592
|
+
done
|
|
9593
|
+
local count_total=${#findings[@]}
|
|
9594
|
+
|
|
9595
|
+
# Determine exit code
|
|
9596
|
+
local exit_code=0
|
|
9597
|
+
[ "$has_high" = true ] && exit_code=1 || true
|
|
9598
|
+
[ "$has_critical" = true ] && exit_code=2 || true
|
|
9599
|
+
|
|
9600
|
+
# --- Output ---
|
|
9601
|
+
if [ "$review_format" = "json" ]; then
|
|
9602
|
+
local json_findings="["
|
|
9603
|
+
local first=true
|
|
9604
|
+
for f in "${findings[@]}"; do
|
|
9605
|
+
local f_file f_line f_sev f_cat f_finding f_suggestion
|
|
9606
|
+
f_file=$(echo "$f" | cut -d'|' -f1)
|
|
9607
|
+
f_line=$(echo "$f" | cut -d'|' -f2)
|
|
9608
|
+
f_sev=$(echo "$f" | cut -d'|' -f3)
|
|
9609
|
+
f_cat=$(echo "$f" | cut -d'|' -f4)
|
|
9610
|
+
f_finding=$(echo "$f" | cut -d'|' -f5)
|
|
9611
|
+
f_suggestion=$(echo "$f" | cut -d'|' -f6)
|
|
9612
|
+
# Escape quotes for JSON
|
|
9613
|
+
f_finding=$(echo "$f_finding" | sed 's/"/\\"/g')
|
|
9614
|
+
f_suggestion=$(echo "$f_suggestion" | sed 's/"/\\"/g')
|
|
9615
|
+
f_file=$(echo "$f_file" | sed 's/"/\\"/g')
|
|
9616
|
+
[ "$first" = true ] && first=false || json_findings+=","
|
|
9617
|
+
json_findings+="{\"file\":\"$f_file\",\"line\":$f_line,\"severity\":\"$f_sev\",\"category\":\"$f_cat\",\"finding\":\"$f_finding\",\"suggestion\":\"$f_suggestion\"}"
|
|
9168
9618
|
done
|
|
9169
|
-
|
|
9170
|
-
printf '{"
|
|
9171
|
-
"$
|
|
9619
|
+
json_findings+="]"
|
|
9620
|
+
printf '{"source":"%s","findings":%s,"summary":{"critical":%d,"high":%d,"medium":%d,"low":%d,"info":%d,"total":%d},"exit_code":%d}\n' \
|
|
9621
|
+
"$source_desc" "$json_findings" "$count_critical" "$count_high" "$count_medium" "$count_low" "$count_info" "$count_total" "$exit_code"
|
|
9172
9622
|
else
|
|
9173
|
-
echo -e "${BOLD}Loki Review
|
|
9174
|
-
echo -e "
|
|
9623
|
+
echo -e "${BOLD}Loki Code Review${NC}"
|
|
9624
|
+
echo -e "Source: ${CYAN}$source_desc${NC}"
|
|
9175
9625
|
echo "---"
|
|
9176
|
-
for r in "${results[@]}"; do
|
|
9177
|
-
local gate="${r%%:*}" status="${r#*:}"
|
|
9178
|
-
case "$status" in
|
|
9179
|
-
PASS) echo -e " ${GREEN}[PASS]${NC} $gate" ;;
|
|
9180
|
-
WARN) echo -e " ${YELLOW}[WARN]${NC} $gate" ;;
|
|
9181
|
-
FAIL) echo -e " ${RED}[FAIL]${NC} $gate" ;;
|
|
9182
|
-
esac
|
|
9183
|
-
done
|
|
9184
|
-
echo "---"
|
|
9185
|
-
echo -e "Results: ${GREEN}$total_pass passed${NC}, ${YELLOW}$total_warn warnings${NC}, ${RED}$total_fail failed${NC}"
|
|
9186
9626
|
|
|
9187
|
-
if [
|
|
9188
|
-
echo ""
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9627
|
+
if [ "$count_total" -eq 0 ]; then
|
|
9628
|
+
echo -e "${GREEN}No findings. Code looks clean.${NC}"
|
|
9629
|
+
else
|
|
9630
|
+
# Group by severity
|
|
9631
|
+
for sev_name in CRITICAL HIGH MEDIUM LOW INFO; do
|
|
9632
|
+
local printed_header=false
|
|
9633
|
+
for f in "${findings[@]}"; do
|
|
9634
|
+
local f_sev
|
|
9635
|
+
f_sev=$(echo "$f" | cut -d'|' -f3)
|
|
9636
|
+
[ "$f_sev" != "$sev_name" ] && continue
|
|
9637
|
+
if [ "$printed_header" = false ]; then
|
|
9638
|
+
local sev_color="$NC"
|
|
9639
|
+
case "$sev_name" in
|
|
9640
|
+
CRITICAL) sev_color="$RED" ;;
|
|
9641
|
+
HIGH) sev_color="$RED" ;;
|
|
9642
|
+
MEDIUM) sev_color="$YELLOW" ;;
|
|
9643
|
+
LOW) sev_color="$CYAN" ;;
|
|
9644
|
+
INFO) sev_color="$DIM" ;;
|
|
9645
|
+
esac
|
|
9646
|
+
echo ""
|
|
9647
|
+
echo -e "${sev_color}${BOLD}[$sev_name]${NC}"
|
|
9648
|
+
printed_header=true
|
|
9649
|
+
fi
|
|
9650
|
+
local f_file f_line f_cat f_finding f_suggestion
|
|
9651
|
+
f_file=$(echo "$f" | cut -d'|' -f1)
|
|
9652
|
+
f_line=$(echo "$f" | cut -d'|' -f2)
|
|
9653
|
+
f_cat=$(echo "$f" | cut -d'|' -f4)
|
|
9654
|
+
f_finding=$(echo "$f" | cut -d'|' -f5)
|
|
9655
|
+
f_suggestion=$(echo "$f" | cut -d'|' -f6)
|
|
9656
|
+
echo -e " ${DIM}$f_file:$f_line${NC} [$f_cat] $f_finding"
|
|
9657
|
+
echo -e " -> $f_suggestion"
|
|
9658
|
+
done
|
|
9194
9659
|
done
|
|
9195
9660
|
fi
|
|
9661
|
+
|
|
9662
|
+
echo ""
|
|
9663
|
+
echo "---"
|
|
9664
|
+
echo -e "Summary: ${RED}$count_critical critical${NC}, ${RED}$count_high high${NC}, ${YELLOW}$count_medium medium${NC}, ${CYAN}$count_low low${NC}, ${DIM}$count_info info${NC} ($count_total total)"
|
|
9665
|
+
|
|
9666
|
+
if [ "$exit_code" -eq 2 ]; then
|
|
9667
|
+
echo -e "${RED}CRITICAL findings detected. Review required.${NC}"
|
|
9668
|
+
elif [ "$exit_code" -eq 1 ]; then
|
|
9669
|
+
echo -e "${RED}HIGH severity findings detected.${NC}"
|
|
9670
|
+
else
|
|
9671
|
+
echo -e "${GREEN}No HIGH or CRITICAL findings.${NC}"
|
|
9672
|
+
fi
|
|
9196
9673
|
fi
|
|
9197
9674
|
|
|
9198
|
-
|
|
9199
|
-
[ "$total_fail" -gt 0 ] && return 1
|
|
9200
|
-
return 0
|
|
9675
|
+
return "$exit_code"
|
|
9201
9676
|
}
|
|
9202
9677
|
|
|
9678
|
+
|
|
9203
9679
|
# Worktree management (v6.7.0)
|
|
9204
9680
|
cmd_worktree() {
|
|
9205
9681
|
local subcommand="${1:-list}"
|
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