loki-mode 7.7.22 → 7.7.24

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 CHANGED
@@ -24,15 +24,15 @@
24
24
 
25
25
  ## Why Loki Mode?
26
26
 
27
- - **Truly autonomous** -- Describe what you want, walk away, come back to working code with tests
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
- - **Open source** -- Free for personal, internal, and academic use. No vendor lock-in.
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
- ## Multi-Provider Support
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: 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.
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.22
6
+ # Loki Mode v7.7.24
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
- **Multi-provider (stable since v5.0.0):** Claude/Codex/Cline/Aider with abstract model tiers and degraded mode for non-Claude providers. 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`).
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.22 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.7.24 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.7.22
1
+ 7.7.24
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.7.22"
10
+ __version__ = "7.7.24"
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.7.22
5
+ **Version:** v7.7.24
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
- - 4-provider support: Claude (full), Codex, Cline, Aider
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
 
@@ -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.22";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}
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.24";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=67ACBDA1E9391E4564756E2164756E21
588
+ //# debugId=2B5B8BCEF68E54B364756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.7.22'
60
+ __version__ = '7.7.24'
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
- if _capture_disabled():
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 via LOKI_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
- """Honor `LOKI_MEMORY_CAPTURE_DISABLED=true` escape hatch."""
130
- return os.environ.get("LOKI_MEMORY_CAPTURE_DISABLED", "").lower() in (
131
- "true",
132
- "1",
133
- "yes",
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
- """Simple keyword search across stored patterns.
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 across 'name', 'pattern', 'description', and 'category'
174
- fields for compatibility with both simple dicts and SemanticPattern
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
- name = p.get('name', '').lower()
183
- pattern_text = p.get('pattern', '').lower()
184
- desc = p.get('description', '').lower()
185
- category = p.get('category', '').lower()
186
- if query_lower in name:
187
- score += 3
188
- if query_lower in pattern_text:
189
- score += 3
190
- if query_lower in desc:
191
- score += 1
192
- if query_lower in category:
193
- score += 2
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,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "7.7.22",
3
+ "version": "7.7.24",
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",