loki-mode 9.17.2 → 9.18.2
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/loki +439 -2
- package/autonomy/run.sh +215 -11
- package/autonomy/trigger-server.py +518 -17
- package/bin/loki +7 -1
- package/dashboard/__init__.py +1 -1
- package/docs/COMPETITIVE-SCORECARD.md +38 -0
- package/docs/COMPETITOR-DEPLOYMENT-MODELS.md +475 -0
- package/docs/DEPLOYMENT.md +542 -0
- package/docs/STALE-STATE-AUDIT.md +174 -0
- package/docs/VERIFICATION-COST.md +31 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/autonomy/run.sh
CHANGED
|
@@ -1519,6 +1519,22 @@ PYREG
|
|
|
1519
1519
|
# if any other project is still running (KEEP) it is left up. NEVER uses a
|
|
1520
1520
|
# blanket pkill and NEVER touches another folder's pids. Best-effort and
|
|
1521
1521
|
# failure-swallowed: teardown bookkeeping must never block a clean exit.
|
|
1522
|
+
# Is this pid plausibly OUR dashboard, or a recycled number now naming something
|
|
1523
|
+
# else? Fails OPEN (returns 0) whenever ps cannot tell us, so the only behavior
|
|
1524
|
+
# change is refusing to kill a process that is positively identified as NOT a
|
|
1525
|
+
# dashboard. See the call site for the measured stale-file evidence.
|
|
1526
|
+
_loki_pid_looks_like_dashboard() {
|
|
1527
|
+
local _p="$1" _cmd
|
|
1528
|
+
case "$_p" in ''|*[!0-9]*) return 1 ;; esac
|
|
1529
|
+
[ "$_p" -gt 1 ] 2>/dev/null || return 1 # never signal pid 0/1
|
|
1530
|
+
_cmd="$(ps -o command= -p "$_p" 2>/dev/null)"
|
|
1531
|
+
[ -n "$_cmd" ] || return 0 # ps unavailable: behave as before
|
|
1532
|
+
case "$_cmd" in
|
|
1533
|
+
*dashboard*|*uvicorn*|*loki*) return 0 ;;
|
|
1534
|
+
esac
|
|
1535
|
+
return 1
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1522
1538
|
loki_mark_project_stopped_and_maybe_kill_shared_dashboard() {
|
|
1523
1539
|
local _skill="${LOKI_SKILL_DIR:-${PROJECT_DIR:-$SCRIPT_DIR/..}}"
|
|
1524
1540
|
local _shared_pidfile="${HOME}/.loki/dashboard/dashboard.pid"
|
|
@@ -1566,7 +1582,19 @@ PYCHECK
|
|
|
1566
1582
|
if [ -f "$_shared_pidfile" ]; then
|
|
1567
1583
|
local _shared_pid
|
|
1568
1584
|
_shared_pid=$(cat "$_shared_pidfile" 2>/dev/null)
|
|
1569
|
-
|
|
1585
|
+
# Identity check before signalling. dashboard.pid is removed only on
|
|
1586
|
+
# the explicit stop paths (:16608, :16658, :17399) -- nothing covers
|
|
1587
|
+
# a crash, so the file outlives its dashboard. Measured in this very
|
|
1588
|
+
# checkout: .loki/dashboard/dashboard.pid held a DEAD 87992 with an
|
|
1589
|
+
# 8-day-old mtime. PIDs recycle, so a stale number eventually names
|
|
1590
|
+
# an unrelated LIVE process and this `kill -9` hits it.
|
|
1591
|
+
#
|
|
1592
|
+
# kill -0 is NOT sufficient: a recycled pid is alive by definition,
|
|
1593
|
+
# which is exactly the insufficiency the loki.pgid self-check had.
|
|
1594
|
+
# Mirrors _app_runner_pid_is_ours (app-runner.sh:242) and fails OPEN
|
|
1595
|
+
# the same way -- if `ps` says nothing we signal as before, so a
|
|
1596
|
+
# legitimate dashboard is never left running by this check.
|
|
1597
|
+
if [ -n "$_shared_pid" ] && _loki_pid_looks_like_dashboard "$_shared_pid"; then
|
|
1570
1598
|
kill "$_shared_pid" 2>/dev/null || true
|
|
1571
1599
|
sleep 0.5
|
|
1572
1600
|
kill -9 "$_shared_pid" 2>/dev/null || true
|
|
@@ -11467,15 +11495,61 @@ sys.stdout.write(t.strip())
|
|
|
11467
11495
|
"${TARGET_DIR:-.}"/apps/*/package.json \
|
|
11468
11496
|
"${TARGET_DIR:-.}"/services/*/package.json; do
|
|
11469
11497
|
[ -f "$pkg_json" ] || continue
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11498
|
+
# Read the workspace's DECLARED test script, same rule as the
|
|
11499
|
+
# single-package path above. A bare grep for '"jest"' matches
|
|
11500
|
+
# a devDependency, so a workspace that merely depends on jest
|
|
11501
|
+
# got labelled monorepo-jest.
|
|
11502
|
+
#
|
|
11503
|
+
# LOWER SEVERITY than the single-package case, and worth being
|
|
11504
|
+
# precise about why: all three branches below dispatch the
|
|
11505
|
+
# project's own script (turbo test / pnpm test --recursive /
|
|
11506
|
+
# npm test), so the grep only ever picked the LABEL, never
|
|
11507
|
+
# what ran. This corrects the evidence, not the execution.
|
|
11508
|
+
local _ws_script
|
|
11509
|
+
_ws_script=$(_LOKI_PKG="$pkg_json" python3 -c "
|
|
11510
|
+
import json, os, sys
|
|
11511
|
+
try:
|
|
11512
|
+
with open(os.environ['_LOKI_PKG']) as f:
|
|
11513
|
+
d = json.load(f)
|
|
11514
|
+
except Exception:
|
|
11515
|
+
sys.exit(0)
|
|
11516
|
+
if not isinstance(d, dict):
|
|
11517
|
+
sys.exit(0)
|
|
11518
|
+
t = (d.get('scripts') or {}).get('test') or ''
|
|
11519
|
+
if 'no test specified' in t.lower():
|
|
11520
|
+
sys.exit(0)
|
|
11521
|
+
sys.stdout.write(t.strip())
|
|
11522
|
+
" 2>/dev/null || echo "")
|
|
11523
|
+
case "$_ws_script" in
|
|
11524
|
+
*vitest*) workspace_runner="vitest"; break ;;
|
|
11525
|
+
*jest*) workspace_runner="jest"; break ;;
|
|
11526
|
+
*mocha*) workspace_runner="mocha"; break ;;
|
|
11527
|
+
"") : ;;
|
|
11528
|
+
*) workspace_runner="npm-test"; break ;;
|
|
11529
|
+
esac
|
|
11477
11530
|
done
|
|
11478
11531
|
|
|
11532
|
+
# FAIL-SAFE. If no workspace DECLARES a test script but one has a
|
|
11533
|
+
# runner installed, still run the monorepo suite -- labelled
|
|
11534
|
+
# "installed" so the evidence does not claim a declaration that
|
|
11535
|
+
# does not exist.
|
|
11536
|
+
#
|
|
11537
|
+
# Without this the change would trade a wrong label for a SKIPPED
|
|
11538
|
+
# GATE, which is the dangerous direction: the old grep at least
|
|
11539
|
+
# dispatched `npm test`. Reporting a runner inaccurately is a
|
|
11540
|
+
# documentation bug; silently not testing a monorepo is not.
|
|
11541
|
+
if [ -z "$workspace_runner" ]; then
|
|
11542
|
+
for pkg_json in "${TARGET_DIR:-.}"/packages/*/package.json \
|
|
11543
|
+
"${TARGET_DIR:-.}"/apps/*/package.json \
|
|
11544
|
+
"${TARGET_DIR:-.}"/services/*/package.json; do
|
|
11545
|
+
[ -f "$pkg_json" ] || continue
|
|
11546
|
+
if grep -qE '"(vitest|jest|mocha)"' "$pkg_json" 2>/dev/null; then
|
|
11547
|
+
workspace_runner="installed"
|
|
11548
|
+
break
|
|
11549
|
+
fi
|
|
11550
|
+
done
|
|
11551
|
+
fi
|
|
11552
|
+
|
|
11479
11553
|
if [ -n "$workspace_runner" ]; then
|
|
11480
11554
|
test_runner="monorepo-$workspace_runner"
|
|
11481
11555
|
local output
|
|
@@ -24622,6 +24696,58 @@ kill_provider_child() {
|
|
|
24622
24696
|
# ~run.sh:15034); in interactive foreground we share the user's shell group,
|
|
24623
24697
|
# leave the pgid absent, and skip this reap entirely -- so Ctrl+C semantics
|
|
24624
24698
|
# and the user's shell are untouched.
|
|
24699
|
+
# --- pgid stamp helpers (P0: stale-pgid session killer) ---------------------
|
|
24700
|
+
# The pgid file is written as `pgid=<n> boot=<id> started=<epoch>` so a reader
|
|
24701
|
+
# can tell a live record from a week-old orphan. See reap_own_process_group.
|
|
24702
|
+
|
|
24703
|
+
# Extract one field from a stamp. A bare number (legacy file) yields the pgid
|
|
24704
|
+
# and nothing else, so the reader's fail-closed branch rejects it.
|
|
24705
|
+
_loki_pgid_field() {
|
|
24706
|
+
local _s="$1" _k="$2"
|
|
24707
|
+
case "$_s" in
|
|
24708
|
+
*=*) ;;
|
|
24709
|
+
*) [ "$_k" = "pgid" ] && printf '%s' "$(printf '%s' "$_s" | tr -d ' ')"; return 0 ;;
|
|
24710
|
+
esac
|
|
24711
|
+
# Prefix/suffix trimming, not `for _tok in $_s`. Two reasons:
|
|
24712
|
+
# - `while read` in a pipeline runs in a SUBSHELL and drops a final field
|
|
24713
|
+
# with no trailing newline, silently returning an empty `started`.
|
|
24714
|
+
# - unquoted `$_s` in a `for` relies on word splitting, which bash does
|
|
24715
|
+
# and ZSH DOES NOT. run.sh is bash, but this file gets sourced and
|
|
24716
|
+
# probed from other shells, and a helper whose correctness depends on
|
|
24717
|
+
# the caller's shell is a trap. This form needs no word splitting.
|
|
24718
|
+
local _rest="$_s" _tok
|
|
24719
|
+
while [ -n "$_rest" ]; do
|
|
24720
|
+
_tok="${_rest%% *}" # first space-delimited token
|
|
24721
|
+
case "$_tok" in
|
|
24722
|
+
"$_k"=*) printf '%s' "${_tok#*=}"; return 0 ;;
|
|
24723
|
+
esac
|
|
24724
|
+
case "$_rest" in
|
|
24725
|
+
*" "*) _rest="${_rest#* }" ;; # advance past this token
|
|
24726
|
+
*) _rest="" ;; # last token: stop
|
|
24727
|
+
esac
|
|
24728
|
+
done
|
|
24729
|
+
}
|
|
24730
|
+
|
|
24731
|
+
# A value that changes on every reboot, so a pgid recorded before a reboot can
|
|
24732
|
+
# never validate afterwards. Empty when unavailable -- callers fail closed.
|
|
24733
|
+
_loki_boot_id() {
|
|
24734
|
+
if [ -r /proc/stat ]; then
|
|
24735
|
+
awk '/^btime /{print $2; exit}' /proc/stat 2>/dev/null && return 0
|
|
24736
|
+
fi
|
|
24737
|
+
# macOS: `kern.boottime` prints `{ sec = 1234567890, usec = 0 } ...`
|
|
24738
|
+
sysctl -n kern.boottime 2>/dev/null | sed -n 's/.*sec *= *\([0-9][0-9]*\).*/\1/p'
|
|
24739
|
+
}
|
|
24740
|
+
|
|
24741
|
+
# Start time of a pid as a unix epoch, or empty when it cannot be determined.
|
|
24742
|
+
_loki_proc_start_epoch() {
|
|
24743
|
+
local _p="${1:-$$}" _lstart
|
|
24744
|
+
_lstart="$(ps -o lstart= -p "$_p" 2>/dev/null)"
|
|
24745
|
+
[ -n "$_lstart" ] || return 0
|
|
24746
|
+
date -j -f '%a %b %e %T %Y' "$_lstart" '+%s' 2>/dev/null \
|
|
24747
|
+
|| date -d "$_lstart" '+%s' 2>/dev/null \
|
|
24748
|
+
|| true
|
|
24749
|
+
}
|
|
24750
|
+
|
|
24625
24751
|
reap_own_process_group() {
|
|
24626
24752
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
24627
24753
|
# Resolve the pgid file the same way main() recorded it (global or per-session).
|
|
@@ -24632,8 +24758,13 @@ reap_own_process_group() {
|
|
|
24632
24758
|
_reap_pgid_file="${_reap_pgid_file%.pid}.pgid"
|
|
24633
24759
|
[ -f "$_reap_pgid_file" ] || return 0 # interactive / no own session: skip
|
|
24634
24760
|
|
|
24635
|
-
|
|
24636
|
-
|
|
24761
|
+
# Parse the stamped format `pgid=<n> boot=<id> started=<epoch>`, falling back
|
|
24762
|
+
# to a bare number for a file written by an older version.
|
|
24763
|
+
local _stamp _pgid _boot _started
|
|
24764
|
+
_stamp=$(cat "$_reap_pgid_file" 2>/dev/null | tr -d '\n')
|
|
24765
|
+
_pgid=$(_loki_pgid_field "$_stamp" pgid)
|
|
24766
|
+
_boot=$(_loki_pgid_field "$_stamp" boot)
|
|
24767
|
+
_started=$(_loki_pgid_field "$_stamp" started)
|
|
24637
24768
|
case "$_pgid" in ''|*[!0-9]*) return 0 ;; esac
|
|
24638
24769
|
[ "$_pgid" -gt 1 ] 2>/dev/null || return 0 # never touch pgid 0/1
|
|
24639
24770
|
|
|
@@ -24644,6 +24775,40 @@ reap_own_process_group() {
|
|
|
24644
24775
|
_my_pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')
|
|
24645
24776
|
[ -n "$_my_pgid" ] && [ "$_pgid" = "$_my_pgid" ] || return 0
|
|
24646
24777
|
|
|
24778
|
+
# BOOT + AGE GUARD (the fix for the session-killer).
|
|
24779
|
+
#
|
|
24780
|
+
# The self-check above is NOT sufficient on its own. loki.pgid was removed
|
|
24781
|
+
# only on the normal exit path, so a Ctrl+C'd or crashed run left the file
|
|
24782
|
+
# behind indefinitely -- measured on a real machine: two orphans aged 155h
|
|
24783
|
+
# and 202h. PIDs recycle (macOS wraps near 99999; max observed 99762), so a
|
|
24784
|
+
# week-old pgid eventually matches a LIVE, unrelated shell group. The
|
|
24785
|
+
# self-check then passes and every sibling in the user's terminal -- their
|
|
24786
|
+
# editor, another agent session -- is TERM'd then KILL'd.
|
|
24787
|
+
#
|
|
24788
|
+
# A recycled pgid cannot forge both of these:
|
|
24789
|
+
# - boot id: a file from a previous boot can never match this boot.
|
|
24790
|
+
# - started: the recording run must not predate THIS process.
|
|
24791
|
+
#
|
|
24792
|
+
# Fails CLOSED. A missing or malformed stamp means we do nothing: an
|
|
24793
|
+
# orphaned agent (the v7.41.x problem this reap was added for) is strictly
|
|
24794
|
+
# less harmful than killing a user's editor.
|
|
24795
|
+
local _now_boot
|
|
24796
|
+
_now_boot="$(_loki_boot_id)"
|
|
24797
|
+
if [ -z "$_boot" ] || [ -z "$_started" ]; then
|
|
24798
|
+
return 0 # unstamped legacy file: never trust it
|
|
24799
|
+
fi
|
|
24800
|
+
if [ -z "$_now_boot" ] || [ "$_boot" != "$_now_boot" ]; then
|
|
24801
|
+
return 0 # different boot (or boot id unavailable): refuse
|
|
24802
|
+
fi
|
|
24803
|
+
case "$_started" in ''|*[!0-9]*) return 0 ;; esac
|
|
24804
|
+
local _self_started
|
|
24805
|
+
_self_started="$(_loki_proc_start_epoch $$)"
|
|
24806
|
+
# The stamp must not predate this process by more than a small clock skew.
|
|
24807
|
+
# A stale file is always OLDER than the process now reading it.
|
|
24808
|
+
if [ -n "$_self_started" ] && [ "$_started" -lt "$(( _self_started - 5 ))" ]; then
|
|
24809
|
+
return 0
|
|
24810
|
+
fi
|
|
24811
|
+
|
|
24647
24812
|
# Collect protected pids (dashboard, app-runner, registered children) so the
|
|
24648
24813
|
# reap never takes down the shared dashboard if it happens to share our
|
|
24649
24814
|
# group. Mirrors the `loki stop` / dashboard reaper protection set.
|
|
@@ -25045,6 +25210,20 @@ _loki_remove_temp_self_copy() {
|
|
|
25045
25210
|
fi
|
|
25046
25211
|
}
|
|
25047
25212
|
|
|
25213
|
+
# P0 (stale-pgid session killer): the pgid file must never outlive its run.
|
|
25214
|
+
# Recorded by the writer (see _LOKI_PGID_FILE below) rather than re-derived, so
|
|
25215
|
+
# the removal hits the exact path written -- global OR per-session -- and can
|
|
25216
|
+
# never delete a concurrent run's file.
|
|
25217
|
+
_loki_remove_pgid_file() {
|
|
25218
|
+
[ -n "${_LOKI_PGID_FILE:-}" ] || return 0
|
|
25219
|
+
# A `trap ... EXIT` fires in SUBSHELLS too, and a subshell exiting must not
|
|
25220
|
+
# delete a LIVE parent's file (repo scar: a bare trap deleted a parent's
|
|
25221
|
+
# lock). $$ does NOT change in a subshell -- BASHPID does, so it is the only
|
|
25222
|
+
# real discriminator here.
|
|
25223
|
+
[ "${BASHPID:-$$}" = "${_LOKI_PGID_OWNER:-$$}" ] || return 0
|
|
25224
|
+
rm -f "$_LOKI_PGID_FILE" 2>/dev/null || true
|
|
25225
|
+
}
|
|
25226
|
+
|
|
25048
25227
|
_loki_session_exit_cleanup() {
|
|
25049
25228
|
local exit_code=$?
|
|
25050
25229
|
_loki_terminal_record 2>/dev/null || true
|
|
@@ -25052,6 +25231,12 @@ _loki_session_exit_cleanup() {
|
|
|
25052
25231
|
&& type safe_release_lock >/dev/null 2>&1; then
|
|
25053
25232
|
safe_release_lock "$_LOKI_SESSION_LOCK_FILE" 2>/dev/null || true
|
|
25054
25233
|
fi
|
|
25234
|
+
# Covers EXIT plus INT/TERM: every cleanup() path that actually terminates
|
|
25235
|
+
# does so via `exit`, which fires this trap. Deliberately NOT called from
|
|
25236
|
+
# cleanup() itself -- its perpetual/pause branches RETURN and the run
|
|
25237
|
+
# continues, so removing the file there would silently disable the
|
|
25238
|
+
# completion-path reap for the rest of the run.
|
|
25239
|
+
_loki_remove_pgid_file
|
|
25055
25240
|
_loki_remove_temp_self_copy
|
|
25056
25241
|
return "$exit_code"
|
|
25057
25242
|
}
|
|
@@ -25720,6 +25905,10 @@ main() {
|
|
|
25720
25905
|
fi
|
|
25721
25906
|
else
|
|
25722
25907
|
# Lock helper not loaded (lib/lock.sh missing). PID-only fallback.
|
|
25908
|
+
# This branch still falls through to the pgid writer below, so it needs
|
|
25909
|
+
# the same EXIT cleanup or the pgid file leaks on every interrupt here.
|
|
25910
|
+
# An empty lock file is safe: the release is [ -n ]-guarded.
|
|
25911
|
+
_loki_arm_session_exit_cleanup ""
|
|
25723
25912
|
if [ -f "$pid_file" ]; then
|
|
25724
25913
|
local existing_pid
|
|
25725
25914
|
existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
@@ -25750,7 +25939,22 @@ main() {
|
|
|
25750
25939
|
if [ "${LOKI_OWN_SESSION:-}" = "1" ]; then
|
|
25751
25940
|
_loki_pgid="$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')"
|
|
25752
25941
|
if [ -n "$_loki_pgid" ]; then
|
|
25753
|
-
|
|
25942
|
+
# STAMPED, not a bare number. The reader requires boot id + start
|
|
25943
|
+
# epoch to match before it will signal anything, because this file
|
|
25944
|
+
# outlived its run (measured: 155h and 202h orphans) and PIDs
|
|
25945
|
+
# recycle -- a stale bare pgid eventually matches a live shell group
|
|
25946
|
+
# and the reap killed the user's other terminal sessions.
|
|
25947
|
+
_loki_pgid_started="$(_loki_proc_start_epoch $$)"
|
|
25948
|
+
[ -n "$_loki_pgid_started" ] || _loki_pgid_started="$(date +%s 2>/dev/null)"
|
|
25949
|
+
# Record the EXACT path written (global or per-session) plus the
|
|
25950
|
+
# owning shell, so the EXIT trap removes THIS file and only from
|
|
25951
|
+
# this process. Without removal on the interrupt/crash paths the
|
|
25952
|
+
# file outlived its run -- measured orphans aged 155h and 202h.
|
|
25953
|
+
_LOKI_PGID_FILE="${pid_file%.pid}.pgid"
|
|
25954
|
+
_LOKI_PGID_OWNER="${BASHPID:-$$}"
|
|
25955
|
+
printf 'pgid=%s boot=%s started=%s\n' \
|
|
25956
|
+
"$_loki_pgid" "$(_loki_boot_id)" "$_loki_pgid_started" \
|
|
25957
|
+
> "$_LOKI_PGID_FILE" 2>/dev/null || true
|
|
25754
25958
|
fi
|
|
25755
25959
|
fi
|
|
25756
25960
|
# Store session ID in state for dashboard/status visibility
|