loki-mode 7.104.4 → 7.104.5

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 v7.104.4
6
+ # Loki Mode v7.104.5
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
408
408
 
409
409
  ---
410
410
 
411
- **v7.104.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.104.5 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.104.4
1
+ 7.104.5
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.104.4"
10
+ __version__ = "7.104.5"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -5007,41 +5007,79 @@ def _sanitize_agent_id(agent_id: str) -> str:
5007
5007
  )
5008
5008
  return agent_id
5009
5009
 
5010
+
5011
+ # --- Memory file enumeration (single source of truth) ------------------------
5012
+ # v7.104.5 (PR #178 follow-up): the memory store writes episodes and skills into
5013
+ # DATED subdirectories (episodic/YYYY-MM-DD/*.json, skills/*/*.json), so a flat
5014
+ # .glob("*.json") finds nothing and every endpoint that used it under-reported.
5015
+ # PR #178 fixed only /api/memory/summary; the sibling /api/memory/episodes and
5016
+ # the skills listings still used a flat glob, so the count and the drill-in list
5017
+ # disagreed (summary said 28, the list returned 0) -- a trust-eroding
5018
+ # inconsistency. These two helpers are the ONE way to enumerate memory files, so
5019
+ # counts and lists can never diverge again. Both recurse and skip .lock files.
5020
+ def _memory_episode_files(ep_dir) -> list:
5021
+ """All episode JSON files under episodic/ (recursing dated subdirs), sorted."""
5022
+ try:
5023
+ return sorted(p for p in ep_dir.rglob("*.json") if not p.name.endswith(".lock"))
5024
+ except Exception:
5025
+ return []
5026
+
5027
+
5028
+ def _memory_skill_files(skills_dir) -> list:
5029
+ """All skill JSON files under skills/ (recursing subdirs), sorted."""
5030
+ try:
5031
+ return sorted(p for p in skills_dir.rglob("*.json") if not p.name.endswith(".lock"))
5032
+ except Exception:
5033
+ return []
5034
+
5035
+
5010
5036
  @app.get("/api/memory/summary", dependencies=[Depends(auth.require_scope("read"))])
5011
5037
  async def get_memory_summary():
5012
5038
  """Get memory system summary from .loki/memory/."""
5013
- # Try SQLite backend first for accurate counts
5039
+ # Try SQLite backend first for accurate counts.
5040
+ # NOTE (PR #178, @iizotov): _get_memory_storage() returns a SQLiteMemoryStorage
5041
+ # object whenever the module imports, even if no .db file exists yet. On a
5042
+ # JSON-backed project that yields all-zero stats, so we must NOT return here on
5043
+ # empty counts -- otherwise the JSON file fallback below never runs and the
5044
+ # dashboard shows 0 episodes/patterns/skills even though they exist on disk.
5045
+ # Only trust SQLite when it actually reports data; else fall through to JSON.
5014
5046
  storage = _get_memory_storage()
5015
5047
  if storage is not None:
5016
5048
  try:
5017
5049
  stats = storage.get_stats()
5018
- summary = {
5019
- "episodic": {"count": stats.get("episode_count", 0), "latestDate": None},
5020
- "semantic": {"patterns": stats.get("pattern_count", 0), "antiPatterns": 0},
5021
- "procedural": {"skills": stats.get("skill_count", 0)},
5022
- "backend": "sqlite",
5023
- }
5024
- # Get latest episode date
5025
- episode_ids = storage.list_episodes(limit=1)
5026
- if episode_ids:
5027
- ep = storage.load_episode(episode_ids[0])
5028
- if ep:
5029
- summary["episodic"]["latestDate"] = ep.get("timestamp", "")
5030
- # Token economics from JSON (not in SQLite)
5031
- econ_file = _get_loki_dir() / "memory" / "token_economics.json"
5032
- if econ_file.exists():
5033
- try:
5034
- econ = json.loads(econ_file.read_text())
5035
- summary["tokenEconomics"] = {
5036
- "discoveryTokens": econ.get("discoveryTokens", 0),
5037
- "readTokens": econ.get("readTokens", 0),
5038
- "savingsPercent": econ.get("savingsPercent", 0),
5039
- }
5040
- except Exception:
5050
+ total = (
5051
+ stats.get("episode_count", 0)
5052
+ + stats.get("pattern_count", 0)
5053
+ + stats.get("skill_count", 0)
5054
+ )
5055
+ if total > 0:
5056
+ summary = {
5057
+ "episodic": {"count": stats.get("episode_count", 0), "latestDate": None},
5058
+ "semantic": {"patterns": stats.get("pattern_count", 0), "antiPatterns": 0},
5059
+ "procedural": {"skills": stats.get("skill_count", 0)},
5060
+ "backend": "sqlite",
5061
+ }
5062
+ # Get latest episode date
5063
+ episode_ids = storage.list_episodes(limit=1)
5064
+ if episode_ids:
5065
+ ep = storage.load_episode(episode_ids[0])
5066
+ if ep:
5067
+ summary["episodic"]["latestDate"] = ep.get("timestamp", "")
5068
+ # Token economics from JSON (not in SQLite)
5069
+ econ_file = _get_loki_dir() / "memory" / "token_economics.json"
5070
+ if econ_file.exists():
5071
+ try:
5072
+ econ = json.loads(econ_file.read_text())
5073
+ summary["tokenEconomics"] = {
5074
+ "discoveryTokens": econ.get("discoveryTokens", 0),
5075
+ "readTokens": econ.get("readTokens", 0),
5076
+ "savingsPercent": econ.get("savingsPercent", 0),
5077
+ }
5078
+ except Exception:
5079
+ summary["tokenEconomics"] = {"discoveryTokens": 0, "readTokens": 0, "savingsPercent": 0}
5080
+ else:
5041
5081
  summary["tokenEconomics"] = {"discoveryTokens": 0, "readTokens": 0, "savingsPercent": 0}
