loki-mode 7.104.3 → 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.3
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.3 | [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.3
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.3"
10
+ __version__ = "7.104.5"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -1784,6 +1784,15 @@ async def list_tasks(
1784
1784
  try:
1785
1785
  state = json.loads(state_file.read_text())
1786
1786
  task_groups = state.get("tasks", {})
1787
+ # v7.104.4: dashboard-state.json is user/agent-written and can be
1788
+ # malformed or partially written. If "tasks" is not a dict, or a
1789
+ # group is not a list, or an item is not a dict, the old code raised
1790
+ # AttributeError (task_groups.get / task.get) which is NOT caught by
1791
+ # the surrounding (JSONDecodeError, KeyError) handler -> the whole
1792
+ # /api/tasks request 500s and the board goes blank. Coerce defensively
1793
+ # so a bad shape degrades to "skip that part", never a crash.
1794
+ if not isinstance(task_groups, dict):
1795
+ task_groups = {}
1787
1796
 
1788
1797
  status_map = {
1789
1798
  "pending": "pending",
@@ -1794,9 +1803,16 @@ async def list_tasks(
1794
1803
  }
1795
1804
 
1796
1805
  for group_key, mapped_status in status_map.items():
1797
- for i, task in enumerate(task_groups.get(group_key, [])):
1806
+ _group = task_groups.get(group_key, [])
1807
+ if not isinstance(_group, list):
1808
+ continue # malformed group (e.g. a dict) -> skip, don't crash
1809
+ for i, task in enumerate(_group):
1810
+ if not isinstance(task, dict):
1811
+ continue # malformed entry (string/None) -> skip
1798
1812
  task_id = task.get("id", f"{group_key}-{i}")
1799
1813
  payload = task.get("payload", {})
1814
+ if not isinstance(payload, dict):
1815
+ payload = {} # non-dict payload -> ignore, don't crash on payload.get
1800
1816
  # v7.7.32: read enrichment fields from the TOP LEVEL of the
1801
1817
  # task object (where run.sh writes them), falling back to
1802
1818
  # payload only for legacy entries. Previously description was
@@ -4991,41 +5007,79 @@ def _sanitize_agent_id(agent_id: str) -> str:
4991
5007
  )
4992
5008
  return agent_id
4993
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
+
4994
5036
  @app.get("/api/memory/summary", dependencies=[Depends(auth.require_scope("read"))])
4995
5037
  async def get_memory_summary():
4996
5038
  """Get memory system summary from .loki/memory/."""
4997
- # 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.
4998
5046
  storage = _get_memory_storage()
4999
5047
  if storage is not None:
5000
5048
  try:
5001
5049
  stats = storage.get_stats()
5002
- summary = {
5003
- "episodic": {"count": stats.get("episode_count", 0), "latestDate": None},
5004
- "semantic": {"patterns": stats.get("pattern_count", 0), "antiPatterns": 0},
5005
- "procedural": {"skills": stats.get("skill_count", 0)},
5006
- "backend": "sqlite",
5007
- }
5008
- # Get latest episode date
5009
- episode_ids = storage.list_episodes(limit=1)
5010
- if episode_ids:
5011
- ep = storage.load_episode(episode_ids[0])
5012
- if ep:
5013
- summary["episodic"]["latestDate"] = ep.get("timestamp", "")
5014
- # Token economics from JSON (not in SQLite)
5015
- econ_file = _get_loki_dir() / "memory" / "token_economics.json"
5016
- if econ_file.exists():
5017
- try:
5018
- econ = json.loads(econ_file.read_text())
5019
- summary["tokenEconomics"] = {
5020
- "discoveryTokens": econ.get("discoveryTokens", 0),
5021
- "readTokens": econ.get("readTokens", 0),
5022
- "savingsPercent": econ.get("savingsPercent", 0),
5023
- }
5024
- 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:
5025
5081
  summary["tokenEconomics"] = {"discoveryTokens": 0, "readTokens": 0, "savingsPercent": 0}
5026
- else:
5027
- summary["tokenEconomics"] = {"discoveryTokens": 0, "readTokens": 0, "savingsPercent": 0}
5028
- return summary
5082
+ return summary
5029
5083
  except Exception:
5030
5084
  pass
5031
5085
 
@@ -5041,14 +5095,28 @@ async def get_memory_summary():
5041
5095
 
5042
5096
  ep_dir = memory_dir / "episodic"
5043
5097
  if ep_dir.exists():
5044
- 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)
5045
5103
  summary["episodic"]["count"] = len(episodes)
5046
5104
  if episodes:
5047
- try:
5048
- latest = json.loads(episodes[-1].read_text())
5049
- summary["episodic"]["latestDate"] = latest.get("timestamp", "")
5050
- except Exception:
5051
- 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
5052
5120
 
5053
5121
  sem_dir = memory_dir / "semantic"
5054
5122
  patterns_file = sem_dir / "patterns.json"
@@ -5068,7 +5136,7 @@ async def get_memory_summary():
5068
5136
 
5069
5137
  skills_dir = memory_dir / "skills"
5070
5138
  if skills_dir.exists():
5071
- summary["procedural"]["skills"] = len(list(skills_dir.glob("*.json")))
5139
+ summary["procedural"]["skills"] = len(_memory_skill_files(skills_dir))
5072
5140
 
5073
5141
  econ_file = memory_dir / "token_economics.json"
5074
5142
  if econ_file.exists():
@@ -5106,20 +5174,22 @@ async def list_episodes(limit: int = Query(default=50, ge=1, le=1000)):
5106
5174
  except Exception:
5107
5175
  pass
5108
5176
 
5109
- # Fallback to JSON files -- use heapq to avoid sorting all files
5110
- 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).
5111
5183
  ep_dir = _get_loki_dir() / "memory" / "episodic"
