loki-mode 7.21.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 +433 -7
- 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/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
|