loki-mode 7.30.0 → 7.32.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 +4 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/context-tracker.py +8 -0
- package/autonomy/loki +799 -123
- package/autonomy/mcp-launch.sh +149 -36
- package/autonomy/run.sh +168 -4
- package/bin/loki +71 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +326 -1
- package/dashboard/static/index.html +105 -39
- package/docs/INSTALLATION.md +1 -1
- package/docs/competitive/replit-lovable-analysis.md +1 -1
- package/loki-ts/data/model-pricing.json +1 -0
- package/loki-ts/dist/loki.js +233 -231
- package/mcp/__init__.py +1 -1
- package/mcp/_sdk_loader.py +157 -0
- package/mcp/lsp_proxy.py +61 -61
- package/mcp/server.py +35 -129
- package/package.json +1 -1
- package/providers/claude.sh +76 -19
- package/providers/model_catalog.json +9 -0
- package/skills/model-selection.md +49 -1
package/autonomy/mcp-launch.sh
CHANGED
|
@@ -26,10 +26,20 @@
|
|
|
26
26
|
# * The ONLY command run on the user's behalf is, after explicit consent:
|
|
27
27
|
# <venv>/bin/pip install -r mcp/requirements.txt
|
|
28
28
|
# The exact command is printed before it runs.
|
|
29
|
-
# * Non-interactive / CI: NEVER install. Print the manual command
|
|
30
|
-
# and exit 2 (mirrors autonomy/provider-offer.sh gate semantics).
|
|
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).
|
|
31
40
|
# * Opt-out: LOKI_NO_INSTALL_OFFER=1 -> never prompt, print manual command,
|
|
32
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).
|
|
33
43
|
#
|
|
34
44
|
# Self-containment: depends only on bash builtins + python3 on PATH. Defines
|
|
35
45
|
# its own colors so it behaves identically whether sourced by autonomy/loki or
|
|
@@ -58,10 +68,20 @@ _ml_repo_root() {
|
|
|
58
68
|
(cd "$self_dir/.." && pwd)
|
|
59
69
|
}
|
|
60
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
|
+
|
|
61
81
|
# _ml_assume_yes: true when the user opted into unattended confirmation.
|
|
62
82
|
_ml_assume_yes() {
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
_ml_truthy "${LOKI_ASSUME_YES:-}" && return 0
|
|
84
|
+
_ml_truthy "${LOKI_AUTO_CONFIRM:-}" && return 0
|
|
65
85
|
return 1
|
|
66
86
|
}
|
|
67
87
|
|
|
@@ -93,15 +113,22 @@ _ml_python() {
|
|
|
93
113
|
# `--check-sdk` probe, which runs the exact loader the server uses and exits 0
|
|
94
114
|
# only when FastMCP loaded.
|
|
95
115
|
#
|
|
96
|
-
# Critical: we
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
116
|
+
# Critical: we probe with the SAME FILE-EXEC form the launch uses
|
|
117
|
+
# (`"$root/mcp/server.py"`, NOT `-m mcp.server`), with PYTHONPATH set to the
|
|
118
|
+
# install root and WITHOUT cd-ing into it, so the probe exercises byte-identical
|
|
119
|
+
# module resolution to the real launch (which preserves the user's cwd). This
|
|
120
|
+
# matters because `-m mcp.server` puts the user's cwd at sys.path[0] AHEAD of
|
|
121
|
+
# PYTHONPATH=$root, so a cwd that happens to contain a regular `mcp/` python
|
|
122
|
+
# package would shadow Loki's server during the probe (false SDK-missing) while
|
|
123
|
+
# the file-exec launch -- immune to cwd shadowing -- would succeed. Probing by
|
|
124
|
+
# file path keeps probe and launch resolving the IDENTICAL module. The redirect
|
|
125
|
+
# of stdin from /dev/null is insurance: if the pip SDK's own `mcp.server` were
|
|
126
|
+
# ever reached, its stub starts a stdio receive loop; the EOF makes it exit
|
|
127
|
+
# instead of hanging.
|
|
101
128
|
_ml_sdk_importable() {
|
|
102
129
|
local py="$1" root="$2"
|
|
103
130
|
PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" \
|
|
104
|
-
"$py"
|
|
131
|
+
"$py" "$root/mcp/server.py" --check-sdk </dev/null >/dev/null 2>&1
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
# _ml_print_manual <root> <venv>: print the honest manual install commands.
|
|
@@ -109,10 +136,13 @@ _ml_sdk_importable() {
|
|
|
109
136
|
# requirements.txt is shipped under the install root.
|
|
110
137
|
_ml_print_manual() {
|
|
111
138
|
local root="$1" venv="$2"
|
|
139
|
+
# Display-only quoting: single-quote the substituted paths so the printed
|
|
140
|
+
# commands copy-paste correctly even when the project root or venv path
|
|
141
|
+
# contains spaces. This is presentation only; nothing is executed here.
|
|
112
142
|
printf 'Install the MCP server dependencies manually:\n' >&2
|
|
113
|
-
printf
|
|
114
|
-
printf '
|
|
115
|
-
printf
|
|
143
|
+
printf " python3 -m venv '%s'\n" "$venv" >&2
|
|
144
|
+
printf " '%s/bin/pip' install -r '%s/mcp/requirements.txt'\n" "$venv" "$root" >&2
|
|
145
|
+
printf " PYTHONPATH='%s' '%s/bin/python' '%s/mcp/server.py'\n" "$root" "$venv" "$root" >&2
|
|
116
146
|
}
|
|
117
147
|
|
|
118
148
|
_ml_help() {
|
|
@@ -135,27 +165,73 @@ Options:
|
|
|
135
165
|
--help, -h Show this help and exit.
|
|
136
166
|
|
|
137
167
|
Environment:
|
|
138
|
-
LOKI_MCP_VENV=/abs/path
|
|
139
|
-
LOKI_NO_INSTALL_OFFER=1
|
|
140
|
-
|
|
168
|
+
LOKI_MCP_VENV=/abs/path Use a custom venv location instead of .loki/mcp-venv.
|
|
169
|
+
LOKI_NO_INSTALL_OFFER=1 Never prompt to install; print the manual command.
|
|
170
|
+
Wins over LOKI_MCP_AUTO_BOOTSTRAP (explicit no beats
|
|
171
|
+
explicit yes).
|
|
172
|
+
LOKI_MCP_AUTO_BOOTSTRAP=1 Written consent for non-interactive bootstrap. MCP
|
|
173
|
+
clients (Claude Desktop etc.) spawn the server over
|
|
174
|
+
piped stdio with no TTY; set this in your client
|
|
175
|
+
config to authorize the one-time venv bootstrap when
|
|
176
|
+
the SDK is missing. Progress goes to stderr only so
|
|
177
|
+
stdout stays clean for JSON-RPC. On a TTY it also
|
|
178
|
+
skips the consent prompt (consent already given).
|
|
179
|
+
Accepts 1/true/yes/on (case-insensitive).
|
|
180
|
+
--yes / LOKI_ASSUME_YES=1 Auto-accept the dependency install. --yes is a
|
|
181
|
+
launcher flag (equivalent to LOKI_ASSUME_YES=1); it
|
|
182
|
+
is consumed here and never forwarded to the server.
|
|
183
|
+
LOKI_ASSUME_YES / LOKI_AUTO_CONFIRM accept
|
|
184
|
+
1/true/yes/on (case-insensitive).
|
|
141
185
|
|
|
142
|
-
|
|
186
|
+
Argument handling: launcher flags (--help, --yes) are consumed here; every other
|
|
187
|
+
argument is forwarded verbatim to the server, which accepts --transport/--port.
|
|
188
|
+
A bare `--` ends launcher parsing so anything after it reaches the server as-is.
|
|
189
|
+
|
|
190
|
+
Behavior in non-interactive / CI shells: never installs UNLESS
|
|
191
|
+
LOKI_MCP_AUTO_BOOTSTRAP is set (1/true/yes/on). Without it, prints the manual
|
|
143
192
|
install command to stderr and exits 2.
|
|
144
193
|
EOF
|
|
145
194
|
}
|
|
146
195
|
|
|
147
196
|
# mcp_launch_main: dispatcher invoked by cmd_mcp() (autonomy/loki) or directly.
|
|
148
197
|
mcp_launch_main() {
|
|
149
|
-
#
|
|
198
|
+
# Split argv into launcher-owned flags (consumed here) and server argv
|
|
199
|
+
# (forwarded verbatim to the file-exec launch `python "$root/mcp/server.py"`).
|
|
200
|
+
# The server's argparse only
|
|
201
|
+
# accepts --transport/--port/--check-sdk; forwarding a launcher flag like
|
|
202
|
+
# --yes would make it abort with exit 2, so launcher flags MUST be stripped.
|
|
203
|
+
# A bare `--` ends launcher parsing: everything after it is forwarded as-is
|
|
204
|
+
# (escape hatch for any future server flag that collides with a launcher one).
|
|
150
205
|
local arg
|
|
206
|
+
local _ml_server_argv=()
|
|
207
|
+
local _ml_after_sep=0
|
|
151
208
|
for arg in "$@"; do
|
|
209
|
+
if [ "$_ml_after_sep" -eq 1 ]; then
|
|
210
|
+
_ml_server_argv+=("$arg")
|
|
211
|
+
continue
|
|
212
|
+
fi
|
|
152
213
|
case "$arg" in
|
|
214
|
+
--)
|
|
215
|
+
_ml_after_sep=1
|
|
216
|
+
;;
|
|
153
217
|
--help|-h|help)
|
|
154
218
|
_ml_help
|
|
155
219
|
return 0
|
|
156
220
|
;;
|
|
221
|
+
--yes)
|
|
222
|
+
# Launcher-owned: equivalent to LOKI_ASSUME_YES=1. Consumed here,
|
|
223
|
+
# never forwarded to the server.
|
|
224
|
+
LOKI_ASSUME_YES=1
|
|
225
|
+
;;
|
|
226
|
+
*)
|
|
227
|
+
_ml_server_argv+=("$arg")
|
|
228
|
+
;;
|
|
157
229
|
esac
|
|
158
230
|
done
|
|
231
|
+
# Replace the positional parameters with the filtered server argv so every
|
|
232
|
+
# downstream `exec ... "$@"` forwards only server-valid arguments. Safe
|
|
233
|
+
# empty-array expansion (bash 3.2 + set -u when no server args remain).
|
|
234
|
+
set -- ${_ml_server_argv[@]+"${_ml_server_argv[@]}"}
|
|
159
235
|
|
|
160
236
|
local root
|
|
161
237
|
root="$(_ml_repo_root)"
|
|
@@ -175,39 +251,72 @@ mcp_launch_main() {
|
|
|
175
251
|
local venv="${LOKI_MCP_VENV:-$PWD/${LOKI_DIR:-.loki}/mcp-venv}"
|
|
176
252
|
local venv_py="$venv/bin/python"
|
|
177
253
|
|
|
254
|
+
# Progress destination for the bootstrap. On an interactive TTY, progress goes
|
|
255
|
+
# to stdout (the user is watching a terminal). On the non-interactive
|
|
256
|
+
# auto-bootstrap path (LOKI_MCP_AUTO_BOOTSTRAP=1 over piped stdio), stdout is
|
|
257
|
+
# the JSON-RPC channel to the MCP client and MUST stay clean, so ALL progress
|
|
258
|
+
# (printfs AND the stdout of venv/pip) is routed to fd 2. Keyed on
|
|
259
|
+
# non-interactive, not on the flag: TTY+flag still prints to the terminal.
|
|
260
|
+
local out_fd=1
|
|
261
|
+
if _ml_non_interactive; then
|
|
262
|
+
out_fd=2
|
|
263
|
+
fi
|
|
264
|
+
|
|
178
265
|
# 3. If the venv already has the SDK, use it directly. The server is launched
|
|
179
|
-
#
|
|
266
|
+
# by FILE PATH ($root/mcp/server.py) rather than `-m mcp.server`, with
|
|
267
|
+
# PYTHONPATH=$root (NOT by cd-ing) so the user's cwd is preserved for
|
|
180
268
|
# .loki resolution; see _ml_sdk_importable for why.
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
269
|
+
# Running the file directly avoids the runpy RuntimeWarning that `-m`
|
|
270
|
+
# emits (the local mcp/ package is imported during SDK-namespace setup
|
|
271
|
+
# before runpy executes mcp.server). server.py uses only absolute imports
|
|
272
|
+
# (e.g. `from mcp._sdk_loader import ...`), which resolve via PYTHONPATH=$root
|
|
273
|
+
# under file execution. File-exec also removes the old narrow cwd-shadowing
|
|
274
|
+
# residual: an explicit path can never be shadowed by a cwd `mcp/` package.
|
|
185
275
|
if [ -x "$venv_py" ] && _ml_sdk_importable "$venv_py" "$root"; then
|
|
186
|
-
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py"
|
|
276
|
+
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" "$root/mcp/server.py" "$@"
|
|
187
277
|
fi
|
|
188
278
|
|
|
189
279
|
# 4. If the BASE python already has the SDK (e.g. user pip-installed it),
|
|
190
280
|
# use it -- no venv needed.
|
|
191
281
|
if _ml_sdk_importable "$base_py" "$root"; then
|
|
192
|
-
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$base_py"
|
|
282
|
+
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$base_py" "$root/mcp/server.py" "$@"
|
|
193
283
|
fi
|
|
194
284
|
|
|
195
285
|
# 5. SDK missing. Decide whether we may bootstrap.
|
|
196
|
-
|
|
286
|
+
# Precedence: LOKI_NO_INSTALL_OFFER (explicit no) wins over
|
|
287
|
+
# LOKI_MCP_AUTO_BOOTSTRAP (explicit yes).
|
|
288
|
+
if _ml_truthy "${LOKI_NO_INSTALL_OFFER:-}"; then
|
|
289
|
+
if _ml_truthy "${LOKI_MCP_AUTO_BOOTSTRAP:-}"; then
|
|
290
|
+
printf 'LOKI_NO_INSTALL_OFFER overrides LOKI_MCP_AUTO_BOOTSTRAP (explicit no beats explicit yes); not installing.\n' >&2
|
|
291
|
+
fi
|
|
197
292
|
printf '%sMCP SDK not installed.%s\n' "$_ML_YELLOW" "$_ML_NC" >&2
|
|
198
293
|
_ml_print_manual "$root" "$venv"
|
|
199
294
|
return 2
|
|
200
295
|
fi
|
|
201
296
|
|
|
297
|
+
# Track whether we reached the bootstrap via non-interactive written consent.
|
|
298
|
+
# When true, the consent prompt is skipped and all progress goes to fd 2.
|
|
299
|
+
local auto_consent=0
|
|
202
300
|
if _ml_non_interactive; then
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
301
|
+
if _ml_truthy "${LOKI_MCP_AUTO_BOOTSTRAP:-}"; then
|
|
302
|
+
# Written consent: bootstrap non-interactively, progress to stderr
|
|
303
|
+
# only (stdout is the client's JSON-RPC channel). Fall through to the
|
|
304
|
+
# bootstrap below with auto-accept.
|
|
305
|
+
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
|
|
306
|
+
auto_consent=1
|
|
307
|
+
else
|
|
308
|
+
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
|
|
309
|
+
_ml_print_manual "$root" "$venv"
|
|
310
|
+
return 2
|
|
311
|
+
fi
|
|
206
312
|
fi
|
|
207
313
|
|
|
208
|
-
# 6.
|
|
314
|
+
# 6. Offer the consent-gated bootstrap. Consent is already given when:
|
|
315
|
+
# - we arrived via the non-interactive auto-bootstrap path (auto_consent), or
|
|
316
|
+
# - LOKI_MCP_AUTO_BOOTSTRAP=1 is set on a TTY (explicit yes skips the prompt), or
|
|
317
|
+
# - --yes / LOKI_ASSUME_YES / LOKI_AUTO_CONFIRM.
|
|
209
318
|
local answer=""
|
|
210
|
-
if _ml_assume_yes; then
|
|
319
|
+
if [ "$auto_consent" -eq 1 ] || _ml_truthy "${LOKI_MCP_AUTO_BOOTSTRAP:-}" || _ml_assume_yes; then
|
|
211
320
|
answer="y"
|
|
212
321
|
else
|
|
213
322
|
printf '\n'
|
|
@@ -239,8 +348,10 @@ mcp_launch_main() {
|
|
|
239
348
|
_ml_print_manual "$root" "$venv"
|
|
240
349
|
return 2
|
|
241
350
|
fi
|
|
242
|
-
printf 'Creating virtualenv (%s) ...\n' "$venv"
|
|
243
|
-
|
|
351
|
+
printf 'Creating virtualenv (%s) ...\n' "$venv" >&"$out_fd"
|
|
352
|
+
# Route venv's stdout to out_fd (stderr on the auto path) so the JSON-RPC
|
|
353
|
+
# channel stays clean; its stderr is left as-is for real diagnostics.
|
|
354
|
+
if ! "$base_py" -m venv "$venv" >&"$out_fd"; then
|
|
244
355
|
printf '%sFailed to create virtualenv at %s.%s\n' "$_ML_RED" "$venv" "$_ML_NC" >&2
|
|
245
356
|
_ml_print_manual "$root" "$venv"
|
|
246
357
|
return 2
|
|
@@ -253,9 +364,11 @@ mcp_launch_main() {
|
|
|
253
364
|
printf '%smcp/requirements.txt not found at %s.%s\n' "$_ML_RED" "$req" "$_ML_NC" >&2
|
|
254
365
|
return 2
|
|
255
366
|
fi
|
|
256
|
-
printf 'Installing MCP dependencies (%s/bin/pip install -r %s) ...\n' "$venv" "$req"
|
|
367
|
+
printf 'Installing MCP dependencies (%s/bin/pip install -r %s) ...\n' "$venv" "$req" >&"$out_fd"
|
|
257
368
|
local code=0
|
|
258
|
-
|
|
369
|
+
# pip writes its progress to stdout; route it to out_fd (stderr on the auto
|
|
370
|
+
# path) so the JSON-RPC channel stays clean. pip's stderr is left as-is.
|
|
371
|
+
"$venv/bin/pip" install -r "$req" >&"$out_fd" || code=$?
|
|
259
372
|
if [ "$code" -ne 0 ]; then
|
|
260
373
|
printf '%sInstall failed (pip exited %s).%s You can retry manually:\n' "$_ML_RED" "$code" "$_ML_NC" >&2
|
|
261
374
|
_ml_print_manual "$root" "$venv"
|
|
@@ -272,7 +385,7 @@ mcp_launch_main() {
|
|
|
272
385
|
return 2
|
|
273
386
|
fi
|
|
274
387
|
printf "%sMCP dependencies ready. Launching server over stdio ...%s\n" "$_ML_BOLD" "$_ML_NC" >&2
|
|
275
|
-
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py"
|
|
388
|
+
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" "$root/mcp/server.py" "$@"
|
|
276
389
|
}
|
|
277
390
|
|
|
278
391
|
# Executed directly (tests, manual): run the dispatcher.
|
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
|
|
@@ -12270,14 +12328,60 @@ except Exception as exc:
|
|
|
12270
12328
|
# helpers (which expect tier names) resolve correctly. Unknown
|
|
12271
12329
|
# model strings are passed through as-is; provider loaders fall
|
|
12272
12330
|
# back to a sane default.
|
|
12273
|
-
|
|
12331
|
+
#
|
|
12332
|
+
# Normalize case + surrounding whitespace BEFORE the match so
|
|
12333
|
+
# 'OPUS' and ' opus ' resolve identically to 'opus'. We do NOT use
|
|
12334
|
+
# loki_normalize_model_alias here: that helper is the narrow
|
|
12335
|
+
# OVERRIDE-file allowlist (haiku|sonnet|opus|fable) and would strip
|
|
12336
|
+
# the documented tier-name pins (planning|development|fast) to
|
|
12337
|
+
# empty, collapsing them onto the default tier. The session pin
|
|
12338
|
+
# legitimately accepts tier names (skills/model-selection.md), and
|
|
12339
|
+
# the estimator + dashboard mirror this exact tier route, so the
|
|
12340
|
+
# canonical session-pin rule is trim+lowercase WITHOUT the alias
|
|
12341
|
+
# allowlist. Interior whitespace is preserved (so 'fab le' stays a
|
|
12342
|
+
# junk value that falls through the '*' default arm), matching the
|
|
12343
|
+
# estimator/dashboard ports.
|
|
12344
|
+
local _session_pin="${LOKI_SESSION_MODEL:-sonnet}"
|
|
12345
|
+
_session_pin="${_session_pin#"${_session_pin%%[![:space:]]*}"}"
|
|
12346
|
+
_session_pin="${_session_pin%"${_session_pin##*[![:space:]]}"}"
|
|
12347
|
+
_session_pin="$(printf '%s' "$_session_pin" | tr '[:upper:]' '[:lower:]')"
|
|
12348
|
+
case "$_session_pin" in
|
|
12274
12349
|
opus) CURRENT_TIER="planning" ;;
|
|
12275
12350
|
sonnet) CURRENT_TIER="development" ;;
|
|
12276
12351
|
haiku) CURRENT_TIER="fast" ;;
|
|
12277
|
-
|
|
12278
|
-
|
|
12352
|
+
fable) CURRENT_TIER="fable" ;;
|
|
12353
|
+
planning|development|fast) CURRENT_TIER="$_session_pin" ;;
|
|
12354
|
+
*) CURRENT_TIER="$_session_pin" ;;
|
|
12279
12355
|
esac
|
|
12280
12356
|
fi
|
|
12357
|
+
# Architect opt-in (LOKI_FABLE_ARCHITECT=1): route ONLY the first
|
|
12358
|
+
# iteration (the architecture/REASON pass) to Fable, then fall back to
|
|
12359
|
+
# the session tier for all later iterations. This is the honest
|
|
12360
|
+
# implementation of "fable for architecture only": run.sh is the only
|
|
12361
|
+
# scope that has ITERATION_COUNT, so the decision lives here (not in the
|
|
12362
|
+
# stateless provider resolver). An EXPLICIT planning-model override still
|
|
12363
|
+
# wins, and the LOKI_MAX_TIER ceiling clamps fable down via the resolver.
|
|
12364
|
+
# Default OFF (Fable is 2x Opus). Without this scoping, a session pinned
|
|
12365
|
+
# to opus would route EVERY iteration to fable.
|
|
12366
|
+
#
|
|
12367
|
+
# NOTE on the index: ITERATION_COUNT is incremented at the TOP of the
|
|
12368
|
+
# loop (see "((ITERATION_COUNT++))" above), so the FIRST in-loop pass
|
|
12369
|
+
# has ITERATION_COUNT==1, not 0. The guard matches 1 so the architecture
|
|
12370
|
+
# iteration actually fires (a -eq 0 guard here would be a silent no-op,
|
|
12371
|
+
# the exact bug this fix removes). The estimator models this same first
|
|
12372
|
+
# iteration as its 0-indexed range() i==0, so quote and run agree.
|
|
12373
|
+
#
|
|
12374
|
+
# PRECEDENCE: a mid-flight model override (.loki/state/model-override,
|
|
12375
|
+
# applied later in this iteration body) WINS over this architect pin.
|
|
12376
|
+
# Deliberate: a live user action in the dashboard outranks an env
|
|
12377
|
+
# opt-in set at launch. The override is still clamped by LOKI_MAX_TIER.
|
|
12378
|
+
if [ "${ITERATION_COUNT:-0}" -eq 1 ] \
|
|
12379
|
+
&& [ "${LOKI_FABLE_ARCHITECT:-0}" = "1" ] \
|
|
12380
|
+
&& [ -z "${LOKI_CLAUDE_MODEL_PLANNING:-}" ] \
|
|
12381
|
+
&& [ -z "${LOKI_MODEL_PLANNING:-}" ]; then
|
|
12382
|
+
CURRENT_TIER="fable"
|
|
12383
|
+
log_info "LOKI_FABLE_ARCHITECT=1: routing the first (architecture) iteration to fable; later iterations use the session tier"
|
|
12384
|
+
fi
|
|
12281
12385
|
# Export LOKI_CURRENT_TIER so provider helper functions
|
|
12282
12386
|
# can resolve the correct model.
|
|
12283
12387
|
# Without this, LOKI_CURRENT_TIER is always empty and defaults to "planning".
|
|
@@ -12285,6 +12389,66 @@ except Exception as exc:
|
|
|
12285
12389
|
export LOKI_CURRENT_TIER
|
|
12286
12390
|
local rarv_phase=$(get_rarv_phase_name "$ITERATION_COUNT")
|
|
12287
12391
|
local tier_param=$(get_provider_tier_param "$CURRENT_TIER")
|
|
12392
|
+
# Mid-flight model override: the dashboard (POST /api/session/model) or a
|
|
12393
|
+
# CLI user may rewrite .loki/state/model-override between iterations to
|
|
12394
|
+
# change the model a live run uses. Read it here, after tier_param is
|
|
12395
|
+
# resolved and before the claude argv is built (--model "$tier_param" is
|
|
12396
|
+
# assembled below), so the override flows through effort/budget/fallback
|
|
12397
|
+
# with no other change. Each iteration spawns a fresh `claude -p`, so the
|
|
12398
|
+
# switch takes effect at THIS iteration boundary and never mid-invocation
|
|
12399
|
+
# (claude -p fixes the model per call). Clearing/emptying the file reverts
|
|
12400
|
+
# to the tier mapping. The file is fed straight into --model, so only an
|
|
12401
|
+
# allowlisted alias is honored; invalid content is ignored with one warn.
|
|
12402
|
+
# The override applies ONLY to the claude provider; other providers map
|
|
12403
|
+
# tier_param to effort/model strings and have no fable equivalent.
|
|
12404
|
+
if [ "${PROVIDER_NAME:-claude}" = "claude" ] && [ -s ".loki/state/model-override" ]; then
|
|
12405
|
+
local _loki_override_file _loki_override_alias
|
|
12406
|
+
_loki_override_file="$(cat .loki/state/model-override 2>/dev/null)"
|
|
12407
|
+
# Canonical normalization shared with the dashboard + estimator
|
|
12408
|
+
# (trim + lowercase + exact allowlist). "fab le" and other non-exact
|
|
12409
|
+
# values normalize to empty and are rejected, so all three readers
|
|
12410
|
+
# agree on what the file means. Falls back to a local case only if
|
|
12411
|
+
# the provider helper is somehow not in scope.
|
|
12412
|
+
if type loki_normalize_model_alias >/dev/null 2>&1; then
|
|
12413
|
+
_loki_override_alias="$(loki_normalize_model_alias "$_loki_override_file")"
|
|
12414
|
+
else
|
|
12415
|
+
# Fallback only if the provider helper is not sourced. Mirror the
|
|
12416
|
+
# canonical rule EXACTLY: trim ends + lowercase + exact allowlist,
|
|
12417
|
+
# so interior whitespace ("fab le") is REJECTED here too (do NOT
|
|
12418
|
+
# use `tr -d [:space:]`, which would collapse it into a false
|
|
12419
|
+
# accept and re-introduce the normalization divergence).
|
|
12420
|
+
_loki_override_alias=""
|
|
12421
|
+
local _loki_ov_trim="$_loki_override_file"
|
|
12422
|
+
_loki_ov_trim="${_loki_ov_trim#"${_loki_ov_trim%%[![:space:]]*}"}"
|
|
12423
|
+
_loki_ov_trim="${_loki_ov_trim%"${_loki_ov_trim##*[![:space:]]}"}"
|
|
12424
|
+
_loki_ov_trim="$(printf '%s' "$_loki_ov_trim" | tr '[:upper:]' '[:lower:]')"
|
|
12425
|
+
case "$_loki_ov_trim" in
|
|
12426
|
+
haiku|sonnet|opus|fable) _loki_override_alias="$_loki_ov_trim" ;;
|
|
12427
|
+
esac
|
|
12428
|
+
fi
|
|
12429
|
+
if [ -n "$_loki_override_alias" ]; then
|
|
12430
|
+
# Apply the SAME LOKI_MAX_TIER ceiling the tier resolver uses, so
|
|
12431
|
+
# a mid-flight override cannot silently bypass the operator's cost
|
|
12432
|
+
# cap. Clamp via the shared helper when available.
|
|
12433
|
+
local _loki_override_effective="$_loki_override_alias"
|
|
12434
|
+
if type loki_apply_max_tier_clamp >/dev/null 2>&1; then
|
|
12435
|
+
_loki_override_effective="$(loki_apply_max_tier_clamp "$_loki_override_alias" "$_loki_override_alias")"
|
|
12436
|
+
fi
|
|
12437
|
+
if [ "$_loki_override_effective" != "$_loki_override_alias" ]; then
|
|
12438
|
+
tier_param="$_loki_override_effective"
|
|
12439
|
+
log_warn "model override '$_loki_override_alias' exceeds LOKI_MAX_TIER=${LOKI_MAX_TIER}; clamped to $tier_param (applies this iteration)"
|
|
12440
|
+
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"
|
|
12441
|
+
else
|
|
12442
|
+
tier_param="$_loki_override_effective"
|
|
12443
|
+
log_info "model override: $tier_param (applies this iteration)"
|
|
12444
|
+
echo "=== Model override: $tier_param (applies this iteration $ITERATION_COUNT) ===" | tee -a "$log_file" "$agent_log"
|
|
12445
|
+
fi
|
|
12446
|
+
elif [ -z "$(printf '%s' "$_loki_override_file" | tr -d '[:space:]')" ]; then
|
|
12447
|
+
: # empty file means no override; fall back to tier mapping
|
|
12448
|
+
else
|
|
12449
|
+
log_warn "Ignoring invalid model override '$_loki_override_file' (allowed: haiku, sonnet, opus, fable); using tier $tier_param"
|
|
12450
|
+
fi
|
|
12451
|
+
fi
|
|
12288
12452
|
echo "=== RARV Phase: $rarv_phase, Tier: $CURRENT_TIER ($tier_param) ===" | tee -a "$log_file" "$agent_log"
|
|
12289
12453
|
log_info "RARV Phase: $rarv_phase -> Tier: $CURRENT_TIER ($tier_param)"
|
|
12290
12454
|
|
package/bin/loki
CHANGED
|
@@ -65,6 +65,21 @@ elif [ "${BUN_FROM_SOURCE:-0}" = "1" ] || [ "${BUN_FROM_SOURCE:-}" = "true" ]; t
|
|
|
65
65
|
fi
|
|
66
66
|
elif [ -f "$REPO_ROOT/loki-ts/dist/loki.js" ]; then
|
|
67
67
|
BUN_CLI="$REPO_ROOT/loki-ts/dist/loki.js"
|
|
68
|
+
# Stale-dist freshness guard. dist/loki.js is gitignored (loki-ts/.gitignore)
|
|
69
|
+
# and rebuilt by package.json's prepack and the release Docker build, so on
|
|
70
|
+
# released channels (npm/Docker/brew) src/cli.ts is absent and this guard is
|
|
71
|
+
# a no-op -- dist is always current there. On a DEV machine / worktree the
|
|
72
|
+
# gitignored dist can predate the current dispatcher: e.g. a new shim->Bun
|
|
73
|
+
# route (`report kpis`) added in src that the old dist bundle does not know,
|
|
74
|
+
# which would make the canonical form fail with "Unknown command" while a
|
|
75
|
+
# deprecated alias the old dist still knows silently works -- the exact
|
|
76
|
+
# deprecation inversion the report-kpis route exists to prevent. So when src
|
|
77
|
+
# exists AND is newer than dist (`-nt`, available on bash 3.2), prefer the src
|
|
78
|
+
# form so dev runs never execute a stale dispatcher. Released channels have no
|
|
79
|
+
# src, so they keep using dist unchanged.
|
|
80
|
+
if [ -f "$REPO_ROOT/loki-ts/src/cli.ts" ] && [ "$REPO_ROOT/loki-ts/src/cli.ts" -nt "$REPO_ROOT/loki-ts/dist/loki.js" ]; then
|
|
81
|
+
BUN_CLI="$REPO_ROOT/loki-ts/src/cli.ts"
|
|
82
|
+
fi
|
|
68
83
|
elif [ -f "$REPO_ROOT/loki-ts/src/cli.ts" ]; then
|
|
69
84
|
BUN_CLI="$REPO_ROOT/loki-ts/src/cli.ts"
|
|
70
85
|
else
|
|
@@ -112,6 +127,62 @@ if ! command -v bun &>/dev/null; then
|
|
|
112
127
|
exec "$BASH_CLI" "$@"
|
|
113
128
|
fi
|
|
114
129
|
|
|
130
|
+
# CLI consolidation (Phase A): `trust detail` is the grouped form of the
|
|
131
|
+
# trust-metrics breakdown. The Bun `trust` handler only knows the trajectory
|
|
132
|
+
# view (it rejects unknown args), and Phase A moves no handler logic, so the
|
|
133
|
+
# `detail` subcommand lives only in bash (cmd_trust -> cmd_trust_metrics).
|
|
134
|
+
# Force the bash route whenever `detail` appears as a trust arg (flag-anywhere:
|
|
135
|
+
# `trust detail`, `trust --json detail`, `trust detail --json` all resolve
|
|
136
|
+
# identically to the bash `trust-metrics` breakdown). Without this, only the
|
|
137
|
+
# exact `trust detail` order routed to bash and `trust --json detail` hit the
|
|
138
|
+
# Bun handler, which rejected `detail` to stderr while bash rejects to stdout --
|
|
139
|
+
# divergent error channels for the same malformed input. Bare `trust` /
|
|
140
|
+
# `trust --json` (no `detail`) stay on the Bun-native trajectory path above.
|
|
141
|
+
if [ "${1:-}" = "trust" ]; then
|
|
142
|
+
for _trust_arg in "${@:2}"; do
|
|
143
|
+
if [ "$_trust_arg" = "detail" ]; then
|
|
144
|
+
exec "$BASH_CLI" "$@"
|
|
145
|
+
fi
|
|
146
|
+
done
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# CLI consolidation (Phase B): `report kpis` is the canonical form of the
|
|
150
|
+
# Bun-only KPI snapshot. The `report` noun is otherwise bash-owned (every other
|
|
151
|
+
# subcommand -- session/metrics/cost/export/share/dogfood -- routes to bash
|
|
152
|
+
# cmd_report), so it is NOT in the Bun allowlist below. But kpis has no bash
|
|
153
|
+
# implementation (it reuses the canonical cost arithmetic in runner/budget.ts),
|
|
154
|
+
# so the canonical `report kpis` must reach the Bun handler -- otherwise the
|
|
155
|
+
# canonical form would print the honest "requires Bun" message on a Bun machine
|
|
156
|
+
# while the deprecated `kpis` alias actually worked, inverting the deprecation.
|
|
157
|
+
# Route `report kpis` to Bun when `kpis` is the report SUBCOMMAND, i.e. the FIRST
|
|
158
|
+
# non-flag token after `report`. This satisfies `report kpis`, `report kpis
|
|
159
|
+
# --json`, and `report --json kpis` (the flag-anywhere orderings the trust-detail
|
|
160
|
+
# precedent established), while NOT hijacking `kpis` when it appears as a
|
|
161
|
+
# positional VALUE of a different report subcommand. `report export json kpis`
|
|
162
|
+
# (kpis = the export output filename) must keep working exactly as on main
|
|
163
|
+
# (v7.31.0): exit 0, file `kpis` created. So we scan past leading flags, take the
|
|
164
|
+
# first real token, and only route to Bun if it is literally `kpis`. Fire the
|
|
165
|
+
# same cli_command telemetry the Bun case-arm below fires (command=report), so a
|
|
166
|
+
# Bun-routed `report kpis` is not invisible to usage analytics (the v7.8.2 parity
|
|
167
|
+
# the case-arm comment documents). Backgrounded, FD-detached, opt-out honored by
|
|
168
|
+
# loki_telemetry itself.
|
|
169
|
+
if [ "${1:-}" = "report" ]; then
|
|
170
|
+
_report_first_sub=""
|
|
171
|
+
for _report_arg in "${@:2}"; do
|
|
172
|
+
case "$_report_arg" in
|
|
173
|
+
-*) continue ;;
|
|
174
|
+
*) _report_first_sub="$_report_arg"; break ;;
|
|
175
|
+
esac
|
|
176
|
+
done
|
|
177
|
+
if [ "$_report_first_sub" = "kpis" ]; then
|
|
178
|
+
if command -v curl &>/dev/null && [ -f "$REPO_ROOT/autonomy/telemetry.sh" ]; then
|
|
179
|
+
( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null && loki_telemetry "cli_command" "command=${1:-}" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
|
|
180
|
+
disown 2>/dev/null || true
|
|
181
|
+
fi
|
|
182
|
+
exec bun "$BUN_CLI" "$@"
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
|
|
115
186
|
# Commands ported in Phase 2 -- route to Bun. Everything else goes to bash.
|
|
116
187
|
# Two-token routes (provider show/list, memory list/index) match on the first
|
|
117
188
|
# token only; the Bun dispatcher handles subcommand routing internally.
|