loki-mode 7.6.4 → 7.6.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: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.6.4
6
+ # Loki Mode v7.6.5
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
381
381
 
382
382
  ---
383
383
 
384
- **v7.6.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.6.5 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.6.4
1
+ 7.6.5
package/autonomy/loki CHANGED
@@ -14212,9 +14212,63 @@ except Exception as e:
14212
14212
  ;;
14213
14213
 
14214
14214
  economics)
14215
- # Show token economics
14216
- if [ -f ".loki/memory/token_economics.json" ]; then
14217
- cat .loki/memory/token_economics.json | python3 -m json.tool 2>/dev/null || echo "Error parsing token economics JSON"
14215
+ # v7.6.6 B-3d fix: previously only read .loki/memory/token_economics.json
14216
+ # which is rarely written by current sessions, so users saw
14217
+ # "No token economics data" while `loki kpis` correctly read
14218
+ # .loki/metrics/efficiency/iter-*.json. Unify: prefer the
14219
+ # canonical kpis source; fall back to the legacy file for
14220
+ # backward compat with pre-v7.6.6 sessions.
14221
+ local _eff_dir=".loki/metrics/efficiency"
14222
+ local _legacy_file=".loki/memory/token_economics.json"
14223
+ local _have_iters=0
14224
+ if [ -d "$_eff_dir" ]; then
14225
+ if compgen -G "$_eff_dir/iter-*.json" >/dev/null 2>&1; then
14226
+ _have_iters=1
14227
+ fi
14228
+ fi
14229
+ if [ "$_have_iters" -eq 1 ]; then
14230
+ PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 - <<'PYEOF' 2>/dev/null
14231
+ import json, glob, os, sys
14232
+ files = sorted(glob.glob('.loki/metrics/efficiency/iter-*.json'))
14233
+ totals = {
14234
+ 'source': '.loki/metrics/efficiency/iter-*.json',
14235
+ 'iterations': len(files),
14236
+ 'total_input_tokens': 0,
14237
+ 'total_output_tokens': 0,
14238
+ 'total_tokens': 0,
14239
+ 'total_cost_usd': 0.0,
14240
+ 'total_duration_ms': 0,
14241
+ 'by_model': {},
14242
+ 'by_phase': {},
14243
+ }
14244
+ def _num(x, cast=float, default=0):
14245
+ try:
14246
+ return cast(x) if x is not None else default
14247
+ except (TypeError, ValueError):
14248
+ return default
14249
+ for f in files:
14250
+ try:
14251
+ d = json.load(open(f))
14252
+ except Exception:
14253
+ continue
14254
+ ti = int(_num(d.get('input_tokens'), int))
14255
+ to = int(_num(d.get('output_tokens'), int))
14256
+ co = float(_num(d.get('cost_usd'), float))
14257
+ du = int(_num(d.get('duration_ms'), int))
14258
+ totals['total_input_tokens'] += ti
14259
+ totals['total_output_tokens'] += to
14260
+ totals['total_tokens'] += ti + to
14261
+ totals['total_cost_usd'] += co
14262
+ totals['total_duration_ms'] += du
14263
+ m = d.get('model') or 'unknown'
14264
+ totals['by_model'][m] = totals['by_model'].get(m, 0) + 1
14265
+ p = d.get('phase') or 'unknown'
14266
+ totals['by_phase'][p] = totals['by_phase'].get(p, 0) + 1
14267
+ print(json.dumps(totals, indent=2))
14268
+ PYEOF
14269
+ elif [ -f "$_legacy_file" ]; then
14270
+ # Backward compat: pre-v7.6.6 sessions wrote here.
14271
+ cat "$_legacy_file" | python3 -m json.tool 2>/dev/null || echo "Error parsing token economics JSON"
14218
14272
  else
14219
14273
  echo "No token economics data. Run a session first."
