loki-mode 7.5.30 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: 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.5.30
6
+ # Loki Mode v7.6.0
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.5.30 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.6.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.5.30
1
+ 7.6.0
package/autonomy/loki CHANGED
@@ -3689,6 +3689,12 @@ cmd_web_help() {
3689
3689
  echo " loki web Start Purple Lab and open in browser"
3690
3690
  echo " loki web --no-open Start without opening browser"
3691
3691
  echo " loki web stop Stop the Purple Lab server"
3692
+ echo ""
3693
+ echo "Note (since v7.5.30):"
3694
+ echo " Purple Lab is also embedded in the Dashboard as a Lab sidebar entry."
3695
+ echo " Run 'loki dashboard start' and click 'Lab' to use the same UI"
3696
+ echo " without spawning a second port. 'loki web' standalone remains"
3697
+ echo " supported (Rule 0); both modes serve the same React bundle."
3692
3698
  }
3693
3699
 
3694
3700
  cmd_web_start() {
@@ -13993,7 +13999,7 @@ if total_removed > 0:
13993
13999
  index)
13994
14000
  # Show or rebuild the memory index layer
13995
14001
  if [ "${2:-}" = "rebuild" ]; then
13996
- python3 -c "
14002
+ PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
13997
14003
  try:
13998
14004
  from memory.layers import IndexLayer
13999
14005
  layer = IndexLayer('.loki/memory')
@@ -14017,7 +14023,7 @@ except Exception as e:
14017
14023
  consolidate)
14018
14024
  # Run consolidation pipeline
14019
14025
  local hours="${2:-24}"
14020
- LOKI_HOURS="$hours" python3 -c "
14026
+ LOKI_HOURS="$hours" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
14021
14027
  import os
14022
14028
  try:
14023
14029
  from memory.consolidation import ConsolidationPipeline
@@ -14054,7 +14060,7 @@ except Exception as e:
14054
14060
  echo "Usage: loki memory retrieve <query>"
14055
14061
  exit 1
14056
14062
  fi
14057
- LOKI_QUERY="$query" python3 -c "
14063
+ LOKI_QUERY="$query" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
14058
14064
  import os
14059
14065
  try:
14060
14066
  from memory.retrieval import MemoryRetrieval
@@ -14085,7 +14091,7 @@ except Exception as e:
14085
14091
  echo "Usage: loki memory episode <id>"
14086
14092
  exit 1
14087
14093
  fi
14088
- LOKI_ID="$id" python3 -c "
14094
+ LOKI_ID="$id" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
14089
14095
  import os
14090
14096
  try:
14091
14097
  from memory.engine import MemoryEngine
@@ -14109,7 +14115,7 @@ except Exception as e:
14109
14115
  local id="${2:-}"
14110
14116
  if [ -z "$id" ]; then
14111
14117
  # List all patterns
14112
- python3 -c "
14118
+ PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
14113
14119
  try:
14114
14120
  from memory.engine import MemoryEngine
14115
14121
  engine = MemoryEngine(base_path='.loki/memory')
@@ -14127,7 +14133,7 @@ except Exception as e:
14127
14133
  print(f'Error: {e}')
14128
14134
  " 2>/dev/null
14129
14135
  else
14130
- LOKI_ID="$id" python3 -c "
14136
+ LOKI_ID="$id" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
14131
14137
  import os
14132
14138
  try:
14133
14139
  from memory.engine import MemoryEngine
