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.
@@ -0,0 +1,174 @@
1
+ # Stale-State Audit
2
+
3
+ Audit for siblings of the `loki.pgid` session-killer (fixed in 4792b521).
4
+
5
+ **The shape being hunted:** a file recording a PID, PGID, port, lock, or session
6
+ id, written by one run, NOT removed on abnormal exit, later TRUSTED by another
7
+ run after the OS recycled the identifier.
8
+
9
+ **Method:** grep for the write, find its `rm -f`, check whether a trap covers
10
+ INT/TERM/crash, then check whether the reader proves the record is still
11
+ current. Every candidate was measured on this host rather than judged by
12
+ reading, because the pgid finding was credible on evidence (155h/202h orphans),
13
+ not on the code looking wrong.
14
+
15
+ Scope: read-only across the repo; fix applied to one file (`autonomy/run.sh`).
16
+
17
+ ---
18
+
19
+ ## Measured state of this host
20
+
21
+ Run from the repo root on 2026-08-08:
22
+
23
+ | File | Contents | Live? | Age |
24
+ |---|---|---|---|
25
+ | `~/.loki/dashboard/dashboard.pid` | absent | n/a | n/a |
26
+ | `.loki/dashboard/dashboard.pid` | `87992` | **DEAD** | **mtime Jul 31 (8 days)** |
27
+ | `$TMPDIR/loki-local-ci.lock` | `61876` | LIVE (real local-ci) | current |
28
+ | `.loki/app-runner/app.pid` | absent | n/a | n/a |
29
+
30
+ The dashboard pid file is the same measured shape as the pgid orphans: a dead
31
+ identifier, days old, still on disk, still trusted by a code path that kills.
32
+
33
+ ---
34
+
35
+ ## Findings, ranked by blast radius
36
+
37
+ ### FINDING 1 (HIGH -- fixed here): shared dashboard pid is killed unverified
38
+
39
+ **Evidence.** `autonomy/run.sh:1597-1600` reads a pid from
40
+ `~/.loki/dashboard/dashboard.pid` and sends it `kill` then `kill -9` with no
41
+ liveness and no identity check.
42
+
43
+ Removal of that file happens ONLY on explicit stop paths -- `run.sh:16608`,
44
+ `:16658`, `:17399`, and `autonomy/loki:7078-7079`. **No trap covers a crash or
45
+ Ctrl+C.** That is precisely why the measured copy in this checkout is 8 days
46
+ stale with a dead pid.
47
+
48
+ **Reachable:** yes. Called from `cleanup` on both stop paths (`run.sh:25311`,
49
+ `:25384`).
50
+
51
+ **Blast radius: the worst class.** The file lives under `~/.loki`, so it is
52
+ machine-global -- the victim need not belong to this project. A recycled pid
53
+ names an unrelated live process and receives `kill -9`.
54
+
55
+ **Why `kill -0` would not have fixed it:** a recycled pid IS alive, so a
56
+ liveness check passes. This is the identical insufficiency the `loki.pgid`
57
+ self-check had. The guard has to check IDENTITY.
58
+
59
+ **Fix applied.** `_loki_pid_looks_like_dashboard()` at `autonomy/run.sh:1526`,
60
+ gating the kill at `:1597`. It mirrors `_app_runner_pid_is_ours`
61
+ (`app-runner.sh:242`) and **fails OPEN**: when `ps` reports nothing we signal
62
+ exactly as before, so a legitimate dashboard is never left running by this
63
+ check. The only behavior change is refusing to kill a process positively
64
+ identified as not a dashboard.
65
+
66
+ Deliberately NOT done: adding a trap to remove the pid file on crash. That is a
67
+ larger change across the dashboard lifecycle, and the identity guard already
68
+ makes a stale file harmless at the point where it does damage. The stale file
69
+ still being present is untidy, not dangerous, once the killer verifies.
70
+
71
+ **Test:** `tests/test-stale-dashboard-pid.sh`, 6/6, mutation-verified below.
72
+
73
+ ### FINDING 2 (MEDIUM -- reported, not fixed): `app_runner_stop` group-kills unverified
74
+
75
+ **Evidence.** `app-runner.sh:1407` falls back to reading `app.pid` from disk,
76
+ then `:1456` sends `kill -TERM "-$_APP_RUNNER_PID"` -- a **process-group**
77
+ signal -- without an identity check.
78
+
79
+ The repo already has the right guard: `_app_runner_pid_is_ours`
80
+ (`app-runner.sh:242`). It is called at `:1657` and `:1829` but **not** on the
81
+ stop path. `app-runner.sh` has **no trap at all** (`grep -n "trap " ` returns
82
+ nothing), so `app.pid` survives a crash exactly like the pgid file did.
83
+
84
+ **Blast radius:** higher per-hit than Finding 1 (a group signal reaches a whole
85
+ tree) but narrower reach: the file is project-local (`.loki/app-runner/`), not
86
+ machine-global, and was absent on this host. Not fixed because it is a third
87
+ file and the lead scoped this task to one; it needs its own change adding
88
+ `_app_runner_pid_is_ours` to the stop path.
89
+
90
+ ### FINDING 3 (LOW -- reported, not fixed): local-ci lock guard is a no-op under `/bin/bash`
91
+
92
+ **Evidence.** `scripts/local-ci.sh:104`:
93
+
94
+ ```
95
+ trap '[ "$$" = "$_lci_owner" ] && rm -f "$_lci_lock" 2>/dev/null || true' EXIT
96
+ ```
97
+
98
+ The scar comment above it (`:99-103`) correctly diagnoses that a bare EXIT trap
99
+ fires in every subshell and that a finishing child deleted the parent's lock.
100
+ **But `$$` does not change in a bash subshell, so this guard does not
101
+ discriminate.** Verified:
102
+
103
+ ```
104
+ $ bash -c 'p=$$; ( [ "$$" = "$p" ] && echo same )'
105
+ same
106
+ ```
107
+
108
+ The correct discriminators are `BASHPID` (bash 4+) or `BASH_SUBSHELL` (works in
109
+ 3.2). Measured on this host: the script is `#!/usr/bin/env bash` which resolves
110
+ to **bash 5.3**, where `BASHPID` is available. But `/bin/bash` here is **3.2**,
111
+ where `BASHPID` is unset and `BASH_SUBSHELL` is the version-safe choice.
112
+
113
+ **Blast radius: benign** by the lead's own criterion. Worst case is two
114
+ concurrent local-ci runs starving each other into phantom failures -- costly in
115
+ time and trust, but it kills nothing. Left unfixed as out of scope; the one-line
116
+ change is `[ "${BASH_SUBSHELL:-0}" = 0 ]`.
117
+
118
+ Note on the sibling fix already shipped: `_loki_remove_pgid_file`
119
+ (`run.sh:25189`) uses `${BASHPID:-$$}`. Under bash 3.2 that collapses to `$$`
120
+ and the guard degrades to the no-op. **The degradation direction is safe** -- a
121
+ subshell deletes the pgid file early, the reap then finds no file and skips, so
122
+ an orphan survives and nobody gets killed. `run.sh` is also `#!/usr/bin/env
123
+ bash` (5.3 here), so this is a portability note, not a live defect. Worth
124
+ knowing: the mutation test for that guard asserts *source text* contains
125
+ `BASHPID`, so it proves the code is present, not that it behaves correctly on a
126
+ 3.2 host.
127
+
128
+ ---
129
+
130
+ ## Already guarded -- do not re-audit
131
+
132
+ Checked and found correct. Listed explicitly so this ground is not covered
133
+ twice.
134
+
135
+ - **`autonomy/lib/lock.sh:56-67`** -- `_loki_lock_is_stale` requires the
136
+ sentinel PID to be dead **AND** mtime > 30s. Both conditions, not either.
137
+ Correct.
138
+ - **`cleanup_orphan_pids` (`run.sh:2300+`)** -- reaps only on liveness AND
139
+ parent death AND (for wrappers) idle-past-budget AND no live engine child.
140
+ Also self-skips `$$`. Correct, and notably stricter than the pgid reap was.
141
+ - **`_app_runner_pid_is_ours` (`app-runner.sh:242`)** -- a real identity token
142
+ captured post-exec, failing open on a missing token so a live app is never
143
+ falsely killed. Correct where it is called; see Finding 2 for where it is not.
144
+ - **`_app_runner_collect_descendants` (`app-runner.sh:289`)** -- refuses pid
145
+ 0/1 and walks parent-child links from our own pid only, so it structurally
146
+ cannot signal outside our subtree. Correct.
147
+ - **`status.ts:266-270` and the bash status reader (`loki:5092`)** -- both do
148
+ `os.kill(pid, 0)` before reporting. A wrong answer only mislabels a URL in
149
+ status output. Benign even when stale.
150
+ - **The `CLEAR`/`KEEP` registry check (`run.sh:1545-1560`)** -- correctly
151
+ refuses to tear the shared dashboard down while any other project holds a
152
+ live pid. This gates Finding 1's call site; the defect was the missing check
153
+ on the pid itself, not this decision.
154
+
155
+ ---
156
+
157
+ ## Mutation verification (Finding 1's fix)
158
+
159
+ Each guard was reverted individually; the test must go red, and only on its own
160
+ assertion.
161
+
162
+ | Mutation | Before | After | Assertion killed |
163
+ |---|---|---|---|
164
+ | Identity check downgraded to bare `kill -0` | 6 pass / 0 fail | 5 / 1 | "a live non-dashboard process is refused" |
165
+ | Call site ungated (guard present but unused) | 6 / 0 | 5 / 1 | "the kill is gated on the identity guard" |
166
+ | `pid > 1` check removed | 6 / 0 | 5 / 1 | "pid 0/1, empty and malformed inputs refused" |
167
+
168
+ Restored after each. Final: `bash -n autonomy/run.sh` clean,
169
+ `tests/test-stale-dashboard-pid.sh` 6/6, `tests/test-pgid-stale-reap.sh` 8/8
170
+ (unaffected).
171
+
172
+ The first mutation is the important one: it replaces the identity check with
173
+ exactly the insufficient guard (`kill -0`) that a reviewer would most likely
174
+ propose, and the test catches it.
@@ -115,9 +115,39 @@ above less trustworthy:
115
115
  LOCAL_CI_TIER=full bash scripts/local-ci.sh # the full gate, timed