14220
14274
  fi
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.6.4"
10
+ __version__ = "7.6.5"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.6.4
5
+ **Version:** v7.6.5
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var b=(K,$)=>{for(var z in $)_7(K,z,{get:$[z],enumerable:!0,configurable:!0,set:P7.bind($,z)})};var L=(K,$)=>()=>(K&&($=K(K=0)),$);var V1=import.meta.require;var e1={};b(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>u});import{resolve as f,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(f(K,"VERSION"))&&J1(f(K,"autonomy/run.sh")))return K;let z=S1(K);if(z===K)break;K=z}return f(i1,"..","..","..")}function N1(K){let $=K;for(let z=0;z<6;z++){if(J1(f($,"VERSION"))&&J1(f($,"autonomy/run.sh")))return $;let Q=S1($);if(Q===$)break;$=Q}return f(K,"..","..","..")}function P(){return process.env.LOKI_DIR??f(process.cwd(),".loki")}function k1(){return f(R7(),".loki")}var i1,u;var y=L(()=>{i1=S1(L7(import.meta.url));u=E7()});import{readFileSync as x7}from"fs";import{resolve as F7,dirname as w7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.6.4";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=w7(S7(import.meta.url)),z=N1($);o=x7(F7(z,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var D1=L(()=>{y()});var $0={};b($0,{runOrThrow:()=>N7,run:()=>w,commandVersion:()=>D7,commandExists:()=>h,ShellError:()=>C1});async function w(K,$={}){let z=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),Q,X;if($.timeoutMs&&$.timeoutMs>0)Q=setTimeout(()=>{try{z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{z.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[H,Z,q]=await Promise.all([new Response(z.stdout).text(),new Response(z.stderr).text(),z.exited]);return{stdout:H,stderr:Z,exitCode:q}}finally{if(Q)clearTimeout(Q);if(X)clearTimeout(X)}}async function N7(K,$={}){let z=await w(K,$);if(z.exitCode!==0)throw new C1(`command failed (${z.exitCode}): ${K.join(" ")}`,z.exitCode,z.stdout,z.stderr);return z}async function h(K){let $=k7(K),z=await w(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(z.exitCode===0)return z.stdout.trim()||null;return null}function k7(K){if(!/^[A-Za-z0-9._/-]+$/.test(K))throw Error(`refused to shell-escape suspect token: ${K}`);return K}async function D7(K,$="--version"){if(!await h(K))return null;let Q=await w([K,$],{timeoutMs:5000});if(Q.exitCode!==0)return null;return((Q.stdout||Q.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var C1;var p=L(()=>{C1=class C1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,z,Q){super(K);this.message=K;this.exitCode=$;this.stdout=z;this.stderr=Q;this.name="ShellError"}}});function c(K){return C7?"":K}var C7,R,D,E,O6,O,k,x,W;var n=L(()=>{C7=(process.env.NO_COLOR??"").length>0;R=c("\x1B[0;31m"),D=c("\x1B[0;32m"),E=c("\x1B[1;33m"),O6=c("\x1B[0;34m"),O=c("\x1B[0;36m"),k=c("\x1B[1m"),x=c("\x1B[2m"),W=c("\x1B[0m")});import{existsSync as c7}from"fs";async function r(){if(z1!==void 0)return z1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return z1=K,K;let $=await h("python3.12");if($)return z1=$,$;let z=await h("python3");return z1=z,z}async function a(K,$={}){let z=await r();if(!z)return{stdout:"",stderr:"python3 not found",exitCode:127};return w([z,"-c",K],$)}var z1;var Q1=L(()=>{p()});var G0={};b(G0,{runStatus:()=>z5});import{existsSync as S,readFileSync as Z1,readdirSync as H0,statSync as W0}from"fs";import{resolve as F,basename as a7}from"path";async function r7(){if(await h("jq"))return!0;return process.stdout.write(`${R}Error: jq is required but not installed.${W}
2
+ var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var b=(K,$)=>{for(var z in $)_7(K,z,{get:$[z],enumerable:!0,configurable:!0,set:P7.bind($,z)})};var L=(K,$)=>()=>(K&&($=K(K=0)),$);var V1=import.meta.require;var e1={};b(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>u});import{resolve as f,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(f(K,"VERSION"))&&J1(f(K,"autonomy/run.sh")))return K;let z=S1(K);if(z===K)break;K=z}return f(i1,"..","..","..")}function N1(K){let $=K;for(let z=0;z<6;z++){if(J1(f($,"VERSION"))&&J1(f($,"autonomy/run.sh")))return $;let Q=S1($);if(Q===$)break;$=Q}return f(K,"..","..","..")}function P(){return process.env.LOKI_DIR??f(process.cwd(),".loki")}function k1(){return f(R7(),".loki")}var i1,u;var y=L(()=>{i1=S1(L7(import.meta.url));u=E7()});import{readFileSync as x7}from"fs";import{resolve as F7,dirname as w7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.6.5";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=w7(S7(import.meta.url)),z=N1($);o=x7(F7(z,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var D1=L(()=>{y()});var $0={};b($0,{runOrThrow:()=>N7,run:()=>w,commandVersion:()=>D7,commandExists:()=>h,ShellError:()=>C1});async function w(K,$={}){let z=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),Q,X;if($.timeoutMs&&$.timeoutMs>0)Q=setTimeout(()=>{try{z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{z.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[H,Z,q]=await Promise.all([new Response(z.stdout).text(),new Response(z.stderr).text(),z.exited]);return{stdout:H,stderr:Z,exitCode:q}}finally{if(Q)clearTimeout(Q);if(X)clearTimeout(X)}}async function N7(K,$={}){let z=await w(K,$);if(z.exitCode!==0)throw new C1(`command failed (${z.exitCode}): ${K.join(" ")}`,z.exitCode,z.stdout,z.stderr);return z}async function h(K){let $=k7(K),z=await w(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(z.exitCode===0)return z.stdout.trim()||null;return null}function k7(K){if(!/^[A-Za-z0-9._/-]+$/.test(K))throw Error(`refused to shell-escape suspect token: ${K}`);return K}async function D7(K,$="--version"){if(!await h(K))return null;let Q=await w([K,$],{timeoutMs:5000});if(Q.exitCode!==0)return null;return((Q.stdout||Q.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var C1;var p=L(()=>{C1=class C1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,z,Q){super(K);this.message=K;this.exitCode=$;this.stdout=z;this.stderr=Q;this.name="ShellError"}}});function c(K){return C7?"":K}var C7,R,D,E,O6,O,k,x,W;var n=L(()=>{C7=(process.env.NO_COLOR??"").length>0;R=c("\x1B[0;31m"),D=c("\x1B[0;32m"),E=c("\x1B[1;33m"),O6=c("\x1B[0;34m"),O=c("\x1B[0;36m"),k=c("\x1B[1m"),x=c("\x1B[2m"),W=c("\x1B[0m")});import{existsSync as c7}from"fs";async function r(){if(z1!==void 0)return z1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return z1=K,K;let $=await h("python3.12");if($)return z1=$,$;let z=await h("python3");return z1=z,z}async function a(K,$={}){let z=await r();if(!z)return{stdout:"",stderr:"python3 not found",exitCode:127};return w([z,"-c",K],$)}var z1;var Q1=L(()=>{p()});var G0={};b(G0,{runStatus:()=>z5});import{existsSync as S,readFileSync as Z1,readdirSync as H0,statSync as W0}from"fs";import{resolve as F,basename as a7}from"path";async function r7(){if(await h("jq"))return!0;return process.stdout.write(`${R}Error: jq is required but not installed.${W}
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)
@@ -534,4 +534,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
534
534
  `),2}default:return process.stderr.write(`Unknown command: ${$}
535
535
  `),process.stderr.write(j7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var z6=await $6(Bun.argv.slice(2));process.exit(z6);
536
536
 
537
- //# debugId=75643AA67185D81E64756E2164756E21
537
+ //# debugId=062EAA592CDADAB764756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.6.4'
60
+ __version__ = '7.6.5'
package/memory/engine.py CHANGED
@@ -308,12 +308,79 @@ class MemoryEngine:
308
308
  # Update timeline with action summary
309
309
  self._update_timeline_with_episode(trace_dict)
310
310
 
311
+ # v7.6.5 B-3c fix: previously index.json topics were ONLY populated by
312
+ # consolidated patterns, so a real session that wrote 5 episodes left
313
+ # topics:[] until the user manually ran `loki memory consolidate`.
314
+ # Now every episode also stamps a lightweight topic into index.json
315
+ # derived from its phase + goal keywords. Topic count grows
316
+ # monotonically with episodes; consolidation still refines them.
317
+ self._update_index_with_episode(trace_dict)
318
+
311
319
  # Queue for embedding if embeddings are enabled
312
320
  if self._embedding_func is not None:
313
321
  self._queue_for_embedding(episode_id, "episodic", trace_dict)
314
322
 
315
323
  return episode_id
316
324
 
325
+ def _update_index_with_episode(self, episode: Dict[str, Any]) -> None:
326
+ """Stamp a lightweight topic into index.json from an episode.
327
+
328
+ v7.6.5 B-3c fix. Keeps the index alive between consolidation cycles
329
+ so the dashboard Memory Files panel and `loki memory index` show
330
+ real topics immediately after a session ends.
331
+ """
332
+ try:
333
+ index = self.storage.read_json("index.json") or {
334
+ "version": "1.1.0",
335
+ "topics": [],
336
+ "total_memories": 0,
337
+ }
338
+ context = episode.get("context", {}) if isinstance(episode.get("context"), dict) else {}
339
+ phase = (context.get("phase") or episode.get("phase") or "general").lower()
340
+ goal = (context.get("goal") or episode.get("goal") or "")[:200]
341
+ # Topic id = phase. Multiple episodes in the same phase share a topic.
342
+ topic_id = phase or "general"
343
+ now = datetime.now(timezone.utc).isoformat()
344
+ episode_id = episode.get("id")
345
+ cost = float(episode.get("cost_usd", 0) or 0)
346
+ tokens = int(episode.get("tokens_used", 0) or 0)
347
+ files = list(episode.get("files_modified", []) or [])
348
+
349
+ found = None
350
+ for topic in index.get("topics", []):
351
+ if topic.get("id") == topic_id:
352
+ found = topic
353
+ break
354
+ if found is None:
355
+ index.setdefault("topics", []).append({
356
+ "id": topic_id,
357
+ "summary": goal or f"Activity in phase {topic_id}",
358
+ "episode_ids": [episode_id] if episode_id else [],
359
+ "episode_count": 1,
360
+ "total_cost_usd": cost,
361
+ "total_tokens": tokens,
362
+ "files_touched": files[:20],
363
+ "first_seen": now,
364
+ "last_accessed": now,
365
+ "relevance_score": 0.5,
366
+ })
367
+ index["total_memories"] = index.get("total_memories", 0) + 1
368
+ else:
369
+ if episode_id and episode_id not in found.get("episode_ids", []):
370
+ found.setdefault("episode_ids", []).append(episode_id)
371
+ found["episode_count"] = found.get("episode_count", 0) + 1
372
+ found["total_cost_usd"] = float(found.get("total_cost_usd", 0) or 0) + cost
373
+ found["total_tokens"] = int(found.get("total_tokens", 0) or 0) + tokens
374
+ merged = set(found.get("files_touched", []) or []) | set(files[:20])
375
+ found["files_touched"] = sorted(merged)[:50]
376
+ found["last_accessed"] = now
377
+
378
+ index["last_updated"] = now
379
+ self.storage.write_json("index.json", index)
380
+ except Exception: # noqa: BLE001
381
+ # Never let index update break episode storage.
382
+ pass
383
+
317
384
  def get_episode(self, episode_id: str) -> Optional[EpisodeTrace]:
318
385
  """
319
386
  Retrieve an episode by ID.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "7.6.4",
3
+ "version": "7.6.5",
4
4
  "description": "Loki Mode by Autonomi. Multi-agent autonomous SDLC framework. Spec to deployed app: PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief. 4 AI providers (Claude Code, OpenAI Codex, Cline, Aider). 11 quality gates.",
5
5
  "keywords": [
6
6
  "agent",