loki-mode 8.4.0 → 8.5.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/README.md CHANGED
@@ -15,7 +15,7 @@ _The free, source-available autonomous coding agent by [Autonomi](https://www.au
15
15
 
16
16
  [Website](https://www.autonomi.dev/) | [Documentation](wiki/Home.md) | [Installation](docs/INSTALLATION.md) | [Changelog](CHANGELOG.md) | [Purple Lab -- deprecated v7.44.0](#purple-lab)
17
17
 
18
- **Current release: v8.4.0**
18
+ **Current release: v8.5.0**
19
19
 
20
20
  </div>
21
21
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.4.0
1
+ 8.5.0
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """Explain WHY a run took the iterations it took.
3
+
4
+ WHY THIS EXISTS. Iterations are the direct multiplier on both wall clock and
5
+ cost, and the Evidence Receipt reports only `{count, succeeded, failed}`. A user
6
+ who sees "6 iterations" cannot tell real work from a gate false-positive that
7
+ forced five redos. Neither can we, which is worse: it means we cannot tell
8
+ whether an expensive run was the model being slow or the harness being wrong.
9
+
10
+ That distinction has already bitten this project. A measured run had the agent
11
+ claim done on EVERY iteration while a mock-integrity false positive blocked all
12
+ six, and a start-sha bug made the council see a permanently empty diff so it
13
+ could never vote done. Both were HARNESS defects billed to the user as model
14
+ cost. Removing a false positive raises accuracy AND cuts iterations, which is
15
+ the rare change that improves both axes at once -- but only if you can see it.
16
+
17
+ WHAT THIS DOES NOT DO. It does not guess. Every field is derived from records
18
+ the engine already writes, and anything not recorded is reported as unknown
19
+ rather than inferred. An attribution that invents a reason would be worse than
20
+ no attribution, because it would send someone optimising the wrong thing.
21
+
22
+ Sources, all already written by the engine:
23
+ .loki/metrics/efficiency/iteration-N.json status, duration_ms, cost_usd, model
24
+ .loki/events.jsonl iteration_complete events
25
+
26
+ Usage:
27
+ python3 autonomy/lib/iteration_attribution.py [--loki-dir .loki] [--json]
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import json
34
+ import os
35
+ import sys
36
+
37
+
38
+ def _read_json(path, default=None):
39
+ try:
40
+ with open(path, encoding="utf-8") as fh:
41
+ return json.load(fh)
42
+ except Exception:
43
+ return default
44
+
45
+
46
+ def _iteration_records(loki_dir):
47
+ """Return per-iteration records sorted by iteration number.
48
+
49
+ Reads only what the engine already wrote. A malformed or partial record is
50
+ SKIPPED rather than defaulted, because a zero-cost placeholder would silently
51
+ understate the total and make the attribution wrong in the direction that
52
+ flatters us.
53
+ """
54
+ eff_dir = os.path.join(loki_dir, "metrics", "efficiency")
55
+ out = []
56
+ try:
57
+ names = sorted(os.listdir(eff_dir))
58
+ except OSError:
59
+ return out
60
+ for name in names:
61
+ if not (name.startswith("iteration-") and name.endswith(".json")):
62
+ continue
63
+ rec = _read_json(os.path.join(eff_dir, name))
64
+ if not isinstance(rec, dict) or "iteration" not in rec:
65
+ continue
66
+ out.append(rec)
67
+ out.sort(key=lambda r: r.get("iteration", 0))
68
+ return out
69
+
70
+
71
+ def attribute(loki_dir):
72
+ """Split iteration cost and time into PROGRESS versus REWORK.
73
+
74
+ The definition is deliberately conservative and stated plainly, because a
75
+ generous definition of "progress" is how a tool flatters itself:
76
+
77
+ progress an iteration that COMPLETED (exit 0)
78
+ rework an iteration that FAILED and therefore had to be repeated
79
+ unknown an iteration whose status was never recorded
80
+
81
+ This is a floor on rework, not a ceiling. An iteration that "completed" but
82
+ was forced to run again by a false-positive gate is counted as progress here,
83
+ because the engine does not currently record the blocking gate per iteration.
84
+ Saying so is the point: the number is honest about what it cannot see.
85
+ """
86
+ recs = _iteration_records(loki_dir)
87
+
88
+ summary = {
89
+ "iterations": len(recs),
90
+ "progress": {"count": 0, "cost_usd": 0.0, "duration_ms": 0},
91
+ "rework": {"count": 0, "cost_usd": 0.0, "duration_ms": 0},
92
+ "unknown": {"count": 0, "cost_usd": 0.0, "duration_ms": 0},
93
+ "cost_recorded": False,
94
+ "notes": [],
95
+ }
96
+
97
+ for r in recs:
98
+ status = str(r.get("status", "")).strip().lower()
99
+ if status == "completed":
100
+ bucket = "progress"
101
+ elif status == "failed":
102
+ bucket = "rework"
103
+ else:
104
+ bucket = "unknown"
105
+ summary[bucket]["count"] += 1
106
+ cost = r.get("cost_usd")
107
+ if isinstance(cost, (int, float)):
108
+ summary[bucket]["cost_usd"] += float(cost)
109
+ if cost > 0:
110
+ summary["cost_recorded"] = True
111
+ dur = r.get("duration_ms")
112
+ if isinstance(dur, (int, float)) and dur >= 0:
113
+ summary[bucket]["duration_ms"] += int(dur)
114
+
115
+ for b in ("progress", "rework", "unknown"):
116
+ summary[b]["cost_usd"] = round(summary[b]["cost_usd"], 4)
117
+
118
+ total_cost = sum(summary[b]["cost_usd"] for b in ("progress", "rework", "unknown"))
119
+ summary["total_cost_usd"] = round(total_cost, 4) if summary["cost_recorded"] else None
120
+
121
+ if not recs:
122
+ summary["notes"].append(
123
+ "no efficiency records: this run predates the recorder, or no iteration completed"
124
+ )
125
+ if not summary["cost_recorded"]:
126
+ # Distinguishing "not recorded" from a genuine $0.00 is the same honesty
127
+ # property the Evidence Receipt depends on. A skeptic reading $0.00
128
+ # concludes the artifact is fake.
129
+ summary["notes"].append(
130
+ "cost not recorded for any iteration (reported as null, not as $0.00)"
131
+ )
132
+ if summary["rework"]["count"] and summary["cost_recorded"]:
133
+ share = summary["rework"]["cost_usd"] / total_cost if total_cost else 0.0
134
+ summary["rework_cost_share"] = round(share, 4)
135
+ summary["notes"].append(
136
+ f"{summary['rework']['count']} of {len(recs)} iterations failed and were "
137
+ f"repeated, costing {share:.0%} of the run"
138
+ )
139
+ summary["notes"].append(
140
+ "rework is a FLOOR: an iteration that completed but was forced to repeat by a "
141
+ "gate is counted as progress, because the blocking gate is not recorded per "
142
+ "iteration"
143
+ )
144
+ return summary
145
+
146
+
147
+ def _render(s):
148
+ lines = ["Iteration attribution", "====================", ""]
149
+ lines.append(f"Iterations: {s['iterations']}")
150
+ lines.append(f" progress: {s['progress']['count']}")
151
+ lines.append(f" rework: {s['rework']['count']}")
152
+ if s["unknown"]["count"]:
153
+ lines.append(f" unknown: {s['unknown']['count']}")
154
+ if s["total_cost_usd"] is None:
155
+ lines.append("Cost: not recorded")
156
+ else:
157
+ lines.append(f"Cost: ${s['total_cost_usd']}")
158
+ lines.append(f" progress: ${s['progress']['cost_usd']}")
159
+ lines.append(f" rework: ${s['rework']['cost_usd']}")
160
+ lines.append("")
161
+ for n in s["notes"]:
162
+ lines.append(f"note: {n}")
163
+ return "\n".join(lines)
164
+
165
+
166
+ def main():
167
+ ap = argparse.ArgumentParser(description=__doc__)
168
+ ap.add_argument("--loki-dir", default=".loki")
169
+ ap.add_argument("--json", action="store_true")
170
+ args = ap.parse_args()
171
+
172
+ s = attribute(args.loki_dir)
173
+ if args.json:
174
+ print(json.dumps(s, indent=2))
175
+ else:
176
+ print(_render(s))
177
+ return 0
178
+
179
+
180
+ if __name__ == "__main__":
181
+ sys.exit(main())
@@ -1081,6 +1081,37 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
1081
1081
 
1082
1082
  files_changed, diffs, diff_base_sha = _git_diffstat(target_dir, args.include_diffs)
1083
1083
  iterations = _collect_iterations(loki_dir)
1084
+ # Attribute iterations to PROGRESS vs REWORK. The receipt previously reported
1085
+ # only {count, succeeded, failed}, so a user seeing "6 iterations" could not
1086
+ # tell real work from a gate false-positive that forced five redos -- and
1087
+ # neither could we, which is worse, because it hides whether an expensive run
1088
+ # was a slow model or our own harness being wrong. Measured here: an agent
1089
+ # once claimed done on EVERY iteration while a mock-integrity false positive
1090
+ # blocked all six.
1091
+ #
1092
+ # Deterministic and derived only from records the engine already writes, so
1093
+ # this belongs with the FACTS, not the AI assessments. Failure to import or
1094
+ # attribute leaves iterations untouched rather than guessing: a fabricated
1095
+ # attribution would send someone optimising the wrong thing.
1096
+ try:
1097
+ from iteration_attribution import attribute as _attribute_iterations
1098
+ _attr = _attribute_iterations(loki_dir)
1099
+ if _attr.get("iterations"):
1100
+ iterations["attribution"] = {
1101
+ "progress": _attr["progress"],
1102
+ "rework": _attr["rework"],
1103
+ "rework_cost_share": _attr.get("rework_cost_share"),
1104
+ # Stated in the artifact, not just the tool: rework is a FLOOR.
1105
+ # An iteration that completed but was forced to repeat by a gate
1106
+ # counts as progress, because the blocking gate is not recorded
1107
+ # per iteration. Overstating certainty here would be worse than
1108
+ # omitting the split.
1109
+ "basis": "rework counts FAILED iterations only; a completed "
1110
+ "iteration forced to repeat by a gate is counted as "
1111
+ "progress, so rework is a floor",
1112
+ }
1113
+ except Exception:
1114
+ pass
1084
1115
  spec = _collect_spec(loki_dir, target_dir)
1085
1116
  council = _collect_council(loki_dir)
1086
1117
  quality_gates = _collect_quality_gates(loki_dir)
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env python3
2
+ """Find the longest stretches where a build told the user nothing.
3
+
4
+ WHY THIS EXISTS. The loudest complaints about agents in this category are not
5
+ "it was wrong", they are "it went quiet and I could not tell if it was working":
6
+ opencode#11112 "always stuck at Preparing write" (76 comments), continue#7143
7
+ "not making changes to code", continue#5696 "agent does not execute functions".
8
+ In each case the agent may well be working. The user cannot tell, so they kill
9
+ it or leave.
10
+
11
+ Wall-clock total is the wrong metric for that experience. A ten-minute build
12
+ that reports something every twenty seconds feels fast; a four-minute build that
13
+ goes silent for three of them feels broken. What matters is the LONGEST GAP.
14
+
15
+ This reads the events the engine already writes and reports the worst silences,
16
+ so the number can be driven down instead of guessed at.
17
+
18
+ HOW TO MEASURE IT HONESTLY, learned the hard way here:
19
+ - Read wall clock from the engine's own event stream, never from `ps etime` on
20
+ a supervising process. A hung harness once reported 39 minutes for a 20
21
+ minute run.
22
+ - Separate IDLE time between sessions from SILENCE during a build. A gap
23
+ between two `session_resume` events is a human having lunch, not a defect,
24
+ and counting it makes the report useless.
25
+ - Report the event on each side of a gap. "Silent for 214s" is not actionable;
26
+ "silent for 214s after code_review_start" names the step to fix.
27
+
28
+ Usage:
29
+ python3 autonomy/lib/silence_report.py [--events .loki/events.jsonl]
30
+ [--threshold 30] [--json]
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import argparse
36
+ import json
37
+ import sys
38
+ from datetime import datetime
39
+
40
+ # Events that mark a session boundary rather than build progress. A gap
41
+ # alongside one of these is a human being away, not the engine being silent.
42
+ _BOUNDARY = {
43
+ "session_resume",
44
+ "session_start",
45
+ "claude_hook_sessionstart",
46
+ "cli_command_deprecated",
47
+ }
48
+
49
+
50
+ def _parse(path):
51
+ """Yield (datetime, event_type). Malformed lines are skipped, not defaulted.
52
+
53
+ A line we cannot parse must not become a synthetic timestamp: that would
54
+ invent a gap or hide one, and either way the report stops being evidence.
55
+ """
56
+ out = []
57
+ try:
58
+ fh = open(path, encoding="utf-8", errors="replace")
59
+ except OSError:
60
+ return out
61
+ with fh:
62
+ for line in fh:
63
+ line = line.strip()
64
+ if not line:
65
+ continue
66
+ try:
67
+ d = json.loads(line)
68
+ except Exception:
69
+ continue
70
+ t = d.get("timestamp") or d.get("ts") or d.get("at")
71
+ if not isinstance(t, str) or not t:
72
+ continue
73
+ try:
74
+ dt = datetime.fromisoformat(t.replace("Z", "+00:00"))
75
+ except Exception:
76
+ continue
77
+ kind = d.get("type") or d.get("event") or "unknown"
78
+ out.append((dt, str(kind)))
79
+ out.sort(key=lambda r: r[0])
80
+ return out
81
+
82
+
83
+ def analyse(events_path, threshold_s=30.0, session_gap_s=1800.0):
84
+ evs = _parse(events_path)
85
+ if len(evs) < 2:
86
+ return {
87
+ "events": len(evs),
88
+ "gaps_over_threshold": 0,
89
+ "worst": [],
90
+ "threshold_s": threshold_s,
91
+ "notes": ["fewer than two timestamped events: nothing to measure"],
92
+ }
93
+
94
+ gaps = []
95
+ skipped_boundary = 0
96
+ for i in range(1, len(evs)):
97
+ prev_t, prev_k = evs[i - 1]
98
+ cur_t, cur_k = evs[i]
99
+ secs = (cur_t - prev_t).total_seconds()
100
+ if secs <= 0:
101
+ continue
102
+ # A gap longer than the session window, or one touching a session
103
+ # boundary event, is idle time rather than in-build silence.
104
+ if secs >= session_gap_s or prev_k in _BOUNDARY or cur_k in _BOUNDARY:
105
+ skipped_boundary += 1
106
+ continue
107
+ gaps.append({"seconds": round(secs, 1), "after": prev_k, "before": cur_k})
108
+
109
+ gaps.sort(key=lambda g: g["seconds"], reverse=True)
110
+ over = [g for g in gaps if g["seconds"] > threshold_s]
111
+
112
+ notes = []
113
+ if not gaps:
114
+ notes.append(
115
+ "no in-build gaps found: this event stream is session/CLI telemetry, "
116
+ "not a build. Point --events at a real build's .loki/events.jsonl"
117
+ )
118
+ if skipped_boundary:
119
+ notes.append(
120
+ f"{skipped_boundary} gap(s) ignored as idle time between sessions, "
121
+ "not in-build silence"
122
+ )
123
+ return {
124
+ "events": len(evs),
125
+ "in_build_gaps": len(gaps),
126
+ "gaps_over_threshold": len(over),
127
+ "threshold_s": threshold_s,
128
+ "worst": gaps[:10],
129
+ "notes": notes,
130
+ }
131
+
132
+
133
+ def _render(r):
134
+ lines = ["Silence report", "==============", ""]
135
+ lines.append(f"Events: {r['events']}")
136
+ lines.append(f"In-build gaps: {r.get('in_build_gaps', 0)}")
137
+ lines.append(f"Over {int(r['threshold_s'])}s: {r['gaps_over_threshold']}")
138
+ if r["worst"]:
139
+ lines.append("")
140
+ lines.append("Longest silences:")
141
+ for g in r["worst"][:5]:
142
+ lines.append(
143
+ f" {g['seconds']:>8.1f}s after '{g['after']}' before '{g['before']}'"
144
+ )
145
+ for n in r.get("notes", []):
146
+ lines.append("")
147
+ lines.append(f"note: {n}")
148
+ return "\n".join(lines)
149
+
150
+
151
+ def main():
152
+ ap = argparse.ArgumentParser(description=__doc__)
153
+ ap.add_argument("--events", default=".loki/events.jsonl")
154
+ ap.add_argument("--threshold", type=float, default=30.0)
155
+ ap.add_argument("--json", action="store_true")
156
+ args = ap.parse_args()
157
+ r = analyse(args.events, args.threshold)
158
+ print(json.dumps(r, indent=2) if args.json else _render(r))
159
+ return 0
160
+
161
+
162
+ if __name__ == "__main__":
163
+ 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.4.0"
10
+ __version__ = "8.5.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var S_=Object.create;var{getPrototypeOf:y_,defineProperty:oK,getOwnPropertyNames:b_}=Object;var __=Object.prototype.hasOwnProperty;function f_(Z){return this[Z]}var h_,v_,g_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?h_??=new WeakMap:v_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?S_(y_(Z)):{};let K=X||!Z||!Z.__esModule?oK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of b_(Z))if(!__.call(K,$))oK(K,$,{get:f_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var VQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var m_=(Z)=>Z;function u_(Z,X){this[Z]=m_.bind(null,X)}var c0=(Z,X)=>{for(var Q in X)oK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:u_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var IO={};c0(IO,{lokiDir:()=>j0,homeLokiDir:()=>I4,findRepoRootForVersion:()=>tK,REPO_ROOT:()=>Q8});import{resolve as s7,dirname as rK}from"path";import{fileURLToPath as p_}from"url";import{existsSync as qQ}from"fs";import{homedir as d_}from"os";function c_(){let Z=EO;for(let X=0;X<6;X++){if(qQ(s7(Z,"VERSION"))&&qQ(s7(Z,"autonomy/run.sh")))return Z;let Q=rK(Z);if(Q===Z)break;Z=Q}return s7(EO,"..","..","..")}function tK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(qQ(s7(X,"VERSION"))&&qQ(s7(X,"autonomy/run.sh")))return X;let Y=rK(X);if(Y===X)break;X=Y}return s7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??s7(process.cwd(),".loki")}function I4(){return s7(d_(),".loki")}var EO,Q8;var G8=p(()=>{EO=rK(p_(import.meta.url));Q8=c_()});import{readFileSync as l_}from"fs";import{resolve as i_,dirname as a_}from"path";import{fileURLToPath as s_}from"url";function _3(){if(f5!==null)return f5;let Z="8.4.0";if(typeof Z==="string"&&Z.length>0)return f5=Z,f5;try{let X=a_(s_(import.meta.url)),Q=tK(X);f5=l_(i_(Q,"VERSION"),"utf-8").trim()}catch{f5="unknown"}return f5}var f5=null;var GQ=p(()=>{G8()});var xO={};c0(xO,{runOrThrow:()=>qf,run:()=>E0,readStreamCapped:()=>HQ,commandVersion:()=>Hf,commandExists:()=>X9,ShellError:()=>eK,MAX_STDOUT_BYTES:()=>kO});async function HQ(Z,X=kO){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([HQ(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 qf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new eK(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Gf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Gf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Hf(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 kO=16777216,eK;var k9=p(()=>{eK=class eK 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 n7(Z){return Uf?"":Z}var Uf,M0,k8,l0,dW0,a0,H8,Q9,g;var S6=p(()=>{Uf=(process.env.NO_COLOR??"").length>0;M0=n7("\x1B[0;31m"),k8=n7("\x1B[0;32m"),l0=n7("\x1B[1;33m"),dW0=n7("\x1B[0;34m"),a0=n7("\x1B[0;36m"),H8=n7("\x1B[1m"),Q9=n7("\x1B[2m"),g=n7("\x1B[0m")});import{existsSync as Ff}from"fs";async function F7(){if(R4!==void 0)return R4;let Z="/opt/homebrew/bin/python3.12";if(Ff(Z))return R4=Z,Z;let X=await X9("python3.12");if(X)return R4=X,X;let Q=await X9("python3");return R4=Q,Q}async function D7(Z,X={}){let Q=await F7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var R4;var o7=p(()=>{k9()});var rO={};c0(rO,{runStatus:()=>tf});import{existsSync as Y9,readFileSync as h3,readdirSync as dO,statSync as cO}from"fs";import{resolve as h8,basename as pf}from"path";import{homedir as df}from"os";function lO(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 iO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*BQ/X);if(J>BQ)J=BQ;let z=BQ-J,K=k8;if(Y>=80)K=M0;else if(Y>=50)K=l0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=lO(Z),V=lO(X);return` ${H8}${Q}${g} ${K}[${$}]${g} ${Y}% (${W} / ${V})`}async function lf(){if(await X9("jq"))return!0;return process.stdout.write(`${M0}Error: jq is required but not installed.${g}
2
+ var S_=Object.create;var{getPrototypeOf:y_,defineProperty:oK,getOwnPropertyNames:b_}=Object;var __=Object.prototype.hasOwnProperty;function f_(Z){return this[Z]}var h_,v_,g_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?h_??=new WeakMap:v_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?S_(y_(Z)):{};let K=X||!Z||!Z.__esModule?oK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of b_(Z))if(!__.call(K,$))oK(K,$,{get:f_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var VQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var m_=(Z)=>Z;function u_(Z,X){this[Z]=m_.bind(null,X)}var c0=(Z,X)=>{for(var Q in X)oK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:u_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var IO={};c0(IO,{lokiDir:()=>j0,homeLokiDir:()=>I4,findRepoRootForVersion:()=>tK,REPO_ROOT:()=>Q8});import{resolve as s7,dirname as rK}from"path";import{fileURLToPath as p_}from"url";import{existsSync as qQ}from"fs";import{homedir as d_}from"os";function c_(){let Z=EO;for(let X=0;X<6;X++){if(qQ(s7(Z,"VERSION"))&&qQ(s7(Z,"autonomy/run.sh")))return Z;let Q=rK(Z);if(Q===Z)break;Z=Q}return s7(EO,"..","..","..")}function tK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(qQ(s7(X,"VERSION"))&&qQ(s7(X,"autonomy/run.sh")))return X;let Y=rK(X);if(Y===X)break;X=Y}return s7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??s7(process.cwd(),".loki")}function I4(){return s7(d_(),".loki")}var EO,Q8;var G8=p(()=>{EO=rK(p_(import.meta.url));Q8=c_()});import{readFileSync as l_}from"fs";import{resolve as i_,dirname as a_}from"path";import{fileURLToPath as s_}from"url";function _3(){if(f5!==null)return f5;let Z="8.5.0";if(typeof Z==="string"&&Z.length>0)return f5=Z,f5;try{let X=a_(s_(import.meta.url)),Q=tK(X);f5=l_(i_(Q,"VERSION"),"utf-8").trim()}catch{f5="unknown"}return f5}var f5=null;var GQ=p(()=>{G8()});var xO={};c0(xO,{runOrThrow:()=>qf,run:()=>E0,readStreamCapped:()=>HQ,commandVersion:()=>Hf,commandExists:()=>X9,ShellError:()=>eK,MAX_STDOUT_BYTES:()=>kO});async function HQ(Z,X=kO){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([HQ(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 qf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new eK(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Gf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Gf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Hf(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 kO=16777216,eK;var k9=p(()=>{eK=class eK 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 n7(Z){return Uf?"":Z}var Uf,M0,k8,l0,dW0,a0,H8,Q9,g;var S6=p(()=>{Uf=(process.env.NO_COLOR??"").length>0;M0=n7("\x1B[0;31m"),k8=n7("\x1B[0;32m"),l0=n7("\x1B[1;33m"),dW0=n7("\x1B[0;34m"),a0=n7("\x1B[0;36m"),H8=n7("\x1B[1m"),Q9=n7("\x1B[2m"),g=n7("\x1B[0m")});import{existsSync as Ff}from"fs";async function F7(){if(R4!==void 0)return R4;let Z="/opt/homebrew/bin/python3.12";if(Ff(Z))return R4=Z,Z;let X=await X9("python3.12");if(X)return R4=X,X;let Q=await X9("python3");return R4=Q,Q}async function D7(Z,X={}){let Q=await F7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var R4;var o7=p(()=>{k9()});var rO={};c0(rO,{runStatus:()=>tf});import{existsSync as Y9,readFileSync as h3,readdirSync as dO,statSync as cO}from"fs";import{resolve as h8,basename as pf}from"path";import{homedir as df}from"os";function lO(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 iO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*BQ/X);if(J>BQ)J=BQ;let z=BQ-J,K=k8;if(Y>=80)K=M0;else if(Y>=50)K=l0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=lO(Z),V=lO(X);return` ${H8}${Q}${g} ${K}[${$}]${g} ${Y}% (${W} / ${V})`}async function lf(){if(await X9("jq"))return!0;return process.stdout.write(`${M0}Error: jq is required but not installed.${g}
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)
@@ -1206,4 +1206,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1206
1206
  `),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (R_(),P_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
1207
1207
  `),process.stderr.write(k_),2}}uO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var DW0=await FW0(Bun.argv.slice(2));process.exit(DW0);
1208
1208
 
1209
- //# debugId=8285CFE544C8109D64756E2164756E21
1209
+ //# debugId=FFE6617DCF7BFED164756E2164756E21
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.4.0",
4
+ "version": "8.5.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.4.0",
5
+ "version": "8.5.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",