loki-mode 7.22.0 → 7.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +78 -1
- package/autonomy/run.sh +88 -3
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +79 -1
- package/package.json +1 -1
- package/references/mcp-integration.md +65 -0
- package/skills/00-index.md +2 -0
- package/skills/parallel-workflows.md +96 -0
- package/skills/quality-gates.md +18 -0
- package/tools/hybrid_search.py +451 -0
- package/tools/index-codebase.py +296 -0
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 11 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.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
|
|
|
@@ -383,4 +383,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
383
383
|
|
|
384
384
|
---
|
|
385
385
|
|
|
386
|
-
**v7.
|
|
386
|
+
**v7.23.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.23.0
|
package/autonomy/loki
CHANGED
|
@@ -579,7 +579,7 @@ show_help() {
|
|
|
579
579
|
echo " ci [opts] CI/CD quality gate integration (--pr, --report, --github-comment)"
|
|
580
580
|
echo " test [opts] AI-powered test generation (--file, --dir, --changed, --dry-run)"
|
|
581
581
|
echo " context [cmd] Context window management (show|files|tools|add|clear)"
|
|
582
|
-
echo " code [cmd] Codebase intelligence (overview|symbols|deps|hotspots|diff)"
|
|
582
|
+
echo " code [cmd] Codebase intelligence (overview|symbols|deps|hotspots|diff|search)"
|
|
583
583
|
echo " report [opts] Session report generator (--format text|markdown|html, --output)"
|
|
584
584
|
echo " share [opts] Share session report as GitHub Gist (--private, --format)"
|
|
585
585
|
echo " proof [cmd] Inspect/share proof-of-run artifacts (list|show|open|share)"
|
|
@@ -19712,6 +19712,7 @@ cmd_code() {
|
|
|
19712
19712
|
echo "Subcommands:"
|
|
19713
19713
|
echo " overview Generate workspace overview (file types, LOC, structure)"
|
|
19714
19714
|
echo " symbols Search for symbols (functions, classes, variables)"
|
|
19715
|
+
echo " search Hybrid grep + semantic codebase search"
|
|
19715
19716
|
echo " deps Show dependency graph for a file or module"
|
|
19716
19717
|
echo " hotspots Find code hotspots (most-changed files, complexity)"
|
|
19717
19718
|
echo " diff Show colored diff of recent changes"
|
|
@@ -19720,6 +19721,9 @@ cmd_code() {
|
|
|
19720
19721
|
echo " loki code overview # Full codebase overview"
|
|
19721
19722
|
echo " loki code overview --silent # Compact output"
|
|
19722
19723
|
echo " loki code symbols 'class.*Service' # Find service classes"
|
|
19724
|
+
echo " loki code search 'rate limit backoff' # Hybrid grep + semantic search"
|
|
19725
|
+
echo " loki code search 'council vote' --grep-only # Lexical only"
|
|
19726
|
+
echo " loki code search 'memory retrieval' --semantic-only --top 15"
|
|
19723
19727
|
echo " loki code deps src/main.py # Show dependencies"
|
|
19724
19728
|
echo " loki code hotspots --top 10 # Top 10 changed files"
|
|
19725
19729
|
echo " loki code diff # Colored diff of uncommitted changes"
|
|
@@ -19733,6 +19737,9 @@ cmd_code() {
|
|
|
19733
19737
|
symbols)
|
|
19734
19738
|
_code_symbols "$@"
|
|
19735
19739
|
;;
|
|
19740
|
+
search)
|
|
19741
|
+
_code_search "$@"
|
|
19742
|
+
;;
|
|
19736
19743
|
deps)
|
|
19737
19744
|
_code_deps "$@"
|
|
19738
19745
|
;;
|
|
@@ -19750,6 +19757,76 @@ cmd_code() {
|
|
|
19750
19757
|
esac
|
|
19751
19758
|
}
|
|
19752
19759
|
|
|
19760
|
+
# Hybrid codebase search: grep + semantic (ChromaDB) fused with RRF.
|
|
19761
|
+
# Delegates to tools/hybrid_search.py (needs python3.12 for chromadb). The
|
|
19762
|
+
# Python side handles the rg / grep / python-scan fallback and the grep-only
|
|
19763
|
+
# degradation when ChromaDB is unreachable, so this stays thin.
|
|
19764
|
+
_code_search() {
|
|
19765
|
+
local query=""
|
|
19766
|
+
local -a pyargs=()
|
|
19767
|
+
while [[ $# -gt 0 ]]; do
|
|
19768
|
+
case "$1" in
|
|
19769
|
+
--grep-only) pyargs+=("--grep-only"); shift ;;
|
|
19770
|
+
--semantic-only) pyargs+=("--semantic-only"); shift ;;
|
|
19771
|
+
--budget) pyargs+=("--budget" "${2:-3000}"); shift 2 ;;
|
|
19772
|
+
--top) pyargs+=("--top" "${2:-10}"); shift 2 ;;
|
|
19773
|
+
--json) pyargs+=("--json"); shift ;;
|
|
19774
|
+
-h|--help)
|
|
19775
|
+
echo -e "${BOLD}loki code search${NC} - Hybrid grep + semantic codebase search"
|
|
19776
|
+
echo ""
|
|
19777
|
+
echo "Usage: loki code search \"<query>\" [flags]"
|
|
19778
|
+
echo ""
|
|
19779
|
+
echo "Flags:"
|
|
19780
|
+
echo " --grep-only Lexical search only (skip semantic)"
|
|
19781
|
+
echo " --semantic-only Semantic search only (skip grep)"
|
|
19782
|
+
echo " --budget N Token budget for merged results (default 3000)"
|
|
19783
|
+
echo " --top N Max results (default 10)"
|
|
19784
|
+
echo " --json Output JSON"
|
|
19785
|
+
echo ""
|
|
19786
|
+
echo "Falls back to grep-only when ChromaDB is unreachable."
|
|
19787
|
+
return 0
|
|
19788
|
+
;;
|
|
19789
|
+
*)
|
|
19790
|
+
if [[ -z "$query" ]]; then
|
|
19791
|
+
query="$1"
|
|
19792
|
+
else
|
|
19793
|
+
query="$query $1"
|
|
19794
|
+
fi
|
|
19795
|
+
shift
|
|
19796
|
+
;;
|
|
19797
|
+
esac
|
|
19798
|
+
done
|
|
19799
|
+
|
|
19800
|
+
if [[ -z "$query" ]]; then
|
|
19801
|
+
echo -e "${RED}Usage: loki code search \"<query>\" [--grep-only|--semantic-only] [--budget N] [--top N]${NC}"
|
|
19802
|
+
return 1
|
|
19803
|
+
fi
|
|
19804
|
+
|
|
19805
|
+
local script_dir hybrid py
|
|
19806
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
19807
|
+
hybrid="$script_dir/tools/hybrid_search.py"
|
|
19808
|
+
if [[ ! -f "$hybrid" ]]; then
|
|
19809
|
+
echo -e "${RED}hybrid_search.py not found at $hybrid${NC}"
|
|
19810
|
+
return 1
|
|
19811
|
+
fi
|
|
19812
|
+
|
|
19813
|
+
# chromadb requires python3.12 on this stack; fall back to python3 for the
|
|
19814
|
+
# grep-only path if 3.12 is absent.
|
|
19815
|
+
if [[ -x /opt/homebrew/bin/python3.12 ]]; then
|
|
19816
|
+
py=/opt/homebrew/bin/python3.12
|
|
19817
|
+
else
|
|
19818
|
+
py="$(command -v python3 || true)"
|
|
19819
|
+
fi
|
|
19820
|
+
if [[ -z "$py" ]]; then
|
|
19821
|
+
echo -e "${RED}python3 not found${NC}"
|
|
19822
|
+
return 1
|
|
19823
|
+
fi
|
|
19824
|
+
|
|
19825
|
+
# Safe array expansion: "${pyargs[@]}" triggers "unbound variable" under
|
|
19826
|
+
# bash 3.2 (macOS default) + set -u when the array is empty (no flags).
|
|
19827
|
+
"$py" "$hybrid" "$query" ${pyargs[@]+"${pyargs[@]}"}
|
|
19828
|
+
}
|
|
19829
|
+
|
|
19753
19830
|
_code_overview() {
|
|
19754
19831
|
local silent=false
|
|
19755
19832
|
local target_dir="."
|
package/autonomy/run.sh
CHANGED
|
@@ -781,6 +781,21 @@ MAX_PARALLEL_SESSIONS=${LOKI_MAX_PARALLEL_SESSIONS:-3}
|
|
|
781
781
|
PARALLEL_TESTING=${LOKI_PARALLEL_TESTING:-true}
|
|
782
782
|
PARALLEL_DOCS=${LOKI_PARALLEL_DOCS:-true}
|
|
783
783
|
|
|
784
|
+
# Dynamic resource-aware session concurrency (Release 3, slice 3).
|
|
785
|
+
# DEFAULT OFF: when LOKI_DYNAMIC_CONCURRENCY is unset, effective_session_cap()
|
|
786
|
+
# returns exactly MAX_PARALLEL_SESSIONS, so behavior is identical to before.
|
|
787
|
+
# Opt in with LOKI_DYNAMIC_CONCURRENCY=1 to scale the session cap down when
|
|
788
|
+
# system CPU or memory is under pressure (read from .loki/state/resources.json).
|
|
789
|
+
DYNAMIC_CONCURRENCY=${LOKI_DYNAMIC_CONCURRENCY:-0}
|
|
790
|
+
# Optional higher ceiling on capable machines. Only takes effect with dynamic
|
|
791
|
+
# concurrency enabled; still resource-gated. Defaults to MAX_PARALLEL_SESSIONS.
|
|
792
|
+
MAX_PARALLEL_SESSIONS_CEILING=${LOKI_MAX_PARALLEL_SESSIONS_CEILING:-$MAX_PARALLEL_SESSIONS}
|
|
793
|
+
# Usage thresholds (percent). At/above CPU or MEM threshold the cap is halved.
|
|
794
|
+
CONCURRENCY_CPU_THRESHOLD=${LOKI_CONCURRENCY_CPU_THRESHOLD:-85}
|
|
795
|
+
CONCURRENCY_MEM_THRESHOLD=${LOKI_CONCURRENCY_MEM_THRESHOLD:-85}
|
|
796
|
+
# Critical threshold (percent). At/above this the cap is forced to 1.
|
|
797
|
+
CONCURRENCY_CRITICAL_THRESHOLD=${LOKI_CONCURRENCY_CRITICAL_THRESHOLD:-95}
|
|
798
|
+
|
|
784
799
|
# Gate Escalation Ladder (v6.10.0)
|
|
785
800
|
GATE_CLEAR_LIMIT=${LOKI_GATE_CLEAR_LIMIT:-3}
|
|
786
801
|
GATE_ESCALATE_LIMIT=${LOKI_GATE_ESCALATE_LIMIT:-5}
|
|
@@ -2759,6 +2774,72 @@ remove_worktree() {
|
|
|
2759
2774
|
log_info "Removed worktree: $stream_name"
|
|
2760
2775
|
}
|
|
2761
2776
|
|
|
2777
|
+
# Compute the effective parallel-session cap for the current scheduling pass.
|
|
2778
|
+
# Default-off contract: when LOKI_DYNAMIC_CONCURRENCY is not "1" this echoes
|
|
2779
|
+
# exactly MAX_PARALLEL_SESSIONS with zero file reads and zero subprocesses, so
|
|
2780
|
+
# the spawn decision is byte-identical to the pre-feature behavior.
|
|
2781
|
+
# When enabled, it starts from the configured ceiling and scales DOWN based on
|
|
2782
|
+
# .loki/state/resources.json. All reads are best-effort: a missing, empty, or
|
|
2783
|
+
# unparseable file (or non-numeric values) leaves the cap at the ceiling. The
|
|
2784
|
+
# result is always clamped to the range [1, ceiling] and never exceeds it.
|
|
2785
|
+
effective_session_cap() {
|
|
2786
|
+
# Fast default-off path: identical to today, no I/O, no subprocesses.
|
|
2787
|
+
if [ "${DYNAMIC_CONCURRENCY:-0}" != "1" ]; then
|
|
2788
|
+
echo "$MAX_PARALLEL_SESSIONS"
|
|
2789
|
+
return 0
|
|
2790
|
+
fi
|
|
2791
|
+
|
|
2792
|
+
# Ceiling is the upper bound when dynamic scaling is on.
|
|
2793
|
+
local ceiling="${MAX_PARALLEL_SESSIONS_CEILING:-$MAX_PARALLEL_SESSIONS}"
|
|
2794
|
+
# Guard against a non-numeric or sub-1 ceiling override.
|
|
2795
|
+
case "$ceiling" in
|
|
2796
|
+
''|*[!0-9]*) ceiling="$MAX_PARALLEL_SESSIONS" ;;
|
|
2797
|
+
esac
|
|
2798
|
+
[ "$ceiling" -lt 1 ] 2>/dev/null && ceiling=1
|
|
2799
|
+
|
|
2800
|
+
local cap="$ceiling"
|
|
2801
|
+
local resources_file=".loki/state/resources.json"
|
|
2802
|
+
|
|
2803
|
+
# No resource data -> best-effort, leave at ceiling.
|
|
2804
|
+
if [ ! -f "$resources_file" ]; then
|
|
2805
|
+
echo "$cap"
|
|
2806
|
+
return 0
|
|
2807
|
+
fi
|
|
2808
|
+
|
|
2809
|
+
# Read usage and status best-effort. Defaults keep the cap at the ceiling
|
|
2810
|
+
# if the file is empty, malformed, or missing keys.
|
|
2811
|
+
local cpu_usage mem_usage status
|
|
2812
|
+
cpu_usage=$(python3 -c "import json; print(json.load(open('$resources_file')).get('cpu', {}).get('usage_percent', 0))" 2>/dev/null || echo "0")
|
|
2813
|
+
mem_usage=$(python3 -c "import json; print(json.load(open('$resources_file')).get('memory', {}).get('usage_percent', 0))" 2>/dev/null || echo "0")
|
|
2814
|
+
status=$(python3 -c "import json; print(json.load(open('$resources_file')).get('overall_status', 'ok'))" 2>/dev/null || echo "ok")
|
|
2815
|
+
|
|
2816
|
+
# usage_percent can be a float (e.g. 85.3). Reduce to an integer part for
|
|
2817
|
+
# comparison and fall back to 0 if anything is non-numeric.
|
|
2818
|
+
cpu_usage="${cpu_usage%%.*}"
|
|
2819
|
+
mem_usage="${mem_usage%%.*}"
|
|
2820
|
+
case "$cpu_usage" in ''|*[!0-9]*) cpu_usage=0 ;; esac
|
|
2821
|
+
case "$mem_usage" in ''|*[!0-9]*) mem_usage=0 ;; esac
|
|
2822
|
+
|
|
2823
|
+
local crit="${CONCURRENCY_CRITICAL_THRESHOLD:-95}"
|
|
2824
|
+
local cpu_thr="${CONCURRENCY_CPU_THRESHOLD:-85}"
|
|
2825
|
+
local mem_thr="${CONCURRENCY_MEM_THRESHOLD:-85}"
|
|
2826
|
+
|
|
2827
|
+
if [ "$cpu_usage" -ge "$crit" ] || [ "$mem_usage" -ge "$crit" ]; then
|
|
2828
|
+
# Critical pressure: drop to a single session.
|
|
2829
|
+
cap=1
|
|
2830
|
+
elif [ "$cpu_usage" -ge "$cpu_thr" ] || [ "$mem_usage" -ge "$mem_thr" ] || [ "$status" != "ok" ]; then
|
|
2831
|
+
# Elevated pressure or a non-ok overall status: halve (integer floor).
|
|
2832
|
+
cap=$(( ceiling / 2 ))
|
|
2833
|
+
fi
|
|
2834
|
+
|
|
2835
|
+
# Clamp to [1, ceiling]. Never runaway, never zero.
|
|
2836
|
+
[ "$cap" -lt 1 ] && cap=1
|
|
2837
|
+
[ "$cap" -gt "$ceiling" ] && cap="$ceiling"
|
|
2838
|
+
|
|
2839
|
+
echo "$cap"
|
|
2840
|
+
return 0
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2762
2843
|
# Spawn a Claude session in a worktree
|
|
2763
2844
|
spawn_worktree_session() {
|
|
2764
2845
|
local stream_name="$1"
|
|
@@ -2778,9 +2859,11 @@ spawn_worktree_session() {
|
|
|
2778
2859
|
fi
|
|
2779
2860
|
done
|
|
2780
2861
|
|
|
2781
|
-
|
|
2862
|
+
local session_cap
|
|
2863
|
+
session_cap=$(effective_session_cap)
|
|
2864
|
+
if [ "$active_count" -ge "$session_cap" ]; then
|
|
2782
2865
|
# BUG-PAR-014: Max-sessions rejection queues spawn for retry
|
|
2783
|
-
log_warn "Max parallel sessions reached ($
|
|
2866
|
+
log_warn "Max parallel sessions reached ($session_cap). Queuing $stream_name for retry."
|
|
2784
2867
|
mkdir -p "${TARGET_DIR:-.}/.loki/signals"
|
|
2785
2868
|
echo "{\"stream\":\"$stream_name\",\"task\":\"$(echo "$task_prompt" | head -c 200)\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
|
|
2786
2869
|
> "${TARGET_DIR:-.}/.loki/signals/SPAWN_QUEUED_${stream_name}"
|
|
@@ -3212,7 +3295,9 @@ run_parallel_orchestrator() {
|
|
|
3212
3295
|
((active_count++))
|
|
3213
3296
|
fi
|
|
3214
3297
|
done
|
|
3215
|
-
|
|
3298
|
+
local _session_cap
|
|
3299
|
+
_session_cap=$(effective_session_cap)
|
|
3300
|
+
if [ "$active_count" -lt "$_session_cap" ]; then
|
|
3216
3301
|
for queued_signal in "${TARGET_DIR:-.}"/.loki/signals/SPAWN_QUEUED_*; do
|
|
3217
3302
|
[ -f "$queued_signal" ] || continue
|
|
3218
3303
|
local queued_stream
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.23.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var f8=Object.defineProperty;var u8=($)=>$;function c8($,Q){this[$]=u8.bind(null,Q)}var g=($,Q)=>{for(var Z in Q)f8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:c8.bind(Q,Z)})};var k=($,Q)=>()=>($&&(Q=$($=0)),Q);var X1=import.meta.require;var F$={};g(F$,{lokiDir:()=>P,homeLokiDir:()=>o1,findRepoRootForVersion:()=>d1,REPO_ROOT:()=>f});import{resolve as n,dirname as l1}from"path";import{fileURLToPath as p8}from"url";import{existsSync as L1}from"fs";import{homedir as l8}from"os";function d8(){let $=j$;for(let Q=0;Q<6;Q++){if(L1(n($,"VERSION"))&&L1(n($,"autonomy/run.sh")))return $;let Z=l1($);if(Z===$)break;$=Z}return n(j$,"..","..","..")}function d1($){let Q=$;for(let Z=0;Z<6;Z++){if(L1(n(Q,"VERSION"))&&L1(n(Q,"autonomy/run.sh")))return Q;let z=l1(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o1(){return n(l8(),".loki")}var j$,f;var y=k(()=>{j$=l1(p8(import.meta.url));f=d8()});import{readFileSync as o8}from"fs";import{resolve as n8,dirname as a8}from"path";import{fileURLToPath as s8}from"url";function k1(){if($1!==null)return $1;let $="7.
|
|
2
|
+
var f8=Object.defineProperty;var u8=($)=>$;function c8($,Q){this[$]=u8.bind(null,Q)}var g=($,Q)=>{for(var Z in Q)f8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:c8.bind(Q,Z)})};var k=($,Q)=>()=>($&&(Q=$($=0)),Q);var X1=import.meta.require;var F$={};g(F$,{lokiDir:()=>P,homeLokiDir:()=>o1,findRepoRootForVersion:()=>d1,REPO_ROOT:()=>f});import{resolve as n,dirname as l1}from"path";import{fileURLToPath as p8}from"url";import{existsSync as L1}from"fs";import{homedir as l8}from"os";function d8(){let $=j$;for(let Q=0;Q<6;Q++){if(L1(n($,"VERSION"))&&L1(n($,"autonomy/run.sh")))return $;let Z=l1($);if(Z===$)break;$=Z}return n(j$,"..","..","..")}function d1($){let Q=$;for(let Z=0;Z<6;Z++){if(L1(n(Q,"VERSION"))&&L1(n(Q,"autonomy/run.sh")))return Q;let z=l1(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o1(){return n(l8(),".loki")}var j$,f;var y=k(()=>{j$=l1(p8(import.meta.url));f=d8()});import{readFileSync as o8}from"fs";import{resolve as n8,dirname as a8}from"path";import{fileURLToPath as s8}from"url";function k1(){if($1!==null)return $1;let $="7.23.0";if(typeof $==="string"&&$.length>0)return $1=$,$1;try{let Q=a8(s8(import.meta.url)),Z=d1(Q);$1=o8(n8(Z,"VERSION"),"utf-8").trim()}catch{$1="unknown"}return $1}var $1=null;var n1=k(()=>{y()});var E$={};g(E$,{runOrThrow:()=>t8,run:()=>j,commandVersion:()=>i8,commandExists:()=>v,ShellError:()=>a1});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,K;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}K=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[H,X,q]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:H,stderr:X,exitCode:q}}finally{if(z)clearTimeout(z);if(K)clearTimeout(K)}}async function t8($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function v($){let Q=r8($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function r8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function i8($,Q="--version"){if(!await v($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a1;var d=k(()=>{a1=class a1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return e8?"":$}var e8,T,N,_,KZ,A,R,h,J;var c=k(()=>{e8=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),N=a("\x1B[0;32m"),_=a("\x1B[1;33m"),KZ=a("\x1B[0;34m"),A=a("\x1B[0;36m"),R=a("\x1B[1m"),h=a("\x1B[2m"),J=a("\x1B[0m")});import{existsSync as U7}from"fs";async function Q1(){if(B1!==void 0)return B1;let $="/opt/homebrew/bin/python3.12";if(U7($))return B1=$,$;let Q=await v("python3.12");if(Q)return B1=Q,Q;let Z=await v("python3");return B1=Z,Z}async function Z1($,Q={}){let Z=await Q1();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B1;var H1=k(()=>{d()});var d$={};g(d$,{runStatus:()=>N7});import{existsSync as b,readFileSync as q1,readdirSync as v$,statSync as f$}from"fs";import{resolve as D,basename as P7}from"path";import{homedir as L7}from"os";async function j7(){if(await v("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -787,4 +787,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
787
787
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
788
788
|
`),process.stderr.write(v8),2}}g$();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var l3=await p3(Bun.argv.slice(2));process.exit(l3);
|
|
789
789
|
|
|
790
|
-
//# debugId=
|
|
790
|
+
//# debugId=54BA5C5C5E9F714F64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -22,6 +22,7 @@ import logging
|
|
|
22
22
|
import threading
|
|
23
23
|
import uuid
|
|
24
24
|
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
25
26
|
from typing import Optional, List, Dict, Any
|
|
26
27
|
|
|
27
28
|
# Add parent directory to path for imports
|
|
@@ -1517,6 +1518,67 @@ CHROMA_HOST = os.environ.get("LOKI_CHROMA_HOST", "localhost")
|
|
|
1517
1518
|
CHROMA_PORT = int(os.environ.get("LOKI_CHROMA_PORT", "8100"))
|
|
1518
1519
|
CHROMA_COLLECTION = os.environ.get("LOKI_CHROMA_COLLECTION", "loki-codebase")
|
|
1519
1520
|
|
|
1521
|
+
# Code-index freshness manifest (written by tools/index-codebase.py). Resolved
|
|
1522
|
+
# relative to the repo root the same way the indexer resolves it, so the two
|
|
1523
|
+
# agree on a single location. mcp/server.py -> parent.parent == repo root.
|
|
1524
|
+
_CODE_INDEX_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
1525
|
+
CODE_INDEX_MANIFEST_PATH = _CODE_INDEX_REPO_ROOT / ".loki" / "state" / "code-index-manifest.json"
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
def _code_index_staleness() -> dict:
|
|
1529
|
+
"""Compare the code-index manifest mtimes against current files on disk.
|
|
1530
|
+
|
|
1531
|
+
Self-contained (no import of tools/index-codebase.py, which loads chromadb
|
|
1532
|
+
at module top under a possibly-different Python). Mirrors the mtime
|
|
1533
|
+
staleness pattern in memory/retrieval.py. A missing manifest degrades to
|
|
1534
|
+
not-stale so the happy path on a fresh repo is unaffected.
|
|
1535
|
+
|
|
1536
|
+
Returns {"stale": bool, "stale_files": int}.
|
|
1537
|
+
"""
|
|
1538
|
+
try:
|
|
1539
|
+
data = json.loads(CODE_INDEX_MANIFEST_PATH.read_text())
|
|
1540
|
+
files = data.get("files", {})
|
|
1541
|
+
if not isinstance(files, dict) or not files:
|
|
1542
|
+
return {"stale": False, "stale_files": 0}
|
|
1543
|
+
except Exception:
|
|
1544
|
+
return {"stale": False, "stale_files": 0}
|
|
1545
|
+
|
|
1546
|
+
stale = 0
|
|
1547
|
+
for rel, entry in files.items():
|
|
1548
|
+
abs_path = _CODE_INDEX_REPO_ROOT / rel
|
|
1549
|
+
try:
|
|
1550
|
+
if not abs_path.exists():
|
|
1551
|
+
stale += 1
|
|
1552
|
+
elif os.path.getmtime(abs_path) != entry.get("mtime"):
|
|
1553
|
+
stale += 1
|
|
1554
|
+
except OSError:
|
|
1555
|
+
stale += 1
|
|
1556
|
+
return {"stale": stale > 0, "stale_files": stale}
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def _maybe_autoreindex_code() -> None:
|
|
1560
|
+
"""Opt-in incremental re-index when the manifest is stale.
|
|
1561
|
+
|
|
1562
|
+
Gated behind LOKI_CODE_INDEX_AUTOREINDEX=1 because embeddings cost compute.
|
|
1563
|
+
Default behavior is warn-if-stale (the tools just report the staleness
|
|
1564
|
+
fields). Best-effort: never raises into the caller.
|
|
1565
|
+
"""
|
|
1566
|
+
if os.environ.get("LOKI_CODE_INDEX_AUTOREINDEX", "0") != "1":
|
|
1567
|
+
return
|
|
1568
|
+
if not _code_index_staleness().get("stale"):
|
|
1569
|
+
return
|
|
1570
|
+
try:
|
|
1571
|
+
import subprocess
|
|
1572
|
+
indexer = _CODE_INDEX_REPO_ROOT / "tools" / "index-codebase.py"
|
|
1573
|
+
py = "/opt/homebrew/bin/python3.12"
|
|
1574
|
+
if not Path(py).exists():
|
|
1575
|
+
py = sys.executable
|
|
1576
|
+
subprocess.run([py, str(indexer), "--changed"],
|
|
1577
|
+
cwd=str(_CODE_INDEX_REPO_ROOT),
|
|
1578
|
+
capture_output=True, timeout=300)
|
|
1579
|
+
except Exception as e:
|
|
1580
|
+
logger.warning(f"Auto-reindex (LOKI_CODE_INDEX_AUTOREINDEX) failed: {e}")
|
|
1581
|
+
|
|
1520
1582
|
|
|
1521
1583
|
def _get_chroma_collection():
|
|
1522
1584
|
"""Get or create ChromaDB collection (lazy connection).
|
|
@@ -1581,6 +1643,10 @@ async def loki_code_search(
|
|
|
1581
1643
|
'language': language, 'file_filter': file_filter,
|
|
1582
1644
|
'type_filter': type_filter})
|
|
1583
1645
|
|
|
1646
|
+
# Warn-if-stale (default) or opt-in auto-reindex before querying.
|
|
1647
|
+
_maybe_autoreindex_code()
|
|
1648
|
+
_staleness = _code_index_staleness()
|
|
1649
|
+
|
|
1584
1650
|
collection = _get_chroma_collection()
|
|
1585
1651
|
if collection is None:
|
|
1586
1652
|
return json.dumps({
|
|
@@ -1637,7 +1703,13 @@ async def loki_code_search(
|
|
|
1637
1703
|
|
|
1638
1704
|
_emit_tool_event_async('loki_code_search', 'complete',
|
|
1639
1705
|
result_status='success', result_count=len(output))
|
|
1640
|
-
return json.dumps({
|
|
1706
|
+
return json.dumps({
|
|
1707
|
+
"query": query,
|
|
1708
|
+
"results": output,
|
|
1709
|
+
"total": len(output),
|
|
1710
|
+
"stale": _staleness["stale"],
|
|
1711
|
+
"stale_files": _staleness["stale_files"],
|
|
1712
|
+
})
|
|
1641
1713
|
|
|
1642
1714
|
except Exception as e:
|
|
1643
1715
|
logger.error(f"Code search failed: {e}")
|
|
@@ -1659,6 +1731,8 @@ async def loki_code_search_stats() -> str:
|
|
|
1659
1731
|
Shows total chunks, files indexed, breakdown by language and type.
|
|
1660
1732
|
Useful for verifying the index is up to date.
|
|
1661
1733
|
"""
|
|
1734
|
+
_staleness = _code_index_staleness()
|
|
1735
|
+
|
|
1662
1736
|
collection = _get_chroma_collection()
|
|
1663
1737
|
if collection is None:
|
|
1664
1738
|
return json.dumps({"error": "ChromaDB not available"})
|
|
@@ -1674,6 +1748,8 @@ async def loki_code_search_stats() -> str:
|
|
|
1674
1748
|
"by_language": {},
|
|
1675
1749
|
"by_type": {},
|
|
1676
1750
|
"reindex_command": "python3.12 tools/index-codebase.py --reset",
|
|
1751
|
+
"stale": _staleness["stale"],
|
|
1752
|
+
"stale_files": _staleness["stale_files"],
|
|
1677
1753
|
})
|
|
1678
1754
|
|
|
1679
1755
|
results = collection.get(limit=count, include=["metadatas"])
|
|
@@ -1694,6 +1770,8 @@ async def loki_code_search_stats() -> str:
|
|
|
1694
1770
|
"by_language": langs,
|
|
1695
1771
|
"by_type": types,
|
|
1696
1772
|
"reindex_command": "python3.12 tools/index-codebase.py --reset",
|
|
1773
|
+
"stale": _staleness["stale"],
|
|
1774
|
+
"stale_files": _staleness["stale_files"],
|
|
1697
1775
|
})
|
|
1698
1776
|
except Exception as e:
|
|
1699
1777
|
logger.error(f"Code search stats failed: {e}")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.23.0",
|
|
4
4
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -147,6 +147,71 @@ See `skills/documentation.md` for documentation generation using Repowise.
|
|
|
147
147
|
|
|
148
148
|
---
|
|
149
149
|
|
|
150
|
+
## Built-in Hybrid Codebase Search (loki_code_search)
|
|
151
|
+
|
|
152
|
+
Loki Mode ships its own codebase search that does not require any third-party MCP
|
|
153
|
+
server. It is exposed both as MCP tools (`loki_code_search`,
|
|
154
|
+
`loki_code_search_stats`) and as a CLI subcommand (`loki code search`). It combines
|
|
155
|
+
lexical and semantic retrieval over the indexed codebase:
|
|
156
|
+
|
|
157
|
+
- Lexical: ripgrep / grep (with a python scan fallback)
|
|
158
|
+
- Semantic: ChromaDB over the `loki-codebase` collection
|
|
159
|
+
- Fusion: reciprocal rank fusion (RRF) merges the two ranked lists
|
|
160
|
+
- Truncation: results are deduped by file:line and trimmed to a token budget
|
|
161
|
+
(greedy, highest fused score first)
|
|
162
|
+
|
|
163
|
+
When ChromaDB or its docker container is unreachable, search degrades to grep-only
|
|
164
|
+
so it still returns results instead of erroring. The implementation lives in
|
|
165
|
+
`tools/hybrid_search.py`.
|
|
166
|
+
|
|
167
|
+
### Index freshness and staleness reporting
|
|
168
|
+
|
|
169
|
+
The semantic index is backed by a manifest at
|
|
170
|
+
`.loki/state/code-index-manifest.json`. The MCP tools (`loki_code_search` and
|
|
171
|
+
`loki_code_search_stats`) compare the manifest against the files on disk and report
|
|
172
|
+
two fields in their JSON output:
|
|
173
|
+
|
|
174
|
+
- `stale`: boolean, true when one or more indexed files have changed since the last
|
|
175
|
+
index
|
|
176
|
+
- `stale_files`: count of changed files
|
|
177
|
+
|
|
178
|
+
Staleness is computed from the manifest alone (no ChromaDB call), and a missing
|
|
179
|
+
manifest degrades to not-stale so a fresh repo is unaffected.
|
|
180
|
+
|
|
181
|
+
Default behavior is warn-if-stale: the tools report staleness but do not re-index.
|
|
182
|
+
Set `LOKI_CODE_INDEX_AUTOREINDEX=1` to opt into an automatic incremental re-index
|
|
183
|
+
before querying (off by default because embeddings cost compute). The incremental
|
|
184
|
+
re-index is driven by `tools/index-codebase.py --changed`, which re-chunks only
|
|
185
|
+
files whose mtime or sha1 differ from the manifest, upserts the new chunks, deletes
|
|
186
|
+
orphaned chunk IDs for changed files, and drops chunks for files removed from disk.
|
|
187
|
+
|
|
188
|
+
### CLI usage: `loki code search`
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
loki code search "rate limit backoff" # hybrid grep + semantic
|
|
192
|
+
loki code search "council vote" --grep-only # lexical only
|
|
193
|
+
loki code search "memory retrieval" --semantic-only --top 15
|
|
194
|
+
loki code search "build prompt" --budget 4000 # widen the token budget
|
|
195
|
+
loki code search "save state" --json # machine-readable output
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Flags (see `loki code search --help`):
|
|
199
|
+
|
|
200
|
+
| Flag | Default | Effect |
|
|
201
|
+
|------|---------|--------|
|
|
202
|
+
| `--grep-only` | off | lexical search only (skip semantic) |
|
|
203
|
+
| `--semantic-only` | off | semantic search only (skip grep) |
|
|
204
|
+
| `--budget N` | 3000 | token budget for the merged result set |
|
|
205
|
+
| `--top N` | 10 | maximum number of results |
|
|
206
|
+
| `--json` | off | emit JSON instead of formatted output |
|
|
207
|
+
|
|
208
|
+
`loki code search` is one of the `loki code` codebase-intelligence subcommands
|
|
209
|
+
(alongside `overview`, `symbols`, `deps`, `hotspots`, and `diff`). It falls back to
|
|
210
|
+
grep-only when ChromaDB is unreachable, and requires python3.12 for the semantic
|
|
211
|
+
path because that is what the chromadb client needs on this stack.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
150
215
|
## MCP Configuration Location
|
|
151
216
|
|
|
152
217
|
Claude Code reads MCP configuration from:
|
package/skills/00-index.md
CHANGED
|
@@ -107,6 +107,8 @@
|
|
|
107
107
|
- Inter-stream communication via signals
|
|
108
108
|
- Auto-merge completed features
|
|
109
109
|
- Orchestrator state management
|
|
110
|
+
- Supervisor / judge pattern (CONTINUE / COMPLETE / ESCALATE / PIVOT)
|
|
111
|
+
- Dynamic resource-aware session concurrency (LOKI_DYNAMIC_CONCURRENCY)
|
|
110
112
|
|
|
111
113
|
### github-integration.md
|
|
112
114
|
**When:** Working with GitHub issues, creating PRs, syncing status
|
|
@@ -128,6 +128,102 @@ orchestrator_workflow:
|
|
|
128
128
|
|
|
129
129
|
---
|
|
130
130
|
|
|
131
|
+
## Supervisor / Judge Pattern
|
|
132
|
+
|
|
133
|
+
A supervisor (or judge) is a decision step the orchestrator runs at milestones to
|
|
134
|
+
decide whether the parallel run should keep going, wrap up, ask a human, or change
|
|
135
|
+
course. It is a pattern, not a separate daemon: the orchestrator already makes this
|
|
136
|
+
call at completion checkpoints. The judge verbs below are adopted from Cursor's
|
|
137
|
+
multi-agent learnings (see `references/cursor-learnings.md`).
|
|
138
|
+
|
|
139
|
+
### When the supervisor runs
|
|
140
|
+
|
|
141
|
+
- After a major milestone (a stream merges, a phase completes)
|
|
142
|
+
- When workers report completion
|
|
143
|
+
- When progress stalls (diminishing returns across iterations)
|
|
144
|
+
- Under resource pressure, before deciding to spawn more sessions
|
|
145
|
+
|
|
146
|
+
### Inputs and outputs
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
supervisor:
|
|
150
|
+
inputs:
|
|
151
|
+
- Current state # .loki/state/ for each worktree/stream
|
|
152
|
+
- Original goal # the PRD / spec / brief
|
|
153
|
+
- Recent progress # checklist deltas, merged streams, test results
|
|
154
|
+
- Resource consumption # .loki/state/resources.json (CPU, memory, status)
|
|
155
|
+
outputs:
|
|
156
|
+
- CONTINUE # more work needed; keep streams running
|
|
157
|
+
- COMPLETE # goal achieved; move to cleanup
|
|
158
|
+
- ESCALATE # human intervention needed; raise a PAUSE / handoff
|
|
159
|
+
- PIVOT # current approach is not converging; change strategy
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The closest implemented analog is the completion council (`council_should_stop()`
|
|
163
|
+
in `autonomy/completion-council.sh`), which decides COMPLETE vs CONTINUE from
|
|
164
|
+
evidence rather than a single self-report. The supervisor pattern extends that
|
|
165
|
+
mental model to the parallel case: it also reads `.loki/state/resources.json` so a
|
|
166
|
+
machine under load steers toward CONTINUE-with-fewer-sessions (or ESCALATE) instead
|
|
167
|
+
of oversubscribing the host.
|
|
168
|
+
|
|
169
|
+
### Resource state as input
|
|
170
|
+
|
|
171
|
+
The orchestrator persists a best-effort snapshot to
|
|
172
|
+
`.loki/state/resources.json` (CPU usage percent, memory usage percent, and an
|
|
173
|
+
`overall_status`). The dynamic-concurrency logic below reads the same file, so the
|
|
174
|
+
supervisor's "can I spawn more?" decision and the spawn cap stay consistent.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Dynamic Resource-Aware Session Concurrency
|
|
179
|
+
|
|
180
|
+
By default Loki Mode caps parallel Claude sessions at a fixed
|
|
181
|
+
`LOKI_MAX_PARALLEL_SESSIONS` (default 3). Opt-in dynamic concurrency lets that cap
|
|
182
|
+
scale DOWN under load instead of oversubscribing the machine. It only ever reduces
|
|
183
|
+
the cap; it never raises it above the configured ceiling.
|
|
184
|
+
|
|
185
|
+
`effective_session_cap()` (`autonomy/run.sh`) is the single source of truth:
|
|
186
|
+
|
|
187
|
+
- Default off (`LOKI_DYNAMIC_CONCURRENCY` unset or not `1`): returns exactly
|
|
188
|
+
`LOKI_MAX_PARALLEL_SESSIONS` with no file reads and no subprocesses, so the spawn
|
|
189
|
+
decision is byte-identical to the pre-feature behavior.
|
|
190
|
+
- Enabled: starts from `LOKI_MAX_PARALLEL_SESSIONS_CEILING` and reads
|
|
191
|
+
`.loki/state/resources.json`. At/above the CPU or memory threshold (default 85
|
|
192
|
+
percent), or when `overall_status` is not `ok`, it halves the cap. At/above the
|
|
193
|
+
critical threshold (default 95 percent) it forces the cap to 1. The result is
|
|
194
|
+
always clamped to `[1, ceiling]`. A missing, empty, or unparseable resources file
|
|
195
|
+
is treated as no-pressure and leaves the cap at the ceiling.
|
|
196
|
+
|
|
197
|
+
### Env knobs (all read in `autonomy/run.sh`)
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
LOKI_DYNAMIC_CONCURRENCY=1 # opt in; default 0 (off = today's behavior)
|
|
201
|
+
LOKI_MAX_PARALLEL_SESSIONS_CEILING=N # upper bound when dynamic is on;
|
|
202
|
+
# default = LOKI_MAX_PARALLEL_SESSIONS (3)
|
|
203
|
+
LOKI_CONCURRENCY_CPU_THRESHOLD=85 # CPU percent at/above which the cap halves
|
|
204
|
+
LOKI_CONCURRENCY_MEM_THRESHOLD=85 # memory percent at/above which the cap halves
|
|
205
|
+
LOKI_CONCURRENCY_CRITICAL_THRESHOLD=95 # CPU or memory percent at/above which
|
|
206
|
+
# the cap is forced to 1
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Honest ceiling: dozens, not thousands
|
|
210
|
+
|
|
211
|
+
Raising `LOKI_MAX_PARALLEL_SESSIONS_CEILING` is safe because the system
|
|
212
|
+
auto-throttles under pressure, but the realistic target is dozens of adaptive
|
|
213
|
+
concurrent sessions, not hundreds or thousands. Two hard limits cap the useful
|
|
214
|
+
ceiling on a single host:
|
|
215
|
+
|
|
216
|
+
- The 3-reviewer council is a serialization point: every non-trivial change funnels
|
|
217
|
+
through blind review before merge, so review throughput, not spawn count, sets the
|
|
218
|
+
end-to-end pace.
|
|
219
|
+
- Local resources (CPU, memory, API rate limits, disk for worktrees) bound how many
|
|
220
|
+
Claude sessions a single developer machine can usefully run at once.
|
|
221
|
+
|
|
222
|
+
So treat dynamic concurrency as a way to set a higher ceiling safely and let the
|
|
223
|
+
host throttle down, not as a path to a thousand subagents.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
131
227
|
## Claude Session Per Worktree
|
|
132
228
|
|
|
133
229
|
Each worktree runs an independent Claude session:
|
package/skills/quality-gates.md
CHANGED
|
@@ -110,6 +110,24 @@ LOKI_HANDOFF_MD=1 # write a structured handoff doc to
|
|
|
110
110
|
Optional: `LOKI_AUTO_LEARNINGS_EPISODE=1` also writes the learning into
|
|
111
111
|
the Python episodic memory layer via `memory.engine.save_episode`.
|
|
112
112
|
|
|
113
|
+
## Other opt-in environment flags (Release 3)
|
|
114
|
+
|
|
115
|
+
Two more default-off flags added for hybrid search and parallel concurrency.
|
|
116
|
+
Both are no-ops when unset (behavior identical to before).
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
LOKI_DYNAMIC_CONCURRENCY=1 # scale the parallel-session cap DOWN under
|
|
120
|
+
# CPU/memory pressure (default off). Full
|
|
121
|
+
# knobs and defaults: skills/parallel-workflows.md
|
|
122
|
+
# (Dynamic Resource-Aware Session Concurrency)
|
|
123
|
+
|
|
124
|
+
LOKI_CODE_INDEX_AUTOREINDEX=1 # auto incremental re-index of the semantic
|
|
125
|
+
# code index before a search when stale
|
|
126
|
+
# (default off = warn-if-stale). Details:
|
|
127
|
+
# references/mcp-integration.md (Built-in
|
|
128
|
+
# Hybrid Codebase Search)
|
|
129
|
+
```
|
|
130
|
+
|
|
113
131
|
## Verified-completion evidence gate (v7.19.1, default-on)
|
|
114
132
|
|
|
115
133
|
The completion council will not accept a "done" claim without evidence. Before
|