116
116
  loki outcomes --json # post-merge outcomes, or UNKNOWN with reasons
117
117
  loki intent status --json # spec-vs-intent drift
118
- loki proof verify <id> # re-hash a receipt, exit 1 on tamper
118
+ loki proof verify <id> # re-hash a receipt (see below)
119
119
  bash tests/test-competitor-verify-surface.sh # the competitor CLI measurement
120
120
  ```
121
121
 
122
+ ### Prove the tamper detection yourself, in three commands
123
+
124
+ The claim is narrow and worth stating exactly: editing a receipt's recorded
125
+ facts is DETECTED. Run this against any receipt in `.loki/proofs/`:
126
+
127
+ ```bash
128
+ ID=$(ls .loki/proofs | head -1)
129
+ V() { loki proof verify "$ID" --json | python3 -c 'import json,sys;print(json.load(sys.stdin)["hash_ok"])'; }
130
+ V # True
131
+ python3 -c "import json;p='.loki/proofs/$ID/proof.json';d=json.load(open(p));d['files_changed']={'count':999999};json.dump(d,open(p,'w'))"
132
+ V # False
133
+ ```
134
+
135
+ Measured on this repository: `True` -> `False` -> `True` after restoring.
136
+
137
+ **Read `hash_ok`, not `ok`.** They answer different questions and conflating
138
+ them produces a false alarm. `hash_ok` is integrity: do the recorded facts still
139
+ hash to the recorded digest. `ok` also folds in `tree_drift`, which is true
140
+ whenever the working tree has moved since the receipt was written -- so an
141
+ untampered receipt from last month correctly reports `ok: false` with
142
+ `hash_ok: true`. An earlier version of this document said "exit 1 on tamper",
143
+ which is wrong in exactly that way: a drifted-but-intact receipt also exits 1.
144
+
145
+ **What this does NOT establish.** Integrity is not provenance. On the unsigned
146
+ path a party who rewrites the facts AND recomputes the digest passes this check
147
+ -- see the forgeability limit above. Provenance requires the signed path
148
+ (`LOKI_PROOF_GPG_KEY`, [SIGNED-RECEIPTS.md](SIGNED-RECEIPTS.md)), and the remote
149
+ client reports the two separately for that reason: VERIFIED, UNSIGNED,
150
+ UNCHECKED and TAMPERED are four distinct verdicts, never collapsed.
151
+
122
152
  If a number here does not reproduce on your machine, that is a defect and we
123
153
  want the report.
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.17.2";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Yf(Jf(import.meta.url)),Q=X$(X);h5=Xf(Qf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var _O={};l0(_O,{runOrThrow:()=>Tf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Cf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>bO});async function NQ(Z,X=bO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let V=$.byteLength-(z-X);J+=Y.decode($.subarray(0,V),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Tf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=wf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function wf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Cf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var bO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return Ff?"":Z}var Ff,A0,F8,_0,$W0,a0,V8,Q9,h;var S6=p(()=>{Ff=(process.env.NO_COLOR??"").length>0;A0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),_0=o7("\x1B[1;33m"),$W0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),V8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),h=o7("\x1B[0m")});import{existsSync as ff}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(ff(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var XA={};l0(XA,{runStatus:()=>Vh});import{existsSync as Y9,readFileSync as g3,readdirSync as aO,statSync as sO}from"fs";import{resolve as h8,basename as ef}from"path";import{homedir as Zh}from"os";function nO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function oO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*AQ/X);if(J>AQ)J=AQ;let z=AQ-J,K=F8;if(Y>=80)K=A0;else if(Y>=50)K=_0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),V=nO(Z),W=nO(X);return` ${V8}${Q}${h} ${K}[${$}]${h} ${Y}% (${V} / ${W})`}async function Qh(){if(await X9("jq"))return!0;return process.stdout.write(`${A0}Error: jq is required but not installed.${h}
2
+ var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.18.2";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Yf(Jf(import.meta.url)),Q=X$(X);h5=Xf(Qf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var _O={};l0(_O,{runOrThrow:()=>Tf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Cf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>bO});async function NQ(Z,X=bO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let V=$.byteLength-(z-X);J+=Y.decode($.subarray(0,V),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Tf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=wf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function wf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Cf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var bO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return Ff?"":Z}var Ff,A0,F8,_0,$W0,a0,V8,Q9,h;var S6=p(()=>{Ff=(process.env.NO_COLOR??"").length>0;A0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),_0=o7("\x1B[1;33m"),$W0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),V8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),h=o7("\x1B[0m")});import{existsSync as ff}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(ff(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var XA={};l0(XA,{runStatus:()=>Vh});import{existsSync as Y9,readFileSync as g3,readdirSync as aO,statSync as sO}from"fs";import{resolve as h8,basename as ef}from"path";import{homedir as Zh}from"os";function nO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function oO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*AQ/X);if(J>AQ)J=AQ;let z=AQ-J,K=F8;if(Y>=80)K=A0;else if(Y>=50)K=_0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),V=nO(Z),W=nO(X);return` ${V8}${Q}${h} ${K}[${$}]${h} ${Y}% (${V} / ${W})`}async function Qh(){if(await X9("jq"))return!0;return process.stdout.write(`${A0}Error: jq is required but not installed.${h}
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)
@@ -1236,4 +1236,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1236
1236
  `),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (g_(),v_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
1237
1237
  `),process.stderr.write(m_),2}}lO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var dV0=await pV0(Bun.argv.slice(2));process.exit(dV0);
1238
1238
 
1239
- //# debugId=0F59AA02EE13707A64756E2164756E21
1239
+ //# debugId=B34A84808461BB0C64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.17.2'
78
+ __version__ = '9.18.2'
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": "9.17.2",
4
+ "version": "9.18.2",
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": "9.17.2",
5
+ "version": "9.18.2",
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",