loki-mode 7.42.0 → 7.43.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/app-runner.sh +174 -8
- package/autonomy/completion-council.sh +16 -3
- package/autonomy/loki +12 -0
- package/autonomy/run.sh +73 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/static/index.html +9 -9
- package/docs/INSTALLATION.md +1 -1
- package/events/bus.py +9 -6
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +26 -2
- package/memory/vector_index.py +6 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/codex.sh +21 -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, 11 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.43.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -398,4 +398,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
398
398
|
|
|
399
399
|
---
|
|
400
400
|
|
|
401
|
-
**v7.
|
|
401
|
+
**v7.43.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.43.0
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -156,6 +156,112 @@ _rewrite_detection_port() {
|
|
|
156
156
|
_write_detection "$d_type" "$d_command"
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
# Collect the transitive descendant tree of a PID (children, grandchildren, ...).
|
|
160
|
+
#
|
|
161
|
+
# Echoes one PID per line, deepest-LAST is NOT guaranteed; order is breadth-first
|
|
162
|
+
# from the root. The root PID itself is NOT included. Used by the non-setsid stop
|
|
163
|
+
# fallback (BUG 1): the app is started as `( ... ) &` WITHOUT setsid, so on stock
|
|
164
|
+
# macOS the whole tree (subshell -> bash -lc -> npm -> sh -> node -> workers)
|
|
165
|
+
# inherits the ORCHESTRATOR's process group. A `kill -- -PGID` would therefore
|
|
166
|
+
# signal run.sh and the Claude agent driving it (self-termination), so we MUST
|
|
167
|
+
# walk parent->child links from OUR pid only. This guarantees we never signal a
|
|
168
|
+
# process outside our own subtree: every returned pid has our root as an ancestor.
|
|
169
|
+
#
|
|
170
|
+
# Snapshot semantics: the caller MUST collect the full tree BEFORE sending any
|
|
171
|
+
# signal. If we TERM top-down while walking, grandchildren reparent to init and
|
|
172
|
+
# `pgrep -P <dead-parent>` returns nothing, re-creating the orphaned-worker bug
|
|
173
|
+
# this fix exists to close.
|
|
174
|
+
_app_runner_collect_descendants() {
|
|
175
|
+
local root="$1"
|
|
176
|
+
# Guard against empty / init / kernel pids: walking from 0/1 would sweep
|
|
177
|
+
# unrelated processes. A valid app pid is always > 1.
|
|
178
|
+
case "$root" in
|
|
179
|
+
''|0|1) return 0 ;;
|
|
180
|
+
esac
|
|
181
|
+
if ! [[ "$root" =~ ^[0-9]+$ ]]; then
|
|
182
|
+
return 0
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
local -a frontier=("$root")
|
|
186
|
+
local -a found=()
|
|
187
|
+
local pid child
|
|
188
|
+
local -a kids
|
|
189
|
+
# Bound iterations defensively against a pathological/looping tree.
|
|
190
|
+
local guard=0
|
|
191
|
+
while [ "${#frontier[@]}" -gt 0 ] && [ "$guard" -lt 10000 ]; do
|
|
192
|
+
guard=$(( guard + 1 ))
|
|
193
|
+
pid="${frontier[0]}"
|
|
194
|
+
frontier=("${frontier[@]:1}")
|
|
195
|
+
# Direct children of pid.
|
|
196
|
+
kids=()
|
|
197
|
+
while IFS= read -r child; do
|
|
198
|
+
[ -n "$child" ] && kids+=("$child")
|
|
199
|
+
done < <(pgrep -P "$pid" 2>/dev/null)
|
|
200
|
+
local k
|
|
201
|
+
for k in "${kids[@]:-}"; do
|
|
202
|
+
[ -n "$k" ] || continue
|
|
203
|
+
found+=("$k")
|
|
204
|
+
frontier+=("$k")
|
|
205
|
+
done
|
|
206
|
+
done
|
|
207
|
+
|
|
208
|
+
local f
|
|
209
|
+
for f in "${found[@]:-}"; do
|
|
210
|
+
[ -n "$f" ] && printf '%s\n' "$f"
|
|
211
|
+
done
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Signal an EXPLICIT, pre-captured set of PIDs with a given signal.
|
|
215
|
+
#
|
|
216
|
+
# Usage: _app_runner_signal_pids <SIGNAL> <pid> [pid ...]
|
|
217
|
+
#
|
|
218
|
+
# Why an explicit list and not "(re-)walk from root": a worker that traps
|
|
219
|
+
# SIGTERM (a Node server doing graceful shutdown is the textbook case) survives
|
|
220
|
+
# the TERM phase while its intermediate ancestors (npm, sh) die. Once the
|
|
221
|
+
# ancestors die, the surviving worker reparents to init, so re-deriving the tree
|
|
222
|
+
# from the now-dead root via `pgrep -P` would return NOTHING -- the KILL phase
|
|
223
|
+
# would be skipped and the orphaned, port-holding worker would live on. That is
|
|
224
|
+
# exactly the orphaned-worker bug (BUG 1) resurfacing at the force-kill phase.
|
|
225
|
+
# The fix: the caller snapshots root + all descendants ONCE before any signal,
|
|
226
|
+
# and every phase (TERM, aliveness, KILL) operates over that frozen list.
|
|
227
|
+
#
|
|
228
|
+
# Safety: the caller builds the list from _app_runner_collect_descendants, which
|
|
229
|
+
# only ever follows parent->child links from OUR pid, so the list can never
|
|
230
|
+
# contain a process outside our own subtree. We signal pids individually (never
|
|
231
|
+
# a process group) because in the non-setsid path the app inherits the
|
|
232
|
+
# orchestrator's process group; a group signal would kill run.sh and the agent.
|
|
233
|
+
# Pids are signaled in REVERSE capture order so descendants (captured after the
|
|
234
|
+
# root) are signaled before the root.
|
|
235
|
+
_app_runner_signal_pids() {
|
|
236
|
+
local sig="$1"; shift
|
|
237
|
+
local -a pids=("$@")
|
|
238
|
+
local i p
|
|
239
|
+
for (( i=${#pids[@]}-1; i>=0; i-- )); do
|
|
240
|
+
p="${pids[$i]}"
|
|
241
|
+
case "$p" in
|
|
242
|
+
''|0|1) continue ;;
|
|
243
|
+
esac
|
|
244
|
+
kill "-${sig}" "$p" 2>/dev/null || true
|
|
245
|
+
done
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# True (0) if ANY pid in the EXPLICIT pre-captured list is still alive.
|
|
249
|
+
# Used by the non-setsid stop grace-wait so a deep worker that outlived the main
|
|
250
|
+
# subshell does not let us fall through to "stopped" prematurely. Operates over
|
|
251
|
+
# the frozen snapshot for the same reason _app_runner_signal_pids does.
|
|
252
|
+
_app_runner_any_alive() {
|
|
253
|
+
local p
|
|
254
|
+
for p in "$@"; do
|
|
255
|
+
case "$p" in
|
|
256
|
+
''|0|1) continue ;;
|
|
257
|
+
esac
|
|
258
|
+
if kill -0 "$p" 2>/dev/null; then
|
|
259
|
+
return 0
|
|
260
|
+
fi
|
|
261
|
+
done
|
|
262
|
+
return 1
|
|
263
|
+
}
|
|
264
|
+
|
|
159
265
|
# Fix #2 (finding #597): reconcile the recorded port with the port the app
|
|
160
266
|
# ACTUALLY bound, using the listen line in app.log as the source of truth. This
|
|
161
267
|
# corrects the dashboard Live Preview even when the app ignores PORT and picks
|
|
@@ -887,32 +993,82 @@ app_runner_stop() {
|
|
|
887
993
|
fi
|
|
888
994
|
fi
|
|
889
995
|
|
|
996
|
+
# BUG 1 fix: on the non-setsid fallback (the DEFAULT path on stock macOS,
|
|
997
|
+
# which has no setsid) capture the FULL process subtree -- root + every
|
|
998
|
+
# transitive descendant -- ONCE, BEFORE sending any signal. The old
|
|
999
|
+
# `pkill -TERM -P <pid>` reached only ONE level of children, so deep workers
|
|
1000
|
+
# (npm -> sh -> node -> workers) holding the listening socket survived as
|
|
1001
|
+
# orphans and kept the port bound, blocking the next start.
|
|
1002
|
+
#
|
|
1003
|
+
# Capturing once is load-bearing: a worker that traps SIGTERM survives the
|
|
1004
|
+
# TERM phase while its intermediate ancestors die, then reparents to init.
|
|
1005
|
+
# Re-deriving the tree from the now-dead root would return nothing and skip
|
|
1006
|
+
# the KILL phase, leaving the port-holder alive. Every phase below (TERM,
|
|
1007
|
+
# grace-wait, KILL) operates over this one frozen snapshot instead.
|
|
1008
|
+
local -a _stop_snapshot=()
|
|
1009
|
+
if [ "$_APP_RUNNER_HAS_SETSID" != true ]; then
|
|
1010
|
+
_stop_snapshot=("$_APP_RUNNER_PID")
|
|
1011
|
+
local _snap_d
|
|
1012
|
+
while IFS= read -r _snap_d; do
|
|
1013
|
+
[ -n "$_snap_d" ] && _stop_snapshot+=("$_snap_d")
|
|
1014
|
+
done < <(_app_runner_collect_descendants "$_APP_RUNNER_PID")
|
|
1015
|
+
fi
|
|
1016
|
+
|
|
890
1017
|
# Send SIGTERM to process and children
|
|
891
1018
|
if [ "$_APP_RUNNER_HAS_SETSID" = true ]; then
|
|
1019
|
+
# setsid path: the app is its own process group leader, so a group
|
|
1020
|
+
# signal reaches the whole tree safely. Unchanged.
|
|
892
1021
|
kill -TERM "-$_APP_RUNNER_PID" 2>/dev/null || kill -TERM "$_APP_RUNNER_PID" 2>/dev/null || true
|
|
893
1022
|
else
|
|
894
|
-
|
|
895
|
-
|
|
1023
|
+
# Group-kill is NOT used here: in this path the app inherits the
|
|
1024
|
+
# orchestrator's process group, so a group signal would kill run.sh and
|
|
1025
|
+
# the agent driving it. Signal the frozen snapshot, descendants first.
|
|
1026
|
+
_app_runner_signal_pids TERM "${_stop_snapshot[@]}"
|
|
896
1027
|
fi
|
|
897
1028
|
|
|
898
|
-
# Wait up to 5 seconds for graceful shutdown
|
|
1029
|
+
# Wait up to 5 seconds for graceful shutdown. Key the wait on the WHOLE
|
|
1030
|
+
# snapshot being alive (not just the main pid): a deep worker can outlive the
|
|
1031
|
+
# main subshell, and treating the main pid's exit as "done" is exactly what
|
|
1032
|
+
# let workers leak before. setsid path keeps the simpler main-pid check.
|
|
899
1033
|
local waited=0
|
|
900
1034
|
while [ "$waited" -lt 5 ]; do
|
|
901
|
-
if
|
|
902
|
-
break
|
|
1035
|
+
if [ "$_APP_RUNNER_HAS_SETSID" = true ]; then
|
|
1036
|
+
kill -0 "$_APP_RUNNER_PID" 2>/dev/null || break
|
|
1037
|
+
else
|
|
1038
|
+
_app_runner_any_alive "${_stop_snapshot[@]}" || break
|
|
903
1039
|
fi
|
|
904
1040
|
sleep 1
|
|
905
1041
|
waited=$(( waited + 1 ))
|
|
906
1042
|
done
|
|
907
1043
|
|
|
908
1044
|
# Force kill if still running
|
|
909
|
-
|
|
1045
|
+
local _still_alive=false
|
|
1046
|
+
if [ "$_APP_RUNNER_HAS_SETSID" = true ]; then
|
|
1047
|
+
kill -0 "$_APP_RUNNER_PID" 2>/dev/null && _still_alive=true
|
|
1048
|
+
else
|
|
1049
|
+
_app_runner_any_alive "${_stop_snapshot[@]}" && _still_alive=true
|
|
1050
|
+
fi
|
|
1051
|
+
if [ "$_still_alive" = true ]; then
|
|
910
1052
|
log_warn "App Runner: process did not stop gracefully, sending SIGKILL"
|
|
911
1053
|
if [ "$_APP_RUNNER_HAS_SETSID" = true ]; then
|
|
912
1054
|
kill -KILL "-$_APP_RUNNER_PID" 2>/dev/null || kill -KILL "$_APP_RUNNER_PID" 2>/dev/null || true
|
|
913
1055
|
else
|
|
914
|
-
|
|
915
|
-
|
|
1056
|
+
# BUG 1 fix (KILL phase): SIGKILL the SAME frozen snapshot (root +
|
|
1057
|
+
# all descendants captured pre-signal), so a TERM-trapping worker
|
|
1058
|
+
# that reparented to init is still force-killed. SIGKILL cannot be
|
|
1059
|
+
# trapped, so this is the terminal guarantee that no port-holder
|
|
1060
|
+
# survives. The snapshot does the real work. The fresh walk below only
|
|
1061
|
+
# adds anything while the root is still alive (a worker spawned during
|
|
1062
|
+
# shutdown); once the root is dead it is empty and the snapshot covers.
|
|
1063
|
+
_app_runner_signal_pids KILL "${_stop_snapshot[@]}"
|
|
1064
|
+
local -a _kill_fresh=()
|
|
1065
|
+
local _kf
|
|
1066
|
+
while IFS= read -r _kf; do
|
|
1067
|
+
[ -n "$_kf" ] && _kill_fresh+=("$_kf")
|
|
1068
|
+
done < <(_app_runner_collect_descendants "$_APP_RUNNER_PID")
|
|
1069
|
+
if [ "${#_kill_fresh[@]}" -gt 0 ]; then
|
|
1070
|
+
_app_runner_signal_pids KILL "${_kill_fresh[@]}"
|
|
1071
|
+
fi
|
|
916
1072
|
fi
|
|
917
1073
|
fi
|
|
918
1074
|
|
|
@@ -1094,6 +1250,11 @@ app_runner_watchdog() {
|
|
|
1094
1250
|
# it restarts the stack under the same crash-count circuit breaker.
|
|
1095
1251
|
if [ "$_APP_RUNNER_IS_DOCKER" = true ] && echo "$_APP_RUNNER_METHOD" | grep -q "docker compose"; then
|
|
1096
1252
|
if app_runner_health_check; then
|
|
1253
|
+
# BUG 3 fix: the breaker is meant to fire on 5 CONSECUTIVE failures.
|
|
1254
|
+
# A confirmed-healthy observation clears any accumulated count so a
|
|
1255
|
+
# long-lived stack that recovered from a few transient blips is not
|
|
1256
|
+
# tripped permanently on cumulative (non-consecutive) crashes.
|
|
1257
|
+
_APP_RUNNER_CRASH_COUNT=0
|
|
1097
1258
|
return 0
|
|
1098
1259
|
fi
|
|
1099
1260
|
_APP_RUNNER_CRASH_COUNT=$(( _APP_RUNNER_CRASH_COUNT + 1 ))
|
|
@@ -1125,6 +1286,11 @@ app_runner_watchdog() {
|
|
|
1125
1286
|
|
|
1126
1287
|
# Process alive, nothing to do
|
|
1127
1288
|
if kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
|
|
1289
|
+
# BUG 3 fix: a confirmed-alive observation clears the accumulated crash
|
|
1290
|
+
# count so the breaker fires only on 5 CONSECUTIVE deaths, not on 5
|
|
1291
|
+
# cumulative crashes that were each successfully recovered over a long
|
|
1292
|
+
# session (which would trip the breaker on a HEALTHY app).
|
|
1293
|
+
_APP_RUNNER_CRASH_COUNT=0
|
|
1128
1294
|
return 0
|
|
1129
1295
|
fi
|
|
1130
1296
|
|
|
@@ -1519,8 +1519,17 @@ council_evidence_gate() {
|
|
|
1519
1519
|
if committed_files=$(git diff --name-only "$base_sha" HEAD 2>/dev/null); then
|
|
1520
1520
|
:
|
|
1521
1521
|
else
|
|
1522
|
-
# Base present but
|
|
1523
|
-
#
|
|
1522
|
+
# Base present but UNREACHABLE (e.g. shallow clone, history rewrite,
|
|
1523
|
+
# or `git reset --hard` -- a documented live hazard). The diff vs the
|
|
1524
|
+
# run-start SHA cannot be computed, so we can no longer prove that the
|
|
1525
|
+
# committed-union diff is empty. Treat this as INCONCLUSIVE, not as
|
|
1526
|
+
# positive empty-diff fabrication evidence: an agent that committed
|
|
1527
|
+
# all its work leaves a clean working tree, and `git diff HEAD` would
|
|
1528
|
+
# read empty -> a false BLOCK. We still fall back to the working-tree
|
|
1529
|
+
# diff vs HEAD to capture any uncommitted work, but the empty-diff
|
|
1530
|
+
# block is suppressed below via the diff_inconclusive guard.
|
|
1531
|
+
diff_inconclusive="true"
|
|
1532
|
+
diff_inconclusive_reason="base_unreachable"
|
|
1524
1533
|
committed_files=$(git diff --name-only HEAD 2>/dev/null || echo "")
|
|
1525
1534
|
fi
|
|
1526
1535
|
unstaged_files=$(git diff --name-only HEAD 2>/dev/null || echo "")
|
|
@@ -1543,7 +1552,11 @@ council_evidence_gate() {
|
|
|
1543
1552
|
else
|
|
1544
1553
|
diff_files=0
|
|
1545
1554
|
fi
|
|
1546
|
-
|
|
1555
|
+
# Only treat an empty union as positive fabrication evidence when the
|
|
1556
|
+
# baseline was CONCLUSIVE. If the base SHA was unreachable (history
|
|
1557
|
+
# rewrite / reset --hard), a clean committed tree yields an empty
|
|
1558
|
+
# working-tree diff that must NOT read as empty-diff fabrication.
|
|
1559
|
+
if [ "$diff_files" -eq 0 ] && [ "$diff_inconclusive" != "true" ]; then
|
|
1547
1560
|
diff_fails="true"
|
|
1548
1561
|
fi
|
|
1549
1562
|
fi
|
package/autonomy/loki
CHANGED
|
@@ -1057,6 +1057,7 @@ cmd_start() {
|
|
|
1057
1057
|
echo "Options:"
|
|
1058
1058
|
echo " --provider NAME AI provider: claude (default), codex, cline, aider"
|
|
1059
1059
|
echo " --parallel Enable parallel mode with git worktrees"
|
|
1060
|
+
echo " --allow-haiku Enable Haiku model for the fast tier (default: disabled)"
|
|
1060
1061
|
echo " --bg, --background Run in background mode"
|
|
1061
1062
|
echo " --simple Force simple complexity tier (3 phases)"
|
|
1062
1063
|
echo " --complex Force complex complexity tier (8 phases)"
|
|
@@ -1180,6 +1181,17 @@ cmd_start() {
|
|
|
1180
1181
|
args+=("--parallel")
|
|
1181
1182
|
shift
|
|
1182
1183
|
;;
|
|
1184
|
+
--allow-haiku)
|
|
1185
|
+
# Enable Haiku for the fast tier. Mirrors the LOKI_ALLOW_HAIKU=true
|
|
1186
|
+
# env var (consumed by providers/claude.sh and run.sh). Documented in
|
|
1187
|
+
# loki --help and run.sh; previously only the env var worked here, so
|
|
1188
|
+
# `loki start ./prd.md --allow-haiku` aborted with "Unknown option".
|
|
1189
|
+
# Export reaches the runner; also forward as an arg so the run.sh
|
|
1190
|
+
# parser (run.sh:15015) sees it on every route.
|
|
1191
|
+
export LOKI_ALLOW_HAIKU=true
|
|
1192
|
+
args+=("--allow-haiku")
|
|
1193
|
+
shift
|
|
1194
|
+
;;
|
|
1183
1195
|
--regen-prd|--regenerate-prd|--regen|--fresh-prd)
|
|
1184
1196
|
# v7.8.1: force a fresh generated PRD on a no-PRD run, overriding
|
|
1185
1197
|
# the staleness-aware reuse (decide_generated_prd_action in
|
package/autonomy/run.sh
CHANGED
|
@@ -7041,6 +7041,48 @@ enforce_test_coverage() {
|
|
|
7041
7041
|
local output
|
|
7042
7042
|
output=$(cd "${TARGET_DIR:-.}" && timeout "$gate_timeout" npx mocha 2>&1) || test_passed=false
|
|
7043
7043
|
details="mocha: $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
7044
|
+
else
|
|
7045
|
+
# v7.41.x (test-coverage fail-open fix): a real "scripts.test" was
|
|
7046
|
+
# previously missed entirely. A greenfield project whose package.json
|
|
7047
|
+
# has {"scripts":{"test":"node --test"}} (or any non-placeholder test
|
|
7048
|
+
# script) actually runs a working suite via `npm test`, yet the gate
|
|
7049
|
+
# reported runner:none + pass:true -- so a project whose tests FAIL
|
|
7050
|
+
# green-lit identically. Detect a real test script (excluding the npm
|
|
7051
|
+
# placeholder "no test specified") with a JSON parser, not grep (grep
|
|
7052
|
+
# would false-positive on devDeps / unrelated keys), then run the
|
|
7053
|
+
# configured command. This MUST sit before the monorepo/python/go/rust
|
|
7054
|
+
# checks, all of which gate on test_runner=="none".
|
|
7055
|
+
local _pkg_test_script
|
|
7056
|
+
_pkg_test_script=$(_LOKI_PKG="${TARGET_DIR:-.}/package.json" python3 -c "
|
|
7057
|
+
import json, os, sys
|
|
7058
|
+
try:
|
|
7059
|
+
with open(os.environ['_LOKI_PKG']) as f:
|
|
7060
|
+
d = json.load(f)
|
|
7061
|
+
except Exception:
|
|
7062
|
+
sys.exit(0)
|
|
7063
|
+
t = (d.get('scripts') or {}).get('test') or ''
|
|
7064
|
+
# npm's default placeholder; treat as 'no test'.
|
|
7065
|
+
if 'no test specified' in t.lower():
|
|
7066
|
+
sys.exit(0)
|
|
7067
|
+
sys.stdout.write(t.strip())
|
|
7068
|
+
" 2>/dev/null || echo "")
|
|
7069
|
+
if [ -n "$_pkg_test_script" ]; then
|
|
7070
|
+
# LOKI_TEST_COMMAND lets an operator override the invocation; the
|
|
7071
|
+
# default is the project's own `npm test`.
|
|
7072
|
+
local _test_cmd="${LOKI_TEST_COMMAND:-npm test}"
|
|
7073
|
+
# Label the runner by what the script invokes so evidence is
|
|
7074
|
+
# honest (node --test, vitest, jest, etc. all surface here).
|
|
7075
|
+
case "$_pkg_test_script" in
|
|
7076
|
+
*"node --test"*|*"node:test"*) test_runner="node-test" ;;
|
|
7077
|
+
*vitest*) test_runner="vitest" ;;
|
|
7078
|
+
*jest*) test_runner="jest" ;;
|
|
7079
|
+
*mocha*) test_runner="mocha" ;;
|
|
7080
|
+
*) test_runner="npm-test" ;;
|
|
7081
|
+
esac
|
|
7082
|
+
local output
|
|
7083
|
+
output=$(cd "${TARGET_DIR:-.}" && timeout "$gate_timeout" sh -c "$_test_cmd" 2>&1) || test_passed=false
|
|
7084
|
+
details="$test_runner ($_test_cmd): $(echo "$output" | tail -5 | tr '\n' ' ')"
|
|
7085
|
+
fi
|
|
7044
7086
|
fi
|
|
7045
7087
|
fi
|
|
7046
7088
|
|
|
@@ -7165,10 +7207,23 @@ enforce_test_coverage() {
|
|
|
7165
7207
|
fi
|
|
7166
7208
|
|
|
7167
7209
|
if [ "$test_runner" = "none" ]; then
|
|
7168
|
-
log_info "Test coverage: no test runner detected,
|
|
7210
|
+
log_info "Test coverage: no test runner detected, recording inconclusive (not pass)"
|
|
7211
|
+
# v7.41.x fail-open fix: previously this wrote pass:true, so a project
|
|
7212
|
+
# whose tests truly do not run was indistinguishable from one whose tests
|
|
7213
|
+
# passed. Record pass:"inconclusive" instead. The completion-council
|
|
7214
|
+
# evidence gate already treats runner=="none" as pass-through regardless
|
|
7215
|
+
# of the pass value (completion-council.sh: runner=='none' short-circuits
|
|
7216
|
+
# BEFORE the `passed is False` block), so genuinely-no-tests stays
|
|
7217
|
+
# non-blocking (no infinite hang), while the JSON record is now honest:
|
|
7218
|
+
# "no tests" never reads as "tests passed". A DETECTED runner that fails
|
|
7219
|
+
# still writes pass:false below and BLOCKS.
|
|
7220
|
+
#
|
|
7221
|
+
# unit-tests.pass is only read for the status-line display (run.sh ~2183,
|
|
7222
|
+
# PASS vs PENDING); keeping the touch preserves the historical
|
|
7223
|
+
# non-blocking behavior for legitimate no-test projects.
|
|
7169
7224
|
touch "$quality_dir/unit-tests.pass"
|
|
7170
7225
|
cat > "$quality_dir/test-results.json" << TREOF
|
|
7171
|
-
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"none","pass":
|
|
7226
|
+
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"none","pass":"inconclusive","summary":"No test runner detected"}
|
|
7172
7227
|
TREOF
|
|
7173
7228
|
# Finding #598: stamp the per-iteration freshness marker so a later
|
|
7174
7229
|
# completion-route capture (ensure_completion_test_evidence) reuses this
|
|
@@ -14161,6 +14216,22 @@ if __name__ == "__main__":
|
|
|
14161
14216
|
log_warn " Review details under .loki/quality/reviews/ ; gate_failures=${gate_failures}"
|
|
14162
14217
|
_gate_block_for_completion=""
|
|
14163
14218
|
# Fall through; the gate-failed loop continues normally
|
|
14219
|
+
# HIGH (trust-gate): the checklist hard gate must also guard the
|
|
14220
|
+
# DEFAULT completion-promise / loki_complete_task route, not only the
|
|
14221
|
+
# interval-gated council path (council_evaluate) and the dashboard
|
|
14222
|
+
# force-review path -- both of which already call this gate. Without
|
|
14223
|
+
# it, an agent that leaves a `priority: critical` checklist item
|
|
14224
|
+
# `failing` and claims done on a non-council-interval iteration would
|
|
14225
|
+
# ship, bypassing the checklist gate entirely. council_reverify_checklist
|
|
14226
|
+
# ran above (when a claim is present) so statuses are fresh here.
|
|
14227
|
+
# Mirrors the evidence/held-out gate arms below. No-op safe:
|
|
14228
|
+
# council_checklist_gate returns 0 (pass) when there is no checklist
|
|
14229
|
+
# results file or when no critical items are failing, so this branch
|
|
14230
|
+
# never fires on those projects. Gate output is written by the gate.
|
|
14231
|
+
elif [ "$_completion_claimed" = 1 ] && type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
|
|
14232
|
+
log_warn "Completion claim rejected: critical checklist item(s) failing (hard gate)."
|
|
14233
|
+
log_warn " Details under .loki/council/gate-block.json"
|
|
14234
|
+
# Fall through; keep iterating until critical checklist items pass.
|
|
14164
14235
|
# v7.19.1: the verified-completion evidence gate must also guard the
|
|
14165
14236
|
# DEFAULT completion route (a completion claim via loki_complete_task
|
|
14166
14237
|
# / the completion-promise text), not only the interval-gated council
|
package/dashboard/__init__.py
CHANGED
|
@@ -3910,7 +3910,7 @@ var LokiDashboard=(()=>{var Ee=Object.defineProperty;var rt=Object.getOwnPropert
|
|
|
3910
3910
|
`:e.steps!==void 0?`
|
|
3911
3911
|
<div class="detail-panel">
|
|
3912
3912
|
<div class="detail-header">
|
|
3913
|
-
<h3>Skill: ${e.name}</h3>
|
|
3913
|
+
<h3>Skill: ${this._escapeHtml(e.name)}</h3>
|
|
3914
3914
|
<button class="close-btn" id="close-detail">×</button>
|
|
3915
3915
|
</div>
|
|
3916
3916
|
<div class="detail-body">
|
|
@@ -5518,7 +5518,7 @@ var LokiDashboard=(()=>{var Ee=Object.defineProperty;var rt=Object.getOwnPropert
|
|
|
5518
5518
|
${this._renderTabContent()}
|
|
5519
5519
|
</div>
|
|
5520
5520
|
|
|
5521
|
-
${this._error?`<div class="error-banner">${this._error}</div>`:""}
|
|
5521
|
+
${this._error?`<div class="error-banner">${this._escapeHtml(this._error)}</div>`:""}
|
|
5522
5522
|
</div>
|
|
5523
5523
|
`,this._attachEventListeners())}_attachEventListeners(){let e=this.shadowRoot;if(!e)return;let t=e.getElementById("force-review-btn");t&&t.addEventListener("click",()=>this._forceReview()),e.querySelectorAll(".tab[data-tab]").forEach(i=>{i.addEventListener("click",()=>this._setTab(i.dataset.tab))})}_renderTabContent(){switch(this._activeTab){case"overview":return this._renderOverview();case"decisions":return this._renderDecisions();case"convergence":return this._renderConvergence();case"agents":return this._renderAgents();default:return""}}_renderOverview(){let e=this._councilState||{},t=e.consecutive_no_change||0,i=e.done_signals||0,a=e.total_votes||0,s=e.approve_votes||0,r=this._verdicts.length>0?this._verdicts[this._verdicts.length-1]:null,o=this._agents.filter(n=>n.alive).length;return`
|
|
5524
5524
|
<div class="overview-grid">
|
|
@@ -5631,27 +5631,27 @@ var LokiDashboard=(()=>{var Ee=Object.defineProperty;var rt=Object.getOwnPropert
|
|
|
5631
5631
|
<div class="agent-card ${this._selectedAgent?.id===t.id?"agent-selected":""}"
|
|
5632
5632
|
data-agent-index="${i}">
|
|
5633
5633
|
<div class="agent-header">
|
|
5634
|
-
<span class="agent-name">${t.name||t.id||"Unknown"}</span>
|
|
5634
|
+
<span class="agent-name">${this._escapeHtml(t.name||t.id||"Unknown")}</span>
|
|
5635
5635
|
<span class="agent-status ${t.alive?"status-alive":"status-dead"}">
|
|
5636
5636
|
${t.alive?"Running":"Stopped"}
|
|
5637
5637
|
</span>
|
|
5638
5638
|
</div>
|
|
5639
5639
|
<div class="agent-meta">
|
|
5640
|
-
${t.type?`<span class="agent-type">${t.type}</span>`:""}
|
|
5640
|
+
${t.type?`<span class="agent-type">${this._escapeHtml(t.type)}</span>`:""}
|
|
5641
5641
|
${t.pid?`<span class="agent-pid">PID: ${t.pid}</span>`:""}
|
|
5642
|
-
${t.task?`<span class="agent-task">Task: ${t.task}</span>`:""}
|
|
5642
|
+
${t.task?`<span class="agent-task">Task: ${this._escapeHtml(t.task)}</span>`:""}
|
|
5643
5643
|
</div>
|
|
5644
5644
|
${this._selectedAgent?.id===t.id?`
|
|
5645
5645
|
<div class="agent-actions">
|
|
5646
5646
|
${t.alive?`
|
|
5647
|
-
<button class="btn btn-sm btn-warn" data-action="pause" data-agent-id="${t.id||t.name}">
|
|
5647
|
+
<button class="btn btn-sm btn-warn" data-action="pause" data-agent-id="${this._escapeHtml(t.id||t.name)}">
|
|
5648
5648
|
Pause
|
|
5649
5649
|
</button>
|
|
5650
|
-
<button class="btn btn-sm btn-danger" data-action="kill" data-agent-id="${t.id||t.name}">
|
|
5650
|
+
<button class="btn btn-sm btn-danger" data-action="kill" data-agent-id="${this._escapeHtml(t.id||t.name)}">
|
|
5651
5651
|
Kill
|
|
5652
5652
|
</button>
|
|
5653
5653
|
`:`
|
|
5654
|
-
<button class="btn btn-sm btn-primary" data-action="resume" data-agent-id="${t.id||t.name}">
|
|
5654
|
+
<button class="btn btn-sm btn-primary" data-action="resume" data-agent-id="${this._escapeHtml(t.id||t.name)}">
|
|
5655
5655
|
Resume
|
|
5656
5656
|
</button>
|
|
5657
5657
|
`}
|
|
@@ -5660,7 +5660,7 @@ var LokiDashboard=(()=>{var Ee=Object.defineProperty;var rt=Object.getOwnPropert
|
|
|
5660
5660
|
</div>
|
|
5661
5661
|
`).join("")}
|
|
5662
5662
|
</div>
|
|
5663
|
-
`;return this._pendingRaf=requestAnimationFrame(()=>{this._pendingRaf=null;let t=this.shadowRoot;t&&t.querySelectorAll(".agent-card[data-agent-index]").forEach(i=>{let a=parseInt(i.dataset.agentIndex,10),s=this._agents[a];s&&(i.addEventListener("click",()=>this._selectAgent(s)),i.querySelectorAll("[data-action]").forEach(r=>{r.addEventListener("click",o=>{o.stopPropagation();let n=r.dataset.action,l=r.dataset.agentId;n==="pause"?this._pauseAgent(l):n==="kill"?this._killAgent(l):n==="resume"&&this._resumeAgent(l)})}))})}),e}_formatTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}catch{return e}}_getStyles(){return`
|
|
5663
|
+
`;return this._pendingRaf=requestAnimationFrame(()=>{this._pendingRaf=null;let t=this.shadowRoot;t&&t.querySelectorAll(".agent-card[data-agent-index]").forEach(i=>{let a=parseInt(i.dataset.agentIndex,10),s=this._agents[a];s&&(i.addEventListener("click",()=>this._selectAgent(s)),i.querySelectorAll("[data-action]").forEach(r=>{r.addEventListener("click",o=>{o.stopPropagation();let n=r.dataset.action,l=r.dataset.agentId;n==="pause"?this._pauseAgent(l):n==="kill"?this._killAgent(l):n==="resume"&&this._resumeAgent(l)})}))})}),e}_formatTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}catch{return e}}_escapeHtml(e){return e?String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}_getStyles(){return`
|
|
5664
5664
|
:host {
|
|
5665
5665
|
display: block;
|
|
5666
5666
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
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.43.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/events/bus.py
CHANGED
|
@@ -328,17 +328,20 @@ class EventBus:
|
|
|
328
328
|
Events as they arrive
|
|
329
329
|
"""
|
|
330
330
|
start_time = time.time()
|
|
331
|
-
last_check = datetime.now(timezone.utc).isoformat()
|
|
332
331
|
|
|
333
332
|
while True:
|
|
334
333
|
if timeout and (time.time() - start_time) > timeout:
|
|
335
334
|
break
|
|
336
335
|
|
|
337
|
-
#
|
|
338
|
-
#
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
336
|
+
# Dedup is driven solely by _processed_ids (maintained via
|
|
337
|
+
# mark_processed), NOT by a wall-clock `since` window. A local
|
|
338
|
+
# `since=now` filter silently drops any event whose timestamp is
|
|
339
|
+
# at or behind the subscriber's clock: cross-process clock skew
|
|
340
|
+
# (an emitter a few ms/s behind) or second-granularity timestamps
|
|
341
|
+
# (emit.sh's .000Z fallback) would lose events forever. This
|
|
342
|
+
# mirrors start_background_processing() and bus.ts, which both
|
|
343
|
+
# call get_pending_events with no `since` argument.
|
|
344
|
+
events = self.get_pending_events(types=types)
|
|
342
345
|
|
|
343
346
|
for event in events:
|
|
344
347
|
yield event
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.
|
|
2
|
+
var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.43.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,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[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ 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 a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
|
|
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)
|
|
@@ -789,4 +789,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
789
789
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
790
790
|
`),process.stderr.write(o6),2}}p1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var ZZ=await QZ(Bun.argv.slice(2));process.exit(ZZ);
|
|
791
791
|
|
|
792
|
-
//# debugId=
|
|
792
|
+
//# debugId=D2E334AA5C606B8064756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -1257,6 +1257,12 @@ async def get_continuity() -> str:
|
|
|
1257
1257
|
return "# CONTINUITY.md not found"
|
|
1258
1258
|
except PathTraversalError:
|
|
1259
1259
|
return "# Access denied"
|
|
1260
|
+
except Exception as e:
|
|
1261
|
+
# Match the tool-handler error-envelope pattern so a corrupt or
|
|
1262
|
+
# unreadable state file (e.g. IsADirectoryError, OSError) returns an
|
|
1263
|
+
# honest error string instead of raising uncaught into the MCP runtime.
|
|
1264
|
+
logger.error(f"get_continuity failed: {e}")
|
|
1265
|
+
return f"# Error reading CONTINUITY.md: {e}"
|
|
1260
1266
|
|
|
1261
1267
|
|
|
1262
1268
|
@mcp.resource("loki://memory/index")
|
|
@@ -1271,14 +1277,26 @@ async def get_memory_index() -> str:
|
|
|
1271
1277
|
return json.dumps(index_data)
|
|
1272
1278
|
return json.dumps({"topics": [], "message": "Index not initialized"})
|
|
1273
1279
|
|
|
1274
|
-
# Fallback to direct file read
|
|
1280
|
+
# Fallback to direct file read. Parse-and-reserialize so a corrupt
|
|
1281
|
+
# index.json yields a clean error envelope instead of serving corrupt
|
|
1282
|
+
# bytes as a successful response (or raising on a downstream consumer).
|
|
1275
1283
|
index_path = safe_path_join('.loki', 'memory', 'index.json')
|
|
1276
1284
|
if os.path.exists(index_path):
|
|
1277
1285
|
with safe_open(index_path, 'r') as f:
|
|
1278
|
-
|
|
1286
|
+
raw = f.read()
|
|
1287
|
+
try:
|
|
1288
|
+
return json.dumps(json.loads(raw))
|
|
1289
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
1290
|
+
logger.error(f"get_memory_index: corrupt index.json: {e}")
|
|
1291
|
+
return json.dumps({"error": f"corrupt index.json: {e}", "topics": []})
|
|
1279
1292
|
return json.dumps({"topics": [], "message": "Index not initialized"})
|
|
1280
1293
|
except PathTraversalError:
|
|
1281
1294
|
return json.dumps({"error": "Access denied", "topics": []})
|
|
1295
|
+
except Exception as e:
|
|
1296
|
+
# Generic envelope so any other state-file failure (OSError,
|
|
1297
|
+
# IsADirectoryError) returns honestly rather than raising uncaught.
|
|
1298
|
+
logger.error(f"get_memory_index failed: {e}")
|
|
1299
|
+
return json.dumps({"error": str(e), "topics": []})
|
|
1282
1300
|
|
|
1283
1301
|
|
|
1284
1302
|
@mcp.resource("loki://queue/pending")
|
|
@@ -1304,6 +1322,12 @@ async def get_pending_tasks() -> str:
|
|
|
1304
1322
|
return json.dumps({"pending_tasks": [], "count": 0})
|
|
1305
1323
|
except PathTraversalError:
|
|
1306
1324
|
return json.dumps({"error": "Access denied", "pending_tasks": [], "count": 0})
|
|
1325
|
+
except Exception as e:
|
|
1326
|
+
# Generic envelope: a degraded install (STATE_MANAGER_AVAILABLE=False)
|
|
1327
|
+
# does a bare json.load on .loki/state/task-queue.json; a corrupt file
|
|
1328
|
+
# raises JSONDecodeError that must return an error, not crash the runtime.
|
|
1329
|
+
logger.error(f"get_pending_tasks failed: {e}")
|
|
1330
|
+
return json.dumps({"error": str(e), "pending_tasks": [], "count": 0})
|
|
1307
1331
|
|
|
1308
1332
|
|
|
1309
1333
|
# ============================================================
|
package/memory/vector_index.py
CHANGED
|
@@ -281,7 +281,12 @@ class VectorIndex:
|
|
|
281
281
|
import tempfile
|
|
282
282
|
npz_path = f"{path}.npz"
|
|
283
283
|
npz_dir = os.path.dirname(npz_path) or "."
|
|
284
|
-
|
|
284
|
+
# The temp file MUST end in ".npz". np.savez appends ".npz" to any
|
|
285
|
+
# target whose name does not already end in ".npz", so a ".npz.tmp"
|
|
286
|
+
# suffix would make numpy write the real archive to <tmp>.npz and leave
|
|
287
|
+
# the original temp file 0 bytes. os.replace would then move the empty
|
|
288
|
+
# file into place and orphan the real data (corrupting every index).
|
|
289
|
+
tmp_fd, tmp_path = tempfile.mkstemp(dir=npz_dir, suffix=".npz")
|
|
285
290
|
os.close(tmp_fd)
|
|
286
291
|
try:
|
|
287
292
|
np.savez(
|
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.43.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 11 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.43.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 11 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
|
@@ -116,10 +116,17 @@ provider_version() {
|
|
|
116
116
|
# Invocation function
|
|
117
117
|
# Note: Codex uses positional prompt, not -p flag
|
|
118
118
|
# Note: Reasoning effort is configured via environment or config, not CLI flag
|
|
119
|
+
# v7.x: pin the resolved model explicitly via -m/--model. Without it, codex
|
|
120
|
+
# falls back to the installed CLI's built-in default (e.g. gpt-5.5 on codex
|
|
121
|
+
# 0.132.0), which silently ignores _codex_validate_model and makes the run.sh
|
|
122
|
+
# cost table (priced for gpt-5.3-codex) wrong. --model is the documented model
|
|
123
|
+
# selector and is readable in process listings.
|
|
119
124
|
provider_invoke() {
|
|
120
125
|
local prompt="$1"
|
|
121
126
|
shift
|
|
122
|
-
codex exec --full-auto --skip-git-repo-check
|
|
127
|
+
codex exec --full-auto --skip-git-repo-check \
|
|
128
|
+
--model "$PROVIDER_MODEL_DEVELOPMENT" \
|
|
129
|
+
"$prompt" "$@"
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
# Model tier to effort level parameter (Codex uses effort, not separate models)
|
|
@@ -197,6 +204,18 @@ provider_invoke_with_tier() {
|
|
|
197
204
|
local effort
|
|
198
205
|
effort=$(resolve_model_for_tier "$tier")
|
|
199
206
|
|
|
207
|
+
# Resolve the model name by tier. These three vars can diverge via the
|
|
208
|
+
# generic LOKI_MODEL_* env (each validated by _codex_validate_model), so
|
|
209
|
+
# honor the tier rather than hardcoding development. Capability aliases
|
|
210
|
+
# (best/balanced/cheap) mirror resolve_model_for_tier's mapping.
|
|
211
|
+
local model
|
|
212
|
+
case "$tier" in
|
|
213
|
+
planning|best) model="$PROVIDER_MODEL_PLANNING" ;;
|
|
214
|
+
development|balanced) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
|
|
215
|
+
fast|cheap) model="$PROVIDER_MODEL_FAST" ;;
|
|
216
|
+
*) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
|
|
217
|
+
esac
|
|
218
|
+
|
|
200
219
|
local extra_flags=()
|
|
201
220
|
if [ "${LOKI_CODEX_WEB_SEARCH:-false}" = "true" ]; then
|
|
202
221
|
extra_flags+=(--search)
|
|
@@ -211,6 +230,7 @@ provider_invoke_with_tier() {
|
|
|
211
230
|
--ask-for-approval never \
|
|
212
231
|
--sandbox danger-full-access \
|
|
213
232
|
--skip-git-repo-check \
|
|
233
|
+
--model "$model" \
|
|
214
234
|
"${extra_flags[@]}" \
|
|
215
235
|
"$prompt" "$@"
|
|
216
236
|
}
|