5112
- episodes = []
5184
+ loaded = []
5113
5185
  if ep_dir.exists():
5114
- all_files = ep_dir.glob("*.json")
5115
- # nlargest by filename (timestamps sort lexicographically) avoids full sort
5116
- files = heapq.nlargest(limit, all_files, key=lambda f: f.name)
5117
- for f in files:
5186
+ for f in _memory_episode_files(ep_dir):
5118
5187
  try:
5119
- episodes.append(json.loads(f.read_text()))
5188
+ loaded.append(json.loads(f.read_text()))
5120
5189
  except Exception:
5121
5190
  pass
5122
- return episodes
5191
+ loaded.sort(key=lambda e: e.get("timestamp", "") if isinstance(e, dict) else "", reverse=True)
5192
+ return loaded[:limit]
5123
5193
 
5124
5194
  return await asyncio.to_thread(_load_episodes)
5125
5195
 
@@ -5143,7 +5213,7 @@ async def get_episode(episode_id: str):
5143
5213
  if not ep_dir.exists():
5144
5214
  raise HTTPException(status_code=404, detail="Episode not found")
5145
5215
  real_loki = os.path.realpath(str(loki_dir))
5146
- for f in ep_dir.glob("*.json"):
5216
+ for f in _memory_episode_files(ep_dir):
5147
5217
  resolved = os.path.realpath(f)
5148
5218
  if not resolved.startswith(real_loki + os.sep) and resolved != real_loki:
5149
5219
  raise HTTPException(status_code=403, detail="Access denied")
@@ -5219,7 +5289,7 @@ async def list_skills():
5219
5289
  skills_dir = _get_loki_dir() / "memory" / "skills"
5220
5290
  skills = []
5221
5291
  if skills_dir.exists():
5222
- for f in sorted(skills_dir.glob("*.json")):
5292
+ for f in _memory_skill_files(skills_dir):
5223
5293
  try:
5224
5294
  skills.append(json.loads(f.read_text()))
5225
5295
  except Exception:
@@ -5237,7 +5307,7 @@ async def get_skill(skill_id: str):
5237
5307
  if not skills_dir.exists():
5238
5308
  raise HTTPException(status_code=404, detail="Skill not found")
5239
5309
  real_loki = os.path.realpath(str(loki_dir))
5240
- for f in skills_dir.glob("*.json"):
5310
+ for f in _memory_skill_files(skills_dir):
5241
5311
  resolved = os.path.realpath(f)
5242
5312
  if not resolved.startswith(real_loki + os.sep) and resolved != real_loki:
5243
5313
  raise HTTPException(status_code=403, detail="Access denied")
@@ -5683,11 +5753,7 @@ async def get_memory_stats():
5683
5753
  ep_count = 0
5684
5754
  ep_dir = memory_dir / "episodic"
5685
5755
  if ep_dir.exists():
5686
- for d in ep_dir.iterdir():
5687
- if d.is_dir():
5688
- ep_count += len(list(d.glob("*.json")))
5689
- elif d.suffix == ".json":
5690
- ep_count += 1
5756
+ ep_count = len(_memory_episode_files(ep_dir))
5691
5757
 
5692
5758
  pat_count = 0
5693
5759
  patterns_file = memory_dir / "semantic" / "patterns.json"
@@ -5701,7 +5767,7 @@ async def get_memory_stats():
5701
5767
  skill_count = 0
5702
5768
  skills_dir = memory_dir / "skills"
5703
5769
  if skills_dir.exists():
5704
- skill_count = len(list(skills_dir.glob("*.json")))
5770
+ skill_count = len(_memory_skill_files(skills_dir))
5705
5771
 
5706
5772
  return {
5707
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.3
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.3";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=7CE53CC97EC3260A64756E2164756E21
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.3'
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.3",
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.3",
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",