package/autonomy/run.sh CHANGED
@@ -9189,6 +9189,13 @@ build_prompt() {
9189
9189
  # Context Memory Instructions (integrated with new memory system)
9190
9190
  local memory_instruction="MEMORY SYSTEM: Relevant context from past sessions is provided below (if any). Your actions will be automatically recorded for future reference. For complex handoffs: create .loki/memory/handoffs/{timestamp}.md. For important decisions: they will be captured in the timeline. Check .loki/CONTINUITY.md for session-level working memory."
9191
9191
 
9192
+ # USAGE.md instruction (v7.6.0) -- always-on end-user handoff doc.
9193
+ # REGARDLESS of whether the PRD mentions it, the agent MUST write USAGE.md
9194
+ # at the project root before signaling completion. This becomes the
9195
+ # canonical "how do I run and verify this" artifact surfaced to the user
9196
+ # and to the dashboard/Purple Lab UI.
9197
+ local usage_doc_instruction="USAGE_DOC_REQUIRED: Before invoking loki_complete_task (or touching .loki/signals/COMPLETION_REQUESTED), write USAGE.md at the project root. Detect the stack from package.json/requirements.txt/Cargo.toml/go.mod/etc. and include these sections: (1) Prerequisites (runtimes, ports, env vars), (2) Install (exact command, e.g. 'npm install' or 'pip install -r requirements.txt'), (3) Start (exact command, e.g. 'npm start' or 'python server.py'), (4) Verify -- 2 to 3 copy-paste commands the user can run to confirm it works (curl examples for APIs with expected output, browser URL for web UIs, command invocation for CLIs), (5) Stop (Ctrl+C or 'lsof -ti:PORT | xargs kill -9' for backgrounded servers). Keep it under 100 lines, plain Markdown, no emojis. If USAGE.md already exists and is accurate, leave it; otherwise create or update it."
9198
+
9192
9199
  # Load existing context if resuming
9193
9200
  local context_injection=""
9194
9201
  if [ $retry -gt 0 ]; then
@@ -9518,15 +9525,15 @@ except Exception:
9518
9525
  else
9519
9526
  if [ $retry -eq 0 ]; then
9520
9527
  if [ -n "$prd" ]; then
9521
- echo "Loki Mode with PRD at $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
9528
+ echo "Loki Mode with PRD at $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
9522
9529
  else
9523
- echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
9530
+ echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $usage_doc_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
9524
9531
  fi
9525
9532
  else
9526
9533
  if [ -n "$prd" ]; then
9527
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
9534
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
9528
9535
  else
9529
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
9536
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $usage_doc_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
9530
9537
  fi
9531
9538
  fi
9532
9539
  fi
@@ -9567,6 +9574,7 @@ except Exception:
9567
9574
  else
9568
9575
  printf 'You are a coding assistant. Analyze this codebase and suggest improvements. Write working code and commit changes.\n'
9569
9576
  fi
9577
+ printf '%s\n' "$usage_doc_instruction"
9570
9578
  printf '</loki_system>\n'
9571
9579
  printf '[CACHE_BREAKPOINT]\n'
9572
9580
 
@@ -9597,6 +9605,7 @@ except Exception:
9597
9605
  printf '%s\n' "$sdlc_instruction"
9598
9606
  printf '%s\n' "$autonomous_suffix"
9599
9607
  printf '%s\n' "$memory_instruction"
9608
+ printf '%s\n' "$usage_doc_instruction"
9600
9609
  # For codebase-analysis mode (no PRD), analysis_instruction is part of the
9601
9610
  # static prefix so it remains cache-stable.
9602
9611
  if [ -z "$prd" ]; then
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.5.30"
10
+ __version__ = "7.6.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2688,6 +2688,167 @@ async def get_memory_timeline():
2688
2688
  return {"entries": episodes, "lastUpdated": None}
2689
2689
 
2690
2690
 
2691
+ # ---------------------------------------------------------------------------
2692
+ # Memory File Browser (v7.6.0) - generic drill-down into .loki/memory/
2693
+ # ---------------------------------------------------------------------------
2694
+ # Exposes raw episodic/learnings/ledgers/handoffs files plus root-level
2695
+ # notes (decisions.md, mistakes.md, patterns.md, investigation-*.md, etc.)
2696
+ # so the dashboard can let users click through what the agent has stored.
2697
+ #
2698
+ # Safety: type is a whitelisted enum; path is validated by resolving against
2699
+ # the memory directory and rejecting anything outside it (also rejects
2700
+ # absolute paths and ".." segments before resolution).
2701
+
2702
+ _MEMORY_FILE_TYPES = {
2703
+ "episodic": "episodic",
2704
+ "learnings": "learnings",
2705
+ "ledgers": "ledgers",
2706
+ "handoffs": "handoffs",
2707
+ "semantic": "semantic",
2708
+ "skills": "skills",
2709
+ "root": "", # files directly under .loki/memory/
2710
+ }
2711
+
2712
+ _MEMORY_FILE_MAX_BYTES = 2 * 1024 * 1024 # 2 MiB cap per file read
2713
+
2714
+
2715
+ def _safe_memory_path(rel_path: str) -> _Path:
2716
+ """Resolve rel_path under .loki/memory/ and reject traversal attempts.
2717
+
2718
+ Raises HTTPException(400) on bad input, HTTPException(403) on traversal.
2719
+ """
2720
+ if not rel_path or not isinstance(rel_path, str):
2721
+ raise HTTPException(status_code=400, detail="path required")
2722
+ # Reject NULs and absolute paths up front
2723
+ if "\x00" in rel_path or rel_path.startswith("/") or rel_path.startswith("\\"):
2724
+ raise HTTPException(status_code=400, detail="invalid path")
2725
+ # Reject explicit traversal segments before touching the filesystem
2726
+ parts = rel_path.replace("\\", "/").split("/")
2727
+ if any(p == ".." for p in parts):
2728
+ raise HTTPException(status_code=403, detail="path traversal blocked")
2729
+ memory_dir = _get_loki_dir() / "memory"
2730
+ try:
2731
+ real_memory = os.path.realpath(str(memory_dir))
2732
+ except Exception:
2733
+ raise HTTPException(status_code=500, detail="memory dir unavailable")
2734
+ candidate = memory_dir / rel_path
2735
+ try:
2736
+ resolved = os.path.realpath(str(candidate))
2737
+ except Exception:
2738
+ raise HTTPException(status_code=400, detail="invalid path")
2739
+ # Must live strictly under real_memory (not be it, not escape via symlink)
2740
+ if not (resolved == real_memory or resolved.startswith(real_memory + os.sep)):
2741
+ raise HTTPException(status_code=403, detail="path outside memory dir")
2742
+ return _Path(resolved)
2743
+
2744
+
2745
+ @app.get("/api/memory/files", dependencies=[Depends(auth.require_scope("read"))])
2746
+ async def list_memory_files(
2747
+ type: str = Query(default="root", description="One of: episodic, learnings, ledgers, handoffs, semantic, skills, root"),
2748
+ limit: int = Query(default=500, ge=1, le=2000),
2749
+ ):
2750
+ """List files under a memory subdirectory.
2751
+
2752
+ Returns: {type, dir, files: [{path, name, size, modified, kind}]}
2753
+ `path` is relative to .loki/memory/ and safe to pass back to /api/memory/file.
2754
+ """
2755
+ if type not in _MEMORY_FILE_TYPES:
2756
+ raise HTTPException(status_code=400, detail=f"unknown type; expected one of {sorted(_MEMORY_FILE_TYPES)}")
2757
+ sub = _MEMORY_FILE_TYPES[type]
2758
+ memory_dir = _get_loki_dir() / "memory"
2759
+ target_dir = memory_dir / sub if sub else memory_dir
2760
+ if not target_dir.exists():
2761
+ return {"type": type, "dir": str(target_dir), "files": []}
2762
+
2763
+ real_memory = os.path.realpath(str(memory_dir))
2764
+ entries: list[dict[str, Any]] = []
2765
+
2766
+ if type == "root":
2767
+ # Only files directly under .loki/memory/ (don't descend into subdirs)
2768
+ iterator = (p for p in target_dir.iterdir() if p.is_file())
2769
+ elif type == "episodic":
2770
+ # Episodic is organized by date subdirectory; walk one level.
2771
+ def _ep_iter():
2772
+ for child in target_dir.iterdir():
2773
+ if child.is_file():
2774
+ yield child
2775
+ elif child.is_dir():
2776
+ for f in child.iterdir():
2777
+ if f.is_file():
2778
+ yield f
2779
+ iterator = _ep_iter()
2780
+ else:
2781
+ # Flat directory listing (learnings, ledgers, handoffs, semantic, skills)
2782
+ iterator = (p for p in target_dir.rglob("*") if p.is_file())
2783
+
2784
+ for f in iterator:
2785
+ try:
2786
+ resolved = os.path.realpath(str(f))
2787
+ if not resolved.startswith(real_memory + os.sep):
2788
+ continue # skip symlinks escaping the memory dir
2789
+ rel = os.path.relpath(resolved, real_memory)
2790
+ st = f.stat()
2791
+ entries.append({
2792
+ "path": rel,
2793
+ "name": f.name,
2794
+ "size": st.st_size,
2795
+ "modified": st.st_mtime,
2796
+ "kind": f.suffix.lstrip(".").lower() or "txt",
2797
+ })
2798
+ except Exception:
2799
+ continue
2800
+ if len(entries) >= limit:
2801
+ break
2802
+
2803
+ # Newest first
2804
+ entries.sort(key=lambda e: e["modified"], reverse=True)
2805
+ return {"type": type, "dir": str(target_dir), "files": entries}
2806
+
2807
+
2808
+ @app.get("/api/memory/file", dependencies=[Depends(auth.require_scope("read"))])
2809
+ async def get_memory_file(
2810
+ path: str = Query(..., min_length=1, max_length=512, description="Path relative to .loki/memory/"),
2811
+ ):
2812
+ """Read a single file under .loki/memory/ with strict path-traversal guards.
2813
+
2814
+ Returns: {path, name, size, modified, kind, content, truncated}
2815
+ JSON files are returned with content as a string; the caller can JSON.parse.
2816
+ """
2817
+ target = _safe_memory_path(path)
2818
+ if not target.exists():
2819
+ raise HTTPException(status_code=404, detail="file not found")
2820
+ if not target.is_file():
2821
+ raise HTTPException(status_code=400, detail="not a file")
2822
+ try:
2823
+ st = target.stat()
2824
+ except Exception:
2825
+ raise HTTPException(status_code=500, detail="stat failed")
2826
+ truncated = False
2827
+ try:
2828
+ if st.st_size > _MEMORY_FILE_MAX_BYTES:
2829
+ with open(target, "rb") as fh:
2830
+ raw = fh.read(_MEMORY_FILE_MAX_BYTES)
2831
+ truncated = True
2832
+ else:
2833
+ with open(target, "rb") as fh:
2834
+ raw = fh.read()
2835
+ # Decode as UTF-8 with replacement so we never 500 on a stray byte.
2836
+ content = raw.decode("utf-8", errors="replace")
2837
+ except HTTPException:
2838
+ raise
2839
+ except Exception as e:
2840
+ raise HTTPException(status_code=500, detail=f"read failed: {e}")
2841
+ return {
2842
+ "path": path,
2843
+ "name": target.name,
2844
+ "size": st.st_size,
2845
+ "modified": st.st_mtime,
2846
+ "kind": target.suffix.lstrip(".").lower() or "txt",
2847
+ "content": content,
2848
+ "truncated": truncated,
2849
+ }
2850
+
2851
+
2691
2852
  # ---------------------------------------------------------------------------
2692
2853
  # Memory Search & Stats (v6.15.0) - SQLite FTS5 powered
2693
2854
  # ---------------------------------------------------------------------------
@@ -615,6 +615,89 @@
615
615
  <h3 style="font-family: 'DM Serif Display', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">Memory</h3>
616
616
  <loki-memory-browser id="memory-browser" tab="summary"></loki-memory-browser>
617
617
  </div>
618
+ <div>
619
+ <h3 style="font-family: 'DM Serif Display', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">Memory Files</h3>
620
+ <div id="memory-files-panel" style="background: var(--loki-bg-card, #1a1a1a); border: 1px solid var(--loki-border, #333); border-radius: 5px; padding: 12px;">
621
+ <div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px;" id="memory-files-tabs"></div>
622
+ <div style="display: grid; grid-template-columns: minmax(220px, 320px) 1fr; gap: 12px; min-height: 280px;">
623
+ <div id="memory-files-list" style="border: 1px solid var(--loki-border, #333); border-radius: 4px; padding: 8px; max-height: 480px; overflow-y: auto; font-size: 12px;">Loading...</div>
624
+ <div id="memory-files-viewer" style="border: 1px solid var(--loki-border, #333); border-radius: 4px; padding: 12px; max-height: 480px; overflow: auto; font-family: 'JetBrains Mono', monospace; font-size: 12px; white-space: pre-wrap; word-break: break-word; color: var(--loki-text-primary, #eee);">Select a file to view its contents.</div>
625
+ </div>
626
+ </div>
627
+ <script>
628
+ (function(){
629
+ var TYPES = [
630
+ {id: 'root', label: 'Notes'},
631
+ {id: 'episodic', label: 'Episodes'},
632
+ {id: 'learnings', label: 'Learnings'},
633
+ {id: 'ledgers', label: 'Ledgers'},
634
+ {id: 'handoffs', label: 'Handoffs'},
635
+ {id: 'semantic', label: 'Semantic'},
636
+ {id: 'skills', label: 'Skills'}
637
+ ];
638
+ var state = {type: 'root', files: [], selected: null};
639
+ var tabsEl = document.getElementById('memory-files-tabs');
640
+ var listEl = document.getElementById('memory-files-list');
641
+ var viewEl = document.getElementById('memory-files-viewer');
642
+ function esc(s){ var d = document.createElement('div'); d.textContent = String(s == null ? '' : s); return d.innerHTML; }
643
+ function fmtSize(n){ if (n < 1024) return n + ' B'; if (n < 1024*1024) return (n/1024).toFixed(1) + ' KB'; return (n/1024/1024).toFixed(2) + ' MB'; }
644
+ function fmtTime(ts){ try { return new Date(ts*1000).toLocaleString(); } catch(e){ return ''; } }
645
+ function renderTabs(){
646
+ tabsEl.innerHTML = TYPES.map(function(t){
647
+ var active = t.id === state.type;
648
+ return '<button data-type="' + t.id + '" style="padding: 5px 10px; font-size: 11px; border-radius: 4px; border: 1px solid ' + (active ? 'var(--loki-accent, #553de9)' : 'var(--loki-border, #333)') + '; background: ' + (active ? 'var(--loki-accent, #553de9)' : 'transparent') + '; color: ' + (active ? '#fff' : 'var(--loki-text-primary, #ccc)') + '; cursor: pointer;">' + esc(t.label) + '</button>';
649
+ }).join('');
650
+ Array.prototype.forEach.call(tabsEl.querySelectorAll('button'), function(b){
651
+ b.addEventListener('click', function(){ loadType(b.getAttribute('data-type')); });
652
+ });
653
+ }
654
+ function renderList(){
655
+ if (!state.files.length){ listEl.innerHTML = '<div style="color: var(--loki-text-muted, #888); padding: 8px;">No files in this section.</div>'; return; }
656
+ listEl.innerHTML = state.files.map(function(f){
657
+ var active = state.selected && state.selected.path === f.path;
658
+ return '<div data-path="' + esc(f.path) + '" style="padding: 6px 8px; border-radius: 3px; cursor: pointer; margin-bottom: 2px; background: ' + (active ? 'var(--loki-accent, #553de9)' : 'transparent') + '; color: ' + (active ? '#fff' : 'inherit') + ';" title="' + esc(f.path) + '"><div style="font-weight: 500;">' + esc(f.name) + '</div><div style="font-size: 10px; color: ' + (active ? 'rgba(255,255,255,0.8)' : 'var(--loki-text-muted, #888)') + ';">' + fmtSize(f.size) + ' -- ' + esc(fmtTime(f.modified)) + '</div></div>';
659
+ }).join('');
660
+ Array.prototype.forEach.call(listEl.querySelectorAll('div[data-path]'), function(d){
661
+ d.addEventListener('click', function(){ loadFile(d.getAttribute('data-path')); });
662
+ });
663
+ }
664
+ function renderViewer(file){
665
+ if (!file){ viewEl.textContent = 'Select a file to view its contents.'; return; }
666
+ var header = file.name + ' (' + fmtSize(file.size) + (file.truncated ? ', truncated' : '') + ')
667
+ ' + file.path + '
668
+
669
+ ';
670
+ var body = file.content || '';
671
+ if (file.kind === 'json'){
672
+ try { body = JSON.stringify(JSON.parse(body), null, 2); } catch(e){ /* leave raw */ }
673
+ }
674
+ viewEl.textContent = header + body;
675
+ }
676
+ function loadType(t){
677
+ state.type = t; state.selected = null;
678
+ renderTabs(); renderViewer(null);
679
+ listEl.innerHTML = 'Loading...';
680
+ fetch('/api/memory/files?type=' + encodeURIComponent(t) + '&limit=500')
681
+ .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
682
+ .then(function(j){ state.files = (j && j.files) || []; renderList(); })
683
+ .catch(function(e){ listEl.innerHTML = '<div style="color: var(--loki-red, #e74c3c); padding: 8px;">Failed: ' + esc(e.message) + '</div>'; });
684
+ }
685
+ function loadFile(p){
686
+ state.selected = state.files.filter(function(f){ return f.path === p; })[0] || {path: p};
687
+ renderList();
688
+ viewEl.textContent = 'Loading ' + p + '...';
689
+ fetch('/api/memory/file?path=' + encodeURIComponent(p))
690
+ .then(function(r){ if (!r.ok) return r.json().then(function(j){ throw new Error(j.detail || ('HTTP ' + r.status)); }); return r.json(); })
691
+ .then(function(j){ renderViewer(j); })
692
+ .catch(function(e){ viewEl.textContent = 'Failed: ' + e.message; });
693
+ }
694
+ // Initialize when Insights becomes visible (or immediately if already visible)
695
+ function init(){ renderTabs(); loadType('root'); }
696
+ if (document.getElementById('page-insights')){ init(); }
697
+ else { document.addEventListener('DOMContentLoaded', init); }
698
+ })();
699
+ </script>
700
+ </div>
618
701
  <div>
619
702
  <h3 style="font-family: 'DM Serif Display', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">Learning Metrics</h3>
620
703
  <loki-learning-dashboard id="learning-dashboard" time-range="7d"></loki-learning-dashboard>
@@ -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.5.30
5
+ **Version:** v7.6.0
6
6
 
7
7
  ---
8
8
 
@@ -451,7 +451,7 @@ Subcommands:
451
451
 
452
452
  This command is invoked by autonomy/run.sh between iterations. Users
453
453
  should not run it directly -- run \`loki start\` instead.
454
- `,iK;var T7=L(()=>{y();_1();iK=o1});y();import{readFileSync as E7}from"fs";import{resolve as x7,dirname as F7}from"path";import{fileURLToPath as w7}from"url";var o=null;function i1(){if(o!==null)return o;let K="7.5.30";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=F7(w7(import.meta.url)),z=S1($);o=E7(x7(z,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}function e1(){return process.stdout.write(`Loki Mode v${i1()}
454
+ `,iK;var T7=L(()=>{y();_1();iK=o1});y();import{readFileSync as E7}from"fs";import{resolve as x7,dirname as F7}from"path";import{fileURLToPath as w7}from"url";var o=null;function i1(){if(o!==null)return o;let K="7.6.0";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=F7(w7(import.meta.url)),z=S1($);o=E7(x7(z,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}function e1(){return process.stdout.write(`Loki Mode v${i1()}
455
455
  `),0}p();n();y();import{readFileSync as C7,existsSync as h7}from"fs";import{resolve as b7}from"path";var y7=["claude","codex","cline","aider"];function $0(){let K=b7(P(),"state","provider");if(!h7(K))return"";try{return C7(K,"utf-8").trim()}catch{return""}}function v7(K,$){return K||$||process.env.LOKI_PROVIDER||"claude"}function g7(K){let $=$0(),z=v7(K,$);switch(process.stdout.write(`${k}Current Provider${W}
456
456
  `),process.stdout.write(`
457
457
  `),process.stdout.write(`${O}Provider:${W} ${z}
@@ -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(A7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var $6=await K6(Bun.argv.slice(2));process.exit($6);
536
536
 
537
- //# debugId=7C8104E1C877694564756E2164756E21
537
+ //# debugId=1FE99E821A4C6C7B64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.5.30'
60
+ __version__ = '7.6.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "7.5.30",
3
+ "version": "7.6.0",
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",
package/web-app/server.py CHANGED
@@ -7720,36 +7720,15 @@ async def get_audit_log() -> JSONResponse:
7720
7720
 
7721
7721
  # ---------------------------------------------------------------------------
7722
7722
  # Static file serving (built React app)
7723
+ # IMPORTANT: serve_spa was previously defined here at line 7725, BEFORE the
7724
+ # /api/magic, /api/deploy, /api/sessions/.../github/actions, and
7725
+ # /api/sessions/.../docs route registrations. FastAPI registers routes in
7726
+ # definition order, so the catch-all '/{full_path:path}' silently swallowed
7727
+ # those 22 endpoints (they returned index.html instead of JSON). Moved to
7728
+ # the very end of the file just before standalone_app in v7.6.0 so all
7729
+ # specific API routes register first.
7723
7730
  # ---------------------------------------------------------------------------
7724
7731
 
7725
- @app.get("/{full_path:path}")
7726
- async def serve_spa(full_path: str) -> FileResponse:
7727
- """Serve the React SPA and static assets from dist/.
7728
-
7729
- The Vite build is configured with base: '/lab/' (Phase Merge-3), so the
7730
- bundled HTML references assets at '/lab/assets/...'. Under the canonical
7731
- standalone_app (and dashboard's Merge-4 mount), Starlette's Mount strips
7732
- '/lab' from the scope path before dispatch, so this handler usually sees
7733
- 'assets/...' directly and the strip below is a no-op. The strip is
7734
- retained as defense-in-depth for direct-`app` invocations (e.g. tests or
7735
- operators running `uvicorn server:app` instead of `server:standalone_app`).
7736
- """
7737
- index = DIST_DIR / "index.html"
7738
- if not index.exists():
7739
- return JSONResponse(
7740
- status_code=503,
7741
- content={"error": "Web app not built. Run: cd web-app && npm run build"},
7742
- )
7743
- # Resolve to the dist directory, tolerating the /lab/ base in standalone mode.
7744
- relative = full_path[4:] if full_path.startswith("lab/") else full_path
7745
- requested = DIST_DIR / relative
7746
- if relative and requested.is_file() and str(requested.resolve()).startswith(str(DIST_DIR.resolve())):
7747
- import mimetypes
7748
- content_type = mimetypes.guess_type(str(requested))[0] or "application/octet-stream"
7749
- return FileResponse(str(requested), media_type=content_type)
7750
- # SPA fallback: return index.html for all non-file routes
7751
- return FileResponse(str(index))
7752
-
7753
7732
 
7754
7733
  # ---------------------------------------------------------------------------
7755
7734
  # Magic Modules API (/api/magic/*)
@@ -8649,6 +8628,45 @@ async def docs_get_file(session_id: str, filename: str) -> Response:
8649
8628
  return Response(content=content, media_type="text/markdown")
8650
8629
 
8651
8630
 
8631
+ # ---------------------------------------------------------------------------
8632
+ # SPA catch-all (MUST be last @app route -- see comment at the old serve_spa
8633
+ # location higher in this file). Any path that isn't matched by a specific
8634
+ # @app.get/post/... above falls through here and serves the React bundle.
8635
+ # v7.6.0 bug fix: previously this was at line 7725 and silently swallowed
8636
+ # 22 downstream API routes (/api/magic/*, /api/deploy/*, /api/sessions/.../
8637
+ # github/actions/*, /api/sessions/.../docs/*) which returned text/html
8638
+ # instead of JSON. Real-user test (Playwright on /lab/magic) surfaced it.
8639
+ # ---------------------------------------------------------------------------
8640
+
8641
+ @app.get("/{full_path:path}")
8642
+ async def serve_spa(full_path: str) -> FileResponse:
8643
+ """Serve the React SPA and static assets from dist/.
8644
+
8645
+ The Vite build is configured with base: '/lab/' (Phase Merge-3), so the
8646
+ bundled HTML references assets at '/lab/assets/...'. Under the canonical
8647
+ standalone_app (and dashboard's Merge-4 mount), Starlette's Mount strips
8648
+ '/lab' from the scope path before dispatch, so this handler usually sees
8649
+ 'assets/...' directly and the strip below is a no-op. The strip is
8650
+ retained as defense-in-depth for direct-`app` invocations (e.g. tests or
8651
+ operators running `uvicorn server:app` instead of `server:standalone_app`).
8652
+ """
8653
+ index = DIST_DIR / "index.html"
8654
+ if not index.exists():
8655
+ return JSONResponse(
8656
+ status_code=503,
8657
+ content={"error": "Web app not built. Run: cd web-app && npm run build"},
8658
+ )
8659
+ # Resolve to the dist directory, tolerating the /lab/ base in standalone mode.
8660
+ relative = full_path[4:] if full_path.startswith("lab/") else full_path
8661
+ requested = DIST_DIR / relative
8662
+ if relative and requested.is_file() and str(requested.resolve()).startswith(str(DIST_DIR.resolve())):
8663
+ import mimetypes
8664
+ content_type = mimetypes.guess_type(str(requested))[0] or "application/octet-stream"
8665
+ return FileResponse(str(requested), media_type=content_type)
8666
+ # SPA fallback: return index.html for all non-file routes
8667
+ return FileResponse(str(index))
8668
+
8669
+
8652
8670
  # ---------------------------------------------------------------------------
8653
8671
  # Standalone wrapper (Phase Merge-3): mounts the FastAPI `app` under '/lab/'
8654
8672
  # so the rebased Vite bundle (with base: '/lab/') routes correctly through