loki-mode 8.84.0 → 8.86.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 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 v8.84.0
6
+ # Loki Mode v8.86.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
469
469
 
470
470
  ---
471
471
 
472
- **v8.84.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.86.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.84.0
1
+ 8.86.0
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env python3
2
+ """Turn a `loki doctor --json` report into an ordered remediation plan.
3
+
4
+ WHY THIS EXISTS. `loki doctor` tells you WHAT is broken. It does not tell you
5
+ what to type. On a bare machine the report names two blockers and the user is
6
+ left to work out the commands and the ORDER themselves -- and the order is not
7
+ obvious: doctor's own fix for a missing provider is
8
+ `npm install -g @anthropic-ai/claude-code`, which cannot run until Node.js is
9
+ installed, and Node.js is the OTHER blocker. Handing someone two commands in
10
+ report order makes the first one fail.
11
+
12
+ WHAT IT REFUSES TO DO, and this is the actual product:
13
+
14
+ 1. It never invents a command. A blocker with no remedy we can justify is
15
+ printed as "no automated fix known", verbatim and unadorned. A wrong
16
+ command is strictly worse than no command: the user runs it, it succeeds
17
+ at doing something irrelevant, and now they trust a broken system. Every
18
+ remedy here is either lifted from the report's own text or from a table
19
+ whose provenance is cited below.
20
+ 2. It never executes. It prints. `loki doctor` is a read-only diagnosis and
21
+ so is this; the human runs the commands.
22
+ 3. It never reads an empty result as success. Zero derived blockers means
23
+ "healthy" ONLY when the input actually parsed as a doctor report -- see
24
+ the vacuity guard in plan().
25
+
26
+ PROVENANCE OF EVERY REMEDY. Nothing here is a parallel table invented to sit
27
+ alongside the repo's existing knowledge:
28
+
29
+ - Provider install commands come from the report itself. `ai_provider.detail`
30
+ carries the literal string "No AI provider CLI. Fix: npm install -g
31
+ @anthropic-ai/claude-code" (written at autonomy/loki:11519 for the text
32
+ path and in cmd_doctor_json for --json). We PARSE that "Fix:" rather than
33
+ restating it, so doctor stays the single source of truth: change the
34
+ command there and this follows automatically.
35
+ - _TOOL_FIXES is MIXED provenance, and the split is stated per entry in the
36
+ table itself rather than summarised here, because "derived from the repo"
37
+ and "conventional package-manager command" are different strengths of
38
+ claim and this file exists to keep them apart:
39
+ SOURCED `brew install python3` (autonomy/loki:500), `brew install jq`
40
+ (:552), and `https://nodejs.org` (autonomy/provider-offer.sh:321)
41
+ are the commands the CLI already prints for these same tools.
42
+ Duplicated rather than imported because they live in bash
43
+ `echo` statements inside cmd_* functions.
44
+ UNSOURCED `brew install node`, `brew install git`, `brew install curl`
45
+ and every `apt-get install ...` appear NOWHERE else in this
46
+ repo. They are the standard invocations for these packages,
47
+ not something doctor told us. They are included because they
48
+ are the ordinary, verifiable way to install these tools -- but
49
+ do not read them as repo-derived, and if one is ever wrong the
50
+ fault is here, not upstream.
51
+ - Auth remedies are deliberately ABSENT. `cmd_why`'s PROVIDER_AUTH map
52
+ (autonomy/loki:4185) keys on a LAST_ERROR error_class, which is a
53
+ different axis entirely -- `loki doctor --json` has no auth blocker to map
54
+ from. Manufacturing one so this tool could show off an ordering
55
+ dependency would be exactly the fabrication requirement 1 forbids.
56
+
57
+ INPUT CONTRACT. Blockers are DERIVED, not read: `doctor --json` emits no
58
+ blocker list (that string is built only in the text path, autonomy/loki:11367).
59
+ Anything with status "fail" is a blocker, and failable things live in three
60
+ places -- `checks[]`, plus the siblings `ai_provider` and `disk`. The advisory
61
+ siblings (`sentrux`, `receipt_signing`, `memory`, `model_catalog`) are never
62
+ "fail" by construction and are not scanned.
63
+
64
+ KNOWN LIMITATION, stated rather than papered over: the text path reports two
65
+ blockers that have NO representation in --json at all -- a broken skill symlink
66
+ (autonomy/loki:11636) and missing quality-gate detectors (:11968). This tool
67
+ cannot see them, because the JSON it consumes does not carry them. It handles
68
+ them if they ever appear as blocker text, but it cannot derive them today.
69
+ """
70
+
71
+ import argparse
72
+ import json
73
+ import re
74
+ import sys
75
+
76
+ # Prerequisite ordering. Lower rank runs first. The only edges asserted are
77
+ # ones with a real causal dependency, because a fabricated ordering is the same
78
+ # class of lie as a fabricated command.
79
+ #
80
+ # disk (0) -- every remedy below writes files; no space means they all fail.
81
+ # runtime (1) -- node/python3 are what the package managers RUN.
82
+ # tool (2) -- jq/git/curl: independent, but cheap and unblocking.
83
+ # provider (3) -- MUST follow runtime: doctor's own provider fix is
84
+ # `npm install -g ...`, and npm ships with Node.js. On the
85
+ # real capture that produced this file, Node.js and the
86
+ # provider were both blockers simultaneously.
87
+ # unknown (9) -- last: we cannot reason about what we cannot identify.
88
+ _RANK = {"disk": 0, "runtime": 1, "tool": 2, "provider": 3, "unknown": 9}
89
+
90
+ # Per-tool remedies, keyed on doctor's own `name` field. A tool ABSENT from
91
+ # this table gets an honest "no known fix" -- never a guess. Adding a row is
92
+ # therefore the one place fabrication can enter this file, so each row is
93
+ # tagged with where its command came from:
94
+ # [repo] this exact command is printed elsewhere in loki-mode (line cited)
95
+ # [conv] conventional package-manager invocation, NOT found in this repo
96
+ # If you add a row, tag it. An untagged row is an unaudited claim.
97
+ _TOOL_FIXES = {
98
+ # [repo] https://nodejs.org -- autonomy/provider-offer.sh:321. [conv] brew install node.
99
+ "Node.js": ("runtime", "Install Node.js 18+: brew install node (macOS) | https://nodejs.org"),
100
+ # [repo] autonomy/loki:500.
101
+ "Python 3": ("runtime", "Install Python 3.8+: brew install python3 (macOS)"),
102
+ # [repo] brew arm -- autonomy/loki:552. [conv] apt-get arm.
103
+ "jq": ("tool", "brew install jq (macOS) | apt-get install jq (Debian/Ubuntu)"),
104
+ # [conv] both arms.
105
+ "git": ("tool", "brew install git (macOS) | apt-get install git (Debian/Ubuntu)"),
106
+ # [conv] both arms.
107
+ "curl": ("tool", "brew install curl (macOS) | apt-get install curl (Debian/Ubuntu)"),
108
+ }
109
+
110
+ _NO_FIX = "no automated fix known for this blocker -- diagnose manually, then re-run: loki doctor"
111
+
112
+ # doctor embeds its own remedy as "Fix: <cmd>" or "Reinstall: <cmd>". Parsing it
113
+ # keeps doctor authoritative instead of duplicating the command here.
114
+ _EMBEDDED_FIX = re.compile(r"(?:Fix|Reinstall):\s*(.+?)\s*$")
115
+
116
+
117
+ def _embedded_fix(text):
118
+ """Return the command doctor embedded in its own blocker text, else None.
119
+
120
+ None is a real answer, not a failure to try: it routes the blocker to the
121
+ honest no-fix path instead of to a guess.
122
+ """
123
+ if not text:
124
+ return None
125
+ m = _EMBEDDED_FIX.search(str(text).strip())
126
+ return m.group(1) if m else None
127
+
128
+
129
+ def derive_blockers(report):
130
+ """Every status=="fail" item in a doctor report, as (id, category, title, fix).
131
+
132
+ fix is None when we have no justified remedy. Callers MUST render None as
133
+ the no-fix line rather than substituting anything.
134
+ """
135
+ out = []
136
+ if not isinstance(report, dict):
137
+ return out
138
+
139
+ for chk in report.get("checks") or []:
140
+ if not isinstance(chk, dict) or chk.get("status") != "fail":
141
+ continue
142
+ name = str(chk.get("name") or "unknown check")
143
+ # Distinguish absent from too-old: same blocker name, different user
144
+ # experience, and doctor already knows which it is.
145
+ if chk.get("found") and chk.get("min_version"):
146
+ title = "%s is older than the required %s (found %s)" % (
147
+ name, chk["min_version"], chk.get("version") or "unknown")
148
+ else:
149
+ title = "%s is not installed" % name
150
+ category, fix = _TOOL_FIXES.get(name, ("unknown", None))
151
+ out.append((name, category, title, fix))
152
+
153
+ ai = report.get("ai_provider")
154
+ if isinstance(ai, dict) and ai.get("status") == "fail":
155
+ detail = ai.get("detail")
156
+ # Reuse doctor's embedded command; do not restate it.
157
+ out.append(("ai_provider", "provider",
158
+ "No AI provider CLI installed (at least one is required)",
159
+ _embedded_fix(detail)))
160
+
161
+ disk = report.get("disk")
162
+ if isinstance(disk, dict) and disk.get("status") == "fail":
163
+ gb = disk.get("available_gb")
164
+ avail = "unknown" if gb is None else "%sGB" % gb
165
+ out.append(("disk", "disk",
166
+ "Insufficient disk space (%s available, need >= 1GB)" % avail,
167
+ "Free at least 1GB on your home volume, then re-run: loki doctor"))
168
+
169
+ return out
170
+
171
+
172
+ def plan(report):
173
+ """Ordered remediation plan for a doctor report.
174
+
175
+ Returns a dict with `status` in {healthy, action_required, not_a_report}
176
+ and `steps`. The three-way status is the vacuity guard: an empty blocker
177
+ list means HEALTHY only if the input was recognisably a doctor report.
178
+ `{}` on stdin also yields zero blockers, and calling that healthy would be
179
+ the exact fake-green this repo exists to refuse.
180
+ """
181
+ if not isinstance(report, dict) or not (
182
+ "checks" in report or "summary" in report or "ai_provider" in report):
183
+ return {
184
+ "status": "not_a_report",
185
+ "executed": False,
186
+ "steps": [],
187
+ "note": "Input is not a loki doctor report (no checks/summary/ai_provider). "
188
+ "Produce one with: loki doctor --json",
189
+ }
190
+
191
+ blockers = derive_blockers(report)
192
+ steps = []
193
+ for i, (bid, category, title, fix) in enumerate(
194
+ sorted(blockers, key=lambda b: (_RANK.get(b[1], 9), b[0]))):
195
+ steps.append({
196
+ "order": i + 1,
197
+ "id": bid,
198
+ "category": category,
199
+ "blocker": title,
200
+ "command": fix,
201
+ "fix_known": fix is not None,
202
+ })
203
+
204
+ result = {
205
+ "status": "healthy" if not steps else "action_required",
206
+ "executed": False,
207
+ "steps": steps,
208
+ }
209
+
210
+ # Cross-check derivation against doctor's own tally. If doctor counted
211
+ # failures we could not turn into blockers, the gap is OURS -- say so,
212
+ # rather than reporting a short plan as a complete one.
213
+ summary = report.get("summary")
214
+ if isinstance(summary, dict):
215
+ failed = summary.get("failed")
216
+ if isinstance(failed, int) and failed > len(blockers):
217
+ result["parse_gap"] = (
218
+ "doctor reported %d failing checks but only %d could be mapped to a "
219
+ "blocker; the remainder are not represented in this plan" % (failed, len(blockers)))
220
+ return result
221
+
222
+
223
+ def render(p):
224
+ """Human-readable plan. Mirrors plan() exactly -- no extra claims."""
225
+ lines = []
226
+ if p["status"] == "not_a_report":
227
+ lines.append("Cannot plan: %s" % p["note"])
228
+ return "\n".join(lines)
229
+
230
+ if p["status"] == "healthy":
231
+ lines.append("System is healthy -- loki doctor reported no blockers.")
232
+ lines.append("Nothing to remediate. Start a build: loki start <spec>")
233
+ return "\n".join(lines)
234
+
235
+ n = len(p["steps"])
236
+ lines.append("Remediation plan -- %d blocker%s, in order (prerequisites first)."
237
+ % (n, "" if n == 1 else "s"))
238
+ lines.append("These commands are NOT run for you. Copy, review, then run them yourself.")
239
+ lines.append("")
240
+ for s in p["steps"]:
241
+ lines.append("%d. %s" % (s["order"], s["blocker"]))
242
+ lines.append(" %s" % (s["command"] if s["fix_known"] else _NO_FIX))
243
+ lines.append("")
244
+ if p.get("parse_gap"):
245
+ lines.append("Note: %s" % p["parse_gap"])
246
+ lines.append("")
247
+ lines.append("Then re-run: loki doctor")
248
+ return "\n".join(lines)
249
+
250
+
251
+ def main(argv=None):
252
+ ap = argparse.ArgumentParser(
253
+ prog="doctor-fix.py",
254
+ description="Plan (never run) the fixes for a `loki doctor --json` report.")
255
+ ap.add_argument("--report", help="doctor JSON file; defaults to stdin")
256
+ ap.add_argument("--json", action="store_true", dest="as_json",
257
+ help="emit the plan as JSON")
258
+ args = ap.parse_args(argv)
259
+
260
+ raw = open(args.report, encoding="utf-8").read() if args.report else sys.stdin.read()
261
+ try:
262
+ report = json.loads(raw)
263
+ except (ValueError, TypeError):
264
+ # Unparseable input is not "healthy" and not a blocker list either.
265
+ report = None
266
+
267
+ p = plan(report)
268
+ if args.as_json:
269
+ print(json.dumps(p, indent=2))
270
+ else:
271
+ print(render(p))
272
+ # Exit 1 on action_required so this is CI-gateable, matching doctor's own
273
+ # convention. not_a_report is exit 2: a broken input is not a clean bill of
274
+ # health and must never be mistaken for one.
275
+ return {"healthy": 0, "action_required": 1, "not_a_report": 2}[p["status"]]
276
+
277
+
278
+ if __name__ == "__main__":
279
+ sys.exit(main())
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.84.0"
10
+ __version__ = "8.86.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.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 s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;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(RO,"..","..","..")}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(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.84.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Xf(Qf(import.meta.url)),Q=X$(X);h5=e_(Zf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var bO={};l0(bO,{runOrThrow:()=>jf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Tf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>yO});async function NQ(Z,X=yO){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 W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{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 jf(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=Mf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Mf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Tf(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 yO=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 wf?"":Z}var wf,L0,F8,p0,zV0,a0,W8,Q9,v;var S6=p(()=>{wf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),zV0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),W8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as bf}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(bf(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 ZL={};l0(ZL,{runStatus:()=>Kh});import{existsSync as Y9,readFileSync as g3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as rf}from"path";import{homedir as tf}from"os";function sO(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 nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*LQ/X);if(J>LQ)J=LQ;let z=LQ-J,K=F8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=sO(Z),V=sO(X);return` ${W8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function Zh(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}Error: jq is required but not installed.${v}
2
+ var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.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 s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;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(RO,"..","..","..")}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(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.86.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Xf(Qf(import.meta.url)),Q=X$(X);h5=e_(Zf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var bO={};l0(bO,{runOrThrow:()=>jf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Tf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>yO});async function NQ(Z,X=yO){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 W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{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 jf(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=Mf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Mf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Tf(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 yO=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 wf?"":Z}var wf,L0,F8,p0,zV0,a0,W8,Q9,v;var S6=p(()=>{wf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),zV0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),W8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as bf}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(bf(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 ZL={};l0(ZL,{runStatus:()=>Kh});import{existsSync as Y9,readFileSync as g3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as rf}from"path";import{homedir as tf}from"os";function sO(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 nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*LQ/X);if(J>LQ)J=LQ;let z=LQ-J,K=F8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=sO(Z),V=sO(X);return` ${W8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function Zh(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}Error: jq is required but not installed.${v}
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)
@@ -1232,4 +1232,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1232
1232
  `),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (h_(),f_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
1233
1233
  `),process.stderr.write(v_),2}}cO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var uW0=await mW0(Bun.argv.slice(2));process.exit(uW0);
1234
1234
 
1235
- //# debugId=3310E830E2BA5FFD64756E2164756E21
1235
+ //# debugId=F5AA0FEF37EC655564756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '8.84.0'
78
+ __version__ = '8.86.0'
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": "8.84.0",
4
+ "version": "8.86.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": "8.84.0",
5
+ "version": "8.86.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",
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env python3
2
+ """Compare two Evidence Receipts and report what actually changed between runs.
3
+
4
+ Verification of ONE receipt already exists (autonomy/lib/proof-verify.py).
5
+ This is the missing half: two runs, side by side. "Did the change make it
6
+ cheaper?" is the question a cost receipt exists to answer, and it cannot be
7
+ answered from a single receipt.
8
+
9
+ THREE RULES, and they are the whole point of this file:
10
+
11
+ 1. A delta against an UNMEASURED value is UNKNOWN, never a number. If run A
12
+ measured cost and run B did not, "-$0.42" is a fabricated saving. The
13
+ measured/unmeasured predicate is record_is_measured() in
14
+ autonomy/lib/efficiency_cost.py -- imported, never restated, because a
15
+ second copy of that predicate is precisely how the honesty rule drifts.
16
+
17
+ 2. A genuinely measured ZERO delta reads 0. Zero is falsy, so every guard in
18
+ this file is an explicit `is None` check. A falsy guard turns a real
19
+ "identical cost" finding into UNKNOWN, which is the same lie pointed the
20
+ other way.
21
+
22
+ 3. Integrity comes FIRST. A tampered receipt must not silently supply half a
23
+ delta, so verify_integrity() runs on both before anything is compared.
24
+
25
+ Non-comparable receipts (different specs) are REFUSED rather than diffed. The
26
+ delta between a landing page and a compiler is arithmetic, not information.
27
+ """
28
+
29
+ import argparse
30
+ import importlib.util
31
+ import json
32
+ import os
33
+ import sys
34
+
35
+ _HERE = os.path.dirname(os.path.abspath(__file__))
36
+ _LIB = os.path.join(os.path.dirname(_HERE), "autonomy", "lib")
37
+ sys.path.insert(0, _LIB)
38
+
39
+ from efficiency_cost import record_is_measured # noqa: E402
40
+
41
+ # proof-verify.py has a hyphen and is not importable as a module; load it by
42
+ # path the same way tests/test_cost_honesty_end_to_end.py does.
43
+ _spec = importlib.util.spec_from_file_location(
44
+ "proof_verify", os.path.join(_LIB, "proof-verify.py"))
45
+ _pv = importlib.util.module_from_spec(_spec)
46
+ _spec.loader.exec_module(_pv)
47
+
48
+ verify_integrity = _pv.verify_integrity
49
+
50
+ UNKNOWN = "UNKNOWN"
51
+
52
+
53
+ class NotComparable(Exception):
54
+ """Raised when two receipts must not be diffed at all."""
55
+
56
+
57
+ def delta(a, b):
58
+ """b - a, or UNKNOWN when either side was never measured.
59
+
60
+ `is None` is load-bearing: a measured 0.0 on either side is a real
61
+ observation and must survive as a number.
62
+ """
63
+ if a is None or b is None:
64
+ return UNKNOWN
65
+ return b - a
66
+
67
+
68
+ def _num(v):
69
+ """A number as itself, anything else (None, "", bool) as None."""
70
+ if isinstance(v, bool) or not isinstance(v, (int, float)):
71
+ return None
72
+ return v
73
+
74
+
75
+ def measured_cost(proof):
76
+ """The cost block as {usd, tokens...}, or None when never measured.
77
+
78
+ cost.available is NOT trusted on its own. A real receipt shipped
79
+ available=true with every field zero (fixed v8.52.0), so the flag is a
80
+ claim, not evidence. record_is_measured reads the VALUES, under the
81
+ per-iteration key names, so the receipt's `usd` is mapped to `cost_usd`.
82
+ """
83
+ cost = proof.get("cost")
84
+ if not isinstance(cost, dict):
85
+ return None
86
+ rec = {
87
+ "cost_usd": _num(cost.get("usd")),
88
+ "input_tokens": _num(cost.get("input_tokens")),
89
+ "output_tokens": _num(cost.get("output_tokens")),
90
+ "cache_read_tokens": _num(cost.get("cache_read_tokens")),
91
+ "cache_creation_tokens": _num(cost.get("cache_creation_tokens")),
92
+ }
93
+ return rec if record_is_measured(rec) else None
94
+
95
+
96
+ def cache_hit_ratio(cost):
97
+ """cache_read / (cache_read + input): the metric that dominates cost.
98
+
99
+ None when unmeasured OR when the denominator is zero -- no tokens read
100
+ means no ratio was observed, which is not a 0% hit rate.
101
+ """
102
+ if cost is None:
103
+ return None
104
+ read = _num(cost.get("cache_read_tokens"))
105
+ fresh = _num(cost.get("input_tokens"))
106
+ if read is None or fresh is None:
107
+ return None
108
+ total = read + fresh
109
+ if total == 0:
110
+ return None
111
+ return read / total
112
+
113
+
114
+ def gate_map(proof):
115
+ """{gate name: status} from the facts projection, falling back to flat."""
116
+ facts = proof.get("facts")
117
+ gates = facts.get("quality_gates") if isinstance(facts, dict) else None
118
+ if not isinstance(gates, list):
119
+ qg = proof.get("quality_gates")
120
+ gates = qg.get("gates") if isinstance(qg, dict) else None
121
+ out = {}
122
+ for g in gates or []:
123
+ if isinstance(g, dict) and g.get("name"):
124
+ out[str(g["name"])] = str(g.get("status", "not_run"))
125
+ return out
126
+
127
+
128
+ def _spec_id(proof):
129
+ spec = proof.get("spec")
130
+ return str(spec.get("source") or "") if isinstance(spec, dict) else ""
131
+
132
+
133
+ def compare(a, b, a_path="A", b_path="B"):
134
+ """Diff two verified receipts. Raises NotComparable when they must not be.
135
+
136
+ Integrity first: a receipt that fails verify_integrity never reaches the
137
+ arithmetic.
138
+ """
139
+ for proof, path in ((a, a_path), (b, b_path)):
140
+ v = verify_integrity(proof)
141
+ if not v["ok"]:
142
+ raise NotComparable(
143
+ "receipt failed integrity verification: %s\n %s"
144
+ % (path, "\n ".join(v["reasons"] or [v["reason"]])))
145
+
146
+ spec_a, spec_b = _spec_id(a), _spec_id(b)
147
+ if spec_a and spec_b and spec_a != spec_b:
148
+ raise NotComparable(
149
+ "receipts are for different specs, so no delta between them is "
150
+ "meaningful:\n %s: %s\n %s: %s" % (a_path, spec_a, b_path, spec_b))
151
+
152
+ cost_a, cost_b = measured_cost(a), measured_cost(b)
153
+ # WHY each field is unavailable, so a reader never has to guess whether
154
+ # UNKNOWN means "not measured" or "tool could not read it".
155
+ incomparable = []
156
+ if cost_a is None or cost_b is None:
157
+ incomparable.append({
158
+ "field": "cost",
159
+ "why": "cost was not measured in %s" % (
160
+ " and ".join(
161
+ [p for p, c in ((a_path, cost_a), (b_path, cost_b))
162
+ if c is None])),
163
+ })
164
+
165
+ ratio_a, ratio_b = cache_hit_ratio(cost_a), cache_hit_ratio(cost_b)
166
+ if ratio_a is None or ratio_b is None:
167
+ incomparable.append({
168
+ "field": "cache_hit_ratio",
169
+ "why": "no cached-plus-fresh input tokens recorded, so no ratio "
170
+ "was observed (this is not a 0% hit rate)",
171
+ })
172
+
173
+ def iters(p):
174
+ it = p.get("iterations")
175
+ return _num(it.get("count")) if isinstance(it, dict) else None
176
+
177
+ it_a, it_b = iters(a), iters(b)
178
+ if it_a is None or it_b is None:
179
+ incomparable.append(
180
+ {"field": "iterations", "why": "iteration count not recorded"})
181
+
182
+ dur_a, dur_b = _num(a.get("wall_clock_sec")), _num(b.get("wall_clock_sec"))
183
+ if dur_a is None or dur_b is None:
184
+ incomparable.append(
185
+ {"field": "duration_sec", "why": "wall clock not recorded"})
186
+
187
+ ga, gb = gate_map(a), gate_map(b)
188
+ gates = {"regressed": [], "fixed": [], "added": [], "removed": []}
189
+ for name in sorted(set(ga) | set(gb)):
190
+ sa, sb = ga.get(name), gb.get(name)
191
+ if sa is None:
192
+ # Absent in A is ADDED, never a pass that broke.
193
+ gates["added"].append({"gate": name, "status": sb})
194
+ elif sb is None:
195
+ gates["removed"].append({"gate": name, "status": sa})
196
+ elif sa != sb:
197
+ bucket = "fixed" if sb == "passed" else "regressed"
198
+ gates[bucket].append({"gate": name, "from": sa, "to": sb})
199
+
200
+ return {
201
+ "comparable": True,
202
+ "a": a_path,
203
+ "b": b_path,
204
+ "spec": spec_a or spec_b,
205
+ "cost_usd": {
206
+ "a": cost_a["cost_usd"] if cost_a else None,
207
+ "b": cost_b["cost_usd"] if cost_b else None,
208
+ "delta": delta(cost_a["cost_usd"] if cost_a else None,
209
+ cost_b["cost_usd"] if cost_b else None),
210
+ },
211
+ "cache_hit_ratio": {
212
+ "a": ratio_a, "b": ratio_b, "delta": delta(ratio_a, ratio_b),
213
+ },
214
+ "iterations": {"a": it_a, "b": it_b, "delta": delta(it_a, it_b)},
215
+ "duration_sec": {"a": dur_a, "b": dur_b, "delta": delta(dur_a, dur_b)},
216
+ "gates": gates,
217
+ "not_comparable": incomparable,
218
+ }
219
+
220
+
221
+ def _fmt(value, unit=""):
222
+ if value == UNKNOWN or value is None:
223
+ return UNKNOWN
224
+ if unit == "$":
225
+ return "%s$%.4f" % ("+" if value >= 0 else "-", abs(value))
226
+ if unit == "%":
227
+ return "%+.1f%%" % (value * 100)
228
+ return "%+g%s" % (value, unit)
229
+
230
+
231
+ def render(d):
232
+ lines = ["Evidence Receipt diff", " A: %s" % d["a"], " B: %s" % d["b"]]
233
+ if d["spec"]:
234
+ lines.append(" spec: %s" % d["spec"])
235
+ lines.append("")
236
+ for label, key, unit in (
237
+ ("cost", "cost_usd", "$"),
238
+ ("cache hit ratio", "cache_hit_ratio", "%"),
239
+ ("iterations", "iterations", ""),
240
+ ("duration", "duration_sec", "s"),
241
+ ):
242
+ f = d[key]
243
+ a = UNKNOWN if f["a"] is None else (
244
+ "%.4f" % f["a"] if unit == "%" else f["a"])
245
+ b = UNKNOWN if f["b"] is None else (
246
+ "%.4f" % f["b"] if unit == "%" else f["b"])
247
+ lines.append(" %-16s %s -> %s delta %s"
248
+ % (label, a, b, _fmt(f["delta"], unit)))
249
+
250
+ g = d["gates"]
251
+ lines.append("")
252
+ if any(g.values()):
253
+ for gate in g["regressed"]:
254
+ lines.append(" REGRESSED %s: %s -> %s"
255
+ % (gate["gate"], gate["from"], gate["to"]))
256
+ for gate in g["fixed"]:
257
+ lines.append(" FIXED %s: %s -> %s"
258
+ % (gate["gate"], gate["from"], gate["to"]))
259
+ for gate in g["added"]:
260
+ lines.append(" ADDED %s (%s), absent in A"
261
+ % (gate["gate"], gate["status"]))
262
+ for gate in g["removed"]:
263
+ lines.append(" REMOVED %s (was %s), absent in B"
264
+ % (gate["gate"], gate["status"]))
265
+ else:
266
+ lines.append(" gates no verdict changes")
267
+
268
+ if d["not_comparable"]:
269
+ lines.append("")
270
+ lines.append(" NOT COMPARABLE (reported UNKNOWN, not zero):")
271
+ for item in d["not_comparable"]:
272
+ lines.append(" %s: %s" % (item["field"], item["why"]))
273
+ return "\n".join(lines)
274
+
275
+
276
+ def main(argv=None):
277
+ ap = argparse.ArgumentParser(
278
+ description="Compare two Evidence Receipts (proof.json).")
279
+ ap.add_argument("a", help="baseline proof.json")
280
+ ap.add_argument("b", help="proof.json to compare against the baseline")
281
+ ap.add_argument("--json", action="store_true", dest="as_json",
282
+ help="emit the diff as JSON")
283
+ args = ap.parse_args(argv)
284
+
285
+ try:
286
+ pa = _pv._load_proof(args.a)
287
+ pb = _pv._load_proof(args.b)
288
+ d = compare(pa, pb, args.a, args.b)
289
+ except (NotComparable, _pv.ProofLoadError) as exc:
290
+ payload = {"comparable": False, "reason": str(exc)}
291
+ if args.as_json:
292
+ print(json.dumps(payload, indent=2))
293
+ else:
294
+ print("REFUSED: %s" % exc)
295
+ return 2
296
+
297
+ print(json.dumps(d, indent=2) if args.as_json else render(d))
298
+ return 0
299
+
300
+
301
+ if __name__ == "__main__":
302
+ sys.exit(main())