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/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({"query": query, "results": output, "total": len(output)})
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.21.0",
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:
@@ -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:
@@ -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