5042
- else:
5043
- summary["tokenEconomics"] = {"discoveryTokens": 0, "readTokens": 0, "savingsPercent": 0}
5044
- return summary
5082
+ return summary
5045
5083
  except Exception:
5046
5084
  pass
5047
5085
 
@@ -5057,14 +5095,28 @@ async def get_memory_summary():
5057
5095
 
5058
5096
  ep_dir = memory_dir / "episodic"
5059
5097
  if ep_dir.exists():
5060
- episodes = sorted(ep_dir.glob("*.json"))
5098
+ # Episodes are stored under dated subdirectories (episodic/YYYY-MM-DD/*.json),
5099
+ # so a flat glob("*.json") finds nothing (PR #178, @iizotov). Recurse and
5100
+ # ignore .lock files. Shared with /api/memory/episodes via _memory_episode_
5101
+ # files() so the summary count and the drill-in list can never disagree.
5102
+ episodes = _memory_episode_files(ep_dir)
5061
5103
  summary["episodic"]["count"] = len(episodes)
5062
5104
  if episodes:
5063
- try:
5064
- latest = json.loads(episodes[-1].read_text())
5065
- summary["episodic"]["latestDate"] = latest.get("timestamp", "")
5066
- except Exception:
5067
- pass
5105
+ # latestDate must be the newest by RECORDED TIMESTAMP, not by filename
5106
+ # sort: intra-day filenames carry a hash tiebreak, not a time, so a
5107
+ # filename sort can report the wrong episode (observed ~15h off). Pick
5108
+ # the max parsed timestamp; fall back to the last file only if no
5109
+ # timestamp is readable, so an honest value is preferred over a
5110
+ # confidently-wrong one.
5111
+ latest_ts = ""
5112
+ for _epf in episodes:
5113
+ try:
5114
+ _ts = json.loads(_epf.read_text()).get("timestamp", "")
5115
+ if isinstance(_ts, str) and _ts > latest_ts:
5116
+ latest_ts = _ts
5117
+ except Exception:
5118
+ continue
5119
+ summary["episodic"]["latestDate"] = latest_ts or None
5068
5120
 
5069
5121
  sem_dir = memory_dir / "semantic"
5070
5122
  patterns_file = sem_dir / "patterns.json"
@@ -5084,7 +5136,7 @@ async def get_memory_summary():
5084
5136
 
5085
5137
  skills_dir = memory_dir / "skills"
5086
5138
  if skills_dir.exists():
5087
- summary["procedural"]["skills"] = len(list(skills_dir.glob("*.json")))
5139
+ summary["procedural"]["skills"] = len(_memory_skill_files(skills_dir))
5088
5140
 
5089
5141
  econ_file = memory_dir / "token_economics.json"
5090
5142
  if econ_file.exists():
@@ -5122,20 +5174,22 @@ async def list_episodes(limit: int = Query(default=50, ge=1, le=1000)):
5122
5174
  except Exception:
5123
5175
  pass
5124
5176
 
5125
- # Fallback to JSON files -- use heapq to avoid sorting all files
5126
- import heapq
5177
+ # Fallback to JSON files. Enumerate via the shared helper (recurses the
5178
+ # dated subdirs) so this list agrees with /api/memory/summary's count --
5179
+ # the flat glob here was the exact parity gap PR #178's review flagged
5180
+ # (summary said 28, this returned 0). Read each file, then return the
5181
+ # newest `limit` by RECORDED timestamp (not filename, whose intra-day
5182
+ # tiebreak is a hash, not a time).
5127
5183
  ep_dir = _get_loki_dir() / "memory" / "episodic"
