loki-mode 7.29.0 → 7.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/context-tracker.py +8 -0
- package/autonomy/loki +525 -122
- package/autonomy/mcp-launch.sh +384 -0
- package/autonomy/run.sh +148 -1
- package/bin/loki +19 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +226 -1
- package/dashboard/static/index.html +105 -39
- package/docs/INSTALLATION.md +1 -1
- package/docs/competitive/emergence-others-analysis.md +1 -1
- package/docs/competitive/replit-lovable-analysis.md +2 -2
- package/loki-ts/data/model-pricing.json +1 -0
- package/loki-ts/dist/loki.js +233 -232
- package/mcp/__init__.py +1 -1
- package/mcp/_sdk_loader.py +157 -0
- package/mcp/lsp_proxy.py +61 -61
- package/mcp/server.py +74 -38
- package/package.json +1 -1
- package/providers/claude.sh +76 -19
- package/providers/model_catalog.json +9 -0
- package/skills/model-selection.md +44 -0
- package/templates/simple-todo-app.md +3 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# mcp-launch.sh -- launch the Loki Mode MCP server, bootstrapping its Python
|
|
3
|
+
# dependencies on first run (task 562).
|
|
4
|
+
#
|
|
5
|
+
# Why this exists: a fresh `npm install -g loki-mode` ships mcp/server.py and
|
|
6
|
+
# mcp/requirements.txt but installs NO Python packages and exposes only the
|
|
7
|
+
# `loki` bin. So `python3 -m mcp.server` exits because the MCP SDK is absent.
|
|
8
|
+
# `loki mcp` closes that gap: it checks for python3 + the MCP SDK, and when the
|
|
9
|
+
# SDK is missing it offers a consent-gated bootstrap into a project-local
|
|
10
|
+
# virtualenv (.loki/mcp-venv), then execs the server over stdio using THAT
|
|
11
|
+
# venv's python so the SDK is actually importable.
|
|
12
|
+
#
|
|
13
|
+
# Design (least-invasive, honest):
|
|
14
|
+
# * Venv location: <user-cwd>/.loki/mcp-venv (the project Loki is invoked in,
|
|
15
|
+
# NOT the install root). Project-local, no global site-packages pollution,
|
|
16
|
+
# no sudo, no curl-pipe-bash, no root-owned writes under a global install.
|
|
17
|
+
# Removing the project's .loki fully uninstalls. Override with
|
|
18
|
+
# LOKI_MCP_VENV=/abs/path. Honors LOKI_DIR (defaults to .loki).
|
|
19
|
+
# * The server is launched with PYTHONPATH set to the install root (NOT by
|
|
20
|
+
# cd-ing into it) so the user's cwd is preserved: mcp/server.py resolves the
|
|
21
|
+
# project .loki from os.getcwd(). Without PYTHONPATH, `import mcp` from an
|
|
22
|
+
# arbitrary cwd resolves to the pip MCP SDK's own `mcp` package (zero Loki
|
|
23
|
+
# tools); PYTHONPATH puts the install root first so the LOCAL mcp/server.py
|
|
24
|
+
# wins, while server.py's namespace juggle still hands the real SDK its own
|
|
25
|
+
# `mcp.*` subtree.
|
|
26
|
+
# * The ONLY command run on the user's behalf is, after explicit consent:
|
|
27
|
+
# <venv>/bin/pip install -r mcp/requirements.txt
|
|
28
|
+
# The exact command is printed before it runs.
|
|
29
|
+
# * Non-interactive / CI: NEVER install by default. Print the manual command
|
|
30
|
+
# to stderr and exit 2 (mirrors autonomy/provider-offer.sh gate semantics).
|
|
31
|
+
# EXCEPTION: LOKI_MCP_AUTO_BOOTSTRAP (1/true/yes/on, case-insensitive) is
|
|
32
|
+
# explicit-env written consent. MCP
|
|
33
|
+
# clients (Claude Desktop etc.) spawn the server non-interactively over piped
|
|
34
|
+
# stdio; a user who writes LOKI_MCP_AUTO_BOOTSTRAP=1 into their client config
|
|
35
|
+
# has consented in advance. On that flag, a missing-SDK non-TTY launch
|
|
36
|
+
# bootstraps the venv exactly like the interactive consent path, but with ALL
|
|
37
|
+
# progress on STDERR (stdout stays clean for JSON-RPC: the client is already
|
|
38
|
+
# attached to it), then execs the server. LOKI_NO_INSTALL_OFFER=1 still wins
|
|
39
|
+
# (explicit no beats explicit yes).
|
|
40
|
+
# * Opt-out: LOKI_NO_INSTALL_OFFER=1 -> never prompt, print manual command,
|
|
41
|
+
# exit 2. --yes / LOKI_ASSUME_YES / LOKI_AUTO_CONFIRM=true -> auto-accept.
|
|
42
|
+
# LOKI_MCP_AUTO_BOOTSTRAP=1 -> non-interactive written consent (see above).
|
|
43
|
+
#
|
|
44
|
+
# Self-containment: depends only on bash builtins + python3 on PATH. Defines
|
|
45
|
+
# its own colors so it behaves identically whether sourced by autonomy/loki or
|
|
46
|
+
# run standalone.
|
|
47
|
+
|
|
48
|
+
# Guard against double-source.
|
|
49
|
+
if [ -n "${_LOKI_MCP_LAUNCH_SOURCED:-}" ]; then
|
|
50
|
+
return 0 2>/dev/null || true
|
|
51
|
+
fi
|
|
52
|
+
_LOKI_MCP_LAUNCH_SOURCED=1
|
|
53
|
+
|
|
54
|
+
# --- Self-contained colors (honor NO_COLOR) --------------------------------
|
|
55
|
+
if [ -n "${NO_COLOR:-}" ] || [ ! -t 1 ]; then
|
|
56
|
+
_ML_RED=''; _ML_YELLOW=''; _ML_BOLD=''; _ML_NC=''
|
|
57
|
+
else
|
|
58
|
+
_ML_RED=$'\033[0;31m'
|
|
59
|
+
_ML_YELLOW=$'\033[1;33m'
|
|
60
|
+
_ML_BOLD=$'\033[1m'
|
|
61
|
+
_ML_NC=$'\033[0m'
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Repo root = parent of the directory holding this script (autonomy/..).
|
|
65
|
+
_ml_repo_root() {
|
|
66
|
+
local self_dir
|
|
67
|
+
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
68
|
+
(cd "$self_dir/.." && pwd)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# _ml_truthy <value>: true (0) when the value is a conventional affirmative
|
|
72
|
+
# spelling (1/true/yes/on/y), case-insensitive. Centralizes consent parsing so
|
|
73
|
+
# every knob accepts the same spellings rather than each hard-coding "1".
|
|
74
|
+
_ml_truthy() {
|
|
75
|
+
case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in
|
|
76
|
+
1|true|yes|on) return 0 ;;
|
|
77
|
+
*) return 1 ;;
|
|
78
|
+
esac
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# _ml_assume_yes: true when the user opted into unattended confirmation.
|
|
82
|
+
_ml_assume_yes() {
|
|
83
|
+
_ml_truthy "${LOKI_ASSUME_YES:-}" && return 0
|
|
84
|
+
_ml_truthy "${LOKI_AUTO_CONFIRM:-}" && return 0
|
|
85
|
+
return 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# _ml_non_interactive: true when we must NEVER prompt (non-TTY or CI).
|
|
89
|
+
_ml_non_interactive() {
|
|
90
|
+
[ ! -t 0 ] && return 0
|
|
91
|
+
[ ! -t 1 ] && return 0
|
|
92
|
+
[ -n "${CI:-}" ] && return 0
|
|
93
|
+
return 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# _ml_python: echo the best base python3 for creating the venv, or empty.
|
|
97
|
+
_ml_python() {
|
|
98
|
+
local p
|
|
99
|
+
for p in python3 python3.12 python3.11; do
|
|
100
|
+
if command -v "$p" >/dev/null 2>&1; then
|
|
101
|
+
printf '%s' "$p"
|
|
102
|
+
return 0
|
|
103
|
+
fi
|
|
104
|
+
done
|
|
105
|
+
return 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# _ml_sdk_importable <python> <root>: true (0) only if the real MCP SDK's
|
|
109
|
+
# FastMCP can actually be CONSTRUCTED -- not merely that the SDK files exist on
|
|
110
|
+
# disk. A file-exists check is a false positive: under the local-vs-SDK `mcp`
|
|
111
|
+
# namespace collision the package-dir FastMCP can be present yet fail to import
|
|
112
|
+
# (`No module named 'mcp.types'`). We delegate to mcp/server.py's own
|
|
113
|
+
# `--check-sdk` probe, which runs the exact loader the server uses and exits 0
|
|
114
|
+
# only when FastMCP loaded.
|
|
115
|
+
#
|
|
116
|
+
# Critical: we set PYTHONPATH to the install root and DO NOT cd into it, so the
|
|
117
|
+
# probe exercises the SAME module resolution as the real launch (which preserves
|
|
118
|
+
# the user's cwd). The redirect of stdin from /dev/null is insurance: if the
|
|
119
|
+
# pip SDK's own `mcp.server` were ever reached, its stub starts a stdio receive
|
|
120
|
+
# loop; the EOF makes it exit instead of hanging.
|
|
121
|
+
_ml_sdk_importable() {
|
|
122
|
+
local py="$1" root="$2"
|
|
123
|
+
PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" \
|
|
124
|
+
"$py" -m mcp.server --check-sdk </dev/null >/dev/null 2>&1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# _ml_print_manual <root> <venv>: print the honest manual install commands.
|
|
128
|
+
# The venv lives in the user's project (.loki/mcp-venv by default), while
|
|
129
|
+
# requirements.txt is shipped under the install root.
|
|
130
|
+
_ml_print_manual() {
|
|
131
|
+
local root="$1" venv="$2"
|
|
132
|
+
# Display-only quoting: single-quote the substituted paths so the printed
|
|
133
|
+
# commands copy-paste correctly even when the project root or venv path
|
|
134
|
+
# contains spaces. This is presentation only; nothing is executed here.
|
|
135
|
+
printf 'Install the MCP server dependencies manually:\n' >&2
|
|
136
|
+
printf " python3 -m venv '%s'\n" "$venv" >&2
|
|
137
|
+
printf " '%s/bin/pip' install -r '%s/mcp/requirements.txt'\n" "$venv" "$root" >&2
|
|
138
|
+
printf " PYTHONPATH='%s' '%s/bin/python' -m mcp.server\n" "$root" "$venv" >&2
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_ml_help() {
|
|
142
|
+
cat <<'EOF'
|
|
143
|
+
Loki Mode -- launch the MCP (Model Context Protocol) server
|
|
144
|
+
|
|
145
|
+
Usage: loki mcp [--transport stdio|http] [--port N] [--help]
|
|
146
|
+
|
|
147
|
+
Starts the Loki Mode MCP server so MCP-aware clients (Claude Code, IDEs)
|
|
148
|
+
can call Loki's tools (memory, task queue, code search, build management).
|
|
149
|
+
|
|
150
|
+
On first run, if the Python MCP SDK is not installed, Loki offers to create
|
|
151
|
+
a project-local virtualenv at .loki/mcp-venv and install mcp/requirements.txt
|
|
152
|
+
into it (with your consent). The exact pip command is printed before it runs.
|
|
153
|
+
It then launches the server using that venv's python.
|
|
154
|
+
|
|
155
|
+
Options:
|
|
156
|
+
--transport stdio|http Transport to use (default: stdio).
|
|
157
|
+
--port N Port for http transport (default: 8421).
|
|
158
|
+
--help, -h Show this help and exit.
|
|
159
|
+
|
|
160
|
+
Environment:
|
|
161
|
+
LOKI_MCP_VENV=/abs/path Use a custom venv location instead of .loki/mcp-venv.
|
|
162
|
+
LOKI_NO_INSTALL_OFFER=1 Never prompt to install; print the manual command.
|
|
163
|
+
Wins over LOKI_MCP_AUTO_BOOTSTRAP (explicit no beats
|
|
164
|
+
explicit yes).
|
|
165
|
+
LOKI_MCP_AUTO_BOOTSTRAP=1 Written consent for non-interactive bootstrap. MCP
|
|
166
|
+
clients (Claude Desktop etc.) spawn the server over
|
|
167
|
+
piped stdio with no TTY; set this in your client
|
|
168
|
+
config to authorize the one-time venv bootstrap when
|
|
169
|
+
the SDK is missing. Progress goes to stderr only so
|
|
170
|
+
stdout stays clean for JSON-RPC. On a TTY it also
|
|
171
|
+
skips the consent prompt (consent already given).
|
|
172
|
+
Accepts 1/true/yes/on (case-insensitive).
|
|
173
|
+
--yes / LOKI_ASSUME_YES=1 Auto-accept the dependency install. --yes is a
|
|
174
|
+
launcher flag (equivalent to LOKI_ASSUME_YES=1); it
|
|
175
|
+
is consumed here and never forwarded to the server.
|
|
176
|
+
LOKI_ASSUME_YES / LOKI_AUTO_CONFIRM accept
|
|
177
|
+
1/true/yes/on (case-insensitive).
|
|
178
|
+
|
|
179
|
+
Argument handling: launcher flags (--help, --yes) are consumed here; every other
|
|
180
|
+
argument is forwarded verbatim to the server, which accepts --transport/--port.
|
|
181
|
+
A bare `--` ends launcher parsing so anything after it reaches the server as-is.
|
|
182
|
+
|
|
183
|
+
Behavior in non-interactive / CI shells: never installs UNLESS
|
|
184
|
+
LOKI_MCP_AUTO_BOOTSTRAP is set (1/true/yes/on). Without it, prints the manual
|
|
185
|
+
install command to stderr and exits 2.
|
|
186
|
+
EOF
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# mcp_launch_main: dispatcher invoked by cmd_mcp() (autonomy/loki) or directly.
|
|
190
|
+
mcp_launch_main() {
|
|
191
|
+
# Split argv into launcher-owned flags (consumed here) and server argv
|
|
192
|
+
# (forwarded verbatim to `python -m mcp.server`). The server's argparse only
|
|
193
|
+
# accepts --transport/--port/--check-sdk; forwarding a launcher flag like
|
|
194
|
+
# --yes would make it abort with exit 2, so launcher flags MUST be stripped.
|
|
195
|
+
# A bare `--` ends launcher parsing: everything after it is forwarded as-is
|
|
196
|
+
# (escape hatch for any future server flag that collides with a launcher one).
|
|
197
|
+
local arg
|
|
198
|
+
local _ml_server_argv=()
|
|
199
|
+
local _ml_after_sep=0
|
|
200
|
+
for arg in "$@"; do
|
|
201
|
+
if [ "$_ml_after_sep" -eq 1 ]; then
|
|
202
|
+
_ml_server_argv+=("$arg")
|
|
203
|
+
continue
|
|
204
|
+
fi
|
|
205
|
+
case "$arg" in
|
|
206
|
+
--)
|
|
207
|
+
_ml_after_sep=1
|
|
208
|
+
;;
|
|
209
|
+
--help|-h|help)
|
|
210
|
+
_ml_help
|
|
211
|
+
return 0
|
|
212
|
+
;;
|
|
213
|
+
--yes)
|
|
214
|
+
# Launcher-owned: equivalent to LOKI_ASSUME_YES=1. Consumed here,
|
|
215
|
+
# never forwarded to the server.
|
|
216
|
+
LOKI_ASSUME_YES=1
|
|
217
|
+
;;
|
|
218
|
+
*)
|
|
219
|
+
_ml_server_argv+=("$arg")
|
|
220
|
+
;;
|
|
221
|
+
esac
|
|
222
|
+
done
|
|
223
|
+
# Replace the positional parameters with the filtered server argv so every
|
|
224
|
+
# downstream `exec ... "$@"` forwards only server-valid arguments. Safe
|
|
225
|
+
# empty-array expansion (bash 3.2 + set -u when no server args remain).
|
|
226
|
+
set -- ${_ml_server_argv[@]+"${_ml_server_argv[@]}"}
|
|
227
|
+
|
|
228
|
+
local root
|
|
229
|
+
root="$(_ml_repo_root)"
|
|
230
|
+
|
|
231
|
+
# 1. python3 presence.
|
|
232
|
+
local base_py
|
|
233
|
+
if ! base_py="$(_ml_python)"; then
|
|
234
|
+
printf '%sNo python3 found on PATH.%s The MCP server needs Python 3.\n' "$_ML_RED" "$_ML_NC" >&2
|
|
235
|
+
printf 'Install Python 3 (https://www.python.org/downloads), then re-run: loki mcp\n' >&2
|
|
236
|
+
return 2
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# 2. venv location. Lives in the USER'S project (their cwd), NOT the install
|
|
240
|
+
# root: $root may be a root-owned global npm prefix where we must never
|
|
241
|
+
# write. LOKI_DIR (default .loki) keeps this consistent with every other
|
|
242
|
+
# .loki artifact; LOKI_MCP_VENV overrides outright.
|
|
243
|
+
local venv="${LOKI_MCP_VENV:-$PWD/${LOKI_DIR:-.loki}/mcp-venv}"
|
|
244
|
+
local venv_py="$venv/bin/python"
|
|
245
|
+
|
|
246
|
+
# Progress destination for the bootstrap. On an interactive TTY, progress goes
|
|
247
|
+
# to stdout (the user is watching a terminal). On the non-interactive
|
|
248
|
+
# auto-bootstrap path (LOKI_MCP_AUTO_BOOTSTRAP=1 over piped stdio), stdout is
|
|
249
|
+
# the JSON-RPC channel to the MCP client and MUST stay clean, so ALL progress
|
|
250
|
+
# (printfs AND the stdout of venv/pip) is routed to fd 2. Keyed on
|
|
251
|
+
# non-interactive, not on the flag: TTY+flag still prints to the terminal.
|
|
252
|
+
local out_fd=1
|
|
253
|
+
if _ml_non_interactive; then
|
|
254
|
+
out_fd=2
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
# 3. If the venv already has the SDK, use it directly. The server is launched
|
|
258
|
+
# with PYTHONPATH=$root (NOT by cd-ing) so the user's cwd is preserved for
|
|
259
|
+
# .loki resolution; see _ml_sdk_importable for why.
|
|
260
|
+
# Known narrow residual: if the user's cwd itself contains a Python
|
|
261
|
+
# package literally named mcp/ with a server submodule, python -m puts
|
|
262
|
+
# the cwd ahead of PYTHONPATH and that package wins. Essentially never
|
|
263
|
+
# true for real projects; documented rather than fought.
|
|
264
|
+
if [ -x "$venv_py" ] && _ml_sdk_importable "$venv_py" "$root"; then
|
|
265
|
+
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" -m mcp.server "$@"
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
# 4. If the BASE python already has the SDK (e.g. user pip-installed it),
|
|
269
|
+
# use it -- no venv needed.
|
|
270
|
+
if _ml_sdk_importable "$base_py" "$root"; then
|
|
271
|
+
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$base_py" -m mcp.server "$@"
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# 5. SDK missing. Decide whether we may bootstrap.
|
|
275
|
+
# Precedence: LOKI_NO_INSTALL_OFFER (explicit no) wins over
|
|
276
|
+
# LOKI_MCP_AUTO_BOOTSTRAP (explicit yes).
|
|
277
|
+
if _ml_truthy "${LOKI_NO_INSTALL_OFFER:-}"; then
|
|
278
|
+
if _ml_truthy "${LOKI_MCP_AUTO_BOOTSTRAP:-}"; then
|
|
279
|
+
printf 'LOKI_NO_INSTALL_OFFER overrides LOKI_MCP_AUTO_BOOTSTRAP (explicit no beats explicit yes); not installing.\n' >&2
|
|
280
|
+
fi
|
|
281
|
+
printf '%sMCP SDK not installed.%s\n' "$_ML_YELLOW" "$_ML_NC" >&2
|
|
282
|
+
_ml_print_manual "$root" "$venv"
|
|
283
|
+
return 2
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# Track whether we reached the bootstrap via non-interactive written consent.
|
|
287
|
+
# When true, the consent prompt is skipped and all progress goes to fd 2.
|
|
288
|
+
local auto_consent=0
|
|
289
|
+
if _ml_non_interactive; then
|
|
290
|
+
if _ml_truthy "${LOKI_MCP_AUTO_BOOTSTRAP:-}"; then
|
|
291
|
+
# Written consent: bootstrap non-interactively, progress to stderr
|
|
292
|
+
# only (stdout is the client's JSON-RPC channel). Fall through to the
|
|
293
|
+
# bootstrap below with auto-accept.
|
|
294
|
+
printf '%sMCP SDK not installed.%s LOKI_MCP_AUTO_BOOTSTRAP set: bootstrapping the project venv non-interactively (progress on stderr; stdout reserved for JSON-RPC).\n' "$_ML_YELLOW" "$_ML_NC" >&2
|
|
295
|
+
auto_consent=1
|
|
296
|
+
else
|
|
297
|
+
printf '%sMCP SDK not installed%s and this is a non-interactive shell, so Loki will not install it automatically. Set LOKI_MCP_AUTO_BOOTSTRAP=1 (also accepts true/yes) to authorize this for MCP clients.\n' "$_ML_YELLOW" "$_ML_NC" >&2
|
|
298
|
+
_ml_print_manual "$root" "$venv"
|
|
299
|
+
return 2
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# 6. Offer the consent-gated bootstrap. Consent is already given when:
|
|
304
|
+
# - we arrived via the non-interactive auto-bootstrap path (auto_consent), or
|
|
305
|
+
# - LOKI_MCP_AUTO_BOOTSTRAP=1 is set on a TTY (explicit yes skips the prompt), or
|
|
306
|
+
# - --yes / LOKI_ASSUME_YES / LOKI_AUTO_CONFIRM.
|
|
307
|
+
local answer=""
|
|
308
|
+
if [ "$auto_consent" -eq 1 ] || _ml_truthy "${LOKI_MCP_AUTO_BOOTSTRAP:-}" || _ml_assume_yes; then
|
|
309
|
+
answer="y"
|
|
310
|
+
else
|
|
311
|
+
printf '\n'
|
|
312
|
+
printf 'The MCP server needs Python dependencies that are not installed.\n'
|
|
313
|
+
printf 'Loki can create a project-local virtualenv and install them:\n'
|
|
314
|
+
printf ' python3 -m venv %s\n' "$venv"
|
|
315
|
+
printf ' %s/bin/pip install -r %s/mcp/requirements.txt\n' "$venv" "$root"
|
|
316
|
+
printf '\n'
|
|
317
|
+
printf 'Nothing is installed globally and no sudo is used. Proceed? [Y/n] '
|
|
318
|
+
read -r answer || answer="n"
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
case "$answer" in
|
|
322
|
+
""|y|Y|yes|YES) ;;
|
|
323
|
+
*)
|
|
324
|
+
printf 'Skipped.\n'
|
|
325
|
+
_ml_print_manual "$root" "$venv"
|
|
326
|
+
return 2
|
|
327
|
+
;;
|
|
328
|
+
esac
|
|
329
|
+
|
|
330
|
+
# 7. Create the venv if needed. Ensure the parent .loki exists in the user's
|
|
331
|
+
# project first (never write under the install root).
|
|
332
|
+
if [ ! -x "$venv_py" ]; then
|
|
333
|
+
local venv_parent
|
|
334
|
+
venv_parent="$(dirname "$venv")"
|
|
335
|
+
if [ ! -d "$venv_parent" ] && ! mkdir -p "$venv_parent"; then
|
|
336
|
+
printf '%sCannot create %s (no write access).%s\n' "$_ML_RED" "$venv_parent" "$_ML_NC" >&2
|
|
337
|
+
_ml_print_manual "$root" "$venv"
|
|
338
|
+
return 2
|
|
339
|
+
fi
|
|
340
|
+
printf 'Creating virtualenv (%s) ...\n' "$venv" >&"$out_fd"
|
|
341
|
+
# Route venv's stdout to out_fd (stderr on the auto path) so the JSON-RPC
|
|
342
|
+
# channel stays clean; its stderr is left as-is for real diagnostics.
|
|
343
|
+
if ! "$base_py" -m venv "$venv" >&"$out_fd"; then
|
|
344
|
+
printf '%sFailed to create virtualenv at %s.%s\n' "$_ML_RED" "$venv" "$_ML_NC" >&2
|
|
345
|
+
_ml_print_manual "$root" "$venv"
|
|
346
|
+
return 2
|
|
347
|
+
fi
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# 8. Install requirements into the venv.
|
|
351
|
+
local req="$root/mcp/requirements.txt"
|
|
352
|
+
if [ ! -f "$req" ]; then
|
|
353
|
+
printf '%smcp/requirements.txt not found at %s.%s\n' "$_ML_RED" "$req" "$_ML_NC" >&2
|
|
354
|
+
return 2
|
|
355
|
+
fi
|
|
356
|
+
printf 'Installing MCP dependencies (%s/bin/pip install -r %s) ...\n' "$venv" "$req" >&"$out_fd"
|
|
357
|
+
local code=0
|
|
358
|
+
# pip writes its progress to stdout; route it to out_fd (stderr on the auto
|
|
359
|
+
# path) so the JSON-RPC channel stays clean. pip's stderr is left as-is.
|
|
360
|
+
"$venv/bin/pip" install -r "$req" >&"$out_fd" || code=$?
|
|
361
|
+
if [ "$code" -ne 0 ]; then
|
|
362
|
+
printf '%sInstall failed (pip exited %s).%s You can retry manually:\n' "$_ML_RED" "$code" "$_ML_NC" >&2
|
|
363
|
+
_ml_print_manual "$root" "$venv"
|
|
364
|
+
return 2
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
# 9. Verify, then exec the server using the venv python (critical: the
|
|
368
|
+
# site-packages walk in server.py only finds the SDK if we run the
|
|
369
|
+
# venv's interpreter, not the ambient python3). PYTHONPATH=$root keeps the
|
|
370
|
+
# user's cwd intact for .loki resolution; see _ml_sdk_importable.
|
|
371
|
+
if ! _ml_sdk_importable "$venv_py" "$root"; then
|
|
372
|
+
printf '%sDependencies installed but the MCP SDK still is not importable.%s\n' "$_ML_RED" "$_ML_NC" >&2
|
|
373
|
+
_ml_print_manual "$root" "$venv"
|
|
374
|
+
return 2
|
|
375
|
+
fi
|
|
376
|
+
printf "%sMCP dependencies ready. Launching server over stdio ...%s\n" "$_ML_BOLD" "$_ML_NC" >&2
|
|
377
|
+
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" -m mcp.server "$@"
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
# Executed directly (tests, manual): run the dispatcher.
|
|
381
|
+
# When sourced by autonomy/loki, this block does not run.
|
|
382
|
+
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
383
|
+
mcp_launch_main "$@"
|
|
384
|
+
fi
|
package/autonomy/run.sh
CHANGED
|
@@ -1809,9 +1809,31 @@ get_provider_tier_param() {
|
|
|
1809
1809
|
case "${PROVIDER_NAME:-claude}" in
|
|
1810
1810
|
claude)
|
|
1811
1811
|
case "$tier" in
|
|
1812
|
-
planning)
|
|
1812
|
+
planning)
|
|
1813
|
+
# Evidence-based routing (scoped): the official model-config
|
|
1814
|
+
# docs explicitly name "architecture decisions" and
|
|
1815
|
+
# "root-cause investigations" as where Fable 5's extra
|
|
1816
|
+
# investigation and self-verification pay off. So the
|
|
1817
|
+
# planning/architecture tier may opt in to Fable via
|
|
1818
|
+
# LOKI_FABLE_ARCHITECT=1. Default OFF because Fable is 2x
|
|
1819
|
+
# Opus per token; reserve it for the REASON/architecture
|
|
1820
|
+
# iterations the user explicitly wants. An explicit
|
|
1821
|
+
# PROVIDER_MODEL_PLANNING still wins (operator override).
|
|
1822
|
+
if [ -n "${PROVIDER_MODEL_PLANNING:-}" ]; then
|
|
1823
|
+
echo "${PROVIDER_MODEL_PLANNING}"
|
|
1824
|
+
elif [ "${LOKI_FABLE_ARCHITECT:-0}" = "1" ]; then
|
|
1825
|
+
echo "fable"
|
|
1826
|
+
else
|
|
1827
|
+
echo "opus"
|
|
1828
|
+
fi
|
|
1829
|
+
;;
|
|
1813
1830
|
development) echo "${PROVIDER_MODEL_DEVELOPMENT:-opus}" ;;
|
|
1814
1831
|
fast) echo "${PROVIDER_MODEL_FAST:-sonnet}" ;;
|
|
1832
|
+
# Honor the fable lever here too: without this arm an
|
|
1833
|
+
# unsourced-claude.sh environment (this static fallback) would
|
|
1834
|
+
# silently downgrade a fable-pinned tier to sonnet via the `*`
|
|
1835
|
+
# default. Matches resolve_model_for_tier's explicit fable) arm.
|
|
1836
|
+
fable) echo "fable" ;;
|
|
1815
1837
|
*) echo "sonnet" ;;
|
|
1816
1838
|
esac
|
|
1817
1839
|
;;
|
|
@@ -3803,6 +3825,8 @@ _write_pricing_json() {
|
|
|
3803
3825
|
"updated": "${updated}",
|
|
3804
3826
|
"source": "static",
|
|
3805
3827
|
"models": {
|
|
3828
|
+
"fable": {"input": 10.00, "output": 50.00, "label": "Fable 5 (top, 2x Opus)", "provider": "claude"},
|
|
3829
|
+
"claude-fable-5": {"input": 10.00, "output": 50.00, "label": "Fable 5 (top, 2x Opus)", "provider": "claude"},
|
|
3806
3830
|
"opus": {"input": 5.00, "output": 25.00, "label": "Opus (latest)", "provider": "claude"},
|
|
3807
3831
|
"sonnet": {"input": 3.00, "output": 15.00, "label": "Sonnet (latest)", "provider": "claude"},
|
|
3808
3832
|
"haiku": {"input": 1.00, "output": 5.00, "label": "Haiku (latest)", "provider": "claude"},
|
|
@@ -7648,6 +7672,21 @@ BUILD_PROMPT
|
|
|
7648
7672
|
prompt_text=$(cat "$review_prompt_file")
|
|
7649
7673
|
case "${PROVIDER_NAME:-claude}" in
|
|
7650
7674
|
claude)
|
|
7675
|
+
# SECURITY-REVIEW MODEL GUARD (evidence-based routing, item 4b):
|
|
7676
|
+
# Reviewers deliberately do NOT pass --model, so they run on
|
|
7677
|
+
# the account default model and are NEVER routed to Fable by a
|
|
7678
|
+
# mid-flight model override or LOKI_FABLE_ARCHITECT (those only
|
|
7679
|
+
# rewrite the iteration's tier_param, not this dispatch). This
|
|
7680
|
+
# must stay true. The official model-config docs CONTRADICT
|
|
7681
|
+
# routing security review to Fable: Fable's safety classifiers
|
|
7682
|
+
# refuse cybersecurity content, and in non-interactive (-p)
|
|
7683
|
+
# mode a flagged request ends the turn with stop_reason
|
|
7684
|
+
# "refusal" instead of a transparent Opus re-run. A refused
|
|
7685
|
+
# security reviewer would return no VERDICT and break the
|
|
7686
|
+
# unanimous-council gate. Defensive-cyber capability lives in
|
|
7687
|
+
# Mythos 5 (Project Glasswing), not Fable. If a future change
|
|
7688
|
+
# adds --model here, the security-sentinel reviewer must be
|
|
7689
|
+
# pinned to opus, never fable.
|
|
7651
7690
|
claude --dangerously-skip-permissions -p "$prompt_text" \
|
|
7652
7691
|
--output-format text > "$review_output" 2>/dev/null
|
|
7653
7692
|
;;
|
|
@@ -9058,6 +9097,8 @@ check_budget_limit() {
|
|
|
9058
9097
|
import json, glob
|
|
9059
9098
|
total = 0.0
|
|
9060
9099
|
pricing = {
|
|
9100
|
+
'fable': {'input': 10.00, 'output': 50.00},
|
|
9101
|
+
'claude-fable-5': {'input': 10.00, 'output': 50.00},
|
|
9061
9102
|
'opus': {'input': 5.00, 'output': 25.00},
|
|
9062
9103
|
'sonnet': {'input': 3.00, 'output': 15.00},
|
|
9063
9104
|
'haiku': {'input': 1.00, 'output': 5.00},
|
|
@@ -12052,6 +12093,23 @@ run_autonomous() {
|
|
|
12052
12093
|
_LOKI_RUN_START_SHA="$(cat "$_start_sha_file" 2>/dev/null || echo "")"
|
|
12053
12094
|
export _LOKI_RUN_START_SHA
|
|
12054
12095
|
|
|
12096
|
+
# Session-scope the mid-flight model override (model-honesty fix). The
|
|
12097
|
+
# override file (.loki/state/model-override) is a LIVE-RUN control: the
|
|
12098
|
+
# dashboard UI and docs state it "applies to the current run". A leftover
|
|
12099
|
+
# file from a previous run must NOT silently pin every future `loki start`
|
|
12100
|
+
# to that model (and to its cost). So clear it once at the start of a FRESH
|
|
12101
|
+
# run (ITERATION_COUNT==0). A genuine resume (ITERATION_COUNT>0) and any
|
|
12102
|
+
# mid-flight switch made at iteration>0 are preserved, because the clear is
|
|
12103
|
+
# guarded on the fresh-run condition only.
|
|
12104
|
+
if [ "${ITERATION_COUNT:-0}" -eq 0 ] && [ -f ".loki/state/model-override" ]; then
|
|
12105
|
+
local _stale_override
|
|
12106
|
+
_stale_override="$(cat .loki/state/model-override 2>/dev/null | tr -d '[:space:]')"
|
|
12107
|
+
rm -f ".loki/state/model-override" 2>/dev/null || true
|
|
12108
|
+
if [ -n "$_stale_override" ]; then
|
|
12109
|
+
log_info "Cleared leftover model override ('$_stale_override') at session start; the override applies to the current run only."
|
|
12110
|
+
fi
|
|
12111
|
+
fi
|
|
12112
|
+
|
|
12055
12113
|
# Trust-metrics instrumentation marker: record one run_start event per
|
|
12056
12114
|
# fresh run so the trust-metrics denominator counts ONLY instrumented runs.
|
|
12057
12115
|
# This is what lets the aggregator distinguish "0 blocks measured" from
|
|
@@ -12274,10 +12332,39 @@ except Exception as exc:
|
|
|
12274
12332
|
opus) CURRENT_TIER="planning" ;;
|
|
12275
12333
|
sonnet) CURRENT_TIER="development" ;;
|
|
12276
12334
|
haiku) CURRENT_TIER="fast" ;;
|
|
12335
|
+
fable) CURRENT_TIER="fable" ;;
|
|
12277
12336
|
planning|development|fast) CURRENT_TIER="${LOKI_SESSION_MODEL}" ;;
|
|
12278
12337
|
*) CURRENT_TIER="${LOKI_SESSION_MODEL}" ;;
|
|
12279
12338
|
esac
|
|
12280
12339
|
fi
|
|
12340
|
+
# Architect opt-in (LOKI_FABLE_ARCHITECT=1): route ONLY the first
|
|
12341
|
+
# iteration (the architecture/REASON pass) to Fable, then fall back to
|
|
12342
|
+
# the session tier for all later iterations. This is the honest
|
|
12343
|
+
# implementation of "fable for architecture only": run.sh is the only
|
|
12344
|
+
# scope that has ITERATION_COUNT, so the decision lives here (not in the
|
|
12345
|
+
# stateless provider resolver). An EXPLICIT planning-model override still
|
|
12346
|
+
# wins, and the LOKI_MAX_TIER ceiling clamps fable down via the resolver.
|
|
12347
|
+
# Default OFF (Fable is 2x Opus). Without this scoping, a session pinned
|
|
12348
|
+
# to opus would route EVERY iteration to fable.
|
|
12349
|
+
#
|
|
12350
|
+
# NOTE on the index: ITERATION_COUNT is incremented at the TOP of the
|
|
12351
|
+
# loop (see "((ITERATION_COUNT++))" above), so the FIRST in-loop pass
|
|
12352
|
+
# has ITERATION_COUNT==1, not 0. The guard matches 1 so the architecture
|
|
12353
|
+
# iteration actually fires (a -eq 0 guard here would be a silent no-op,
|
|
12354
|
+
# the exact bug this fix removes). The estimator models this same first
|
|
12355
|
+
# iteration as its 0-indexed range() i==0, so quote and run agree.
|
|
12356
|
+
#
|
|
12357
|
+
# PRECEDENCE: a mid-flight model override (.loki/state/model-override,
|
|
12358
|
+
# applied later in this iteration body) WINS over this architect pin.
|
|
12359
|
+
# Deliberate: a live user action in the dashboard outranks an env
|
|
12360
|
+
# opt-in set at launch. The override is still clamped by LOKI_MAX_TIER.
|
|
12361
|
+
if [ "${ITERATION_COUNT:-0}" -eq 1 ] \
|
|
12362
|
+
&& [ "${LOKI_FABLE_ARCHITECT:-0}" = "1" ] \
|
|
12363
|
+
&& [ -z "${LOKI_CLAUDE_MODEL_PLANNING:-}" ] \
|
|
12364
|
+
&& [ -z "${LOKI_MODEL_PLANNING:-}" ]; then
|
|
12365
|
+
CURRENT_TIER="fable"
|
|
12366
|
+
log_info "LOKI_FABLE_ARCHITECT=1: routing the first (architecture) iteration to fable; later iterations use the session tier"
|
|
12367
|
+
fi
|
|
12281
12368
|
# Export LOKI_CURRENT_TIER so provider helper functions
|
|
12282
12369
|
# can resolve the correct model.
|
|
12283
12370
|
# Without this, LOKI_CURRENT_TIER is always empty and defaults to "planning".
|
|
@@ -12285,6 +12372,66 @@ except Exception as exc:
|
|
|
12285
12372
|
export LOKI_CURRENT_TIER
|
|
12286
12373
|
local rarv_phase=$(get_rarv_phase_name "$ITERATION_COUNT")
|
|
12287
12374
|
local tier_param=$(get_provider_tier_param "$CURRENT_TIER")
|
|
12375
|
+
# Mid-flight model override: the dashboard (POST /api/session/model) or a
|
|
12376
|
+
# CLI user may rewrite .loki/state/model-override between iterations to
|
|
12377
|
+
# change the model a live run uses. Read it here, after tier_param is
|
|
12378
|
+
# resolved and before the claude argv is built (--model "$tier_param" is
|
|
12379
|
+
# assembled below), so the override flows through effort/budget/fallback
|
|
12380
|
+
# with no other change. Each iteration spawns a fresh `claude -p`, so the
|
|
12381
|
+
# switch takes effect at THIS iteration boundary and never mid-invocation
|
|
12382
|
+
# (claude -p fixes the model per call). Clearing/emptying the file reverts
|
|
12383
|
+
# to the tier mapping. The file is fed straight into --model, so only an
|
|
12384
|
+
# allowlisted alias is honored; invalid content is ignored with one warn.
|
|
12385
|
+
# The override applies ONLY to the claude provider; other providers map
|
|
12386
|
+
# tier_param to effort/model strings and have no fable equivalent.
|
|
12387
|
+
if [ "${PROVIDER_NAME:-claude}" = "claude" ] && [ -s ".loki/state/model-override" ]; then
|
|
12388
|
+
local _loki_override_file _loki_override_alias
|
|
12389
|
+
_loki_override_file="$(cat .loki/state/model-override 2>/dev/null)"
|
|
12390
|
+
# Canonical normalization shared with the dashboard + estimator
|
|
12391
|
+
# (trim + lowercase + exact allowlist). "fab le" and other non-exact
|
|
12392
|
+
# values normalize to empty and are rejected, so all three readers
|
|
12393
|
+
# agree on what the file means. Falls back to a local case only if
|
|
12394
|
+
# the provider helper is somehow not in scope.
|
|
12395
|
+
if type loki_normalize_model_alias >/dev/null 2>&1; then
|
|
12396
|
+
_loki_override_alias="$(loki_normalize_model_alias "$_loki_override_file")"
|
|
12397
|
+
else
|
|
12398
|
+
# Fallback only if the provider helper is not sourced. Mirror the
|
|
12399
|
+
# canonical rule EXACTLY: trim ends + lowercase + exact allowlist,
|
|
12400
|
+
# so interior whitespace ("fab le") is REJECTED here too (do NOT
|
|
12401
|
+
# use `tr -d [:space:]`, which would collapse it into a false
|
|
12402
|
+
# accept and re-introduce the normalization divergence).
|
|
12403
|
+
_loki_override_alias=""
|
|
12404
|
+
local _loki_ov_trim="$_loki_override_file"
|
|
12405
|
+
_loki_ov_trim="${_loki_ov_trim#"${_loki_ov_trim%%[![:space:]]*}"}"
|
|
12406
|
+
_loki_ov_trim="${_loki_ov_trim%"${_loki_ov_trim##*[![:space:]]}"}"
|
|
12407
|
+
_loki_ov_trim="$(printf '%s' "$_loki_ov_trim" | tr '[:upper:]' '[:lower:]')"
|
|
12408
|
+
case "$_loki_ov_trim" in
|
|
12409
|
+
haiku|sonnet|opus|fable) _loki_override_alias="$_loki_ov_trim" ;;
|
|
12410
|
+
esac
|
|
12411
|
+
fi
|
|
12412
|
+
if [ -n "$_loki_override_alias" ]; then
|
|
12413
|
+
# Apply the SAME LOKI_MAX_TIER ceiling the tier resolver uses, so
|
|
12414
|
+
# a mid-flight override cannot silently bypass the operator's cost
|
|
12415
|
+
# cap. Clamp via the shared helper when available.
|
|
12416
|
+
local _loki_override_effective="$_loki_override_alias"
|
|
12417
|
+
if type loki_apply_max_tier_clamp >/dev/null 2>&1; then
|
|
12418
|
+
_loki_override_effective="$(loki_apply_max_tier_clamp "$_loki_override_alias" "$_loki_override_alias")"
|
|
12419
|
+
fi
|
|
12420
|
+
if [ "$_loki_override_effective" != "$_loki_override_alias" ]; then
|
|
12421
|
+
tier_param="$_loki_override_effective"
|
|
12422
|
+
log_warn "model override '$_loki_override_alias' exceeds LOKI_MAX_TIER=${LOKI_MAX_TIER}; clamped to $tier_param (applies this iteration)"
|
|
12423
|
+
echo "=== Model override: $_loki_override_alias clamped to $tier_param by LOKI_MAX_TIER=${LOKI_MAX_TIER} (applies this iteration $ITERATION_COUNT) ===" | tee -a "$log_file" "$agent_log"
|
|
12424
|
+
else
|
|
12425
|
+
tier_param="$_loki_override_effective"
|
|
12426
|
+
log_info "model override: $tier_param (applies this iteration)"
|
|
12427
|
+
echo "=== Model override: $tier_param (applies this iteration $ITERATION_COUNT) ===" | tee -a "$log_file" "$agent_log"
|
|
12428
|
+
fi
|
|
12429
|
+
elif [ -z "$(printf '%s' "$_loki_override_file" | tr -d '[:space:]')" ]; then
|
|
12430
|
+
: # empty file means no override; fall back to tier mapping
|
|
12431
|
+
else
|
|
12432
|
+
log_warn "Ignoring invalid model override '$_loki_override_file' (allowed: haiku, sonnet, opus, fable); using tier $tier_param"
|
|
12433
|
+
fi
|
|
12434
|
+
fi
|
|
12288
12435
|
echo "=== RARV Phase: $rarv_phase, Tier: $CURRENT_TIER ($tier_param) ===" | tee -a "$log_file" "$agent_log"
|
|
12289
12436
|
log_info "RARV Phase: $rarv_phase -> Tier: $CURRENT_TIER ($tier_param)"
|
|
12290
12437
|
|
package/bin/loki
CHANGED
|
@@ -112,6 +112,25 @@ if ! command -v bun &>/dev/null; then
|
|
|
112
112
|
exec "$BASH_CLI" "$@"
|
|
113
113
|
fi
|
|
114
114
|
|
|
115
|
+
# CLI consolidation (Phase A): `trust detail` is the grouped form of the
|
|
116
|
+
# trust-metrics breakdown. The Bun `trust` handler only knows the trajectory
|
|
117
|
+
# view (it rejects unknown args), and Phase A moves no handler logic, so the
|
|
118
|
+
# `detail` subcommand lives only in bash (cmd_trust -> cmd_trust_metrics).
|
|
119
|
+
# Force the bash route whenever `detail` appears as a trust arg (flag-anywhere:
|
|
120
|
+
# `trust detail`, `trust --json detail`, `trust detail --json` all resolve
|
|
121
|
+
# identically to the bash `trust-metrics` breakdown). Without this, only the
|
|
122
|
+
# exact `trust detail` order routed to bash and `trust --json detail` hit the
|
|
123
|
+
# Bun handler, which rejected `detail` to stderr while bash rejects to stdout --
|
|
124
|
+
# divergent error channels for the same malformed input. Bare `trust` /
|
|
125
|
+
# `trust --json` (no `detail`) stay on the Bun-native trajectory path above.
|
|
126
|
+
if [ "${1:-}" = "trust" ]; then
|
|
127
|
+
for _trust_arg in "${@:2}"; do
|
|
128
|
+
if [ "$_trust_arg" = "detail" ]; then
|
|
129
|
+
exec "$BASH_CLI" "$@"
|
|
130
|
+
fi
|
|
131
|
+
done
|
|
132
|
+
fi
|
|
133
|
+
|
|
115
134
|
# Commands ported in Phase 2 -- route to Bun. Everything else goes to bash.
|
|
116
135
|
# Two-token routes (provider show/list, memory list/index) match on the first
|
|
117
136
|
# token only; the Bun dispatcher handles subcommand routing internally.
|