loki-mode 7.108.0 → 7.110.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/run.sh +12 -3
- package/autonomy/verify.sh +101 -19
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +48 -3
- package/docs/INSTALLATION.md +1 -1
- package/docs/TOP-100-BACKLOG.md +37 -0
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +94 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/codex.sh +13 -5
- package/providers/models.sh +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.110.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|
|
411
|
-
**v7.
|
|
411
|
+
**v7.110.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.110.0
|
package/autonomy/run.sh
CHANGED
|
@@ -12200,8 +12200,11 @@ parse_claude_reset_time() {
|
|
|
12200
12200
|
local current_min=$(date +%M)
|
|
12201
12201
|
local current_sec=$(date +%S)
|
|
12202
12202
|
|
|
12203
|
-
# Calculate seconds until reset
|
|
12204
|
-
|
|
12203
|
+
# Calculate seconds until reset. Force base-10 (10#) on the zero-padded
|
|
12204
|
+
# date values: 08/09 are invalid octal, so bare arithmetic aborts ("value
|
|
12205
|
+
# too great for base") during those clock windows and silently discards the
|
|
12206
|
+
# real rate-limit reset wait, falling back to a too-short generic backoff.
|
|
12207
|
+
local current_secs=$((10#$current_hour * 3600 + 10#$current_min * 60 + 10#$current_sec))
|
|
12205
12208
|
local reset_secs=$((hour * 3600))
|
|
12206
12209
|
|
|
12207
12210
|
local wait_secs=$((reset_secs - current_secs))
|
|
@@ -12634,8 +12637,14 @@ except Exception:
|
|
|
12634
12637
|
# Return the payload on stdout
|
|
12635
12638
|
printf '%s\n' "$payload"
|
|
12636
12639
|
|
|
12637
|
-
# Consume the signal (next iteration would otherwise re-trigger)
|
|
12640
|
+
# Consume the signal (next iteration would otherwise re-trigger).
|
|
12641
|
+
# Also remove the fallback if it coexists: TASK_COMPLETION_CLAIMED and
|
|
12642
|
+
# COMPLETION_REQUESTED are both valid, non-exclusive completion mechanisms, so
|
|
12643
|
+
# a belt-and-suspenders agent can leave both present. Removing only the active
|
|
12644
|
+
# one orphans the other, which then reads as a phantom claim on a later
|
|
12645
|
+
# iteration and forces every-iteration council evaluation. Consume both.
|
|
12638
12646
|
rm -f "$signal_file" 2>/dev/null
|
|
12647
|
+
rm -f "$fallback_file" 2>/dev/null
|
|
12639
12648
|
return 0
|
|
12640
12649
|
}
|
|
12641
12650
|
|
package/autonomy/verify.sh
CHANGED
|
@@ -830,14 +830,22 @@ PYEOF
|
|
|
830
830
|
if grep -qE '"(express|fastify|koa|hapi|@hapi/hapi|next|nuxt|@nestjs/core|@nestjs/platform-express|http-server|connect|restify|polka|@sveltejs/kit|vite)"[[:space:]]*:' "$dir/package.json" 2>/dev/null; then
|
|
831
831
|
_node_http_signal="dep"
|
|
832
832
|
fi
|
|
833
|
-
# (b) a
|
|
834
|
-
#
|
|
835
|
-
#
|
|
836
|
-
#
|
|
833
|
+
# (b) a STRONG server-creation call in a shallow scan of JS/TS sources.
|
|
834
|
+
# "Strong" means a module-qualified server constructor (http/https/
|
|
835
|
+
# http2/net .createServer, or Bun.serve/Deno.serve) -- NOT a bare
|
|
836
|
+
# `.listen(` which is common in tests (`http.createServer().listen(0)`)
|
|
837
|
+
# and unrelated code. Test/example/build dirs are excluded so an
|
|
838
|
+
# incidental server in a test never marks a CLI as bootable-HTTP.
|
|
839
|
+
# Uses grep -q (a boolean, no pipe) so this is safe under
|
|
840
|
+
# set -o pipefail (a piped `| head` would SIGPIPE grep and, under
|
|
841
|
+
# pipefail, drop the result).
|
|
837
842
|
if [ -z "$_node_http_signal" ]; then
|
|
838
|
-
if grep -rqE '
|
|
843
|
+
if grep -rqE 'https?\.createServer|http2\.createServer|net\.createServer|Bun\.serve\(|Deno\.serve\(' \
|
|
839
844
|
"$dir" --include='*.js' --include='*.ts' --include='*.mjs' --include='*.cjs' \
|
|
840
|
-
--exclude-dir=node_modules --exclude-dir=.git
|
|
845
|
+
--exclude-dir=node_modules --exclude-dir=.git \
|
|
846
|
+
--exclude-dir=test --exclude-dir=tests --exclude-dir=__tests__ \
|
|
847
|
+
--exclude-dir=examples --exclude-dir=example \
|
|
848
|
+
--exclude-dir=dist --exclude-dir=build --exclude-dir=spec 2>/dev/null; then
|
|
841
849
|
_node_http_signal="src"
|
|
842
850
|
fi
|
|
843
851
|
fi
|
|
@@ -964,6 +972,11 @@ verify_gate_runtime() {
|
|
|
964
972
|
local deadline=$(( $(date +%s) + boot_timeout ))
|
|
965
973
|
local have_curl="false"
|
|
966
974
|
command -v curl >/dev/null 2>&1 && have_curl="true"
|
|
975
|
+
# The port we actually probe. Starts at the guessed default; if the boot log
|
|
976
|
+
# announces a different bound port (e.g. Vite on 5173, which ignores PORT and
|
|
977
|
+
# the default map cannot know), we re-point the probe THERE. scraped_port is
|
|
978
|
+
# stashed so teardown can also reclaim it (fixes the different-port leak).
|
|
979
|
+
local probe_port="$port" scraped_port=""
|
|
967
980
|
|
|
968
981
|
while [ "$(date +%s)" -lt "$deadline" ]; do
|
|
969
982
|
# If the app process already exited, stop polling (it failed to stay up).
|
|
@@ -971,6 +984,15 @@ verify_gate_runtime() {
|
|
|
971
984
|
# Give one last probe in case it forked a daemon and exited.
|
|
972
985
|
:
|
|
973
986
|
fi
|
|
987
|
+
# Scrape the actually-bound port from the boot log (progressively filled).
|
|
988
|
+
# Only re-point when it differs from the current probe target.
|
|
989
|
+
if [ -z "$scraped_port" ]; then
|
|
990
|
+
scraped_port="$(_verify_runtime_scrape_port "$boot_log" 2>/dev/null || true)"
|
|
991
|
+
if [ -n "$scraped_port" ] && [ "$scraped_port" != "$probe_port" ]; then
|
|
992
|
+
probe_port="$scraped_port"
|
|
993
|
+
url="http://127.0.0.1:${probe_port}${health_path}"
|
|
994
|
+
fi
|
|
995
|
+
fi
|
|
974
996
|
if [ "$have_curl" = "true" ]; then
|
|
975
997
|
http_status="$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$url" 2>/dev/null || echo "")"
|
|
976
998
|
if [ -n "$http_status" ] && [ "$http_status" != "000" ]; then
|
|
@@ -980,7 +1002,7 @@ verify_gate_runtime() {
|
|
|
980
1002
|
else
|
|
981
1003
|
# Portable fallback: raw TCP GET via bash /dev/tcp (best effort).
|
|
982
1004
|
local resp=""
|
|
983
|
-
resp="$(_verify_runtime_raw_probe "$
|
|
1005
|
+
resp="$(_verify_runtime_raw_probe "$probe_port" "$health_path" 2>/dev/null || true)"
|
|
984
1006
|
if [ -n "$resp" ]; then
|
|
985
1007
|
http_status="$resp"
|
|
986
1008
|
answered="true"
|
|
@@ -1007,7 +1029,10 @@ verify_gate_runtime() {
|
|
|
1007
1029
|
fi
|
|
1008
1030
|
|
|
1009
1031
|
# Teardown: kill the launcher and any child it spawned. Best effort; bounded.
|
|
1010
|
-
|
|
1032
|
+
# Reclaim BOTH the detected port and the actually-bound port (scraped from the
|
|
1033
|
+
# boot log) so a server that daemonized onto a different port than we guessed
|
|
1034
|
+
# does not leak. scraped_port may be empty (no banner) -- teardown handles it.
|
|
1035
|
+
_verify_runtime_teardown "$app_pid" "$port" "$scraped_port"
|
|
1011
1036
|
|
|
1012
1037
|
# Interpret the result.
|
|
1013
1038
|
if [ "$answered" != "true" ]; then
|
|
@@ -1073,6 +1098,37 @@ _verify_runtime_raw_probe() {
|
|
|
1073
1098
|
printf '%s' "$line" | grep -oE 'HTTP/[0-9.]+ [0-9]{3}' | grep -oE '[0-9]{3}$' || return 1
|
|
1074
1099
|
}
|
|
1075
1100
|
|
|
1101
|
+
# Scrape the actually-bound localhost port from a dev server's boot log. Dev
|
|
1102
|
+
# servers (Vite, SvelteKit, Nuxt, Next, CRA, ...) print a "Local:" / "listening
|
|
1103
|
+
# on" banner with their real port, which is frequently NOT the guessed default
|
|
1104
|
+
# (bare Vite binds 5173 and ignores PORT). We parse ONLY a localhost/127.0.0.1
|
|
1105
|
+
# bind announcement so we never lock onto an unrelated outbound URL the app might
|
|
1106
|
+
# have logged (which some other live process could answer -> false green). ANSI
|
|
1107
|
+
# color codes are stripped first (dev banners are colorized). Echoes the first
|
|
1108
|
+
# matched port, or nothing. Bounded: reads only the given log file, no network.
|
|
1109
|
+
_verify_runtime_scrape_port() {
|
|
1110
|
+
local log="$1"
|
|
1111
|
+
[ -f "$log" ] || return 1
|
|
1112
|
+
# Strip ANSI escapes first (dev banners are colorized). Then, line by line,
|
|
1113
|
+
# match a localhost/127.0.0.1 bind announcement and extract the PORT that
|
|
1114
|
+
# sits at the end of the match (the number after the final colon / after
|
|
1115
|
+
# "port"), never an IP octet. Emit the first port found.
|
|
1116
|
+
local line p
|
|
1117
|
+
while IFS= read -r line; do
|
|
1118
|
+
# A URL like http://localhost:5173 or http://127.0.0.1:5173/ .
|
|
1119
|
+
p="$(printf '%s' "$line" | grep -oiE 'https?://(localhost|127\.0\.0\.1):[0-9]{2,5}' | grep -oE ':[0-9]{2,5}' | grep -oE '[0-9]{2,5}' | head -1)"
|
|
1120
|
+
# Or a "listening on [127.0.0.1:]5173" / "listening on port 5173" phrase.
|
|
1121
|
+
if [ -z "$p" ]; then
|
|
1122
|
+
p="$(printf '%s' "$line" | grep -oiE 'listening on( port)?[[:space:]:]*([0-9.]+:)?[0-9]{2,5}' | grep -oE '[0-9]{2,5}$' | head -1)"
|
|
1123
|
+
fi
|
|
1124
|
+
if [ -n "$p" ]; then
|
|
1125
|
+
printf '%s' "$p"
|
|
1126
|
+
return 0
|
|
1127
|
+
fi
|
|
1128
|
+
done < <(sed -E $'s/\033\\[[0-9;?]*[A-Za-z]//g' "$log" 2>/dev/null)
|
|
1129
|
+
return 1
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1076
1132
|
# Optional screenshot via the playwright inline smoke script (reused contract:
|
|
1077
1133
|
# url screenshot results timeout). Bounded by the same timeout wrapper. Returns
|
|
1078
1134
|
# 0 if the screenshot file was produced, nonzero otherwise. Never fatal.
|
|
@@ -1110,30 +1166,56 @@ SMOKE_SCRIPT
|
|
|
1110
1166
|
# Teardown: terminate the boot launcher and any process still holding the port.
|
|
1111
1167
|
# Bounded and best effort; never blocks the gate.
|
|
1112
1168
|
_verify_runtime_teardown() {
|
|
1113
|
-
local app_pid="$1" port="$2"
|
|
1169
|
+
local app_pid="$1" port="$2" scraped_port="${3:-}"
|
|
1114
1170
|
if [ -n "$app_pid" ]; then
|
|
1115
|
-
#
|
|
1171
|
+
# Best: kill the launcher's whole process GROUP so a server it spawned in
|
|
1172
|
+
# a subshell (e.g. `vite &`) dies too. GUARDED against self-suicide: in a
|
|
1173
|
+
# non-interactive script job control is off, so a background job can share
|
|
1174
|
+
# the verifier's OWN process group; a negative-PID kill there would kill
|
|
1175
|
+
# us (and our parent). Only group-kill when the child's PGID differs from
|
|
1176
|
+
# ours AND equals the child PID (i.e. it is a real group leader).
|
|
1177
|
+
local child_pgid="" self_pgid=""
|
|
1178
|
+
child_pgid="$(ps -o pgid= -p "$app_pid" 2>/dev/null | tr -d ' ' || true)"
|
|
1179
|
+
self_pgid="$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ' || true)"
|
|
1180
|
+
if [ -n "$child_pgid" ] && [ "$child_pgid" != "$self_pgid" ] \
|
|
1181
|
+
&& [ "$child_pgid" = "$app_pid" ]; then
|
|
1182
|
+
kill -- -"$child_pgid" 2>/dev/null || true
|
|
1183
|
+
fi
|
|
1184
|
+
# Always TERM the launcher PID itself.
|
|
1116
1185
|
kill "$app_pid" 2>/dev/null || true
|
|
1117
1186
|
# Kill any direct children (the timeout wrapper spawns sh -c "$method").
|
|
1118
1187
|
if command -v pkill >/dev/null 2>&1; then
|
|
1119
1188
|
pkill -P "$app_pid" 2>/dev/null || true
|
|
1120
1189
|
fi
|
|
1121
|
-
# Give it a moment, then hard-kill.
|
|
1190
|
+
# Give it a moment, then hard-kill (PID, and group again when safe).
|
|
1122
1191
|
local i=0
|
|
1123
1192
|
while [ "$i" -lt 3 ] && kill -0 "$app_pid" 2>/dev/null; do
|
|
1124
1193
|
sleep 1; i=$((i + 1))
|
|
1125
1194
|
done
|
|
1195
|
+
if [ -n "$child_pgid" ] && [ "$child_pgid" != "$self_pgid" ] \
|
|
1196
|
+
&& [ "$child_pgid" = "$app_pid" ]; then
|
|
1197
|
+
kill -9 -- -"$child_pgid" 2>/dev/null || true
|
|
1198
|
+
fi
|
|
1126
1199
|
kill -9 "$app_pid" 2>/dev/null || true
|
|
1127
1200
|
fi
|
|
1128
|
-
# Reclaim the port
|
|
1201
|
+
# Reclaim BOTH the detected port and the actually-bound (scraped) port from
|
|
1202
|
+
# any orphan that outlived the launcher -- a daemonized server that bound a
|
|
1203
|
+
# different port than we guessed would otherwise leak. Bounded, best effort.
|
|
1129
1204
|
if command -v lsof >/dev/null 2>&1; then
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1205
|
+
# Build a unique, non-empty port list (scraped only if it differs).
|
|
1206
|
+
local _ports="$port"
|
|
1207
|
+
[ -n "$scraped_port" ] && [ "$scraped_port" != "$port" ] && _ports="$_ports $scraped_port"
|
|
1208
|
+
local _rp
|
|
1209
|
+
for _rp in $_ports; do
|
|
1210
|
+
[ -z "$_rp" ] && continue
|
|
1211
|
+
local holders
|
|
1212
|
+
holders="$(lsof -ti tcp:"$_rp" 2>/dev/null || true)"
|
|
1213
|
+
if [ -n "$holders" ]; then
|
|
1214
|
+
printf '%s\n' "$holders" | while IFS= read -r pid; do
|
|
1215
|
+
[ -n "$pid" ] && kill -9 "$pid" 2>/dev/null || true
|
|
1216
|
+
done
|
|
1217
|
+
fi
|
|
1218
|
+
done
|
|
1137
1219
|
fi
|
|
1138
1220
|
}
|
|
1139
1221
|
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -1180,6 +1180,16 @@ async def get_status() -> StatusResponse:
|
|
|
1180
1180
|
if state_file.exists():
|
|
1181
1181
|
try:
|
|
1182
1182
|
state = _safe_json_read(state_file, {})
|
|
1183
|
+
# dashboard-state.json is agent/user-written and, under atomic-write
|
|
1184
|
+
# races or hand edits, its top level can be a list/string/number
|
|
1185
|
+
# rather than an object. state.get(...) would then raise
|
|
1186
|
+
# AttributeError, which the surrounding (JSONDecodeError, KeyError)
|
|
1187
|
+
# handler does NOT catch -> /api/status 500s and blanks the board.
|
|
1188
|
+
# Coerce to {} so a bad shape degrades to the idle payload. Placed
|
|
1189
|
+
# BEFORE the _has_dashboard_state flag so a truthy non-dict (e.g.
|
|
1190
|
+
# [1,2,3]) cannot flip it on with corrupt data.
|
|
1191
|
+
if not isinstance(state, dict):
|
|
1192
|
+
state = {}
|
|
1183
1193
|
if state:
|
|
1184
1194
|
_has_dashboard_state = True
|
|
1185
1195
|
phase = state.get("phase", "")
|
|
@@ -1188,8 +1198,15 @@ async def get_status() -> StatusResponse:
|
|
|
1188
1198
|
mode = state.get("mode", "")
|
|
1189
1199
|
# Count only agents with alive PIDs (not raw array length)
|
|
1190
1200
|
agents_list = state.get("agents", [])
|
|
1201
|
+
if not isinstance(agents_list, list):
|
|
1202
|
+
agents_list = []
|
|
1191
1203
|
running_agents = 0
|
|
1192
1204
|
for agent in agents_list:
|
|
1205
|
+
# agents entries can be bare strings/None in malformed state;
|
|
1206
|
+
# agent.get(...) would raise AttributeError. Skip non-dicts.
|
|
1207
|
+
if not isinstance(agent, dict):
|
|
1208
|
+
running_agents += 1 # legacy/opaque entry -> count as running
|
|
1209
|
+
continue
|
|
1193
1210
|
agent_pid = agent.get("pid")
|
|
1194
1211
|
if agent_pid:
|
|
1195
1212
|
try:
|
|
@@ -1202,10 +1219,17 @@ async def get_status() -> StatusResponse:
|
|
|
1202
1219
|
running_agents += 1
|
|
1203
1220
|
|
|
1204
1221
|
tasks = state.get("tasks", {})
|
|
1205
|
-
|
|
1222
|
+
if not isinstance(tasks, dict):
|
|
1223
|
+
tasks = {}
|
|
1224
|
+
_pending = tasks.get("pending", [])
|
|
1225
|
+
pending_tasks = len(_pending) if isinstance(_pending, list) else 0
|
|
1206
1226
|
in_progress = tasks.get("inProgress", [])
|
|
1207
|
-
if in_progress:
|
|
1208
|
-
|
|
1227
|
+
if not isinstance(in_progress, list):
|
|
1228
|
+
in_progress = []
|
|
1229
|
+
if in_progress and isinstance(in_progress[0], dict):
|
|
1230
|
+
_payload = in_progress[0].get("payload", {})
|
|
1231
|
+
if isinstance(_payload, dict):
|
|
1232
|
+
current_task = _payload.get("action", "")
|
|
1209
1233
|
except (json.JSONDecodeError, KeyError):
|
|
1210
1234
|
pass
|
|
1211
1235
|
|
|
@@ -1783,6 +1807,13 @@ async def list_tasks(
|
|
|
1783
1807
|
if state_file.exists():
|
|
1784
1808
|
try:
|
|
1785
1809
|
state = json.loads(state_file.read_text())
|
|
1810
|
+
# dashboard-state.json top level can itself be a list/string/number
|
|
1811
|
+
# (agent/user-written, atomic-write race). state.get(...) would then
|
|
1812
|
+
# raise AttributeError BEFORE the task_groups guard below, and the
|
|
1813
|
+
# surrounding (JSONDecodeError, KeyError) handler does not catch it
|
|
1814
|
+
# -> /api/tasks 500s and blanks the board. Coerce state first.
|
|
1815
|
+
if not isinstance(state, dict):
|
|
1816
|
+
state = {}
|
|
1786
1817
|
task_groups = state.get("tasks", {})
|
|
1787
1818
|
# v7.104.4: dashboard-state.json is user/agent-written and can be
|
|
1788
1819
|
# malformed or partially written. If "tasks" is not a dict, or a
|
|
@@ -6621,6 +6652,11 @@ def _compute_cost_snapshot() -> dict:
|
|
|
6621
6652
|
if budget_file.exists():
|
|
6622
6653
|
try:
|
|
6623
6654
|
budget_data = json.loads(budget_file.read_text())
|
|
6655
|
+
# budget.json can be a bare number/list/string; guard before .get()
|
|
6656
|
+
# so the cost-summary endpoint degrades instead of raising an
|
|
6657
|
+
# AttributeError uncaught by (JSONDecodeError, KeyError).
|
|
6658
|
+
if not isinstance(budget_data, dict):
|
|
6659
|
+
budget_data = {}
|
|
6624
6660
|
budget_limit = budget_data.get("limit")
|
|
6625
6661
|
if budget_limit is not None:
|
|
6626
6662
|
budget_used = estimated_cost
|
|
@@ -6664,6 +6700,12 @@ async def get_budget():
|
|
|
6664
6700
|
if budget_file.exists():
|
|
6665
6701
|
try:
|
|
6666
6702
|
budget_data = json.loads(budget_file.read_text())
|
|
6703
|
+
# budget.json can be a bare number/list/string (agent/user-written
|
|
6704
|
+
# or atomic-write race), so budget_data.get(...) would raise
|
|
6705
|
+
# AttributeError, uncaught by (JSONDecodeError, KeyError) -> 500.
|
|
6706
|
+
# Coerce to {} so the endpoint degrades to no-limit defaults.
|
|
6707
|
+
if not isinstance(budget_data, dict):
|
|
6708
|
+
budget_data = {}
|
|
6667
6709
|
budget_limit = budget_data.get("limit") or budget_data.get("budget_limit")
|
|
6668
6710
|
budget_used = budget_data.get("budget_used", 0.0)
|
|
6669
6711
|
exceeded = budget_data.get("exceeded", False)
|
|
@@ -6687,6 +6729,9 @@ async def get_budget():
|
|
|
6687
6729
|
if exceeded_at is None:
|
|
6688
6730
|
try:
|
|
6689
6731
|
sig_data = json.loads(signal_file.read_text())
|
|
6732
|
+
# Signal file may parse to a non-dict; guard before .get().
|
|
6733
|
+
if not isinstance(sig_data, dict):
|
|
6734
|
+
sig_data = {}
|
|
6690
6735
|
exceeded_at = sig_data.get("timestamp")
|
|
6691
6736
|
except (json.JSONDecodeError, KeyError):
|
|
6692
6737
|
pass
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.110.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/docs/TOP-100-BACKLOG.md
CHANGED
|
@@ -120,3 +120,40 @@ Generated 2026-07-01 by the top100-backlog-enumeration ultracode workflow (7 age
|
|
|
120
120
|
| 98 | low | S | autonomous | dashboard | Web-app docs: fill video placeholder (coming-soon text) |
|
|
121
121
|
| 99 | low | M | autonomous | cli | C# provider support (roslyn analyzers + dotnet build, deferred) |
|
|
122
122
|
| 100 | low | XL | founder-gated | engine | BMAD-METHOD v6 integration (net-new, needs founder planning) |
|
|
123
|
+
|
|
124
|
+
## Gate split (what can actually close autonomously) - 2026-07-01
|
|
125
|
+
|
|
126
|
+
The /goal asks to work through all 100. The honest structural reality: only a
|
|
127
|
+
subset is autonomously completable. The rest need a founder decision or are
|
|
128
|
+
community/CI-infra work that cannot be forced from this session.
|
|
129
|
+
|
|
130
|
+
- **Autonomous (I can execute end-to-end): ~25 items** (the `gate=autonomous` rows).
|
|
131
|
+
- **Founder-gated (blocked on a decision only the founder can make): ~35 items.**
|
|
132
|
+
Licensing (#5), hosted deploy target (#6, #23, #36), enterprise RBAC/SSO (#15),
|
|
133
|
+
sandbox backend choice (#14, #42), spend (#49 SWE-bench, #18 live-model), npm
|
|
134
|
+
publish + brand (#4, #13, #61), telemetry (#62), GitHub App (#46). Prepped to
|
|
135
|
+
one-approval-away where possible; CANNOT close without the founder.
|
|
136
|
+
- **Community / CI-infra (~40 items):** flake root-causes, coverage thresholds,
|
|
137
|
+
platform runners (ARM64/Windows), distribution resilience. Environmental
|
|
138
|
+
hardening, not single-session feature work.
|
|
139
|
+
|
|
140
|
+
### Shipped this session (accuracy-moat, from the competitive gap-analysis)
|
|
141
|
+
|
|
142
|
+
Net-new accuracy work driven from docs/research-2026-07/gap-analysis-backlog.json
|
|
143
|
+
(a companion list), NOT retroactive Top-100 claims:
|
|
144
|
+
|
|
145
|
+
- v7.105.0 - convergence: council evaluates on completion-claim (4.0x faster, n=3). commit b8a368a0.
|
|
146
|
+
- v7.106.0 - reverse-classical test provenance (tautological tests downgraded). commit 71299faa.
|
|
147
|
+
- v7.107.0 - loki mcp --transport http loopback bind + bearer auth (+ latent crash fix). commit f3aa523c.
|
|
148
|
+
- v7.108.0 - runtime boot smoke gate + annotate-before-act expectation ledger. commit 806fc374.
|
|
149
|
+
|
|
150
|
+
### Top-100 items with a concrete shipped commit
|
|
151
|
+
|
|
152
|
+
- #40 (dashboard memory summary on JSON-backed projects) - commit 330de52d.
|
|
153
|
+
- #22 (3 parallel code reviewers) - already present in the engine (council path).
|
|
154
|
+
|
|
155
|
+
### Next autonomous batch (by value, unblocked, user-visible)
|
|
156
|
+
|
|
157
|
+
Picked from the `autonomous` rows by value + real-user impact (not primitives that
|
|
158
|
+
ship dormant). Verify each before starting; several may overlap the already-built
|
|
159
|
+
private autonomi-verify TS engine (e.g. #7/#8 - check before rebuilding).
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var X8=Object.defineProperty;var K8=($)=>$;function q8($,Q){this[$]=K8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)X8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:q8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var v1={};b(v1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as r,dirname as i$}from"path";import{fileURLToPath as J8}from"url";import{existsSync as x$}from"fs";import{homedir as V8}from"os";function W8(){let $=y1;for(let Q=0;Q<6;Q++){if(x$(r($,"VERSION"))&&x$(r($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return r(y1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(x$(r(Q,"VERSION"))&&x$(r(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return r($,"..","..","..")}function P(){return process.env.LOKI_DIR??r(process.cwd(),".loki")}function T$(){return r(V8(),".loki")}var y1,h;var C=L(()=>{y1=i$(J8(import.meta.url));h=W8()});import{readFileSync as U8}from"fs";import{resolve as H8,dirname as G8}from"path";import{fileURLToPath as B8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.
|
|
2
|
+
var X8=Object.defineProperty;var K8=($)=>$;function q8($,Q){this[$]=K8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)X8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:q8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var v1={};b(v1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as r,dirname as i$}from"path";import{fileURLToPath as J8}from"url";import{existsSync as x$}from"fs";import{homedir as V8}from"os";function W8(){let $=y1;for(let Q=0;Q<6;Q++){if(x$(r($,"VERSION"))&&x$(r($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return r(y1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(x$(r(Q,"VERSION"))&&x$(r(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return r($,"..","..","..")}function P(){return process.env.LOKI_DIR??r(process.cwd(),".loki")}function T$(){return r(V8(),".loki")}var y1,h;var C=L(()=>{y1=i$(J8(import.meta.url));h=W8()});import{readFileSync as U8}from"fs";import{resolve as H8,dirname as G8}from"path";import{fileURLToPath as B8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.110.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=G8(B8(import.meta.url)),Z=e$(Q);Q$=U8(H8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var $1=L(()=>{C()});var c1={};b(c1,{runOrThrow:()=>x8,run:()=>F,readStreamCapped:()=>u1,commandVersion:()=>S8,commandExists:()=>f,ShellError:()=>Q1,MAX_STDOUT_BYTES:()=>f1});async function u1($,Q=f1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([u1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function x8($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new Q1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=N8($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function N8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function S8($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var f1=16777216,Q1;var d=L(()=>{Q1=class Q1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function t($){return D8?"":$}var D8,T,S,_,nZ,I,k,y,J;var c=L(()=>{D8=(process.env.NO_COLOR??"").length>0;T=t("\x1B[0;31m"),S=t("\x1B[0;32m"),_=t("\x1B[1;33m"),nZ=t("\x1B[0;34m"),I=t("\x1B[0;36m"),k=t("\x1B[1m"),y=t("\x1B[2m"),J=t("\x1B[0m")});import{existsSync as c8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(c8($))return A$=$,$;let Q=await f("python3.12");if(Q)return A$=Q,Q;let Z=await f("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var A$;var V$=L(()=>{d()});var J0={};b(J0,{runStatus:()=>U3});import{existsSync as v,readFileSync as U$,readdirSync as e1,statSync as $0}from"fs";import{resolve as D,basename as Q3}from"path";import{homedir as Z3}from"os";function Q0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function Z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*D$/Q);if(X>D$)X=D$;let q=D$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Q0($),U=Q0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${U})`}async function X3(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -805,4 +805,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
805
805
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
806
806
|
`),process.stderr.write(z8),2}}i1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var EZ=await RZ(Bun.argv.slice(2));process.exit(EZ);
|
|
807
807
|
|
|
808
|
-
//# debugId=
|
|
808
|
+
//# debugId=F98EFE21E86DB91C64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/mcp/lsp_proxy.py
CHANGED
|
@@ -1478,6 +1478,95 @@ def write_diagnostics_artifact(root: Optional[str] = None,
|
|
|
1478
1478
|
'count_warnings': count_warnings}
|
|
1479
1479
|
|
|
1480
1480
|
|
|
1481
|
+
def _run_http_transport(port: int) -> None:
|
|
1482
|
+
"""Run the LSP-proxy MCP server over Streamable HTTP, bound to loopback.
|
|
1483
|
+
|
|
1484
|
+
Mirrors mcp/server.py:_run_http_transport for consistency. FastMCP has no
|
|
1485
|
+
'http' transport literal (it uses 'streamable-http') and run() takes no
|
|
1486
|
+
port= kwarg, so the old mcp.run(transport='http', port=...) call never
|
|
1487
|
+
worked (TypeError). We build the Streamable-HTTP ASGI app ourselves, bind
|
|
1488
|
+
127.0.0.1 explicitly, and run it via uvicorn -- the same supported path.
|
|
1489
|
+
|
|
1490
|
+
Two hardening properties over a bare FastMCP HTTP launch:
|
|
1491
|
+
|
|
1492
|
+
1. Explicit loopback bind. We set host='127.0.0.1' ourselves and run the
|
|
1493
|
+
ASGI app via uvicorn rather than relying on any implicit default, so a
|
|
1494
|
+
reader of `lsof`/`ss` sees 127.0.0.1 unambiguously and a future SDK
|
|
1495
|
+
default change cannot silently widen the bind to 0.0.0.0.
|
|
1496
|
+
|
|
1497
|
+
2. Optional bearer-token auth. When LOKI_MCP_AUTH_TOKEN is set in the
|
|
1498
|
+
environment, a Starlette middleware requires every HTTP request to
|
|
1499
|
+
carry `Authorization: Bearer <token>` (rejecting mismatches with 401).
|
|
1500
|
+
When the env var is unset or empty, NO middleware is installed at all,
|
|
1501
|
+
so the request path is exactly the unauthenticated FastMCP behavior.
|
|
1502
|
+
|
|
1503
|
+
hasattr-guarded against the whole supported mcp 1.x range: if the installed
|
|
1504
|
+
SDK lacks streamable_http_app, we fail with a clear message rather than an
|
|
1505
|
+
AttributeError.
|
|
1506
|
+
"""
|
|
1507
|
+
if not hasattr(mcp, "streamable_http_app"):
|
|
1508
|
+
logger.error(
|
|
1509
|
+
"Installed MCP SDK does not support Streamable HTTP transport "
|
|
1510
|
+
"(no FastMCP.streamable_http_app). Upgrade the 'mcp' package "
|
|
1511
|
+
"(pip install -U mcp) or use the default stdio transport."
|
|
1512
|
+
)
|
|
1513
|
+
sys.exit(1)
|
|
1514
|
+
|
|
1515
|
+
host = "127.0.0.1"
|
|
1516
|
+
|
|
1517
|
+
# Pin host/port on the SDK settings too, so any code that reads them (and
|
|
1518
|
+
# the SDK's own transport-security allowed_hosts, which defaults to
|
|
1519
|
+
# 127.0.0.1/localhost) stays consistent with what uvicorn actually binds.
|
|
1520
|
+
try:
|
|
1521
|
+
if hasattr(mcp, "settings"):
|
|
1522
|
+
if hasattr(mcp.settings, "host"):
|
|
1523
|
+
mcp.settings.host = host
|
|
1524
|
+
if hasattr(mcp.settings, "port"):
|
|
1525
|
+
mcp.settings.port = port
|
|
1526
|
+
except Exception as _set_err: # pragma: no cover - defensive
|
|
1527
|
+
logger.warning("Could not pin MCP host/port settings: %s", _set_err)
|
|
1528
|
+
|
|
1529
|
+
app = mcp.streamable_http_app()
|
|
1530
|
+
|
|
1531
|
+
token = os.environ.get("LOKI_MCP_AUTH_TOKEN", "")
|
|
1532
|
+
if token:
|
|
1533
|
+
# Only import the middleware pieces when auth is actually requested, so
|
|
1534
|
+
# the unauthenticated path has zero new dependencies or behavior.
|
|
1535
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
1536
|
+
from starlette.responses import JSONResponse
|
|
1537
|
+
|
|
1538
|
+
expected_header = "Bearer " + token
|
|
1539
|
+
|
|
1540
|
+
async def _require_bearer(request, call_next):
|
|
1541
|
+
# BaseHTTPMiddleware auto-forwards non-http scopes (lifespan etc.),
|
|
1542
|
+
# so the StreamableHTTPSessionManager lifespan still starts.
|
|
1543
|
+
provided = request.headers.get("authorization", "")
|
|
1544
|
+
# Constant-time compare to avoid leaking the token via timing.
|
|
1545
|
+
import hmac
|
|
1546
|
+
if not hmac.compare_digest(provided, expected_header):
|
|
1547
|
+
return JSONResponse(
|
|
1548
|
+
{"error": "unauthorized"},
|
|
1549
|
+
status_code=401,
|
|
1550
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
1551
|
+
)
|
|
1552
|
+
return await call_next(request)
|
|
1553
|
+
|
|
1554
|
+
app.add_middleware(BaseHTTPMiddleware, dispatch=_require_bearer)
|
|
1555
|
+
logger.info(
|
|
1556
|
+
"MCP HTTP auth enabled: bearer token required "
|
|
1557
|
+
"(LOKI_MCP_AUTH_TOKEN is set)."
|
|
1558
|
+
)
|
|
1559
|
+
else:
|
|
1560
|
+
logger.info(
|
|
1561
|
+
"MCP HTTP auth disabled: no LOKI_MCP_AUTH_TOKEN set "
|
|
1562
|
+
"(loopback-only, unauthenticated)."
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1565
|
+
import uvicorn
|
|
1566
|
+
logger.info("MCP Streamable HTTP listening on http://%s:%d/mcp", host, port)
|
|
1567
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
1568
|
+
|
|
1569
|
+
|
|
1481
1570
|
def main() -> None:
|
|
1482
1571
|
import argparse
|
|
1483
1572
|
parser = argparse.ArgumentParser(
|
|
@@ -1536,7 +1625,11 @@ def main() -> None:
|
|
|
1536
1625
|
detected = _detect_lsps()
|
|
1537
1626
|
logger.info("Detected LSPs: %s", sorted(detected.keys()) or 'none')
|
|
1538
1627
|
if args.transport == 'http':
|
|
1539
|
-
|
|
1628
|
+
# Explicit loopback bind + optional bearer-token auth. FastMCP has no
|
|
1629
|
+
# 'http' transport literal (it uses 'streamable-http') and run() takes
|
|
1630
|
+
# no port= kwarg, so the old mcp.run(transport='http', port=...) call
|
|
1631
|
+
# never worked (TypeError); _run_http_transport is the supported path.
|
|
1632
|
+
_run_http_transport(args.port)
|
|
1540
1633
|
else:
|
|
1541
1634
|
mcp.run(transport='stdio')
|
|
1542
1635
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.110.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.
|
|
5
|
+
"version": "7.110.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|
package/providers/codex.sh
CHANGED
|
@@ -234,21 +234,29 @@ provider_invoke_with_tier() {
|
|
|
234
234
|
*) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
|
|
235
235
|
esac
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
# --search is a TOP-LEVEL `codex` flag, not a `codex exec` flag. On codex
|
|
238
|
+
# 0.141.0 `codex exec ... --search` aborts with "unexpected argument
|
|
239
|
+
# '--search' found", silently breaking LOKI_CODEX_WEB_SEARCH. It must be
|
|
240
|
+
# placed before the `exec` subcommand. --output-last-message (-o) IS an
|
|
241
|
+
# `codex exec` flag and stays after `exec`. Keep the two in separate arrays.
|
|
242
|
+
local pre_exec_flags=()
|
|
238
243
|
if [ "${LOKI_CODEX_WEB_SEARCH:-false}" = "true" ]; then
|
|
239
|
-
|
|
244
|
+
pre_exec_flags+=(--search)
|
|
240
245
|
fi
|
|
246
|
+
local extra_flags=()
|
|
241
247
|
if [ "${LOKI_CODEX_OUTPUT_LAST:-true}" != "false" ] && [ -n "${LOKI_LOG_FILE:-}" ]; then
|
|
242
248
|
extra_flags+=(--output-last-message "${LOKI_LOG_FILE}.last-message")
|
|
243
249
|
fi
|
|
244
250
|
|
|
245
251
|
LOKI_CODEX_REASONING_EFFORT="$effort" \
|
|
246
252
|
CODEX_MODEL_REASONING_EFFORT="$effort" \
|
|
247
|
-
# Guard the
|
|
248
|
-
#
|
|
253
|
+
# Guard the array expansions: with no web-search / output-last knobs an
|
|
254
|
+
# array is empty, and a bare "${arr[@]}" under `set -u` aborts with
|
|
249
255
|
# "unbound variable" on bash 3.2 (stock macOS /bin/bash). ${arr[@]+...}
|
|
250
256
|
# expands to nothing when empty and preserves spaced elements otherwise.
|
|
251
|
-
codex
|
|
257
|
+
codex \
|
|
258
|
+
"${pre_exec_flags[@]+"${pre_exec_flags[@]}"}" \
|
|
259
|
+
exec \
|
|
252
260
|
--sandbox workspace-write \
|
|
253
261
|
--skip-git-repo-check \
|
|
254
262
|
--model "$model" \
|
package/providers/models.sh
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Usage:
|
|
10
10
|
# source providers/models.sh
|
|
11
|
-
# model=$(loki_latest_model claude planning) # -> claude-opus-4-
|
|
11
|
+
# model=$(loki_latest_model claude planning) # -> claude-opus-4-8
|
|
12
12
|
#
|
|
13
13
|
# Env override order: LOKI_<PROVIDER>_MODEL_<TIER> > LOKI_<PROVIDER>_MODEL > catalog latest.
|
|
14
14
|
|