loki-mode 6.21.0 → 6.23.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 CHANGED
@@ -9,7 +9,7 @@
9
9
  [![Agent Types](https://img.shields.io/badge/Agent%20Types-41-blue)]()
10
10
  [![Autonomi](https://img.shields.io/badge/Autonomi-autonomi.dev-5B4EEA)](https://www.autonomi.dev/)
11
11
 
12
- **Current Version: v6.15.0**
12
+ **Current Version: v6.22.0**
13
13
 
14
14
  ---
15
15
 
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.21.0
6
+ # Loki Mode v6.23.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.21.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
270
+ **v6.23.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.21.0
1
+ 6.23.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 "$@"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.21.0"
10
+ __version__ = "6.23.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v6.21.0
5
+ **Version:** v6.23.0
6
6
 
7
7
  ---
8
8
 
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.21.0'
60
+ __version__ = '6.23.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "6.21.0",
3
+ "version": "6.23.0",
4
4
  "description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "agent",