loki-mode 7.7.22 → 7.7.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/SKILL.md +4 -4
- package/VERSION +1 -1
- package/autonomy/loki +24 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +51 -4
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +6 -5
- package/memory/ingest.py +30 -9
- package/memory/knowledge_graph.py +40 -16
- package/package.json +3 -2
- package/tools/bench_cross_project_lift.py +218 -0
- package/tools/bench_memory_retrieval.py +157 -0
- package/tools/index-codebase.py +474 -0
- package/tools/probe-model-catalog.py +159 -0
- package/tools/regen-state-machine-refs.py +188 -0
package/README.md
CHANGED
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
|
|
25
25
|
## Why Loki Mode?
|
|
26
26
|
|
|
27
|
-
- **
|
|
27
|
+
- **Spec to product, autonomously** -- Describe what you want, walk away, come back to working code with tests. Loki runs the full RARV-C closure loop (Reason - Act - Reflect - Verify - Close) until the work is actually done, not just attempted.
|
|
28
28
|
- **Production quality built in** -- 11 quality gates (`skills/quality-gates.md`), blind 3-reviewer code review (`run.sh:run_code_review()`), anti-sycophancy checks
|
|
29
|
+
- **Cross-project memory** -- Episodic/semantic/procedural memory with vector search; knowledge learned on one project surfaces on the next (v5.15.0+, see `memory/engine.py`)
|
|
29
30
|
- **Self-hosted and private** -- Your keys, your infrastructure, no data leaves your network
|
|
30
|
-
- **4 active AI providers** -- Claude, Codex, Cline, Aider with automatic failover (`loki-ts/src/runner/providers.ts`). Gemini CLI deprecated v7.5.18; Antigravity CLI coming soon.
|
|
31
31
|
- **Legacy system healing** -- `loki heal` archaeology/stabilize/isolate/modernize/validate phases (v6.67.0, see `skills/healing.md`)
|
|
32
|
-
- **Memory system** -- Episodic/semantic/procedural with vector search (v5.15.0, see `memory/engine.py`)
|
|
33
32
|
- **MCP server** -- 15 tools including ChromaDB code search (`mcp/server.py`)
|
|
34
33
|
- **Full-stack output** -- Source code, tests, Docker configs, CI/CD pipelines, audit logs
|
|
35
|
-
- **
|
|
34
|
+
- **Provider-agnostic** -- runs on Claude, Codex, Cline, or Aider with automatic failover (`loki-ts/src/runner/providers.ts`); no vendor lock-in. Gemini CLI deprecated v7.5.18; Antigravity CLI coming soon.
|
|
35
|
+
- **Open source** -- Free for personal, internal, and academic use.
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
@@ -302,7 +302,9 @@ Loki Mode is the only platform that is fully self-hosted, open source, and inclu
|
|
|
302
302
|
|
|
303
303
|
---
|
|
304
304
|
|
|
305
|
-
##
|
|
305
|
+
## Provider-Agnostic Runtime
|
|
306
|
+
|
|
307
|
+
Loki's autonomy and quality loop are the product; the underlying coding CLI is swappable. Loki runs on any of the providers below so you are never locked to one vendor.
|
|
306
308
|
|
|
307
309
|
| Provider | Status | Autonomous Flag | Parallel Agents | Install |
|
|
308
310
|
|----------|--------|:-:|:-:|---------|
|
package/SKILL.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loki-mode
|
|
3
|
-
description:
|
|
3
|
+
description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.7.
|
|
6
|
+
# Loki Mode v7.7.25
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
10
10
|
**Spec in, product out.** A "spec" is whatever describes the work: a Markdown PRD, a GitHub issue, an OpenAPI doc, a Jira ticket -- a PRD is one form of spec.
|
|
11
11
|
|
|
12
|
-
**
|
|
12
|
+
**Provider-agnostic (stable since v5.0.0):** runs on Claude/Codex/Cline/Aider with abstract model tiers and degraded mode for non-Claude providers; no vendor lock-in. Gemini deprecated v7.5.18. See `skills/providers.md`. **Current track (v7.7.x):** LSP grounding as first-class agent tool (v7.7.0-v7.7.9; lsp_get_diagnostics actually-returns-diagnostics regression fix v7.7.14), provider_source cli (v7.7.11-v7.7.12 bash/bun parity), Docker/bash-3.2 robustness (v7.7.13), audit chain cross-file verification fix (v7.7.15), Phase 1 RARV-C closure (real provider judges, gate-failure flock, synthetic PRD e2e, status `--json`).
|
|
13
13
|
|
|
14
14
|
**Runtime migration:** Bash-to-Bun migration. Read-only commands (`version`, `status`, `stats`, `doctor`, `provider show/list`, `memory list/index`) flow through Bun runtime via `bin/loki` since v7.3.0. Every other command remains on the Bash runtime (`autonomy/loki`). Rollback: `LOKI_LEGACY_BASH=1`. See `UPGRADING.md` and `docs/architecture/ADR-001-runtime-migration.md`.
|
|
15
15
|
|
|
@@ -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.7.
|
|
384
|
+
**v7.7.25 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.7.
|
|
1
|
+
7.7.25
|
package/autonomy/loki
CHANGED
|
@@ -299,6 +299,30 @@ load_memory_context() {
|
|
|
299
299
|
return 0
|
|
300
300
|
fi
|
|
301
301
|
|
|
302
|
+
# v7.7.23 privacy opt-out (excellence bar 6): honor a per-project
|
|
303
|
+
# .loki/config.json {"memory": {"disabled": true}} flag. Lets a user
|
|
304
|
+
# disable ALL memory capture/retrieval for a sensitive project without
|
|
305
|
+
# setting an env var on every invocation.
|
|
306
|
+
if [ -f "$LOKI_DIR/config.json" ]; then
|
|
307
|
+
# v7.7.23 council fix (Opus 2): FAIL-CLOSED. A config.json that
|
|
308
|
+
# exists but cannot be parsed prints 'true' (suppress memory) so
|
|
309
|
+
# a JSON typo on a sensitive project does not silently re-enable
|
|
310
|
+
# retrieval. Only the no-config case (outer -f guard false)
|
|
311
|
+
# proceeds with memory on.
|
|
312
|
+
local _mem_disabled
|
|
313
|
+
_mem_disabled=$(python3 -c "
|
|
314
|
+
import json, sys
|
|
315
|
+
try:
|
|
316
|
+
d = json.load(open('$LOKI_DIR/config.json'))
|
|
317
|
+
print('true' if d.get('memory', {}).get('disabled') is True else 'false')
|
|
318
|
+
except Exception:
|
|
319
|
+
print('true') # malformed config -> fail closed (suppress memory)
|
|
320
|
+
" 2>/dev/null || echo "true")
|
|
321
|
+
if [ "$_mem_disabled" = "true" ]; then
|
|
322
|
+
return 0
|
|
323
|
+
fi
|
|
324
|
+
fi
|
|
325
|
+
|
|
302
326
|
# Check if python3 is available (required for memory system)
|
|
303
327
|
if ! command -v python3 &> /dev/null; then
|
|
304
328
|
echo -e "${YELLOW}Warning: python3 not found - memory context loading disabled${NC}" >&2
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -2751,14 +2751,61 @@ async def get_token_economics():
|
|
|
2751
2751
|
|
|
2752
2752
|
@app.post("/api/memory/consolidate", dependencies=[Depends(auth.require_scope("control"))])
|
|
2753
2753
|
async def consolidate_memory(hours: int = 24):
|
|
2754
|
-
"""
|
|
2755
|
-
|
|
2754
|
+
"""Run the real episodic-to-semantic consolidation pipeline."""
|
|
2755
|
+
memory_dir = _get_loki_dir() / "memory"
|
|
2756
|
+
try:
|
|
2757
|
+
import sys as _sys
|
|
2758
|
+
project_root = str(_Path(__file__).resolve().parent.parent)
|
|
2759
|
+
if project_root not in _sys.path:
|
|
2760
|
+
_sys.path.insert(0, project_root)
|
|
2761
|
+
from memory.storage import MemoryStorage
|
|
2762
|
+
from memory.consolidation import ConsolidationPipeline
|
|
2763
|
+
storage = MemoryStorage(str(memory_dir))
|
|
2764
|
+
pipeline = ConsolidationPipeline(storage=storage, base_path=str(memory_dir))
|
|
2765
|
+
result = pipeline.consolidate(since_hours=hours)
|
|
2766
|
+
d = result.to_dict()
|
|
2767
|
+
return {
|
|
2768
|
+
"status": "ok",
|
|
2769
|
+
"message": f"Consolidated episodes from the last {hours}h",
|
|
2770
|
+
"consolidated": d.get("patterns_created", 0) + d.get("patterns_merged", 0),
|
|
2771
|
+
"patternsCreated": d.get("patterns_created", 0),
|
|
2772
|
+
"patternsMerged": d.get("patterns_merged", 0),
|
|
2773
|
+
"antiPatternsCreated": d.get("anti_patterns_created", 0),
|
|
2774
|
+
"episodesProcessed": d.get("episodes_processed", 0),
|
|
2775
|
+
"durationSeconds": round(d.get("duration_seconds", 0.0), 3),
|
|
2776
|
+
}
|
|
2777
|
+
except Exception as e:
|
|
2778
|
+
raise HTTPException(status_code=503, detail=f"Consolidation unavailable: {e}")
|
|
2756
2779
|
|
|
2757
2780
|
|
|
2758
2781
|
@app.post("/api/memory/retrieve", dependencies=[Depends(auth.require_scope("control"))])
|
|
2759
2782
|
async def retrieve_memory(query: dict = None):
|
|
2760
|
-
"""
|
|
2761
|
-
|
|
2783
|
+
"""Task-aware retrieval against the real memory engine.
|
|
2784
|
+
|
|
2785
|
+
Body: {"goal": str, "phase"?: str, "task_type"?: str, "top_k"?: int}.
|
|
2786
|
+
"""
|
|
2787
|
+
query = query or {}
|
|
2788
|
+
goal = (query.get("goal") or query.get("q") or "").strip()
|
|
2789
|
+
if not goal:
|
|
2790
|
+
return {"results": [], "query": query, "message": "provide a 'goal' to retrieve against"}
|
|
2791
|
+
top_k = int(query.get("top_k", 5))
|
|
2792
|
+
top_k = max(1, min(top_k, 50))
|
|
2793
|
+
memory_dir = _get_loki_dir() / "memory"
|
|
2794
|
+
try:
|
|
2795
|
+
import sys as _sys
|
|
2796
|
+
project_root = str(_Path(__file__).resolve().parent.parent)
|
|
2797
|
+
if project_root not in _sys.path:
|
|
2798
|
+
_sys.path.insert(0, project_root)
|
|
2799
|
+
from memory.storage import MemoryStorage
|
|
2800
|
+
from memory.retrieval import MemoryRetrieval
|
|
2801
|
+
retriever = MemoryRetrieval(MemoryStorage(str(memory_dir)))
|
|
2802
|
+
context = {"goal": goal, "phase": query.get("phase", "development")}
|
|
2803
|
+
if query.get("task_type"):
|
|
2804
|
+
context["task_type"] = query["task_type"]
|
|
2805
|
+
results = retriever.retrieve_task_aware(context, top_k=top_k, token_budget=query.get("token_budget"))
|
|
2806
|
+
return {"results": results, "query": {"goal": goal, "top_k": top_k}, "count": len(results)}
|
|
2807
|
+
except Exception as e:
|
|
2808
|
+
raise HTTPException(status_code=503, detail=f"Retrieval unavailable: {e}")
|
|
2762
2809
|
|
|
2763
2810
|
|
|
2764
2811
|
@app.get("/api/memory/index")
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.7.
|
|
5
|
+
**Version:** v7.7.25
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -32,7 +32,7 @@ setting any flag to `0`.
|
|
|
32
32
|
|
|
33
33
|
### Earlier highlights still in scope
|
|
34
34
|
- Bash-to-Bun runtime migration in progress (see `UPGRADING.md`)
|
|
35
|
-
-
|
|
35
|
+
- Provider-agnostic runtime: Claude (full), Codex, Cline, Aider (no vendor lock-in)
|
|
36
36
|
- Memory system (episodic / semantic / procedural)
|
|
37
37
|
- ChromaDB semantic code search via MCP
|
|
38
38
|
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -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 v=(K,$)=>{for(var Q in $)_7(K,Q,{get:$[Q],enumerable:!0,configurable:!0,set:P7.bind($,Q)})};var R=(K,$)=>()=>(K&&($=K(K=0)),$);var t=import.meta.require;var e1={};v(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>p});import{resolve as u,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(u(K,"VERSION"))&&J1(u(K,"autonomy/run.sh")))return K;let Q=S1(K);if(Q===K)break;K=Q}return u(i1,"..","..","..")}function N1(K){let $=K;for(let Q=0;Q<6;Q++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let X=S1($);if(X===$)break;$=X}return u(K,"..","..","..")}function P(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var g=R(()=>{i1=S1(L7(import.meta.url));p=E7()});import{readFileSync as F7}from"fs";import{resolve as w7,dirname as x7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.7.
|
|
2
|
+
var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var v=(K,$)=>{for(var Q in $)_7(K,Q,{get:$[Q],enumerable:!0,configurable:!0,set:P7.bind($,Q)})};var R=(K,$)=>()=>(K&&($=K(K=0)),$);var t=import.meta.require;var e1={};v(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>p});import{resolve as u,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(u(K,"VERSION"))&&J1(u(K,"autonomy/run.sh")))return K;let Q=S1(K);if(Q===K)break;K=Q}return u(i1,"..","..","..")}function N1(K){let $=K;for(let Q=0;Q<6;Q++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let X=S1($);if(X===$)break;$=X}return u(K,"..","..","..")}function P(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var g=R(()=>{i1=S1(L7(import.meta.url));p=E7()});import{readFileSync as F7}from"fs";import{resolve as w7,dirname as x7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.7.25";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=x7(S7(import.meta.url)),Q=N1($);o=F7(w7(Q,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var D1=R(()=>{g()});var $0={};v($0,{runOrThrow:()=>N7,run:()=>k,commandVersion:()=>D7,commandExists:()=>h,ShellError:()=>C1});async function k(K,$={}){let Q=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),X,Z;if($.timeoutMs&&$.timeoutMs>0)X=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[W,z,q]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text(),Q.exited]);return{stdout:W,stderr:z,exitCode:q}}finally{if(X)clearTimeout(X);if(Z)clearTimeout(Z)}}async function N7(K,$={}){let Q=await k(K,$);if(Q.exitCode!==0)throw new C1(`command failed (${Q.exitCode}): ${K.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function h(K){let $=k7(K),Q=await k(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.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 X=await k([K,$],{timeoutMs:5000});if(X.exitCode!==0)return null;return((X.stdout||X.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var C1;var n=R(()=>{C1=class C1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,Q,X){super(K);this.message=K;this.exitCode=$;this.stdout=Q;this.stderr=X;this.name="ShellError"}}});function c(K){return C7?"":K}var C7,E,b,F,T6,O,D,w,H;var a=R(()=>{C7=(process.env.NO_COLOR??"").length>0;E=c("\x1B[0;31m"),b=c("\x1B[0;32m"),F=c("\x1B[1;33m"),T6=c("\x1B[0;34m"),O=c("\x1B[0;36m"),D=c("\x1B[1m"),w=c("\x1B[2m"),H=c("\x1B[0m")});import{existsSync as c7}from"fs";async function i(){if(X1!==void 0)return X1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return X1=K,K;let $=await h("python3.12");if($)return X1=$,$;let Q=await h("python3");return X1=Q,Q}async function s(K,$={}){let Q=await i();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Q,"-c",K],$)}var X1;var Z1=R(()=>{n()});var G0={};v(G0,{runStatus:()=>Q5});import{existsSync as N,readFileSync as W1,readdirSync as W0,statSync as H0}from"fs";import{resolve as x,basename as a7}from"path";async function r7(){if(await h("jq"))return!0;return process.stdout.write(`${E}Error: jq is required but not installed.${H}
|
|
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)
|
|
@@ -585,4 +585,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
585
585
|
`),2}default:return process.stderr.write(`Unknown command: ${$}
|
|
586
586
|
`),process.stderr.write(j7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var X6=await Q6(Bun.argv.slice(2));process.exit(X6);
|
|
587
587
|
|
|
588
|
-
//# debugId=
|
|
588
|
+
//# debugId=3DA3905BB400BADE64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -1010,17 +1010,18 @@ async def loki_memory_capture_session_summary(
|
|
|
1010
1010
|
try:
|
|
1011
1011
|
from memory.ingest import ingest_from_summary, _capture_disabled
|
|
1012
1012
|
|
|
1013
|
-
|
|
1013
|
+
base_path = safe_path_join('.loki', 'memory')
|
|
1014
|
+
# v7.7.23: pass base_path so the .loki/config.json memory.disabled
|
|
1015
|
+
# opt-out is honored in addition to the env escape hatch.
|
|
1016
|
+
if _capture_disabled(base_path):
|
|
1014
1017
|
_emit_tool_event_async(
|
|
1015
1018
|
'loki_memory_capture_session_summary', 'complete',
|
|
1016
|
-
result_status='skipped', error='disabled via env'
|
|
1019
|
+
result_status='skipped', error='disabled via env or config'
|
|
1017
1020
|
)
|
|
1018
1021
|
return json.dumps({
|
|
1019
|
-
"error": "memory capture disabled
|
|
1022
|
+
"error": "memory capture disabled (LOKI_MEMORY_CAPTURE_DISABLED or .loki/config.json memory.disabled)",
|
|
1020
1023
|
"disabled": True,
|
|
1021
1024
|
})
|
|
1022
|
-
|
|
1023
|
-
base_path = safe_path_join('.loki', 'memory')
|
|
1024
1025
|
path = ingest_from_summary(
|
|
1025
1026
|
base_path,
|
|
1026
1027
|
goal=goal,
|
package/memory/ingest.py
CHANGED
|
@@ -125,13 +125,34 @@ def _scrub_path(path: str) -> str:
|
|
|
125
125
|
return path
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
def _capture_disabled() -> bool:
|
|
129
|
-
"""
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
128
|
+
def _capture_disabled(memory_base: Optional[str] = None) -> bool:
|
|
129
|
+
"""True when capture should be skipped.
|
|
130
|
+
|
|
131
|
+
Honors the `LOKI_MEMORY_CAPTURE_DISABLED=true` env escape hatch AND
|
|
132
|
+
(v7.7.23 privacy opt-out, excellence bar 6) a per-project
|
|
133
|
+
`.loki/config.json` `{"memory": {"disabled": true}}` flag. When
|
|
134
|
+
`memory_base` is provided (e.g. `<root>/.loki/memory`), the config
|
|
135
|
+
is resolved as its sibling `<root>/.loki/config.json`.
|
|
136
|
+
"""
|
|
137
|
+
if os.environ.get("LOKI_MEMORY_CAPTURE_DISABLED", "").lower() in ("true", "1", "yes"):
|
|
138
|
+
return True
|
|
139
|
+
if memory_base:
|
|
140
|
+
# v7.7.23 council fix (Opus 2): FAIL-CLOSED. If a config.json
|
|
141
|
+
# EXISTS but cannot be parsed, the user's intent is ambiguous and
|
|
142
|
+
# the safe privacy default is to SUPPRESS capture (leaked data is
|
|
143
|
+
# irreversible; lost memory is recoverable). Only the no-config
|
|
144
|
+
# case fails open (capture proceeds = default behavior).
|
|
145
|
+
config_path = Path(memory_base).parent / "config.json"
|
|
146
|
+
if config_path.is_file():
|
|
147
|
+
try:
|
|
148
|
+
with open(config_path) as f:
|
|
149
|
+
cfg = json.load(f)
|
|
150
|
+
except Exception:
|
|
151
|
+
# Config present but unreadable/malformed -> fail closed.
|
|
152
|
+
return True
|
|
153
|
+
if isinstance(cfg, dict) and cfg.get("memory", {}).get("disabled") is True:
|
|
154
|
+
return True
|
|
155
|
+
return False
|
|
135
156
|
|
|
136
157
|
|
|
137
158
|
def _log_to_errors(memory_base: str, function_name: str, exc: BaseException) -> None:
|
|
@@ -301,7 +322,7 @@ def ingest_from_claude_transcript(
|
|
|
301
322
|
Path to the written episode JSON on success; None on failure
|
|
302
323
|
(silent fail, error logged to `.errors.log`).
|
|
303
324
|
"""
|
|
304
|
-
if _capture_disabled():
|
|
325
|
+
if _capture_disabled(memory_base):
|
|
305
326
|
return None
|
|
306
327
|
try:
|
|
307
328
|
path = Path(transcript_path)
|
|
@@ -409,7 +430,7 @@ def ingest_from_summary(
|
|
|
409
430
|
|
|
410
431
|
Returns episode path on success, None on failure.
|
|
411
432
|
"""
|
|
412
|
-
if _capture_disabled():
|
|
433
|
+
if _capture_disabled(memory_base):
|
|
413
434
|
return None
|
|
414
435
|
try:
|
|
415
436
|
from memory.engine import MemoryEngine, create_storage
|
|
@@ -167,30 +167,54 @@ class OrganizationKnowledgeGraph:
|
|
|
167
167
|
self._graph = json.load(f)
|
|
168
168
|
return self._graph
|
|
169
169
|
|
|
170
|
+
_STOPWORDS = {
|
|
171
|
+
'the', 'a', 'an', 'to', 'for', 'of', 'and', 'or', 'with', 'without',
|
|
172
|
+
'is', 'are', 'be', 'up', 'on', 'in', 'by', 'not', 'make', 'choose',
|
|
173
|
+
'store', 'how', 'do', 'i', 'we', 'my', 'our', 'this', 'that',
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def _tokenize(cls, text):
|
|
178
|
+
"""Lowercase, split on non-alphanumerics, drop stopwords + 1-2 char tokens."""
|
|
179
|
+
import re
|
|
180
|
+
toks = re.split(r'[^a-z0-9]+', str(text or '').lower())
|
|
181
|
+
return {t for t in toks if len(t) > 2 and t not in cls._STOPWORDS}
|
|
182
|
+
|
|
170
183
|
def query_patterns(self, query, max_results=10):
|
|
171
|
-
"""
|
|
184
|
+
"""Keyword search across stored patterns.
|
|
185
|
+
|
|
186
|
+
Scores by token overlap between the query and each pattern's
|
|
187
|
+
name/pattern/description/category fields. Token overlap (not
|
|
188
|
+
whole-string substring) lets natural-language goals like "make
|
|
189
|
+
the charge endpoint safe to retry" match a pattern named
|
|
190
|
+
"idempotency-key-on-charge". A full-string substring hit still
|
|
191
|
+
gets a bonus, preserving prior behavior for exact queries.
|
|
172
192
|
|
|
173
|
-
Searches
|
|
174
|
-
|
|
175
|
-
schema.
|
|
193
|
+
Searches 'name', 'pattern', 'description', and 'category' for
|
|
194
|
+
compatibility with both simple dicts and the SemanticPattern schema.
|
|
176
195
|
"""
|
|
177
196
|
patterns = self.load_patterns(limit=1000)
|
|
178
197
|
query_lower = query.lower()
|
|
198
|
+
query_tokens = self._tokenize(query)
|
|
179
199
|
scored = []
|
|
200
|
+
# Per-field weight applied to each overlapping token.
|
|
201
|
+
fields = (('name', 3), ('pattern', 3), ('category', 2), ('description', 1))
|
|
180
202
|
for p in patterns:
|
|
181
203
|
score = 0
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
204
|
+
for field, weight in fields:
|
|
205
|
+
value = p.get(field, '')
|
|
206
|
+
if not value:
|
|
207
|
+
continue
|
|
208
|
+
# Coerce non-string field values (a hand-edited or
|
|
209
|
+
# future-schema JSONL row could store a list/int) so we
|
|
210
|
+
# never crash on .lower() in the live retrieval path.
|
|
211
|
+
value_lower = str(value).lower()
|
|
212
|
+
# Whole-query substring bonus (preserves exact-match behavior).
|
|
213
|
+
if query_lower and query_lower in value_lower:
|
|
214
|
+
score += weight
|
|
215
|
+
# Token-overlap scoring (enables NL-goal retrieval).
|
|
216
|
+
overlap = query_tokens & self._tokenize(value)
|
|
217
|
+
score += weight * len(overlap)
|
|
194
218
|
if score > 0:
|
|
195
219
|
scored.append((score, p))
|
|
196
220
|
scored.sort(key=lambda x: -x[0])
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.7.
|
|
4
|
-
"description": "Loki Mode by Autonomi.
|
|
3
|
+
"version": "7.7.25",
|
|
4
|
+
"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 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
7
7
|
"agent-orchestration",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"files": [
|
|
65
65
|
"SKILL.md",
|
|
66
66
|
"VERSION",
|
|
67
|
+
"tools/",
|
|
67
68
|
"autonomy/",
|
|
68
69
|
"providers/",
|
|
69
70
|
"agents/",
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""v7.7.24: cross-project knowledge "lift" report (the memory moat proof).
|
|
3
|
+
|
|
4
|
+
WHAT THIS MEASURES (honestly):
|
|
5
|
+
Loki's moat claim is that knowledge learned on one project helps a
|
|
6
|
+
DIFFERENT project. The transfer mechanism is real and already in the
|
|
7
|
+
codebase: each project's semantic patterns (.loki/memory/semantic/)
|
|
8
|
+
are extracted into an org-wide knowledge graph
|
|
9
|
+
(memory/knowledge_graph.py -> ~/.loki/knowledge/patterns.jsonl), and
|
|
10
|
+
any other project can query that graph (query_patterns).
|
|
11
|
+
|
|
12
|
+
"Lift" here is a RETRIEVAL-COVERAGE metric, not a task-success metric.
|
|
13
|
+
For a target project's set of task goals we count how many RELEVANT
|
|
14
|
+
patterns are retrievable in two conditions:
|
|
15
|
+
baseline: only the target project's own patterns are in the graph
|
|
16
|
+
cross: the target's patterns PLUS sibling projects' patterns
|
|
17
|
+
Lift = (relevant retrieved in cross) - (relevant retrieved in baseline),
|
|
18
|
+
and net-new = relevant patterns that ONLY the sibling projects could
|
|
19
|
+
supply (the target could never have surfaced them alone).
|
|
20
|
+
|
|
21
|
+
WHAT THIS DOES NOT CLAIM:
|
|
22
|
+
- It does NOT claim downstream task success / fewer iterations / lower
|
|
23
|
+
cost. That requires running real LLM tasks end-to-end, which this
|
|
24
|
+
offline harness does not do. Measuring that is a separate, larger
|
|
25
|
+
benchmark.
|
|
26
|
+
- "Relevant" is keyword-overlap against the goal, not semantic ground
|
|
27
|
+
truth. It is a proxy. The number is a coverage signal, not a
|
|
28
|
+
correctness guarantee.
|
|
29
|
+
|
|
30
|
+
The harness is fully self-contained: it seeds synthetic projects in a
|
|
31
|
+
temp dir, points the knowledge graph at a temp knowledge dir, runs both
|
|
32
|
+
conditions, prints a report, and self-cleans. It never touches a real
|
|
33
|
+
~/.loki/knowledge or any real .loki/memory.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import shutil
|
|
41
|
+
import sys
|
|
42
|
+
import tempfile
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
46
|
+
_REPO_ROOT = os.path.dirname(_HERE)
|
|
47
|
+
if _REPO_ROOT not in sys.path:
|
|
48
|
+
sys.path.insert(0, _REPO_ROOT)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Synthetic patterns per source project. Each is a semantic pattern dict
|
|
52
|
+
# matching what memory/knowledge_graph.py reads (name/category/description).
|
|
53
|
+
SOURCE_PROJECTS = {
|
|
54
|
+
"payments-api": [
|
|
55
|
+
{"name": "idempotency-key-on-charge", "category": "reliability",
|
|
56
|
+
"description": "retry-safe charge endpoints require an idempotency key header"},
|
|
57
|
+
{"name": "stripe-webhook-signature-verify", "category": "security",
|
|
58
|
+
"description": "verify stripe webhook signatures before processing payment events"},
|
|
59
|
+
{"name": "decimal-money-never-float", "category": "correctness",
|
|
60
|
+
"description": "represent money as integer cents or Decimal, never float"},
|
|
61
|
+
],
|
|
62
|
+
"auth-service": [
|
|
63
|
+
{"name": "jwt-short-ttl-refresh-rotation", "category": "security",
|
|
64
|
+
"description": "access tokens short ttl with rotating refresh tokens"},
|
|
65
|
+
{"name": "rate-limit-login-by-ip-and-account", "category": "security",
|
|
66
|
+
"description": "rate limit login attempts per ip and per account to stop credential stuffing"},
|
|
67
|
+
{"name": "argon2-password-hash", "category": "security",
|
|
68
|
+
"description": "hash passwords with argon2id not bcrypt for new services"},
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Patterns the TARGET project already knows on its own (so they are NOT
|
|
73
|
+
# net-new from siblings).
|
|
74
|
+
TARGET_OWN_PATTERNS = [
|
|
75
|
+
{"name": "openapi-spec-first", "category": "design",
|
|
76
|
+
"description": "write the openapi spec before implementing the api"},
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# The target project's task goals. Each goal SHOULD be served by a
|
|
80
|
+
# sibling pattern (that the target lacks). These are the realistic
|
|
81
|
+
# overlaps a new billing+login service would hit.
|
|
82
|
+
TARGET_GOALS = [
|
|
83
|
+
"make the charge endpoint safe to retry",
|
|
84
|
+
"verify incoming payment webhooks are authentic",
|
|
85
|
+
"store monetary amounts without rounding errors",
|
|
86
|
+
"secure login against credential stuffing attacks",
|
|
87
|
+
"choose a password hashing algorithm",
|
|
88
|
+
"design the api contract up front", # served by target's OWN pattern
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _seed_project(root: Path, name: str, patterns: list) -> None:
|
|
93
|
+
semantic = root / name / ".loki" / "memory" / "semantic"
|
|
94
|
+
semantic.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
for i, p in enumerate(patterns):
|
|
96
|
+
with open(semantic / f"pattern_{i}.json", "w") as f:
|
|
97
|
+
json.dump(p, f)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _relevant(pattern: dict, goal: str) -> bool:
|
|
101
|
+
"""Keyword-overlap relevance proxy: any meaningful token from the
|
|
102
|
+
pattern name/description appears in the goal, or vice versa."""
|
|
103
|
+
stop = {"the", "a", "an", "to", "for", "of", "and", "or", "with",
|
|
104
|
+
"without", "is", "are", "be", "up", "on", "in", "by", "not",
|
|
105
|
+
"make", "choose", "store"}
|
|
106
|
+
def toks(s):
|
|
107
|
+
return {t for t in s.lower().replace("-", " ").split() if t not in stop and len(t) > 2}
|
|
108
|
+
goal_t = toks(goal)
|
|
109
|
+
pat_t = toks(pattern.get("name", "")) | toks(pattern.get("description", ""))
|
|
110
|
+
return len(goal_t & pat_t) >= 2
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _coverage(graph, goals, top_k):
|
|
114
|
+
"""For each goal, query the graph and count goals that retrieved at
|
|
115
|
+
least one relevant pattern. Returns (covered_goals, served_by_sibling)."""
|
|
116
|
+
covered = 0
|
|
117
|
+
sibling_served = 0
|
|
118
|
+
details = []
|
|
119
|
+
for goal in goals:
|
|
120
|
+
results = graph.query_patterns(goal, max_results=top_k)
|
|
121
|
+
relevant = [r for r in results if _relevant(r, goal)]
|
|
122
|
+
is_covered = len(relevant) > 0
|
|
123
|
+
# served_by_sibling: at least one relevant result came from a
|
|
124
|
+
# non-target source project.
|
|
125
|
+
from_sibling = any(
|
|
126
|
+
r.get("_source_project", "").rsplit("/", 1)[-1] != "target-billing-login"
|
|
127
|
+
for r in relevant
|
|
128
|
+
)
|
|
129
|
+
if is_covered:
|
|
130
|
+
covered += 1
|
|
131
|
+
if is_covered and from_sibling:
|
|
132
|
+
sibling_served += 1
|
|
133
|
+
details.append({
|
|
134
|
+
"goal": goal,
|
|
135
|
+
"covered": is_covered,
|
|
136
|
+
"relevant_count": len(relevant),
|
|
137
|
+
"served_by_sibling": is_covered and from_sibling,
|
|
138
|
+
})
|
|
139
|
+
return covered, sibling_served, details
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run(top_k: int, as_json: bool) -> int:
|
|
143
|
+
tmp = tempfile.mkdtemp(prefix="loki-xproj-lift-")
|
|
144
|
+
try:
|
|
145
|
+
from memory.knowledge_graph import OrganizationKnowledgeGraph
|
|
146
|
+
|
|
147
|
+
projects_root = Path(tmp) / "git"
|
|
148
|
+
projects_root.mkdir(parents=True)
|
|
149
|
+
|
|
150
|
+
# Seed sibling source projects + the target project.
|
|
151
|
+
for name, pats in SOURCE_PROJECTS.items():
|
|
152
|
+
_seed_project(projects_root, name, pats)
|
|
153
|
+
_seed_project(projects_root, "target-billing-login", TARGET_OWN_PATTERNS)
|
|
154
|
+
|
|
155
|
+
target_dir = projects_root / "target-billing-login"
|
|
156
|
+
sibling_dirs = [projects_root / n for n in SOURCE_PROJECTS]
|
|
157
|
+
|
|
158
|
+
# BASELINE: knowledge graph built from the target alone.
|
|
159
|
+
base_kg = OrganizationKnowledgeGraph(
|
|
160
|
+
knowledge_dir=str(Path(tmp) / "knowledge-baseline"))
|
|
161
|
+
base_pats = base_kg.extract_patterns([target_dir])
|
|
162
|
+
base_kg.save_patterns(base_kg.deduplicate_patterns(base_pats))
|
|
163
|
+
base_covered, base_sibling, base_detail = _coverage(base_kg, TARGET_GOALS, top_k)
|
|
164
|
+
|
|
165
|
+
# CROSS: knowledge graph built from target + siblings.
|
|
166
|
+
cross_kg = OrganizationKnowledgeGraph(
|
|
167
|
+
knowledge_dir=str(Path(tmp) / "knowledge-cross"))
|
|
168
|
+
cross_pats = cross_kg.extract_patterns([target_dir] + sibling_dirs)
|
|
169
|
+
cross_kg.save_patterns(cross_kg.deduplicate_patterns(cross_pats))
|
|
170
|
+
cross_covered, cross_sibling, cross_detail = _coverage(cross_kg, TARGET_GOALS, top_k)
|
|
171
|
+
|
|
172
|
+
n = len(TARGET_GOALS)
|
|
173
|
+
lift = cross_covered - base_covered
|
|
174
|
+
report = {
|
|
175
|
+
"goals": n,
|
|
176
|
+
"baseline_covered": base_covered,
|
|
177
|
+
"cross_covered": cross_covered,
|
|
178
|
+
"lift_absolute": lift,
|
|
179
|
+
"lift_pct_points": round(100.0 * lift / n, 1),
|
|
180
|
+
"net_new_from_siblings": cross_sibling - base_sibling,
|
|
181
|
+
"top_k": top_k,
|
|
182
|
+
"method": "retrieval-coverage (keyword-overlap relevance proxy), NOT task-success",
|
|
183
|
+
"per_goal": cross_detail,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if as_json:
|
|
187
|
+
print(json.dumps(report, indent=2))
|
|
188
|
+
else:
|
|
189
|
+
print("Cross-project knowledge LIFT report (memory moat proof)")
|
|
190
|
+
print(f" target goals: {n}")
|
|
191
|
+
print(f" covered (target alone): {base_covered}/{n}")
|
|
192
|
+
print(f" covered (target + siblings): {cross_covered}/{n}")
|
|
193
|
+
print(f" LIFT: +{lift} goals "
|
|
194
|
+
f"(+{report['lift_pct_points']} pts)")
|
|
195
|
+
print(f" net-new served by siblings: {report['net_new_from_siblings']}")
|
|
196
|
+
print(f" method: {report['method']}")
|
|
197
|
+
print(" per-goal:")
|
|
198
|
+
for d in cross_detail:
|
|
199
|
+
tag = "sibling" if d["served_by_sibling"] else ("self" if d["covered"] else "MISS")
|
|
200
|
+
print(f" [{tag:7}] {d['goal']}")
|
|
201
|
+
|
|
202
|
+
# Exit non-zero if there is no measurable lift (so it can gate CI:
|
|
203
|
+
# a regression that breaks cross-project transfer would fail here).
|
|
204
|
+
return 0 if lift > 0 else 1
|
|
205
|
+
finally:
|
|
206
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def main():
|
|
210
|
+
ap = argparse.ArgumentParser(description="Cross-project knowledge lift report")
|
|
211
|
+
ap.add_argument("--top-k", type=int, default=5, help="patterns retrieved per goal")
|
|
212
|
+
ap.add_argument("--json", action="store_true", help="emit JSON")
|
|
213
|
+
args = ap.parse_args()
|
|
214
|
+
sys.exit(run(args.top_k, args.json))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
main()
|