5128
- episodes = []
5184
+ loaded = []
5129
5185
  if ep_dir.exists():
5130
- all_files = ep_dir.glob("*.json")
5131
- # nlargest by filename (timestamps sort lexicographically) avoids full sort
5132
- files = heapq.nlargest(limit, all_files, key=lambda f: f.name)
5133
- for f in files:
5186
+ for f in _memory_episode_files(ep_dir):
5134
5187
  try:
5135
- episodes.append(json.loads(f.read_text()))
5188
+ loaded.append(json.loads(f.read_text()))
5136
5189
  except Exception:
5137
5190
  pass
5138
- return episodes
5191
+ loaded.sort(key=lambda e: e.get("timestamp", "") if isinstance(e, dict) else "", reverse=True)
5192
+ return loaded[:limit]
5139
5193
 
5140
5194
  return await asyncio.to_thread(_load_episodes)
5141
5195
 
@@ -5159,7 +5213,7 @@ async def get_episode(episode_id: str):
5159
5213
  if not ep_dir.exists():
5160
5214
  raise HTTPException(status_code=404, detail="Episode not found")
5161
5215
  real_loki = os.path.realpath(str(loki_dir))
5162
- for f in ep_dir.glob("*.json"):
5216
+ for f in _memory_episode_files(ep_dir):
5163
5217
  resolved = os.path.realpath(f)
5164
5218
  if not resolved.startswith(real_loki + os.sep) and resolved != real_loki:
5165
5219
  raise HTTPException(status_code=403, detail="Access denied")
@@ -5235,7 +5289,7 @@ async def list_skills():
5235
5289
  skills_dir = _get_loki_dir() / "memory" / "skills"
5236
5290
  skills = []
5237
5291
  if skills_dir.exists():
5238
- for f in sorted(skills_dir.glob("*.json")):
5292
+ for f in _memory_skill_files(skills_dir):
5239
5293
  try:
5240
5294
  skills.append(json.loads(f.read_text()))
5241
5295
  except Exception:
@@ -5253,7 +5307,7 @@ async def get_skill(skill_id: str):
5253
5307
  if not skills_dir.exists():
5254
5308
  raise HTTPException(status_code=404, detail="Skill not found")
5255
5309
  real_loki = os.path.realpath(str(loki_dir))
5256
- for f in skills_dir.glob("*.json"):
5310
+ for f in _memory_skill_files(skills_dir):
5257
5311
  resolved = os.path.realpath(f)
5258
5312
  if not resolved.startswith(real_loki + os.sep) and resolved != real_loki:
5259
5313
  raise HTTPException(status_code=403, detail="Access denied")
@@ -5699,11 +5753,7 @@ async def get_memory_stats():
5699
5753
  ep_count = 0
5700
5754
  ep_dir = memory_dir / "episodic"
5701
5755
  if ep_dir.exists():
5702
- for d in ep_dir.iterdir():
5703
- if d.is_dir():
5704
- ep_count += len(list(d.glob("*.json")))
5705
- elif d.suffix == ".json":
5706
- ep_count += 1
5756
+ ep_count = len(_memory_episode_files(ep_dir))
5707
5757
 
5708
5758
  pat_count = 0
5709
5759
  patterns_file = memory_dir / "semantic" / "patterns.json"
@@ -5717,7 +5767,7 @@ async def get_memory_stats():
5717
5767
  skill_count = 0
5718
5768
  skills_dir = memory_dir / "skills"
5719
5769
  if skills_dir.exists():
5720
- skill_count = len(list(skills_dir.glob("*.json")))
5770
+ skill_count = len(_memory_skill_files(skills_dir))
5721
5771
 
5722
5772
  return {
5723
5773
  "backend": "json",
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.104.4
5
+ **Version:** v7.104.5
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var X8=Object.defineProperty;var K8=($)=>$;function q8($,Q){this[$]=K8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)X8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:q8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var v1={};b(v1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as r,dirname as i$}from"path";import{fileURLToPath as J8}from"url";import{existsSync as x$}from"fs";import{homedir as V8}from"os";function W8(){let $=y1;for(let Q=0;Q<6;Q++){if(x$(r($,"VERSION"))&&x$(r($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return r(y1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(x$(r(Q,"VERSION"))&&x$(r(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return r($,"..","..","..")}function P(){return process.env.LOKI_DIR??r(process.cwd(),".loki")}function T$(){return r(V8(),".loki")}var y1,h;var C=L(()=>{y1=i$(J8(import.meta.url));h=W8()});import{readFileSync as U8}from"fs";import{resolve as H8,dirname as G8}from"path";import{fileURLToPath as B8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.104.4";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=G8(B8(import.meta.url)),Z=e$(Q);Q$=U8(H8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var $1=L(()=>{C()});var c1={};b(c1,{runOrThrow:()=>x8,run:()=>F,readStreamCapped:()=>u1,commandVersion:()=>S8,commandExists:()=>f,ShellError:()=>Q1,MAX_STDOUT_BYTES:()=>f1});async function u1($,Q=f1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([u1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function x8($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new Q1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=N8($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function N8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function S8($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var f1=16777216,Q1;var d=L(()=>{Q1=class Q1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function t($){return D8?"":$}var D8,T,S,_,nZ,I,k,y,J;var c=L(()=>{D8=(process.env.NO_COLOR??"").length>0;T=t("\x1B[0;31m"),S=t("\x1B[0;32m"),_=t("\x1B[1;33m"),nZ=t("\x1B[0;34m"),I=t("\x1B[0;36m"),k=t("\x1B[1m"),y=t("\x1B[2m"),J=t("\x1B[0m")});import{existsSync as c8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(c8($))return A$=$,$;let Q=await f("python3.12");if(Q)return A$=Q,Q;let Z=await f("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var A$;var V$=L(()=>{d()});var J0={};b(J0,{runStatus:()=>U3});import{existsSync as v,readFileSync as U$,readdirSync as e1,statSync as $0}from"fs";import{resolve as D,basename as Q3}from"path";import{homedir as Z3}from"os";function Q0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function Z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*D$/Q);if(X>D$)X=D$;let q=D$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Q0($),U=Q0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${U})`}async function X3(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
2
+ var X8=Object.defineProperty;var K8=($)=>$;function q8($,Q){this[$]=K8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)X8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:q8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var v1={};b(v1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as r,dirname as i$}from"path";import{fileURLToPath as J8}from"url";import{existsSync as x$}from"fs";import{homedir as V8}from"os";function W8(){let $=y1;for(let Q=0;Q<6;Q++){if(x$(r($,"VERSION"))&&x$(r($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return r(y1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(x$(r(Q,"VERSION"))&&x$(r(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return r($,"..","..","..")}function P(){return process.env.LOKI_DIR??r(process.cwd(),".loki")}function T$(){return r(V8(),".loki")}var y1,h;var C=L(()=>{y1=i$(J8(import.meta.url));h=W8()});import{readFileSync as U8}from"fs";import{resolve as H8,dirname as G8}from"path";import{fileURLToPath as B8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.104.5";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=G8(B8(import.meta.url)),Z=e$(Q);Q$=U8(H8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var $1=L(()=>{C()});var c1={};b(c1,{runOrThrow:()=>x8,run:()=>F,readStreamCapped:()=>u1,commandVersion:()=>S8,commandExists:()=>f,ShellError:()=>Q1,MAX_STDOUT_BYTES:()=>f1});async function u1($,Q=f1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([u1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function x8($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new Q1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=N8($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function N8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function S8($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var f1=16777216,Q1;var d=L(()=>{Q1=class Q1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function t($){return D8?"":$}var D8,T,S,_,nZ,I,k,y,J;var c=L(()=>{D8=(process.env.NO_COLOR??"").length>0;T=t("\x1B[0;31m"),S=t("\x1B[0;32m"),_=t("\x1B[1;33m"),nZ=t("\x1B[0;34m"),I=t("\x1B[0;36m"),k=t("\x1B[1m"),y=t("\x1B[2m"),J=t("\x1B[0m")});import{existsSync as c8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(c8($))return A$=$,$;let Q=await f("python3.12");if(Q)return A$=Q,Q;let Z=await f("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var A$;var V$=L(()=>{d()});var J0={};b(J0,{runStatus:()=>U3});import{existsSync as v,readFileSync as U$,readdirSync as e1,statSync as $0}from"fs";import{resolve as D,basename as Q3}from"path";import{homedir as Z3}from"os";function Q0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function Z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*D$/Q);if(X>D$)X=D$;let q=D$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Q0($),U=Q0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${U})`}async function X3(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
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)
@@ -805,4 +805,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
805
805
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
806
806
  `),process.stderr.write(z8),2}}i1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var EZ=await RZ(Bun.argv.slice(2));process.exit(EZ);
807
807
 
808
- //# debugId=61E42E2EF5F94BA264756E2164756E21
808
+ //# debugId=CD7A0B6FD83A195A64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.104.4'
60
+ __version__ = '7.104.5'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.104.4",
4
+ "version": "7.104.5",
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": "7.104.4",
5
+ "version": "7.104.5",
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",