loki-mode 6.21.0 → 6.22.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/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +645 -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/README.md
CHANGED
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.22.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.22.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.22.0
|
package/autonomy/loki
CHANGED
|
@@ -438,6 +438,7 @@ show_help() {
|
|
|
438
438
|
echo " failover [cmd] Cross-provider auto-failover (status|--enable|--test|--chain)"
|
|
439
439
|
echo " onboard [path] Analyze a repo and generate CLAUDE.md (structure, conventions, commands)"
|
|
440
440
|
echo " plan <PRD> Dry-run PRD analysis: complexity, cost, and execution plan"
|
|
441
|
+
echo " ci [opts] CI/CD quality gate integration (--pr, --report, --github-comment)"
|
|
441
442
|
echo " version Show version"
|
|
442
443
|
echo " help Show this help"
|
|
443
444
|
echo ""
|
|
@@ -9193,6 +9194,9 @@ main() {
|
|
|
9193
9194
|
onboard)
|
|
9194
9195
|
cmd_onboard "$@"
|
|
9195
9196
|
;;
|
|
9197
|
+
ci)
|
|
9198
|
+
cmd_ci "$@"
|
|
9199
|
+
;;
|
|
9196
9200
|
version|--version|-v)
|
|
9197
9201
|
cmd_version
|
|
9198
9202
|
;;
|
|
@@ -14681,4 +14685,645 @@ Generated by loki onboard (depth $depth) on $(date +%Y-%m-%d)"
|
|
|
14681
14685
|
fi
|
|
14682
14686
|
}
|
|
14683
14687
|
|
|
14688
|
+
# CI/CD quality gate integration (v6.22.0)
|
|
14689
|
+
cmd_ci() {
|
|
14690
|
+
local ci_pr=false
|
|
14691
|
+
local ci_test_suggest=false
|
|
14692
|
+
local ci_report=false
|
|
14693
|
+
local ci_github_comment=false
|
|
14694
|
+
local ci_fail_on=""
|
|
14695
|
+
local ci_format="markdown"
|
|
14696
|
+
|
|
14697
|
+
while [[ $# -gt 0 ]]; do
|
|
14698
|
+
case "$1" in
|
|
14699
|
+
--help|-h)
|
|
14700
|
+
echo -e "${BOLD}loki ci${NC} - CI/CD quality gate integration"
|
|
14701
|
+
echo ""
|
|
14702
|
+
echo "Usage: loki ci [options]"
|
|
14703
|
+
echo ""
|
|
14704
|
+
echo "Runs Loki Mode quality gates as a CI step. Works with GitHub Actions,"
|
|
14705
|
+
echo "GitLab CI, Jenkins, CircleCI, and other CI systems."
|
|
14706
|
+
echo ""
|
|
14707
|
+
echo "Modes:"
|
|
14708
|
+
echo " --pr Review the current PR diff with all quality gates"
|
|
14709
|
+
echo " --test-suggest Generate test suggestions for changed files"
|
|
14710
|
+
echo " --report Generate a quality report"
|
|
14711
|
+
echo ""
|
|
14712
|
+
echo "Options:"
|
|
14713
|
+
echo " --github-comment Post review results as PR comment (needs GITHUB_TOKEN)"
|
|
14714
|
+
echo " --fail-on <levels> Set exit code 1 on severity: critical,high,medium,low"
|
|
14715
|
+
echo " --format <fmt> Output format: json, markdown, github (default: markdown)"
|
|
14716
|
+
echo " --help, -h Show this help"
|
|
14717
|
+
echo ""
|
|
14718
|
+
echo "Exit codes:"
|
|
14719
|
+
echo " 0 All checks passed (or below --fail-on threshold)"
|
|
14720
|
+
echo " 1 Findings exceed --fail-on threshold"
|
|
14721
|
+
echo " 2 Error (missing tools, invalid arguments)"
|
|
14722
|
+
echo ""
|
|
14723
|
+
echo "Environment variables (auto-detected):"
|
|
14724
|
+
echo " GITHUB_ACTIONS Detected when running in GitHub Actions"
|
|
14725
|
+
echo " GITLAB_CI Detected when running in GitLab CI"
|
|
14726
|
+
echo " JENKINS_URL Detected when running in Jenkins"
|
|
14727
|
+
echo " CIRCLECI Detected when running in CircleCI"
|
|
14728
|
+
echo " GITHUB_TOKEN Required for --github-comment"
|
|
14729
|
+
echo " GITHUB_EVENT_PATH Auto-set in GitHub Actions for PR context"
|
|
14730
|
+
echo ""
|
|
14731
|
+
echo "Examples:"
|
|
14732
|
+
echo " loki ci --pr --format json # Review PR diff as JSON"
|
|
14733
|
+
echo " loki ci --pr --fail-on critical,high # Fail CI on critical/high findings"
|
|
14734
|
+
echo " loki ci --pr --github-comment # Post results as PR comment"
|
|
14735
|
+
echo " loki ci --report --format markdown # Generate quality report"
|
|
14736
|
+
echo " loki ci --test-suggest # Suggest tests for changed files"
|
|
14737
|
+
echo ""
|
|
14738
|
+
echo "GitHub Actions example:"
|
|
14739
|
+
echo " - uses: asklokesh/loki-mode-action@v1"
|
|
14740
|
+
echo " with:"
|
|
14741
|
+
echo " command: loki ci --pr --github-comment --fail-on critical"
|
|
14742
|
+
return 0
|
|
14743
|
+
;;
|
|
14744
|
+
--pr) ci_pr=true; shift ;;
|
|
14745
|
+
--test-suggest) ci_test_suggest=true; shift ;;
|
|
14746
|
+
--report) ci_report=true; shift ;;
|
|
14747
|
+
--github-comment) ci_github_comment=true; shift ;;
|
|
14748
|
+
--fail-on)
|
|
14749
|
+
shift
|
|
14750
|
+
ci_fail_on="${1:-}"
|
|
14751
|
+
if [ -z "$ci_fail_on" ]; then
|
|
14752
|
+
echo -e "${RED}Error: --fail-on requires severity levels (e.g., critical,high)${NC}"
|
|
14753
|
+
return 2
|
|
14754
|
+
fi
|
|
14755
|
+
ci_fail_on="$(echo "$ci_fail_on" | tr '[:upper:]' '[:lower:]')"
|
|
14756
|
+
shift
|
|
14757
|
+
;;
|
|
14758
|
+
--format)
|
|
14759
|
+
shift
|
|
14760
|
+
ci_format="${1:-markdown}"
|
|
14761
|
+
ci_format="$(echo "$ci_format" | tr '[:upper:]' '[:lower:]')"
|
|
14762
|
+
if [[ "$ci_format" != "json" && "$ci_format" != "markdown" && "$ci_format" != "github" ]]; then
|
|
14763
|
+
echo -e "${RED}Error: --format must be json, markdown, or github${NC}"
|
|
14764
|
+
return 2
|
|
14765
|
+
fi
|
|
14766
|
+
shift
|
|
14767
|
+
;;
|
|
14768
|
+
-*) echo -e "${RED}Unknown option: $1${NC}"; return 2 ;;
|
|
14769
|
+
*) echo -e "${RED}Unknown argument: $1${NC}"; return 2 ;;
|
|
14770
|
+
esac
|
|
14771
|
+
done
|
|
14772
|
+
|
|
14773
|
+
# If no mode specified, default to --pr --report
|
|
14774
|
+
if [ "$ci_pr" = false ] && [ "$ci_test_suggest" = false ] && [ "$ci_report" = false ]; then
|
|
14775
|
+
ci_pr=true
|
|
14776
|
+
ci_report=true
|
|
14777
|
+
fi
|
|
14778
|
+
|
|
14779
|
+
# --- Detect CI environment ---
|
|
14780
|
+
local ci_env="local"
|
|
14781
|
+
local ci_pr_number=""
|
|
14782
|
+
local ci_base_branch="main"
|
|
14783
|
+
local ci_repo=""
|
|
14784
|
+
|
|
14785
|
+
if [ -n "${GITHUB_ACTIONS:-}" ]; then
|
|
14786
|
+
ci_env="github"
|
|
14787
|
+
ci_repo="${GITHUB_REPOSITORY:-}"
|
|
14788
|
+
ci_base_branch="${GITHUB_BASE_REF:-main}"
|
|
14789
|
+
# Extract PR number from GITHUB_EVENT_PATH
|
|
14790
|
+
if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -f "${GITHUB_EVENT_PATH:-}" ]; then
|
|
14791
|
+
ci_pr_number=$(python3 -c "
|
|
14792
|
+
import json, sys
|
|
14793
|
+
try:
|
|
14794
|
+
with open('${GITHUB_EVENT_PATH}') as f:
|
|
14795
|
+
event = json.load(f)
|
|
14796
|
+
pr = event.get('pull_request', event.get('number', ''))
|
|
14797
|
+
if isinstance(pr, dict):
|
|
14798
|
+
print(pr.get('number', ''))
|
|
14799
|
+
else:
|
|
14800
|
+
print(pr)
|
|
14801
|
+
except Exception:
|
|
14802
|
+
print('')
|
|
14803
|
+
" 2>/dev/null || echo "")
|
|
14804
|
+
fi
|
|
14805
|
+
# Fallback: GITHUB_REF_NAME for PR refs like refs/pull/123/merge
|
|
14806
|
+
if [ -z "$ci_pr_number" ] && [[ "${GITHUB_REF:-}" == refs/pull/*/merge ]]; then
|
|
14807
|
+
ci_pr_number=$(echo "${GITHUB_REF}" | sed 's|refs/pull/\([0-9]*\)/merge|\1|')
|
|
14808
|
+
fi
|
|
14809
|
+
elif [ -n "${GITLAB_CI:-}" ]; then
|
|
14810
|
+
ci_env="gitlab"
|
|
14811
|
+
ci_pr_number="${CI_MERGE_REQUEST_IID:-}"
|
|
14812
|
+
ci_base_branch="${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-main}"
|
|
14813
|
+
ci_repo="${CI_PROJECT_PATH:-}"
|
|
14814
|
+
elif [ -n "${JENKINS_URL:-}" ]; then
|
|
14815
|
+
ci_env="jenkins"
|
|
14816
|
+
ci_pr_number="${CHANGE_ID:-}"
|
|
14817
|
+
ci_base_branch="${CHANGE_TARGET:-main}"
|
|
14818
|
+
elif [ -n "${CIRCLECI:-}" ]; then
|
|
14819
|
+
ci_env="circleci"
|
|
14820
|
+
ci_pr_number="${CIRCLE_PULL_REQUEST##*/}"
|
|
14821
|
+
ci_repo="${CIRCLE_PROJECT_USERNAME:-}/${CIRCLE_PROJECT_REPONAME:-}"
|
|
14822
|
+
fi
|
|
14823
|
+
|
|
14824
|
+
# --- Gather diff ---
|
|
14825
|
+
local diff_content=""
|
|
14826
|
+
local changed_files=""
|
|
14827
|
+
|
|
14828
|
+
if [ "$ci_pr" = true ] || [ "$ci_test_suggest" = true ] || [ "$ci_report" = true ]; then
|
|
14829
|
+
if [ -n "$ci_pr_number" ] && command -v gh &>/dev/null && [ "$ci_env" = "github" ]; then
|
|
14830
|
+
diff_content=$(gh pr diff "$ci_pr_number" 2>/dev/null || echo "")
|
|
14831
|
+
changed_files=$(gh pr diff "$ci_pr_number" --name-only 2>/dev/null || echo "")
|
|
14832
|
+
fi
|
|
14833
|
+
|
|
14834
|
+
# Fallback: git diff against base branch
|
|
14835
|
+
if [ -z "$diff_content" ]; then
|
|
14836
|
+
# Fetch base branch if in CI
|
|
14837
|
+
if [ "$ci_env" != "local" ]; then
|
|
14838
|
+
git fetch origin "$ci_base_branch" --depth=1 2>/dev/null || true
|
|
14839
|
+
fi
|
|
14840
|
+
diff_content=$(git diff "origin/${ci_base_branch}...HEAD" 2>/dev/null || git diff HEAD~1 2>/dev/null || git diff HEAD 2>/dev/null || echo "")
|
|
14841
|
+
changed_files=$(git diff --name-only "origin/${ci_base_branch}...HEAD" 2>/dev/null || git diff --name-only HEAD~1 2>/dev/null || git diff --name-only HEAD 2>/dev/null || echo "")
|
|
14842
|
+
fi
|
|
14843
|
+
|
|
14844
|
+
if [ -z "$diff_content" ]; then
|
|
14845
|
+
if [ "$ci_format" = "json" ]; then
|
|
14846
|
+
echo '{"status":"skip","message":"No changes to review","ci_environment":"'"$ci_env"'","pr_number":"'"$ci_pr_number"'","findings":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"total":0},"exit_code":0}'
|
|
14847
|
+
else
|
|
14848
|
+
echo -e "${GREEN}No changes to review.${NC}"
|
|
14849
|
+
fi
|
|
14850
|
+
return 0
|
|
14851
|
+
fi
|
|
14852
|
+
fi
|
|
14853
|
+
|
|
14854
|
+
# --- Severity helpers ---
|
|
14855
|
+
_ci_sev_level() {
|
|
14856
|
+
case "$1" in
|
|
14857
|
+
CRITICAL) echo 5 ;;
|
|
14858
|
+
HIGH) echo 4 ;;
|
|
14859
|
+
MEDIUM) echo 3 ;;
|
|
14860
|
+
LOW) echo 2 ;;
|
|
14861
|
+
INFO) echo 1 ;;
|
|
14862
|
+
*) echo 0 ;;
|
|
14863
|
+
esac
|
|
14864
|
+
}
|
|
14865
|
+
|
|
14866
|
+
# Parse --fail-on into a minimum severity threshold
|
|
14867
|
+
local fail_threshold=99 # Default: never fail
|
|
14868
|
+
if [ -n "$ci_fail_on" ]; then
|
|
14869
|
+
# Take the lowest severity from the comma-separated list
|
|
14870
|
+
IFS=',' read -ra fail_levels <<< "$ci_fail_on"
|
|
14871
|
+
for level in "${fail_levels[@]}"; do
|
|
14872
|
+
level=$(echo "$level" | tr -d ' ' | tr '[:lower:]' '[:upper:]')
|
|
14873
|
+
local level_num
|
|
14874
|
+
level_num=$(_ci_sev_level "$level")
|
|
14875
|
+
if [ "$level_num" -gt 0 ] && [ "$level_num" -lt "$fail_threshold" ]; then
|
|
14876
|
+
fail_threshold=$level_num
|
|
14877
|
+
fi
|
|
14878
|
+
done
|
|
14879
|
+
fi
|
|
14880
|
+
|
|
14881
|
+
# --- Run quality gates (reuses cmd_review logic) ---
|
|
14882
|
+
local findings=()
|
|
14883
|
+
local has_critical=false
|
|
14884
|
+
local has_high=false
|
|
14885
|
+
|
|
14886
|
+
_ci_add_finding() {
|
|
14887
|
+
local file="$1" line="$2" sev="$3" cat="$4" finding="$5" suggestion="$6"
|
|
14888
|
+
findings+=("${file}|${line}|${sev}|${cat}|${finding}|${suggestion}")
|
|
14889
|
+
[ "$sev" = "CRITICAL" ] && has_critical=true || true
|
|
14890
|
+
[ "$sev" = "HIGH" ] && has_high=true || true
|
|
14891
|
+
}
|
|
14892
|
+
|
|
14893
|
+
# Gate 1: Static Analysis - shellcheck
|
|
14894
|
+
if command -v shellcheck &>/dev/null; then
|
|
14895
|
+
local shell_files_ci
|
|
14896
|
+
shell_files_ci=$(echo "$changed_files" | grep -E '\.(sh|bash)$' || true)
|
|
14897
|
+
if [ -n "$shell_files_ci" ]; then
|
|
14898
|
+
while IFS= read -r sf; do
|
|
14899
|
+
[ -z "$sf" ] && continue
|
|
14900
|
+
[ ! -f "$sf" ] && continue
|
|
14901
|
+
local sc_out
|
|
14902
|
+
sc_out=$(shellcheck -f gcc "$sf" 2>/dev/null || true)
|
|
14903
|
+
while IFS= read -r sc_line; do
|
|
14904
|
+
[ -z "$sc_line" ] && continue
|
|
14905
|
+
local sc_file sc_lineno sc_sev sc_msg
|
|
14906
|
+
sc_file=$(echo "$sc_line" | cut -d: -f1)
|
|
14907
|
+
sc_lineno=$(echo "$sc_line" | cut -d: -f2)
|
|
14908
|
+
sc_sev=$(echo "$sc_line" | sed -n 's/.*: \(warning\|error\|note\|info\):.*/\1/p')
|
|
14909
|
+
sc_msg=$(echo "$sc_line" | sed 's/.*: \(warning\|error\|note\|info\): //')
|
|
14910
|
+
local mapped_sev="LOW"
|
|
14911
|
+
case "$sc_sev" in
|
|
14912
|
+
error) mapped_sev="HIGH" ;;
|
|
14913
|
+
warning) mapped_sev="MEDIUM" ;;
|
|
14914
|
+
*) mapped_sev="LOW" ;;
|
|
14915
|
+
esac
|
|
14916
|
+
_ci_add_finding "$sc_file" "$sc_lineno" "$mapped_sev" "static-analysis" "$sc_msg" "Fix shellcheck finding"
|
|
14917
|
+
done <<< "$sc_out"
|
|
14918
|
+
done <<< "$shell_files_ci"
|
|
14919
|
+
fi
|
|
14920
|
+
fi
|
|
14921
|
+
|
|
14922
|
+
# Gate 1b: Static Analysis - eslint
|
|
14923
|
+
if command -v npx &>/dev/null; then
|
|
14924
|
+
local js_files_ci
|
|
14925
|
+
js_files_ci=$(echo "$changed_files" | grep -E '\.(js|ts|jsx|tsx)$' || true)
|
|
14926
|
+
if [ -n "$js_files_ci" ]; then
|
|
14927
|
+
while IFS= read -r jsf; do
|
|
14928
|
+
[ -z "$jsf" ] && continue
|
|
14929
|
+
[ ! -f "$jsf" ] && continue
|
|
14930
|
+
if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then
|
|
14931
|
+
local eslint_out
|
|
14932
|
+
eslint_out=$(npx eslint --format compact "$jsf" 2>/dev/null || true)
|
|
14933
|
+
while IFS= read -r el; do
|
|
14934
|
+
[ -z "$el" ] && continue
|
|
14935
|
+
[[ "$el" == *"problem"* ]] && continue
|
|
14936
|
+
local el_file el_line el_msg
|
|
14937
|
+
el_file=$(echo "$el" | cut -d: -f1)
|
|
14938
|
+
el_line=$(echo "$el" | cut -d: -f2)
|
|
14939
|
+
el_msg=$(echo "$el" | sed 's/^[^)]*) //')
|
|
14940
|
+
local el_sev="LOW"
|
|
14941
|
+
[[ "$el" == *"Error"* ]] && el_sev="MEDIUM"
|
|
14942
|
+
_ci_add_finding "$el_file" "$el_line" "$el_sev" "static-analysis" "$el_msg" "Fix eslint finding"
|
|
14943
|
+
done <<< "$eslint_out"
|
|
14944
|
+
fi
|
|
14945
|
+
done <<< "$js_files_ci"
|
|
14946
|
+
fi
|
|
14947
|
+
fi
|
|
14948
|
+
|
|
14949
|
+
# Gate 2: Security Scan
|
|
14950
|
+
_ci_scan() {
|
|
14951
|
+
local pattern="$1" sev="$2" cat="$3" finding="$4" suggestion="$5"
|
|
14952
|
+
while IFS= read -r match; do
|
|
14953
|
+
[ -z "$match" ] && continue
|
|
14954
|
+
local ml
|
|
14955
|
+
ml=$(echo "$match" | cut -d: -f1)
|
|
14956
|
+
_ci_add_finding "diff" "$ml" "$sev" "$cat" "$finding" "$suggestion"
|
|
14957
|
+
done < <(echo "$diff_content" | grep -nE "$pattern" 2>/dev/null || true)
|
|
14958
|
+
}
|
|
14959
|
+
|
|
14960
|
+
# Hardcoded secrets
|
|
14961
|
+
local ci_secret_patterns=(
|
|
14962
|
+
'API_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
|
|
14963
|
+
'SECRET_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
|
|
14964
|
+
'PASSWORD\s*[=:]\s*["\x27][^\x27"]{4,}'
|
|
14965
|
+
'PRIVATE_KEY\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{8,}'
|
|
14966
|
+
'AWS_ACCESS_KEY_ID\s*[=:]\s*["\x27]AK[A-Z0-9]{18}'
|
|
14967
|
+
'ghp_[A-Za-z0-9]{36}'
|
|
14968
|
+
'sk-[A-Za-z0-9]{32,}'
|
|
14969
|
+
'Bearer\s+[A-Za-z0-9._-]{20,}'
|
|
14970
|
+
)
|
|
14971
|
+
for pattern in "${ci_secret_patterns[@]}"; do
|
|
14972
|
+
_ci_scan "$pattern" "CRITICAL" "security" \
|
|
14973
|
+
"Potential hardcoded secret detected" \
|
|
14974
|
+
"Use environment variables or a secrets manager"
|
|
14975
|
+
done
|
|
14976
|
+
|
|
14977
|
+
# SQL injection
|
|
14978
|
+
_ci_scan '(SELECT|INSERT|UPDATE|DELETE|DROP)\s.*\+\s*(req\.|request\.|params\.|user)' \
|
|
14979
|
+
"HIGH" "security" \
|
|
14980
|
+
"Potential SQL injection: string concatenation in query" \
|
|
14981
|
+
"Use parameterized queries or prepared statements"
|
|
14982
|
+
|
|
14983
|
+
# eval/exec
|
|
14984
|
+
_ci_scan '(^|\s)(eval|exec)\s*\(' \
|
|
14985
|
+
"HIGH" "security" \
|
|
14986
|
+
"Dangerous eval/exec usage detected" \
|
|
14987
|
+
"Avoid eval/exec with dynamic input"
|
|
14988
|
+
|
|
14989
|
+
# Unsafe deserialization
|
|
14990
|
+
_ci_scan '(pickle\.loads?|yaml\.load\s*\()' \
|
|
14991
|
+
"HIGH" "security" \
|
|
14992
|
+
"Unsafe deserialization detected" \
|
|
14993
|
+
"Use yaml.safe_load or avoid pickle with untrusted data"
|
|
14994
|
+
|
|
14995
|
+
# Disabled SSL
|
|
14996
|
+
_ci_scan '(verify\s*=\s*False|VERIFY_SSL\s*=\s*False|NODE_TLS_REJECT_UNAUTHORIZED.*0|rejectUnauthorized.*false)' \
|
|
14997
|
+
"HIGH" "security" \
|
|
14998
|
+
"SSL verification disabled" \
|
|
14999
|
+
"Enable SSL verification in production"
|
|
15000
|
+
|
|
15001
|
+
# Gate 3: Anti-patterns
|
|
15002
|
+
_ci_scan 'console\.(log|debug)\(' "LOW" "anti-pattern" \
|
|
15003
|
+
"console.log/debug statement found" \
|
|
15004
|
+
"Remove debug logging or use a proper logger"
|
|
15005
|
+
|
|
15006
|
+
_ci_scan 'except\s*:' "MEDIUM" "anti-pattern" \
|
|
15007
|
+
"Bare except clause" \
|
|
15008
|
+
"Catch specific exceptions"
|
|
15009
|
+
|
|
15010
|
+
# Gate 4: TODO/FIXME markers in new code
|
|
15011
|
+
_ci_scan '^\+.*\b(TODO|FIXME|HACK|XXX):' "INFO" "style" \
|
|
15012
|
+
"TODO/FIXME marker in new code" \
|
|
15013
|
+
"Track in issue tracker"
|
|
15014
|
+
|
|
15015
|
+
# --- Test suggestions ---
|
|
15016
|
+
local test_suggestions=""
|
|
15017
|
+
if [ "$ci_test_suggest" = true ] && [ -n "$changed_files" ]; then
|
|
15018
|
+
test_suggestions=$(python3 << 'TEST_SUGGEST_PY'
|
|
15019
|
+
import sys, os
|
|
15020
|
+
|
|
15021
|
+
changed = os.environ.get("LOKI_CI_CHANGED_FILES", "").strip().split("\n")
|
|
15022
|
+
suggestions = []
|
|
15023
|
+
|
|
15024
|
+
for f in changed:
|
|
15025
|
+
f = f.strip()
|
|
15026
|
+
if not f:
|
|
15027
|
+
continue
|
|
15028
|
+
|
|
15029
|
+
base = os.path.basename(f)
|
|
15030
|
+
name, ext = os.path.splitext(base)
|
|
15031
|
+
dirpath = os.path.dirname(f)
|
|
15032
|
+
|
|
15033
|
+
# Skip test files themselves
|
|
15034
|
+
if "test" in name.lower() or "spec" in name.lower():
|
|
15035
|
+
continue
|
|
15036
|
+
# Skip non-code files
|
|
15037
|
+
if ext not in (".py", ".js", ".ts", ".jsx", ".tsx", ".sh", ".bash", ".go", ".rs", ".rb"):
|
|
15038
|
+
continue
|
|
15039
|
+
|
|
15040
|
+
# Suggest test file paths based on language conventions
|
|
15041
|
+
if ext == ".py":
|
|
15042
|
+
test_path = os.path.join(dirpath, f"test_{name}.py")
|
|
15043
|
+
alt_path = os.path.join("tests", f"test_{name}.py")
|
|
15044
|
+
suggestions.append({"file": f, "test_file": test_path, "alt_test_file": alt_path,
|
|
15045
|
+
"framework": "pytest", "hint": f"Add unit tests for {name} module"})
|
|
15046
|
+
elif ext in (".js", ".ts", ".jsx", ".tsx"):
|
|
15047
|
+
test_ext = ext.replace(".ts", ".test.ts").replace(".js", ".test.js").replace(".tsx", ".test.tsx").replace(".jsx", ".test.jsx")
|
|
15048
|
+
if test_ext == ext:
|
|
15049
|
+
test_ext = f".test{ext}"
|
|
15050
|
+
test_path = os.path.join(dirpath, f"{name}{test_ext}")
|
|
15051
|
+
suggestions.append({"file": f, "test_file": test_path,
|
|
15052
|
+
"framework": "jest/vitest", "hint": f"Add tests for {name} component/module"})
|
|
15053
|
+
elif ext in (".sh", ".bash"):
|
|
15054
|
+
test_path = os.path.join("tests", f"test-{name}.sh")
|
|
15055
|
+
suggestions.append({"file": f, "test_file": test_path,
|
|
15056
|
+
"framework": "bash", "hint": f"Add shell tests for {name}"})
|
|
15057
|
+
elif ext == ".go":
|
|
15058
|
+
test_path = os.path.join(dirpath, f"{name}_test.go")
|
|
15059
|
+
suggestions.append({"file": f, "test_file": test_path,
|
|
15060
|
+
"framework": "go test", "hint": f"Add Go tests for {name}"})
|
|
15061
|
+
elif ext == ".rs":
|
|
15062
|
+
suggestions.append({"file": f, "test_file": f"{f} (mod tests)",
|
|
15063
|
+
"framework": "cargo test", "hint": f"Add #[cfg(test)] mod tests in {name}"})
|
|
15064
|
+
elif ext == ".rb":
|
|
15065
|
+
test_path = os.path.join("spec", f"{name}_spec.rb")
|
|
15066
|
+
suggestions.append({"file": f, "test_file": test_path,
|
|
15067
|
+
"framework": "rspec", "hint": f"Add RSpec tests for {name}"})
|
|
15068
|
+
|
|
15069
|
+
import json
|
|
15070
|
+
print(json.dumps(suggestions))
|
|
15071
|
+
TEST_SUGGEST_PY
|
|
15072
|
+
)
|
|
15073
|
+
fi
|
|
15074
|
+
|
|
15075
|
+
# --- Tally findings ---
|
|
15076
|
+
local count_critical=0 count_high=0 count_medium=0 count_low=0 count_info=0
|
|
15077
|
+
for f in "${findings[@]}"; do
|
|
15078
|
+
local sev
|
|
15079
|
+
sev=$(echo "$f" | cut -d'|' -f3)
|
|
15080
|
+
case "$sev" in
|
|
15081
|
+
CRITICAL) count_critical=$((count_critical + 1)) ;;
|
|
15082
|
+
HIGH) count_high=$((count_high + 1)) ;;
|
|
15083
|
+
MEDIUM) count_medium=$((count_medium + 1)) ;;
|
|
15084
|
+
LOW) count_low=$((count_low + 1)) ;;
|
|
15085
|
+
INFO) count_info=$((count_info + 1)) ;;
|
|
15086
|
+
esac
|
|
15087
|
+
done
|
|
15088
|
+
local count_total=${#findings[@]}
|
|
15089
|
+
|
|
15090
|
+
# Determine exit code based on --fail-on threshold
|
|
15091
|
+
local exit_code=0
|
|
15092
|
+
if [ "$fail_threshold" -lt 99 ]; then
|
|
15093
|
+
for f in "${findings[@]}"; do
|
|
15094
|
+
local sev sev_num
|
|
15095
|
+
sev=$(echo "$f" | cut -d'|' -f3)
|
|
15096
|
+
sev_num=$(_ci_sev_level "$sev")
|
|
15097
|
+
if [ "$sev_num" -ge "$fail_threshold" ]; then
|
|
15098
|
+
exit_code=1
|
|
15099
|
+
break
|
|
15100
|
+
fi
|
|
15101
|
+
done
|
|
15102
|
+
fi
|
|
15103
|
+
|
|
15104
|
+
# --- Build output ---
|
|
15105
|
+
local report_timestamp
|
|
15106
|
+
report_timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
15107
|
+
local file_count
|
|
15108
|
+
file_count=$(echo "$changed_files" | grep -c '.' 2>/dev/null || echo 0)
|
|
15109
|
+
|
|
15110
|
+
if [ "$ci_format" = "json" ]; then
|
|
15111
|
+
# JSON output via python for proper escaping
|
|
15112
|
+
export LOKI_CI_JSON_FINDINGS=""
|
|
15113
|
+
for f in "${findings[@]}"; do
|
|
15114
|
+
LOKI_CI_JSON_FINDINGS+="${f}"$'\n'
|
|
15115
|
+
done
|
|
15116
|
+
export LOKI_CI_JSON_META="ci_env=${ci_env}|pr=${ci_pr_number}|ts=${report_timestamp}|files=${file_count}|exit=${exit_code}"
|
|
15117
|
+
export LOKI_CI_JSON_COUNTS="critical=${count_critical}|high=${count_high}|medium=${count_medium}|low=${count_low}|info=${count_info}|total=${count_total}"
|
|
15118
|
+
export LOKI_CI_JSON_TESTS="${test_suggestions:-[]}"
|
|
15119
|
+
|
|
15120
|
+
python3 << 'CI_JSON_OUT'
|
|
15121
|
+
import json, os
|
|
15122
|
+
|
|
15123
|
+
findings_raw = os.environ.get("LOKI_CI_JSON_FINDINGS", "").strip().split("\n")
|
|
15124
|
+
meta_raw = os.environ.get("LOKI_CI_JSON_META", "")
|
|
15125
|
+
counts_raw = os.environ.get("LOKI_CI_JSON_COUNTS", "")
|
|
15126
|
+
tests_raw = os.environ.get("LOKI_CI_JSON_TESTS", "[]")
|
|
15127
|
+
|
|
15128
|
+
meta = dict(item.split("=", 1) for item in meta_raw.split("|") if "=" in item)
|
|
15129
|
+
counts = dict(item.split("=", 1) for item in counts_raw.split("|") if "=" in item)
|
|
15130
|
+
|
|
15131
|
+
findings = []
|
|
15132
|
+
for line in findings_raw:
|
|
15133
|
+
if not line or "|" not in line:
|
|
15134
|
+
continue
|
|
15135
|
+
parts = line.split("|", 5)
|
|
15136
|
+
if len(parts) >= 6:
|
|
15137
|
+
findings.append({
|
|
15138
|
+
"file": parts[0],
|
|
15139
|
+
"line": int(parts[1]) if parts[1].isdigit() else 0,
|
|
15140
|
+
"severity": parts[2],
|
|
15141
|
+
"category": parts[3],
|
|
15142
|
+
"finding": parts[4],
|
|
15143
|
+
"suggestion": parts[5]
|
|
15144
|
+
})
|
|
15145
|
+
|
|
15146
|
+
try:
|
|
15147
|
+
tests = json.loads(tests_raw)
|
|
15148
|
+
except Exception:
|
|
15149
|
+
tests = []
|
|
15150
|
+
|
|
15151
|
+
report = {
|
|
15152
|
+
"status": "fail" if int(meta.get("exit", "0")) > 0 else "pass",
|
|
15153
|
+
"ci_environment": meta.get("ci_env", "local"),
|
|
15154
|
+
"pr_number": meta.get("pr", ""),
|
|
15155
|
+
"timestamp": meta.get("ts", ""),
|
|
15156
|
+
"files_changed": int(meta.get("files", "0")),
|
|
15157
|
+
"findings": findings,
|
|
15158
|
+
"summary": {
|
|
15159
|
+
"critical": int(counts.get("critical", "0")),
|
|
15160
|
+
"high": int(counts.get("high", "0")),
|
|
15161
|
+
"medium": int(counts.get("medium", "0")),
|
|
15162
|
+
"low": int(counts.get("low", "0")),
|
|
15163
|
+
"info": int(counts.get("info", "0")),
|
|
15164
|
+
"total": int(counts.get("total", "0"))
|
|
15165
|
+
},
|
|
15166
|
+
"exit_code": int(meta.get("exit", "0"))
|
|
15167
|
+
}
|
|
15168
|
+
if tests:
|
|
15169
|
+
report["test_suggestions"] = tests
|
|
15170
|
+
|
|
15171
|
+
print(json.dumps(report, indent=2))
|
|
15172
|
+
CI_JSON_OUT
|
|
15173
|
+
unset LOKI_CI_JSON_FINDINGS LOKI_CI_JSON_META LOKI_CI_JSON_COUNTS LOKI_CI_JSON_TESTS
|
|
15174
|
+
|
|
15175
|
+
elif [ "$ci_format" = "github" ]; then
|
|
15176
|
+
# GitHub-flavored markdown for PR comments
|
|
15177
|
+
echo "## Loki CI Quality Report"
|
|
15178
|
+
echo ""
|
|
15179
|
+
echo "| Metric | Count |"
|
|
15180
|
+
echo "|--------|-------|"
|
|
15181
|
+
echo "| Files changed | $file_count |"
|
|
15182
|
+
echo "| Critical | $count_critical |"
|
|
15183
|
+
echo "| High | $count_high |"
|
|
15184
|
+
echo "| Medium | $count_medium |"
|
|
15185
|
+
echo "| Low | $count_low |"
|
|
15186
|
+
echo "| Info | $count_info |"
|
|
15187
|
+
echo "| **Total** | **$count_total** |"
|
|
15188
|
+
echo ""
|
|
15189
|
+
|
|
15190
|
+
if [ "$count_total" -gt 0 ]; then
|
|
15191
|
+
echo "### Findings"
|
|
15192
|
+
echo ""
|
|
15193
|
+
for sev_name in CRITICAL HIGH MEDIUM LOW INFO; do
|
|
15194
|
+
local has_sev=false
|
|
15195
|
+
for f in "${findings[@]}"; do
|
|
15196
|
+
local f_sev
|
|
15197
|
+
f_sev=$(echo "$f" | cut -d'|' -f3)
|
|
15198
|
+
[ "$f_sev" != "$sev_name" ] && continue
|
|
15199
|
+
if [ "$has_sev" = false ]; then
|
|
15200
|
+
echo "#### $sev_name"
|
|
15201
|
+
echo ""
|
|
15202
|
+
has_sev=true
|
|
15203
|
+
fi
|
|
15204
|
+
local f_file f_line f_cat f_finding f_suggestion
|
|
15205
|
+
f_file=$(echo "$f" | cut -d'|' -f1)
|
|
15206
|
+
f_line=$(echo "$f" | cut -d'|' -f2)
|
|
15207
|
+
f_cat=$(echo "$f" | cut -d'|' -f4)
|
|
15208
|
+
f_finding=$(echo "$f" | cut -d'|' -f5)
|
|
15209
|
+
f_suggestion=$(echo "$f" | cut -d'|' -f6)
|
|
15210
|
+
echo "- **\`$f_file:$f_line\`** [$f_cat] $f_finding"
|
|
15211
|
+
echo " - Suggestion: $f_suggestion"
|
|
15212
|
+
done
|
|
15213
|
+
[ "$has_sev" = true ] && echo ""
|
|
15214
|
+
done
|
|
15215
|
+
else
|
|
15216
|
+
echo "No findings. All quality gates passed."
|
|
15217
|
+
fi
|
|
15218
|
+
|
|
15219
|
+
if [ -n "${test_suggestions:-}" ] && [ "$test_suggestions" != "[]" ]; then
|
|
15220
|
+
echo "### Test Suggestions"
|
|
15221
|
+
echo ""
|
|
15222
|
+
python3 -c "
|
|
15223
|
+
import json, os
|
|
15224
|
+
tests = json.loads(os.environ.get('LOKI_CI_TEST_SUGG', '[]'))
|
|
15225
|
+
for t in tests:
|
|
15226
|
+
print(f\"- **{t['file']}**: {t['hint']} ({t['framework']})\")
|
|
15227
|
+
print(f\" - Suggested: \`{t['test_file']}\`\")
|
|
15228
|
+
" 2>/dev/null || true
|
|
15229
|
+
fi
|
|
15230
|
+
|
|
15231
|
+
echo ""
|
|
15232
|
+
if [ "$exit_code" -eq 0 ]; then
|
|
15233
|
+
echo "**Result: PASSED**"
|
|
15234
|
+
else
|
|
15235
|
+
echo "**Result: FAILED** (findings exceed threshold)"
|
|
15236
|
+
fi
|
|
15237
|
+
echo ""
|
|
15238
|
+
echo "_Generated by [Loki Mode](https://github.com/asklokesh/loki-mode) at $report_timestamp_"
|
|
15239
|
+
|
|
15240
|
+
else
|
|
15241
|
+
# Default: markdown (terminal-friendly)
|
|
15242
|
+
echo -e "${BOLD}Loki CI Quality Report${NC}"
|
|
15243
|
+
echo -e "Environment: ${CYAN}$ci_env${NC}"
|
|
15244
|
+
[ -n "$ci_pr_number" ] && echo -e "PR: ${CYAN}#$ci_pr_number${NC}"
|
|
15245
|
+
echo -e "Files changed: ${CYAN}$file_count${NC}"
|
|
15246
|
+
echo -e "Timestamp: ${DIM}$report_timestamp${NC}"
|
|
15247
|
+
echo "---"
|
|
15248
|
+
|
|
15249
|
+
if [ "$count_total" -eq 0 ]; then
|
|
15250
|
+
echo -e "${GREEN}All quality gates passed. No findings.${NC}"
|
|
15251
|
+
else
|
|
15252
|
+
for sev_name in CRITICAL HIGH MEDIUM LOW INFO; do
|
|
15253
|
+
local printed_header=false
|
|
15254
|
+
for f in "${findings[@]}"; do
|
|
15255
|
+
local f_sev
|
|
15256
|
+
f_sev=$(echo "$f" | cut -d'|' -f3)
|
|
15257
|
+
[ "$f_sev" != "$sev_name" ] && continue
|
|
15258
|
+
if [ "$printed_header" = false ]; then
|
|
15259
|
+
local sev_color="$NC"
|
|
15260
|
+
case "$sev_name" in
|
|
15261
|
+
CRITICAL) sev_color="$RED" ;;
|
|
15262
|
+
HIGH) sev_color="$RED" ;;
|
|
15263
|
+
MEDIUM) sev_color="$YELLOW" ;;
|
|
15264
|
+
LOW) sev_color="$CYAN" ;;
|
|
15265
|
+
INFO) sev_color="$DIM" ;;
|
|
15266
|
+
esac
|
|
15267
|
+
echo ""
|
|
15268
|
+
echo -e "${sev_color}${BOLD}[$sev_name]${NC}"
|
|
15269
|
+
printed_header=true
|
|
15270
|
+
fi
|
|
15271
|
+
local f_file f_line f_cat f_finding f_suggestion
|
|
15272
|
+
f_file=$(echo "$f" | cut -d'|' -f1)
|
|
15273
|
+
f_line=$(echo "$f" | cut -d'|' -f2)
|
|
15274
|
+
f_cat=$(echo "$f" | cut -d'|' -f4)
|
|
15275
|
+
f_finding=$(echo "$f" | cut -d'|' -f5)
|
|
15276
|
+
f_suggestion=$(echo "$f" | cut -d'|' -f6)
|
|
15277
|
+
echo -e " ${DIM}$f_file:$f_line${NC} [$f_cat] $f_finding"
|
|
15278
|
+
echo -e " -> $f_suggestion"
|
|
15279
|
+
done
|
|
15280
|
+
done
|
|
15281
|
+
fi
|
|
15282
|
+
|
|
15283
|
+
echo ""
|
|
15284
|
+
echo "---"
|
|
15285
|
+
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)"
|
|
15286
|
+
|
|
15287
|
+
if [ "$ci_test_suggest" = true ] && [ -n "${test_suggestions:-}" ] && [ "$test_suggestions" != "[]" ]; then
|
|
15288
|
+
echo ""
|
|
15289
|
+
echo -e "${BOLD}Test Suggestions${NC}"
|
|
15290
|
+
export LOKI_CI_TEST_SUGG="${test_suggestions}"
|
|
15291
|
+
python3 -c "
|
|
15292
|
+
import json, os
|
|
15293
|
+
tests = json.loads(os.environ.get('LOKI_CI_TEST_SUGG', '[]'))
|
|
15294
|
+
for t in tests:
|
|
15295
|
+
print(f\" {t['file']} -> {t['test_file']} ({t['framework']})\")
|
|
15296
|
+
print(f\" {t['hint']}\")
|
|
15297
|
+
" 2>/dev/null || true
|
|
15298
|
+
unset LOKI_CI_TEST_SUGG
|
|
15299
|
+
fi
|
|
15300
|
+
|
|
15301
|
+
if [ "$exit_code" -eq 0 ]; then
|
|
15302
|
+
echo -e "${GREEN}Result: PASSED${NC}"
|
|
15303
|
+
else
|
|
15304
|
+
echo -e "${RED}Result: FAILED (findings exceed threshold)${NC}"
|
|
15305
|
+
fi
|
|
15306
|
+
fi
|
|
15307
|
+
|
|
15308
|
+
# --- Post GitHub comment ---
|
|
15309
|
+
if [ "$ci_github_comment" = true ]; then
|
|
15310
|
+
if [ -z "${GITHUB_TOKEN:-}" ]; then
|
|
15311
|
+
echo -e "${YELLOW}Warning: --github-comment requires GITHUB_TOKEN. Skipping comment.${NC}" >&2
|
|
15312
|
+
elif [ -z "$ci_pr_number" ]; then
|
|
15313
|
+
echo -e "${YELLOW}Warning: No PR number detected. Skipping comment.${NC}" >&2
|
|
15314
|
+
elif command -v gh &>/dev/null; then
|
|
15315
|
+
# Generate GitHub-format report for comment
|
|
15316
|
+
local comment_body
|
|
15317
|
+
comment_body=$("$0" ci --pr --format github 2>/dev/null || echo "Loki CI report generation failed.")
|
|
15318
|
+
gh pr comment "$ci_pr_number" --body "$comment_body" 2>/dev/null && \
|
|
15319
|
+
echo -e "${GREEN}Posted review comment to PR #$ci_pr_number${NC}" >&2 || \
|
|
15320
|
+
echo -e "${YELLOW}Warning: Failed to post PR comment${NC}" >&2
|
|
15321
|
+
else
|
|
15322
|
+
echo -e "${YELLOW}Warning: gh CLI required for --github-comment. Install: https://cli.github.com${NC}" >&2
|
|
15323
|
+
fi
|
|
15324
|
+
fi
|
|
15325
|
+
|
|
15326
|
+
return "$exit_code"
|
|
15327
|
+
}
|
|
15328
|
+
|
|
14684
15329
|
main "$@"
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED