loki-mode 7.5.17 → 7.5.28

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.
Files changed (47) hide show
  1. package/README.md +10 -9
  2. package/SKILL.md +14 -14
  3. package/VERSION +1 -1
  4. package/autonomy/completion-council.sh +26 -3
  5. package/autonomy/lib/claude-flags.sh +132 -0
  6. package/autonomy/lib/mcp-config.sh +160 -0
  7. package/autonomy/lib/project-graph.sh +685 -0
  8. package/autonomy/lib/voter-agents.sh +356 -0
  9. package/autonomy/loki +108 -111
  10. package/autonomy/run.sh +95 -186
  11. package/bin/loki +12 -1
  12. package/dashboard/__init__.py +1 -1
  13. package/dashboard/requirements.txt +13 -8
  14. package/dashboard/server.py +33 -15
  15. package/dashboard/static/index.html +298 -299
  16. package/docs/INSTALLATION.md +54 -21
  17. package/docs/retrospectives/v7.5.15-fleet-postmortem.md +325 -0
  18. package/docs/retrospectives/v7.5.15-honesty-audit.md +136 -0
  19. package/docs/retrospectives/v7.5.15-llm-failure-modes.md +49 -0
  20. package/loki-ts/data/finding-schema.json +74 -0
  21. package/loki-ts/data/model-pricing.json +12 -0
  22. package/loki-ts/dist/loki.js +198 -172
  23. package/mcp/__init__.py +1 -1
  24. package/mcp/lsp_proxy.py +713 -0
  25. package/mcp/requirements.txt +9 -3
  26. package/mcp/tests/__init__.py +0 -0
  27. package/mcp/tests/test_lsp_proxy.py +377 -0
  28. package/memory/app_graph.py +153 -0
  29. package/memory/storage.py +6 -1
  30. package/memory/tests/test_app_graph.py +134 -0
  31. package/package.json +4 -3
  32. package/providers/claude.sh +115 -4
  33. package/providers/codex.sh +2 -2
  34. package/providers/loader.sh +4 -4
  35. package/providers/model_catalog.json +0 -9
  36. package/providers/models.sh +1 -2
  37. package/references/multi-provider.md +26 -35
  38. package/references/prompt-repetition.md +1 -1
  39. package/references/quality-control.md +1 -1
  40. package/skills/00-index.md +3 -3
  41. package/skills/model-selection.md +11 -14
  42. package/skills/providers.md +17 -57
  43. package/skills/quality-gates.md +2 -2
  44. package/skills/troubleshooting.md +1 -1
  45. package/src/integrations/github/action-handler.js +3 -2
  46. package/src/protocols/tools/start-project.js +1 -1
  47. package/providers/gemini.sh +0 -343
package/README.md CHANGED
@@ -27,7 +27,7 @@
27
27
  - **Truly autonomous** -- Describe what you want, walk away, come back to working code with tests
28
28
  - **Production quality built in** -- 11 quality gates (`skills/quality-gates.md`), blind 3-reviewer code review (`run.sh:run_code_review()`), anti-sycophancy checks
29
29
  - **Self-hosted and private** -- Your keys, your infrastructure, no data leaves your network
30
- - **5 AI providers** -- Claude, Codex, Gemini, Cline, Aider with automatic failover (`loki-ts/src/runner/providers.ts`)
30
+ - **4 active AI providers** -- Claude, Codex, Cline, Aider with automatic failover (`loki-ts/src/runner/providers.ts`). Gemini CLI deprecated v7.5.18; Antigravity CLI coming soon.
31
31
  - **Legacy system healing** -- `loki heal` archaeology/stabilize/isolate/modernize/validate phases (v6.67.0, see `skills/healing.md`)
32
32
  - **Memory system** -- Episodic/semantic/procedural with vector search (v5.15.0, see `memory/engine.py`)
33
33
  - **MCP server** -- 15 tools including ChromaDB code search (`mcp/server.py`)
@@ -304,15 +304,16 @@ Loki Mode is the only platform that is fully self-hosted, open source, and inclu
304
304
 
305
305
  ## Multi-Provider Support
306
306
 
