loki-mode 6.19.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 +437 -184
- 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"
|
|
@@ -9205,224 +9205,477 @@ main() {
|
|
|
9205
9205
|
;;
|
|
9206
9206
|
esac
|
|
9207
9207
|
}
|
|
9208
|
-
|
|
9209
|
-
# Standalone project review - run quality gates on any codebase (v6.14.0)
|
|
9208
|
+
# Standalone code review - diff-based quality gates (v6.20.0)
|
|
9210
9209
|
cmd_review() {
|
|
9211
|
-
local
|
|
9212
|
-
local
|
|
9213
|
-
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"
|
|
9214
9216
|
|
|
9215
9217
|
while [[ $# -gt 0 ]]; do
|
|
9216
9218
|
case "$1" in
|
|
9217
|
-
--json) json_output="true"; shift ;;
|
|
9218
|
-
--verbose|-v) verbose="true"; shift ;;
|
|
9219
9219
|
--help|-h)
|
|
9220
|
-
echo -e "${BOLD}loki review${NC} -
|
|
9220
|
+
echo -e "${BOLD}loki review${NC} - Standalone code review with quality gates"
|
|
9221
9221
|
echo ""
|
|
9222
|
-
echo "Usage: loki review [
|
|
9222
|
+
echo "Usage: loki review [options] [file-or-dir]"
|
|
9223
9223
|
echo ""
|
|
9224
|
-
echo "
|
|
9225
|
-
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"
|
|
9226
9232
|
echo ""
|
|
9227
9233
|
echo "Options:"
|
|
9228
|
-
echo " --json
|
|
9229
|
-
echo " --
|
|
9230
|
-
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"
|
|
9231
9237
|
echo ""
|
|
9232
|
-
echo "
|
|
9233
|
-
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"
|
|
9234
9251
|
return 0
|
|
9235
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
|
+
;;
|
|
9236
9283
|
-*) echo -e "${RED}Unknown option: $1${NC}"; return 1 ;;
|
|
9237
|
-
*)
|
|
9284
|
+
*) review_target="$1"; shift ;;
|
|
9238
9285
|
esac
|
|
9239
9286
|
done
|
|
9240
9287
|
|
|
9241
|
-
#
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
# Helper: record gate result
|
|
9252
|
-
_review_gate() {
|
|
9253
|
-
local gate="$1" status="$2" detail="${3:-}"
|
|
9254
|
-
results+=("$gate:$status")
|
|
9255
|
-
gate_details+=("$gate:$detail")
|
|
9256
|
-
case "$status" in
|
|
9257
|
-
PASS) total_pass=$((total_pass + 1)) ;;
|
|
9258
|
-
WARN) total_warn=$((total_warn + 1)) ;;
|
|
9259
|
-
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 ;;
|
|
9260
9297
|
esac
|
|
9261
9298
|
}
|
|
9262
9299
|
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
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"
|
|
9275
9353
|
else
|
|
9276
|
-
|
|
9354
|
+
diff_content=$(git diff 2>/dev/null)
|
|
9355
|
+
source_desc="uncommitted changes"
|
|
9277
9356
|
fi
|
|
9278
9357
|
|
|
9279
|
-
#
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
fi ;;
|
|
9288
|
-
python)
|
|
9289
|
-
if command -v ruff &>/dev/null; then
|
|
9290
|
-
lint_output=$(cd "$target_dir" && ruff check . 2>&1) || lint_status="FAIL"
|
|
9291
|
-
elif command -v pylint &>/dev/null; then
|
|
9292
|
-
lint_output=$(cd "$target_dir" && pylint --recursive=y . 2>&1) || lint_status="FAIL"
|
|
9293
|
-
else
|
|
9294
|
-
lint_output="No linter available (install ruff or pylint)"; lint_status="WARN"
|
|
9295
|
-
fi ;;
|
|
9296
|
-
go)
|
|
9297
|
-
if command -v golangci-lint &>/dev/null; then
|
|
9298
|
-
lint_output=$(cd "$target_dir" && golangci-lint run 2>&1) || lint_status="FAIL"
|
|
9299
|
-
else
|
|
9300
|
-
lint_output=$(cd "$target_dir" && go vet ./... 2>&1) || lint_status="FAIL"
|
|
9301
|
-
fi ;;
|
|
9302
|
-
rust)
|
|
9303
|
-
lint_output=$(cd "$target_dir" && cargo clippy -- -D warnings 2>&1) || lint_status="FAIL"
|
|
9304
|
-
;;
|
|
9305
|
-
*) lint_output="Skipped (unknown project type)"; lint_status="WARN" ;;
|
|
9306
|
-
esac
|
|
9307
|
-
_review_gate "lint" "$lint_status" "$lint_output"
|
|
9308
|
-
|
|
9309
|
-
# Gate 3: Tests
|
|
9310
|
-
local test_output="" test_status="PASS"
|
|
9311
|
-
case "$project_type" in
|
|
9312
|
-
node)
|
|
9313
|
-
if grep -q '"test"' "$target_dir/package.json" 2>/dev/null; then
|
|
9314
|
-
test_output=$(cd "$target_dir" && npm test 2>&1) || test_status="FAIL"
|
|
9315
|
-
else
|
|
9316
|
-
test_output="No test script in package.json"; test_status="WARN"
|
|
9317
|
-
fi ;;
|
|
9318
|
-
python)
|
|
9319
|
-
if command -v pytest &>/dev/null; then
|
|
9320
|
-
test_output=$(cd "$target_dir" && pytest --tb=short 2>&1) || test_status="FAIL"
|
|
9321
|
-
else
|
|
9322
|
-
test_output="pytest not available"; test_status="WARN"
|
|
9323
|
-
fi ;;
|
|
9324
|
-
go) test_output=$(cd "$target_dir" && go test ./... 2>&1) || test_status="FAIL" ;;
|
|
9325
|
-
rust) test_output=$(cd "$target_dir" && cargo test 2>&1) || test_status="FAIL" ;;
|
|
9326
|
-
*) test_output="Skipped (unknown project type)"; test_status="WARN" ;;
|
|
9327
|
-
esac
|
|
9328
|
-
_review_gate "tests" "$test_status" "$test_output"
|
|
9329
|
-
|
|
9330
|
-
# Gate 4: Security - grep for hardcoded secrets
|
|
9331
|
-
local secret_output="" secret_status="PASS"
|
|
9332
|
-
local secret_patterns='(API_KEY|SECRET_KEY|PASSWORD|TOKEN|PRIVATE_KEY)\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
|
|
9333
|
-
secret_output=$(grep -rEn "$secret_patterns" "$target_dir" \
|
|
9334
|
-
--include="*.js" --include="*.ts" --include="*.py" --include="*.go" --include="*.rs" \
|
|
9335
|
-
--include="*.jsx" --include="*.tsx" --include="*.java" --include="*.rb" \
|
|
9336
|
-
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=vendor \
|
|
9337
|
-
--exclude-dir=__pycache__ --exclude-dir=.venv --exclude-dir=target 2>/dev/null) || true
|
|
9338
|
-
if [ -n "$secret_output" ]; then
|
|
9339
|
-
secret_status="FAIL"
|
|
9340
|
-
secret_output="Potential hardcoded secrets found:
|
|
9341
|
-
$secret_output"
|
|
9342
|
-
else
|
|
9343
|
-
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
|
|
9344
9366
|
fi
|
|
9345
|
-
_review_gate "security" "$secret_status" "$secret_output"
|
|
9346
9367
|
|
|
9347
|
-
#
|
|
9348
|
-
local
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
python)
|
|
9357
|
-
if command -v pip-audit &>/dev/null; then
|
|
9358
|
-
dep_output=$(cd "$target_dir" && pip-audit 2>&1) || dep_status="WARN"
|
|
9359
|
-
else
|
|
9360
|
-
dep_output="pip-audit not available"; dep_status="WARN"
|
|
9361
|
-
fi ;;
|
|
9362
|
-
*) dep_output="No dependency audit available for $project_type"; dep_status="WARN" ;;
|
|
9363
|
-
esac
|
|
9364
|
-
_review_gate "dependencies" "$dep_status" "$dep_output"
|
|
9365
|
-
|
|
9366
|
-
# Gate 6: Structure check
|
|
9367
|
-
local struct_output="" struct_status="PASS" struct_issues=()
|
|
9368
|
-
[ ! -f "$target_dir/README.md" ] && [ ! -f "$target_dir/README.rst" ] && [ ! -f "$target_dir/README" ] && struct_issues+=("Missing README")
|
|
9369
|
-
local file_count
|
|
9370
|
-
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 ' ')
|
|
9371
|
-
[ "$file_count" -gt 5000 ] && struct_issues+=("Large project: $file_count files")
|
|
9372
|
-
local huge_files
|
|
9373
|
-
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)
|
|
9374
|
-
[ -n "$huge_files" ] && struct_issues+=("Files >1MB found: $(echo "$huge_files" | wc -l | tr -d ' ')")
|
|
9375
|
-
if [ ${#struct_issues[@]} -gt 0 ]; then
|
|
9376
|
-
struct_status="WARN"
|
|
9377
|
-
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"
|
|
9378
9377
|
else
|
|
9379
|
-
|
|
9380
|
-
fi
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
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\"}"
|
|
9391
9618
|
done
|
|
9392
|
-
|
|
9393
|
-
printf '{"
|
|
9394
|
-
"$
|
|
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"
|
|
9395
9622
|
else
|
|
9396
|
-
echo -e "${BOLD}Loki Review
|
|
9397
|
-
echo -e "
|
|
9623
|
+
echo -e "${BOLD}Loki Code Review${NC}"
|
|
9624
|
+
echo -e "Source: ${CYAN}$source_desc${NC}"
|
|
9398
9625
|
echo "---"
|
|
9399
|
-
for r in "${results[@]}"; do
|
|
9400
|
-
local gate="${r%%:*}" status="${r#*:}"
|
|
9401
|
-
case "$status" in
|
|
9402
|
-
PASS) echo -e " ${GREEN}[PASS]${NC} $gate" ;;
|
|
9403
|
-
WARN) echo -e " ${YELLOW}[WARN]${NC} $gate" ;;
|
|
9404
|
-
FAIL) echo -e " ${RED}[FAIL]${NC} $gate" ;;
|
|
9405
|
-
esac
|
|
9406
|
-
done
|
|
9407
|
-
echo "---"
|
|
9408
|
-
echo -e "Results: ${GREEN}$total_pass passed${NC}, ${YELLOW}$total_warn warnings${NC}, ${RED}$total_fail failed${NC}"
|
|
9409
9626
|
|
|
9410
|
-
if [
|
|
9411
|
-
echo ""
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
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
|
|
9417
9659
|
done
|
|
9418
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
|
|
9419
9673
|
fi
|
|
9420
9674
|
|
|
9421
|
-
|
|
9422
|
-
[ "$total_fail" -gt 0 ] && return 1
|
|
9423
|
-
return 0
|
|
9675
|
+
return "$exit_code"
|
|
9424
9676
|
}
|
|
9425
9677
|
|
|
9678
|
+
|
|
9426
9679
|
# Worktree management (v6.7.0)
|
|
9427
9680
|
cmd_worktree() {
|
|
9428
9681
|
local subcommand="${1:-list}"
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED