loki-mode 7.30.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 +4 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/context-tracker.py +8 -0
- package/autonomy/loki +462 -105
- package/autonomy/mcp-launch.sh +124 -22
- 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/replit-lovable-analysis.md +1 -1
- 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 +14 -129
- 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/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
|
|
|
@@ -109,10 +129,13 @@ _ml_sdk_importable() {
|
|
|
109
129
|
# requirements.txt is shipped under the install root.
|
|
110
130
|
_ml_print_manual() {
|
|
111
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.
|
|
112
135
|
printf 'Install the MCP server dependencies manually:\n' >&2
|
|
113
|
-
printf
|
|
114
|
-
printf '
|
|
115
|
-
printf
|
|
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
|
|
116
139
|
}
|
|
117
140
|
|
|
118
141
|
_ml_help() {
|
|
@@ -135,27 +158,72 @@ Options:
|
|
|
135
158
|
--help, -h Show this help and exit.
|
|
136
159
|
|
|
137
160
|
Environment:
|
|
138
|
-
LOKI_MCP_VENV=/abs/path
|
|
139
|
-
LOKI_NO_INSTALL_OFFER=1
|
|
140
|
-
|
|
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).
|
|
141
178
|
|
|
142
|
-
|
|
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
|
|
143
185
|
install command to stderr and exits 2.
|
|
144
186
|
EOF
|
|
145
187
|
}
|
|
146
188
|
|
|
147
189
|
# mcp_launch_main: dispatcher invoked by cmd_mcp() (autonomy/loki) or directly.
|
|
148
190
|
mcp_launch_main() {
|
|
149
|
-
#
|
|
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).
|
|
150
197
|
local arg
|
|
198
|
+
local _ml_server_argv=()
|
|
199
|
+
local _ml_after_sep=0
|
|
151
200
|
for arg in "$@"; do
|
|
201
|
+
if [ "$_ml_after_sep" -eq 1 ]; then
|
|
202
|
+
_ml_server_argv+=("$arg")
|
|
203
|
+
continue
|
|
204
|
+
fi
|
|
152
205
|
case "$arg" in
|
|
206
|
+
--)
|
|
207
|
+
_ml_after_sep=1
|
|
208
|
+
;;
|
|
153
209
|
--help|-h|help)
|
|
154
210
|
_ml_help
|
|
155
211
|
return 0
|
|
156
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
|
+
;;
|
|
157
221
|
esac
|
|
158
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[@]}"}
|
|
159
227
|
|
|
160
228
|
local root
|
|
161
229
|
root="$(_ml_repo_root)"
|
|
@@ -175,6 +243,17 @@ mcp_launch_main() {
|
|
|
175
243
|
local venv="${LOKI_MCP_VENV:-$PWD/${LOKI_DIR:-.loki}/mcp-venv}"
|
|
176
244
|
local venv_py="$venv/bin/python"
|
|
177
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
|
+
|
|
178
257
|
# 3. If the venv already has the SDK, use it directly. The server is launched
|
|
179
258
|
# with PYTHONPATH=$root (NOT by cd-ing) so the user's cwd is preserved for
|
|
180
259
|
# .loki resolution; see _ml_sdk_importable for why.
|
|
@@ -193,21 +272,40 @@ mcp_launch_main() {
|
|
|
193
272
|
fi
|
|
194
273
|
|
|
195
274
|
# 5. SDK missing. Decide whether we may bootstrap.
|
|
196
|
-
|
|
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
|
|
197
281
|
printf '%sMCP SDK not installed.%s\n' "$_ML_YELLOW" "$_ML_NC" >&2
|
|
198
282
|
_ml_print_manual "$root" "$venv"
|
|
199
283
|
return 2
|
|
200
284
|
fi
|
|
201
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
|
|
202
289
|
if _ml_non_interactive; then
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
206
301
|
fi
|
|
207
302
|
|
|
208
|
-
# 6.
|
|
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.
|
|
209
307
|
local answer=""
|
|
210
|
-
if _ml_assume_yes; then
|
|
308
|
+
if [ "$auto_consent" -eq 1 ] || _ml_truthy "${LOKI_MCP_AUTO_BOOTSTRAP:-}" || _ml_assume_yes; then
|
|
211
309
|
answer="y"
|
|
212
310
|
else
|
|
213
311
|
printf '\n'
|
|
@@ -239,8 +337,10 @@ mcp_launch_main() {
|
|
|
239
337
|
_ml_print_manual "$root" "$venv"
|
|
240
338
|
return 2
|
|
241
339
|
fi
|
|
242
|
-
printf 'Creating virtualenv (%s) ...\n' "$venv"
|
|
243
|
-
|
|
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
|
|
244
344
|
printf '%sFailed to create virtualenv at %s.%s\n' "$_ML_RED" "$venv" "$_ML_NC" >&2
|
|
245
345
|
_ml_print_manual "$root" "$venv"
|
|
246
346
|
return 2
|
|
@@ -253,9 +353,11 @@ mcp_launch_main() {
|
|
|
253
353
|
printf '%smcp/requirements.txt not found at %s.%s\n' "$_ML_RED" "$req" "$_ML_NC" >&2
|
|
254
354
|
return 2
|
|
255
355
|
fi
|
|
256
|
-
printf 'Installing MCP dependencies (%s/bin/pip install -r %s) ...\n' "$venv" "$req"
|
|
356
|
+
printf 'Installing MCP dependencies (%s/bin/pip install -r %s) ...\n' "$venv" "$req" >&"$out_fd"
|
|
257
357
|
local code=0
|
|
258
|
-
|
|
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=$?
|
|
259
361
|
if [ "$code" -ne 0 ]; then
|
|
260
362
|
printf '%sInstall failed (pip exited %s).%s You can retry manually:\n' "$_ML_RED" "$code" "$_ML_NC" >&2
|
|
261
363
|
_ml_print_manual "$root" "$venv"
|
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.
|