307
- | Provider | Autonomous Flag | Parallel Agents | Install |
308
- |----------|:-:|:-:|---------|
309
- | **Claude Code** | `--dangerously-skip-permissions` | Yes (10+) | `npm i -g @anthropic-ai/claude-code` |
310
- | **Codex CLI** | `--full-auto` | Sequential | `npm i -g @openai/codex` |
311
- | **Gemini CLI** | `--approval-mode=yolo` | Sequential | `npm i -g @google/gemini-cli` |
312
- | **Cline CLI** | `--auto-approve` | Sequential | `npm i -g @anthropic-ai/cline` |
313
- | **Aider** | `--yes-always` | Sequential | `pip install aider-chat` |
307
+ | Provider | Status | Autonomous Flag | Parallel Agents | Install |
308
+ |----------|--------|:-:|:-:|---------|
309
+ | **Claude Code** | Active (Tier 1) | `--dangerously-skip-permissions` | Yes (10+) | `npm i -g @anthropic-ai/claude-code` |
310
+ | **Codex CLI** | Active (Tier 3) | `--full-auto` | Sequential | `npm i -g @openai/codex` |
311
+ | **Cline CLI** | Active (Tier 2) | `--auto-approve` | Sequential | `npm i -g @anthropic-ai/cline` |
312
+ | **Aider** | Active (Tier 3) | `--yes-always` | Sequential | `pip install aider-chat` |
313
+ | **Google Gemini CLI** | DEPRECATED v7.5.18 | -- | -- | Upstream deprecated; runtime removed. `LOKI_PROVIDER=gemini` exits with migration message. |
314
+ | **Anthropic Antigravity CLI** | Coming soon | -- | -- | Integration planned. |
314
315
 
315
- Claude gets full features (subagents, parallelization, MCP, Task tool). Other providers run sequentially. Auto-failover switches providers when rate-limited. See [Provider Guide](skills/providers.md).
316
+ Claude gets full features (subagents, parallelization, MCP, Task tool). Other active providers run sequentially. Auto-failover switches providers when rate-limited. See [Provider Guide](skills/providers.md).
316
317
 
317
318
  ---
318
319
 
package/SKILL.md CHANGED
@@ -3,13 +3,13 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.5.17
6
+ # Loki Mode v7.5.28
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
10
10
  **Spec in, product out.** A "spec" is whatever describes the work: a Markdown PRD, a GitHub issue, an OpenAPI doc, a Jira ticket -- a PRD is one form of spec.
11
11
 
12
- **Multi-provider (stable since v5.0.0):** Claude/Codex/Gemini/Cline/Aider with abstract model tiers and degraded mode for non-Claude providers. See `skills/providers.md`. **Current track (v7.5.x):** Phase 1 RARV-C closure -- real provider judges, gate-failure flock, synthetic PRD e2e, status `--json`, dead code cleanup.
12
+ **Multi-provider (stable since v5.0.0):** Claude/Codex/Cline/Aider with abstract model tiers and degraded mode for non-Claude providers. See `skills/providers.md`. **Current track (v7.5.x):** Phase 1 RARV-C closure -- real provider judges, gate-failure flock, synthetic PRD e2e, status `--json`, dead code cleanup.
13
13
 
14
14
  **Runtime migration in progress:** A bash-to-Bun migration is underway on the `feat/bun-migration` branch. The first phase (shipped in v7.3.0) routes a small set of read-only commands -- `version`, `status`, `stats`, `doctor`, `provider show/list`, `memory list/index` -- through a Bun runtime via `bin/loki`. Every other command remains on the Bash runtime (`autonomy/loki`). Rollback is available with `LOKI_LEGACY_BASH=1`. See `UPGRADING.md` and `docs/architecture/ADR-001-runtime-migration.md` for the full plan.
15
15
 
@@ -93,17 +93,17 @@ These rules guide autonomous operation. Test results and code quality always tak
93
93
 
94
94
  **Default since v5.3.0 (reaffirmed in v7.5.13):** Haiku disabled for quality. Use `--allow-haiku` or `LOKI_ALLOW_HAIKU=true` to enable.
95
95
 
96
- | Task Type | Tier | Claude (default) | Claude (--allow-haiku) | Codex (GPT-5.3) | Gemini |
97
- |-----------|------|------------------|------------------------|------------------|--------|
98
- | Spec analysis, architecture, system design | **planning** | opus | opus | effort=xhigh | thinking=high |
99
- | Feature implementation, complex bugs | **development** | opus | sonnet | effort=high | thinking=medium |
100
- | Code review (planned: 3 parallel reviewers) | **development** | opus | sonnet | effort=high | thinking=medium |
101
- | Integration tests, E2E, deployment | **development** | opus | sonnet | effort=high | thinking=medium |
102
- | Unit tests, linting, docs, simple fixes | **fast** | sonnet | haiku | effort=low | thinking=low |
96
+ | Task Type | Tier | Claude (default) | Claude (--allow-haiku) | Codex (GPT-5.3) |
97
+ |-----------|------|------------------|------------------------|------------------|
98
+ | Spec analysis, architecture, system design | **planning** | opus | opus | effort=xhigh |
99
+ | Feature implementation, complex bugs | **development** | opus | sonnet | effort=high |
100
+ | Code review (planned: 3 parallel reviewers) | **development** | opus | sonnet | effort=high |
101
+ | Integration tests, E2E, deployment | **development** | opus | sonnet | effort=high |
102
+ | Unit tests, linting, docs, simple fixes | **fast** | sonnet | haiku | effort=low |
103
103
 
104
104
  **Parallelization rule (Claude only):** Launch up to 10 agents simultaneously for independent tasks.
105
105
 
106
- **Degraded mode (Codex/Gemini/Cline/Aider):** No parallel agents or Task tool. Codex has MCP support. Runs RARV cycle sequentially. See `skills/model-selection.md`.
106
+ **Degraded mode (Codex/Cline/Aider):** No parallel agents or Task tool. Codex has MCP support. Runs RARV cycle sequentially. See `skills/model-selection.md`.
107
107
 
108
108
  **Git worktree parallelism:** For true parallel feature development, use `--parallel` flag with run.sh. See `skills/parallel-workflows.md`.
109
109
 
@@ -210,7 +210,6 @@ loki start --issue 123 # Explicit issue mode (overrides de
210
210
  # With provider selection (supports .md and .json PRDs)
211
211
  loki start --provider claude ./prd.md # Default, full features
212
212
  loki start --provider codex ./prd.json # GPT-5.3 Codex, degraded mode
213
- loki start --provider gemini ./prd.md # Gemini 3 Pro, degraded mode
214
213
  loki start --provider cline ./prd.md # Cline CLI, degraded mode
215
214
  loki start --provider aider ./prd.md # Aider (18+ providers), degraded mode
216
215
 
@@ -225,9 +224,10 @@ loki start 123 --ship # Issue -> PR -> auto-merge
225
224
  **Provider capabilities:**
226
225
  - **Claude**: Opus 4.6, 1M context (beta), 128K output, adaptive thinking, agent teams, full features (Task tool, parallel agents, MCP)
227
226
  - **Codex**: GPT-5.3, 400K context, 128K output, MCP support, --full-auto mode, degraded (sequential only, no Task tool)
228
- - **Gemini**: Degraded mode (sequential only, no Task tool, 1M context)
229
227
  - **Cline**: Multi-provider CLI, degraded mode (sequential only, no Task tool)
230
228
  - **Aider**: 18+ provider backends, degraded mode (sequential only, no Task tool)
229
+ - **Google Gemini CLI**: DEPRECATED starting v7.5.18 (upstream deprecated; runtime removed)
230
+ - **Anthropic Antigravity CLI**: Coming soon
231
231
 
232
232
  ---
233
233
 
@@ -350,7 +350,7 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
350
350
 
351
351
  | Feature | Added | Notes |
352
352
  |---------|-------|-------|
353
- | Multi-provider support (5 providers) | v5.0.0 | claude, codex, gemini, cline, aider -- see `providers/` |
353
+ | Multi-provider support (4 providers) | v5.0.0 | claude, codex, cline, aider -- see `providers/` |
354
354
  | CONTINUITY.md working memory | v5.35.0 | Auto-managed by run.sh, updated each iteration |
355
355
  | Quality gates 3-reviewer system | v5.35.0 | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
356
356
  | Memory System (episodic/semantic/procedural) | v5.15.0 | Full implementation in `memory/` |
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
381
381
 
382
382
  ---
383
383
 
