loki-mode 7.98.0 → 7.99.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 +2 -2
- package/VERSION +1 -1
- package/autonomy/checklist-verify.py +24 -2
- package/autonomy/run.sh +17 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +7 -2
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/memory/retrieval.py +110 -17
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
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.
|
|
6
|
+
# Loki Mode v7.99.0
|
|
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.
|
|
411
|
+
**v7.99.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.99.0
|
|
@@ -97,8 +97,30 @@ def run_check(check: dict, project_dir: str, timeout: int) -> dict:
|
|
|
97
97
|
text=True,
|
|
98
98
|
timeout=timeout,
|
|
99
99
|
)
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
combined = proc.stdout + proc.stderr
|
|
101
|
+
# Trust gate: a tests_pass check REQUIRES that at least one
|
|
102
|
+
# test was actually discovered and run. jest is invoked with
|
|
103
|
+
# --passWithNoTests, so a zero-match pattern exits 0 ("No
|
|
104
|
+
# tests found ...") -- that would be a fake-green (a required
|
|
105
|
+
# verification passing with nothing run). pytest exits 5 when
|
|
106
|
+
# it collects no tests. Detect either no-test signal and fail
|
|
107
|
+
# the check rather than report success on an empty run.
|
|
108
|
+
no_tests = (
|
|
109
|
+
"No tests found" in combined
|
|
110
|
+
or "no tests ran" in combined.lower()
|
|
111
|
+
or "no tests to run" in combined.lower()
|
|
112
|
+
or proc.returncode == 5 # pytest: no tests collected
|
|
113
|
+
)
|
|
114
|
+
if no_tests:
|
|
115
|
+
result["passed"] = False
|
|
116
|
+
result["output"] = (
|
|
117
|
+
"No tests discovered for required check "
|
|
118
|
+
f"(pattern={pattern!r}); a tests_pass check must run "
|
|
119
|
+
"at least one test. Output: "
|
|
120
|
+
) + combined[:400]
|
|
121
|
+
else:
|
|
122
|
+
result["passed"] = proc.returncode == 0
|
|
123
|
+
result["output"] = combined[:500]
|
|
102
124
|
except subprocess.TimeoutExpired:
|
|
103
125
|
result["passed"] = None # timeout = pending
|
|
104
126
|
result["output"] = f"Timed out after {timeout}s"
|
package/autonomy/run.sh
CHANGED
|
@@ -947,8 +947,24 @@ PYCHECK
|
|
|
947
947
|
fi
|
|
948
948
|
# (d) Defense-in-depth: reclaim the dashboard port only in the CLEAR
|
|
949
949
|
# case, so we never kill a shared dashboard another project owns.
|
|
950
|
+
# BUT never kill a HEALTHY dashboard already serving on the port: that is
|
|
951
|
+
# almost always the user's own live dashboard (open in their browser), and
|
|
952
|
+
# killing it mid-use drops their session (ERR_CONNECTION_REFUSED, WS fail).
|
|
953
|
+
# A healthy server is reusable by every project, so probe /api/status first
|
|
954
|
+
# and only reclaim the port when nothing is answering (a genuinely stale
|
|
955
|
+
# listener). Opt out of the probe with LOKI_DASHBOARD_FORCE_RECLAIM=1.
|
|
950
956
|
if command -v lsof >/dev/null 2>&1; then
|
|
951
|
-
|
|
957
|
+
local _dash_port="${DASHBOARD_PORT:-57374}"
|
|
958
|
+
local _dash_alive=""
|
|
959
|
+
if [ "${LOKI_DASHBOARD_FORCE_RECLAIM:-}" != "1" ] && command -v curl >/dev/null 2>&1; then
|
|
960
|
+
_dash_alive=$(curl -s -o /dev/null -w '%{http_code}' --max-time 1 \
|
|
961
|
+
"http://127.0.0.1:${_dash_port}/api/status" 2>/dev/null || true)
|
|
962
|
+
fi
|
|
963
|
+
if [ "$_dash_alive" = "200" ]; then
|
|
964
|
+
log_info "Reusing the healthy dashboard already serving on port ${_dash_port} (not reclaiming)."
|
|
965
|
+
else
|
|
966
|
+
lsof -ti:"${_dash_port}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
|
|
967
|
+
fi
|
|
952
968
|
fi
|
|
953
969
|
fi
|
|
954
970
|
}
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -6240,9 +6240,14 @@ def _compute_cost_snapshot() -> dict:
|
|
|
6240
6240
|
budget_used = 0.0
|
|
6241
6241
|
budget_remaining = None
|
|
6242
6242
|
|
|
6243
|
-
# Read efficiency files (one JSON file per iteration/task)
|
|
6243
|
+
# Read efficiency files (one JSON file per iteration/task).
|
|
6244
|
+
# Use the iteration-*.json pattern so this reader sees the same
|
|
6245
|
+
# authoritative file set as _compute_budget_snapshot and
|
|
6246
|
+
# _compute_cost_timeline (both glob iteration-*.json, mirroring
|
|
6247
|
+
# check_budget_limit in run.sh). A bare *.json would also pick up
|
|
6248
|
+
# non-iteration JSON in the dir and make the three cost readers disagree.
|
|
6244
6249
|
if efficiency_dir.exists():
|
|
6245
|
-
for eff_file in sorted(efficiency_dir.glob("
|
|
6250
|
+
for eff_file in sorted(efficiency_dir.glob("iteration-*.json")):
|
|
6246
6251
|
try:
|
|
6247
6252
|
data = json.loads(eff_file.read_text())
|
|
6248
6253
|
# A corrupt/truncated efficiency file can parse to a non-object
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
5
|
+
**Version:** v7.99.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var z8=Object.defineProperty;var X8=($)=>$;function K8($,Q){this[$]=X8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)z8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:K8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var y1={};b(y1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as t,dirname as i$}from"path";import{fileURLToPath as q8}from"url";import{existsSync as E$}from"fs";import{homedir as J8}from"os";function V8(){let $=h1;for(let Q=0;Q<6;Q++){if(E$(t($,"VERSION"))&&E$(t($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return t(h1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(E$(t(Q,"VERSION"))&&E$(t(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function P(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(J8(),".loki")}var h1,h;var C=L(()=>{h1=i$(q8(import.meta.url));h=V8()});import{readFileSync as W8}from"fs";import{resolve as U8,dirname as H8}from"path";import{fileURLToPath as G8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.
|
|
2
|
+
var z8=Object.defineProperty;var X8=($)=>$;function K8($,Q){this[$]=X8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)z8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:K8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var y1={};b(y1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as t,dirname as i$}from"path";import{fileURLToPath as q8}from"url";import{existsSync as E$}from"fs";import{homedir as J8}from"os";function V8(){let $=h1;for(let Q=0;Q<6;Q++){if(E$(t($,"VERSION"))&&E$(t($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return t(h1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(E$(t(Q,"VERSION"))&&E$(t(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function P(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(J8(),".loki")}var h1,h;var C=L(()=>{h1=i$(q8(import.meta.url));h=V8()});import{readFileSync as W8}from"fs";import{resolve as U8,dirname as H8}from"path";import{fileURLToPath as G8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.99.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=H8(G8(import.meta.url)),Z=e$(Q);Q$=W8(U8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var $1=L(()=>{C()});var u1={};b(u1,{runOrThrow:()=>x8,run:()=>F,readStreamCapped:()=>f1,commandVersion:()=>N8,commandExists:()=>f,ShellError:()=>Q1,MAX_STDOUT_BYTES:()=>m1});async function f1($,Q=m1){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([f1(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=E8($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function E8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function N8($,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 m1=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 r($){return S8?"":$}var S8,T,S,_,lZ,I,k,y,J;var c=L(()=>{S8=(process.env.NO_COLOR??"").length>0;T=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),lZ=r("\x1B[0;34m"),I=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as u8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(u8($))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 q0={};b(q0,{runStatus:()=>W3});import{existsSync as v,readFileSync as U$,readdirSync as i1,statSync as e1}from"fs";import{resolve as D,basename as $3}from"path";import{homedir as Q3}from"os";function $0($){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 Q0($,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=$0($),U=$0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${U})`}async function z3(){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)
|
|
@@ -802,4 +802,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
802
802
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
803
803
|
`),process.stderr.write(Z8),2}}r1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var FZ=await jZ(Bun.argv.slice(2));process.exit(FZ);
|
|
804
804
|
|
|
805
|
-
//# debugId=
|
|
805
|
+
//# debugId=2654E1AA878C0D9664756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/memory/retrieval.py
CHANGED
|
@@ -680,8 +680,10 @@ class MemoryRetrieval:
|
|
|
680
680
|
for ns in namespaces:
|
|
681
681
|
ns_retrieval = self.with_namespace(ns)
|
|
682
682
|
|
|
683
|
-
# Simple keyword search in this namespace
|
|
684
|
-
|
|
683
|
+
# Simple keyword search in this namespace.
|
|
684
|
+
# BUG-MEM-012: 'anti_patterns' was omitted, so anti-pattern
|
|
685
|
+
# memories were silently missed in cross-namespace search.
|
|
686
|
+
for collection in ["episodic", "semantic", "skills", "anti_patterns"]:
|
|
685
687
|
results = ns_retrieval.retrieve_by_keyword(
|
|
686
688
|
query.split(),
|
|
687
689
|
collection,
|
|
@@ -823,20 +825,36 @@ class MemoryRetrieval:
|
|
|
823
825
|
if collection not in self.vector_indices:
|
|
824
826
|
return self.retrieve_by_keyword(query.split(), collection)[:top_k]
|
|
825
827
|
|
|
826
|
-
# Check if indices need rebuilding after consolidation (BUG-MEM-002
|
|
827
|
-
#
|
|
828
|
-
#
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
828
|
+
# Check if indices need rebuilding after consolidation (BUG-MEM-002,
|
|
829
|
+
# BUG-MEM-007). Consolidation rewrites semantic/patterns.json (and may
|
|
830
|
+
# touch semantic/anti-patterns.json), so any index sourced from those
|
|
831
|
+
# files can go stale. The 'semantic' index reads patterns.json; the
|
|
832
|
+
# 'anti_patterns' index reads BOTH patterns.json and anti-patterns.json
|
|
833
|
+
# (consolidated anti-patterns are bridged from patterns.json). If a
|
|
834
|
+
# source file was modified more recently than we last built indices,
|
|
835
|
+
# fall back to keyword search for accuracy. (episodic/skills read their
|
|
836
|
+
# own per-record files and are not rewritten by consolidation, so they
|
|
837
|
+
# are not checked here.)
|
|
838
|
+
if self._indices_built_at is not None:
|
|
839
|
+
stale_sources: List[str] = []
|
|
840
|
+
if collection == "semantic":
|
|
841
|
+
stale_sources = ["semantic/patterns.json"]
|
|
842
|
+
elif collection == "anti_patterns":
|
|
843
|
+
stale_sources = ["semantic/patterns.json",
|
|
844
|
+
"semantic/anti-patterns.json"]
|
|
845
|
+
if stale_sources:
|
|
832
846
|
import os
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
847
|
+
for rel in stale_sources:
|
|
848
|
+
source_path = self.base_path / rel
|
|
849
|
+
if source_path.exists() and \
|
|
850
|
+
os.path.getmtime(source_path) > self._indices_built_at:
|
|
851
|
+
logger.info(
|
|
852
|
+
"%s index is stale (%s modified after index build). "
|
|
853
|
+
"Falling back to keyword search for accuracy.",
|
|
854
|
+
collection, rel,
|
|
855
|
+
)
|
|
856
|
+
return self.retrieve_by_keyword(
|
|
857
|
+
query.split(), collection)[:top_k]
|
|
840
858
|
|
|
841
859
|
# Generate query embedding
|
|
842
860
|
query_embedding = self.embedding_engine.embed(query)
|
|
@@ -859,6 +877,34 @@ class MemoryRetrieval:
|
|
|
859
877
|
|
|
860
878
|
return items
|
|
861
879
|
|
|
880
|
+
@staticmethod
|
|
881
|
+
def _anti_pattern_content_key(record: Dict[str, Any]) -> tuple:
|
|
882
|
+
"""Normalized content key for an anti-pattern across both schemas.
|
|
883
|
+
|
|
884
|
+
Consolidation writes anti-patterns into semantic/patterns.json as
|
|
885
|
+
SemanticPattern objects (fields incorrect_approach|pattern /
|
|
886
|
+
description / correct_approach), while the legacy
|
|
887
|
+
semantic/anti-patterns.json uses what_fails / why / prevention. The
|
|
888
|
+
same anti-pattern can therefore appear in both stores (no migration
|
|
889
|
+
copies one into the other), which would double-count it on read and in
|
|
890
|
+
the vector index (BUG-MEM-011). Map both schemas onto the same
|
|
891
|
+
lowercased/stripped (what_fails, why, prevention) tuple so the two
|
|
892
|
+
representations of one anti-pattern collide on a single key.
|
|
893
|
+
"""
|
|
894
|
+
def _norm(*candidates: Any) -> str:
|
|
895
|
+
for c in candidates:
|
|
896
|
+
if c:
|
|
897
|
+
return str(c).strip().lower()
|
|
898
|
+
return ""
|
|
899
|
+
|
|
900
|
+
what_fails = _norm(record.get("what_fails"),
|
|
901
|
+
record.get("incorrect_approach"),
|
|
902
|
+
record.get("pattern"))
|
|
903
|
+
why = _norm(record.get("why"), record.get("description"))
|
|
904
|
+
prevention = _norm(record.get("prevention"),
|
|
905
|
+
record.get("correct_approach"))
|
|
906
|
+
return (what_fails, why, prevention)
|
|
907
|
+
|
|
862
908
|
@staticmethod
|
|
863
909
|
def _parse_episode_timestamp(value: Any) -> Optional[datetime]:
|
|
864
910
|
"""Parse an episode timestamp to a tz-aware datetime, or None.
|
|
@@ -1746,6 +1792,25 @@ class MemoryRetrieval:
|
|
|
1746
1792
|
"""Keyword search in anti-patterns."""
|
|
1747
1793
|
results: List[Dict[str, Any]] = []
|
|
1748
1794
|
anti_data = self.storage.read_json("semantic/anti-patterns.json") or {}
|
|
1795
|
+
patterns_data = self.storage.read_json("semantic/patterns.json") or {}
|
|
1796
|
+
|
|
1797
|
+
# BUG-MEM-011: an anti-pattern can live in BOTH the consolidated
|
|
1798
|
+
# patterns.json (category="anti-pattern") and the legacy
|
|
1799
|
+
# anti-patterns.json, with no migration copying one into the other.
|
|
1800
|
+
# Reading both naively double-counts it. Pre-scan the consolidated
|
|
1801
|
+
# store (which we keep) into a seen-set keyed by id AND normalized
|
|
1802
|
+
# content, then skip any legacy record that collides with either.
|
|
1803
|
+
seen_ids: set = set()
|
|
1804
|
+
seen_content: set = set()
|
|
1805
|
+
for pat in patterns_data.get("patterns", []):
|
|
1806
|
+
if not isinstance(pat, dict):
|
|
1807
|
+
continue
|
|
1808
|
+
if pat.get("category") != "anti-pattern":
|
|
1809
|
+
continue
|
|
1810
|
+
pid = pat.get("id")
|
|
1811
|
+
if pid:
|
|
1812
|
+
seen_ids.add(pid)
|
|
1813
|
+
seen_content.add(self._anti_pattern_content_key(pat))
|
|
1749
1814
|
|
|
1750
1815
|
for anti in anti_data.get("anti_patterns", []):
|
|
1751
1816
|
# Defensive: mirror the sibling loop below. A corrupt or
|
|
@@ -1753,6 +1818,11 @@ class MemoryRetrieval:
|
|
|
1753
1818
|
# the isinstance guard and (x or "") avoid AttributeError.
|
|
1754
1819
|
if not isinstance(anti, dict):
|
|
1755
1820
|
continue
|
|
1821
|
+
# Dedup: skip a legacy record already represented in patterns.json.
|
|
1822
|
+
aid = anti.get("id")
|
|
1823
|
+
if (aid and aid in seen_ids) or \
|
|
1824
|
+
self._anti_pattern_content_key(anti) in seen_content:
|
|
1825
|
+
continue
|
|
1756
1826
|
what_fails = (anti.get("what_fails") or "").lower()
|
|
1757
1827
|
why = (anti.get("why") or "").lower()
|
|
1758
1828
|
prevention = (anti.get("prevention") or "").lower()
|
|
@@ -1774,7 +1844,6 @@ class MemoryRetrieval:
|
|
|
1774
1844
|
# description / correct_approach. Without this bridge, consolidated
|
|
1775
1845
|
# anti-patterns were never retrievable. Map them onto the same
|
|
1776
1846
|
# what_fails / why / prevention scoring shape.
|
|
1777
|
-
patterns_data = self.storage.read_json("semantic/patterns.json") or {}
|
|
1778
1847
|
for pat in patterns_data.get("patterns", []):
|
|
1779
1848
|
if not isinstance(pat, dict):
|
|
1780
1849
|
continue
|
|
@@ -1888,8 +1957,33 @@ class MemoryRetrieval:
|
|
|
1888
1957
|
|
|
1889
1958
|
index = self.vector_indices["anti_patterns"]
|
|
1890
1959
|
anti_data = self.storage.read_json("semantic/anti-patterns.json") or {}
|
|
1960
|
+
patterns_data = self.storage.read_json("semantic/patterns.json") or {}
|
|
1961
|
+
|
|
1962
|
+
# BUG-MEM-011: the same anti-pattern may live in BOTH the consolidated
|
|
1963
|
+
# patterns.json and the legacy anti-patterns.json. Indexing both
|
|
1964
|
+
# double-counts it in the vector index. Pre-scan the consolidated store
|
|
1965
|
+
# (which we keep) into a seen-set keyed by id AND normalized content,
|
|
1966
|
+
# then skip any legacy record that collides with either.
|
|
1967
|
+
seen_ids: set = set()
|
|
1968
|
+
seen_content: set = set()
|
|
1969
|
+
for pat in patterns_data.get("patterns", []):
|
|
1970
|
+
if not isinstance(pat, dict):
|
|
1971
|
+
continue
|
|
1972
|
+
if pat.get("category") != "anti-pattern":
|
|
1973
|
+
continue
|
|
1974
|
+
pid = pat.get("id")
|
|
1975
|
+
if pid:
|
|
1976
|
+
seen_ids.add(pid)
|
|
1977
|
+
seen_content.add(self._anti_pattern_content_key(pat))
|
|
1891
1978
|
|
|
1892
1979
|
for anti in anti_data.get("anti_patterns", []):
|
|
1980
|
+
if not isinstance(anti, dict):
|
|
1981
|
+
continue
|
|
1982
|
+
# Dedup: skip a legacy record already represented in patterns.json.
|
|
1983
|
+
aid = anti.get("id")
|
|
1984
|
+
if (aid and aid in seen_ids) or \
|
|
1985
|
+
self._anti_pattern_content_key(anti) in seen_content:
|
|
1986
|
+
continue
|
|
1893
1987
|
# Create text for embedding
|
|
1894
1988
|
text = f"{anti.get('what_fails', '')} {anti.get('why', '')} {anti.get('prevention', '')}"
|
|
1895
1989
|
|
|
@@ -1904,7 +1998,6 @@ class MemoryRetrieval:
|
|
|
1904
1998
|
# category="anti-pattern" entries in semantic/patterns.json, not the
|
|
1905
1999
|
# legacy anti-patterns.json above. Bridge those into the vector index
|
|
1906
2000
|
# too so embedding-based retrieval sees consolidated anti-patterns.
|
|
1907
|
-
patterns_data = self.storage.read_json("semantic/patterns.json") or {}
|
|
1908
2001
|
for pat in patterns_data.get("patterns", []):
|
|
1909
2002
|
if not isinstance(pat, dict):
|
|
1910
2003
|
continue
|
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.
|
|
4
|
+
"version": "7.99.0",
|
|
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.
|
|
5
|
+
"version": "7.99.0",
|
|
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",
|