384
- **v7.5.17 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.5.28 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.5.17
1
+ 7.5.28
@@ -1518,9 +1518,32 @@ council_evaluate() {
1518
1518
  # Compute threshold using the same ceiling(2/3) formula as council_vote and council_aggregate_votes
1519
1519
  local _eval_threshold=$(( (COUNCIL_SIZE * 2 + 2) / 3 ))
1520
1520
 
1521
- # Step 1: Aggregate votes from all members
1522
- local aggregate_result
1523
- aggregate_result=$(council_aggregate_votes)
1521
+ # Step 1: Aggregate votes from all members.
1522
+ # Phase C (v7.5.20): try the Claude `--agents <json>` + `--json-schema`
1523
+ # dispatch first. When the locally installed Claude CLI supports both flags
1524
+ # AND the call returns parseable findings, the helper writes both per-voter
1525
+ # verdict files AND the round-N.json shape consumed by the existing
1526
+ # transcript writer / aggregator readers downstream. On any failure
1527
+ # (unsupported flags, missing binary, parse error, etc.) it returns 1 and
1528
+ # we fall through to the existing heuristic council_aggregate_votes path.
1529
+ local aggregate_result=""
1530
+ local _va_helper
1531
+ _va_helper="$(dirname "${BASH_SOURCE[0]}")/lib/voter-agents.sh"
1532
+ if [ -f "$_va_helper" ]; then
1533
+ # shellcheck disable=SC1090
1534
+ . "$_va_helper" 2>/dev/null || true
1535
+ if declare -f loki_council_dispatch_agents >/dev/null 2>&1; then
1536
+ if loki_council_dispatch_agents "$ITERATION_COUNT" "${COUNCIL_PRD_PATH:-}"; then
1537
+ local _va_round_file="$COUNCIL_STATE_DIR/votes/round-${ITERATION_COUNT}.json"
1538
+ if [ -f "$_va_round_file" ]; then
1539
+ aggregate_result=$(_RF="$_va_round_file" python3 -c "import json, os; print(json.load(open(os.environ['_RF'])).get('verdict', 'CONTINUE'))" 2>/dev/null || echo "")
1540
+ fi
1541
+ fi
1542
+ fi
1543
+ fi
1544
+ if [ -z "$aggregate_result" ]; then
1545
+ aggregate_result=$(council_aggregate_votes)
1546
+ fi
1524
1547
 
1525
1548
  if [ "$aggregate_result" = "COMPLETE" ]; then
1526
1549
  # Step 2: Check if unanimous -- compare complete_count to COUNCIL_SIZE
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # autonomy/lib/claude-flags.sh -- Phase B (v7.5.19) helpers.
3
+ #
4
+ # Compute Claude Code CLI flag values automatically from existing Loki state.
5
+ # No new env vars introduced (per binding constraint: nothing user-facing changes).
6
+ # Every value derives from RARV tier, complexity score, and budget config.
7
+ #
8
+ # Public API (all functions are pure: they read env + filesystem, write stdout).
9
+ # loki_effort_for_tier <tier> [complexity] -- emit one of: low|medium|high|xhigh|max
10
+ # loki_remaining_budget -- emit dollar amount remaining as plain int/float, OR empty when unlimited
11
+ # loki_fallback_for_primary <model> -- emit fallback model alias (opus->sonnet, sonnet->haiku if allowed), OR empty when no fallback applies
12
+ # loki_claude_flag_supported <flag> -- 0 if `claude --help` lists the flag, 1 otherwise
13
+ #
14
+ # All functions are side-effect free. The caller (providers/claude.sh, loki-ts) decides whether to pass the result.
15
+
16
+ # Guard against double-source.
17
+ if [ "${__LOKI_CLAUDE_FLAGS_SH_LOADED:-0}" = "1" ]; then
18
+ return 0 2>/dev/null || true
19
+ fi
20
+ __LOKI_CLAUDE_FLAGS_SH_LOADED=1
21
+
22
+ # ---------- Effort tier mapping ----------
23
+ # RARV tier names: planning (xhigh), development (high), fast (medium).
24
+ # Complexity shift: if the project is "complex", bump each tier one notch up.
25
+ # Honors LOKI_ALLOW_HAIKU=true semantics for the "fast" tier (medium stays medium; haiku is a separate model concept).
26
+ loki_effort_for_tier() {
27
+ local tier="${1:-development}"
28
+ local complexity="${2:-${LOKI_COMPLEXITY:-standard}}"
29
+ local effort=""
30
+ case "$tier" in
31
+ planning) effort="xhigh" ;;
32
+ development) effort="high" ;;
33
+ fast) effort="medium" ;;
34
+ # Capability aliases used elsewhere in the codebase.
35
+ best) effort="xhigh" ;;
36
+ balanced) effort="high" ;;
37
+ cheap) effort="low" ;;
38
+ *) effort="high" ;;
39
+ esac
40
+ # Complex projects get one bump up. "max" is the ceiling and only for explicit user override (not auto-bumped to from xhigh).
41
+ if [ "$complexity" = "complex" ]; then
42
+ case "$effort" in
43
+ low) effort="medium" ;;
44
+ medium) effort="high" ;;
45
+ high) effort="xhigh" ;;
46
+ # xhigh stays xhigh; max not auto-reached.
47
+ esac
48
+ fi
49
+ printf '%s' "$effort"
50
+ }
51
+
52
+ # ---------- Remaining budget ----------
53
+ # Compute remaining budget = LOKI_BUDGET_LIMIT - cumulative spend so far.
54
+ # Spend lives in .loki/metrics/budget.json under "current_spend" (per autonomy/run.sh:8167+).
55
+ # Emit empty string when budget is unlimited (LOKI_BUDGET_LIMIT unset or 0).
56
+ # Emit empty when remaining <= 0 (caller decides what to do; we never emit 0).
57
+ loki_remaining_budget() {
58
+ local limit="${LOKI_BUDGET_LIMIT:-}"
59
+ # Empty or zero limit means no cap; emit nothing.
60
+ if [ -z "$limit" ] || [ "$limit" = "0" ] || [ "$limit" = "0.0" ] || [ "$limit" = "0.00" ]; then
61
+ return 0
62
+ fi
63
+ local budget_file="${TARGET_DIR:-.}/.loki/metrics/budget.json"
64
+ local spend="0"
65
+ if [ -f "$budget_file" ]; then
66
+ spend=$(python3 -c "
67
+ import json, sys
68
+ try:
69
+ with open('$budget_file') as f:
70
+ d = json.load(f)
71
+ v = d.get('current_spend', 0)
72
+ print(float(v))
73
+ except Exception:
74
+ print(0)
75
+ " 2>/dev/null)
76
+ fi
77
+ # Compute remaining via python3 (bash floats are unreliable across awk/bc variations).
78
+ python3 -c "
79
+ import sys
80
+ try:
81
+ limit = float('$limit')
82
+ spend = float('$spend')
83
+ rem = limit - spend
84
+ # Strictly positive; otherwise emit nothing (caller decides whether to bail or warn).
85
+ if rem > 0:
86
+ # Round to 2 decimal places for the CLI.
87
+ print(f'{rem:.2f}')
88
+ except Exception:
89
+ pass
90
+ " 2>/dev/null
91
+ }
92
+
93
+ # ---------- Fallback model ----------
94
+ # Map primary model alias to a sensible fallback for `--fallback-model`.
95
+ # opus -> sonnet (always safe; sonnet is cheaper + similar capability)
96
+ # sonnet -> haiku ONLY when LOKI_ALLOW_HAIKU=true; else emit nothing
97
+ # haiku -> nothing (already at the bottom)
98
+ # Anything else (specific dated IDs, alt-provider names) -> nothing (no auto-fallback we can reason about)
99
+ loki_fallback_for_primary() {
100
+ local primary="${1:-}"
101
+ case "$primary" in
102
+ opus) printf '%s' "sonnet" ;;
103
+ sonnet)
104
+ if [ "${LOKI_ALLOW_HAIKU:-false}" = "true" ]; then
105
+ printf '%s' "haiku"
106
+ fi
107
+ ;;
108
+ # No fallback for haiku or other model names.
109
+ *) ;;
110
+ esac
111
+ }
112
+
113
+ # ---------- Flag-support detection ----------
114
+ # Returns 0 if `claude --help` lists the named flag, 1 otherwise.
115
+ # Cached per-process so we do not shell out N times per iteration.
116
+ loki_claude_flag_supported() {
117
+ local flag="${1:-}"
118
+ [ -z "$flag" ] && return 1
119
+ # Per-process cache: __LOKI_CLAUDE_HELP_CACHE populated on first call.
120
+ if [ -z "${__LOKI_CLAUDE_HELP_CACHE:-}" ]; then
121
+ if command -v claude >/dev/null 2>&1; then
122
+ __LOKI_CLAUDE_HELP_CACHE=$(claude --help 2>&1 || true)
123
+ else
124
+ __LOKI_CLAUDE_HELP_CACHE="__no_claude__"
125
+ fi
126
+ export __LOKI_CLAUDE_HELP_CACHE
127
+ fi
128
+ case "$__LOKI_CLAUDE_HELP_CACHE" in
129
+ *"$flag"*) return 0 ;;
130
+ *) return 1 ;;
131
+ esac
132
+ }
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env bash
2
+ # autonomy/lib/mcp-config.sh -- Phase D (v7.5.22) helpers.
3
+ #
4
+ # Compute MCP-config paths for the Claude Code CLI `--mcp-config` flag.
5
+ # The flag is variadic per Commander (`--mcp-config <configs...>`), which
6
+ # means Claude expects each path as a SEPARATE argv element following the
7
+ # flag. The helper here emits a space-separated string for ergonomics; the
8
+ # caller in `providers/claude.sh::_loki_build_claude_auto_flags` splits on
9
+ # whitespace and pushes each path as its own array entry, so the variadic
10
+ # shape is honored on the wire. Paths must not contain whitespace (both the
11
+ # Loki bundle path and the `$HOME`-rooted overlay path are whitespace-safe
12
+ # under normal use; the helper does not quote inside the joined string).
13
+ #
14
+ # Public API (all functions read env + filesystem, write stdout):
15
+ # loki_mcp_config_path -- emit absolute path to .loki/mcp-config.json.
16
+ # Re-emits (overwrites) the bundle each call.
17
+ # Returns 0 on success, 1 only if the dir
18
+ # cannot be created or the file cannot be
19
+ # written.
20
+ # loki_user_mcp_config_path -- emit absolute path to ~/.claude/mcp.json
21
+ # if present + readable, else empty. Always
22
+ # returns 0.
23
+ # loki_mcp_config_argv -- emit one space-separated string of paths
24
+ # (Loki bundle first, optional user overlay
25
+ # second). Caller MUST split on whitespace
26
+ # and pass each path as a separate argv
27
+ # element to satisfy Claude's variadic flag.
28
+ # Returns 0 on success, 1 if the bundle
29
+ # emission fails.
30
+ #
31
+ # No side effects beyond writing .loki/mcp-config.json (idempotent;
32
+ # regenerated each call).
33
+
34
+ # Guard against double-source.
35
+ if [ "${__LOKI_MCP_CONFIG_SH_LOADED:-0}" = "1" ]; then
36
+ return 0 2>/dev/null || true
37
+ fi
38
+ __LOKI_MCP_CONFIG_SH_LOADED=1
39
+
40
+ # ---------- Loki MCP bundle path ----------
41
+ # Emits the absolute path to .loki/mcp-config.json (TARGET_DIR-relative).
42
+ # Bundle includes the always-on `loki-mode` server. Phase G (v7.5.24):
43
+ # when any supported LSP binary (typescript-language-server, pylsp, gopls,
44
+ # rust-analyzer) is on PATH, the bundle also gets a single `lsp-proxy`
45
+ # entry that fans out to per-language servers at runtime. Detection uses
46
+ # `command -v` only; absence is a normal state, not an error.
47
+ #
48
+ # Idempotent-write: the new bundle bytes are compared against the existing
49
+ # file (`cmp -s`); the file is only rewritten when content differs. This
50
+ # preserves mtime across calls when nothing changed, which matters for the
51
+ # Phase G LSP-detection regression test (no LSP -> no bundle churn).
52
+ #
53
+ # The bundle mirrors the repo's .mcp.json `loki-mode` entry: a single
54
+ # stdio MCP server backed by `python3 -m mcp.server`. Caller may extend
55
+ # this in the future without API breakage; consumers should treat the
56
+ # bundle as opaque JSON.
57
+ loki_mcp_config_path() {
58
+ local base="${TARGET_DIR:-.}"
59
+ local mcp_dir="${base}/.loki"
60
+ local mcp_path="${mcp_dir}/mcp-config.json"
61
+
62
+ # Resolve to absolute path early so callers always get a stable value
63
+ # even if cwd changes later in the iteration.
64
+ if ! mkdir -p "$mcp_dir" 2>/dev/null; then
65
+ return 1
66
+ fi
67
+
68
+ # Phase G: detect supported LSP binaries on PATH. The lsp-proxy server
69
+ # routes by language at runtime, so we register it once when ANY of the
70
+ # supported binaries is present (multiple binaries -> still one entry).
71
+ local lsp_detected=0
72
+ local lsp_bin
73
+ for lsp_bin in typescript-language-server pylsp gopls rust-analyzer; do
74
+ if command -v "$lsp_bin" >/dev/null 2>&1; then
75
+ lsp_detected=1
76
+ break
77
+ fi
78
+ done
79
+
80
+ # Build the bundle to a temp file, then compare bytes before rewriting.
81
+ # This makes the helper idempotent: identical bundle bytes -> no write,
82
+ # mtime stable. python3 is used so JSON encoding is canonical.
83
+ local tmp_bundle
84
+ tmp_bundle="${mcp_path}.tmp.$$"
85
+ if ! _MCP_OUT="$tmp_bundle" _MCP_LSP="$lsp_detected" python3 -c "
86
+ import json, os
87
+ out = os.environ['_MCP_OUT']
88
+ lsp = os.environ.get('_MCP_LSP', '0') == '1'
89
+ servers = {
90
+ 'loki-mode': {
91
+ 'command': 'python3',
92
+ 'args': ['-m', 'mcp.server'],
93
+ },
94
+ }
95
+ if lsp:
96
+ servers['lsp-proxy'] = {
97
+ 'command': 'python3',
98
+ 'args': ['-m', 'mcp.lsp_proxy'],
99
+ }
100
+ bundle = {'mcpServers': servers}
101
+ with open(out, 'w') as f:
102
+ json.dump(bundle, f, indent=2)
103
+ " 2>/dev/null; then
104
+ rm -f "$tmp_bundle" 2>/dev/null
105
+ return 1
106
+ fi
107
+
108
+ # Idempotent write: only replace the file when bytes differ. cmp -s
109
+ # exits 0 when identical, so we keep the existing file in that case.
110
+ if [ -f "$mcp_path" ] && cmp -s "$tmp_bundle" "$mcp_path" 2>/dev/null; then
111
+ rm -f "$tmp_bundle" 2>/dev/null
112
+ else
113
+ mv -f "$tmp_bundle" "$mcp_path" 2>/dev/null || {
114
+ rm -f "$tmp_bundle" 2>/dev/null
115
+ return 1
116
+ }
117
+ fi
118
+
119
+ # Emit absolute path -- python3 handles realpath portably.
120
+ _MCP_OUT="$mcp_path" python3 -c "
121
+ import os, sys
122
+ print(os.path.abspath(os.environ['_MCP_OUT']))
123
+ " 2>/dev/null
124
+ return 0
125
+ }
126
+
127
+ # ---------- User overlay path ----------
128
+ # Echoes ~/.claude/mcp.json if it exists and is readable, else empty.
129
+ # Always returns 0 -- a missing overlay is a normal state, not an error.
130
+ loki_user_mcp_config_path() {
131
+ local user_path="${HOME}/.claude/mcp.json"
132
+ if [ -f "$user_path" ] && [ -r "$user_path" ]; then
133
+ printf '%s' "$user_path"
134
+ fi
135
+ return 0
136
+ }
137
+
138
+ # ---------- Combined --mcp-config argv value ----------
139
+ # Emits a single space-separated string of paths. The caller in
140
+ # `providers/claude.sh` splits on whitespace and pushes each path as its own
141
+ # argv element so Claude's variadic `--mcp-config <configs...>` receives
142
+ # separate argv entries (not one joined value). Loki bundle first, then
143
+ # user overlay if present. Paths must not contain whitespace.
144
+ #
145
+ # Returns 1 if the Loki bundle cannot be emitted (the caller should then
146
+ # skip the flag entirely rather than pass a malformed value).
147
+ loki_mcp_config_argv() {
148
+ local loki_path user_path
149
+ loki_path=$(loki_mcp_config_path) || return 1
150
+ if [ -z "$loki_path" ]; then
151
+ return 1
152
+ fi
153
+ user_path=$(loki_user_mcp_config_path)
154
+ if [ -n "$user_path" ]; then
155
+ printf '%s %s' "$loki_path" "$user_path"
156
+ else
157
+ printf '%s' "$loki_path"
158
+ fi
159
+ return 0
160
+ }