loki-mode 7.74.0 → 7.76.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/run.sh +147 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/static/index.html +385 -126
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/memory/consolidation.py +3 -3
- package/memory/engine.py +7 -15
- package/memory/ingest.py +70 -0
- package/memory/knowledge_graph.py +38 -3
- package/memory/storage.py +47 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
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.76.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.76.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var r6=Object.defineProperty;var i6=($)=>$;function e6($,Q){this[$]=i6.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:e6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var D1={};b(D1,{lokiDir:()=>P,homeLokiDir:()=>t$,findRepoRootForVersion:()=>s$,REPO_ROOT:()=>g});import{resolve as a,dirname as a$}from"path";import{fileURLToPath as $Q}from"url";import{existsSync as F$}from"fs";import{homedir as QQ}from"os";function ZQ(){let $=N1;for(let Q=0;Q<6;Q++){if(F$(a($,"VERSION"))&&F$(a($,"autonomy/run.sh")))return $;let Z=a$($);if(Z===$)break;$=Z}return a(N1,"..","..","..")}function s$($){let Q=$;for(let Z=0;Z<6;Z++){if(F$(a(Q,"VERSION"))&&F$(a(Q,"autonomy/run.sh")))return Q;let z=a$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function t$(){return a(QQ(),".loki")}var N1,g;var C=L(()=>{N1=a$($Q(import.meta.url));g=ZQ()});import{readFileSync as zQ}from"fs";import{resolve as XQ,dirname as KQ}from"path";import{fileURLToPath as qQ}from"url";function R$(){if(Q$!==null)return Q$;let $="7.
|
|
2
|
+
var r6=Object.defineProperty;var i6=($)=>$;function e6($,Q){this[$]=i6.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:e6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var D1={};b(D1,{lokiDir:()=>P,homeLokiDir:()=>t$,findRepoRootForVersion:()=>s$,REPO_ROOT:()=>g});import{resolve as a,dirname as a$}from"path";import{fileURLToPath as $Q}from"url";import{existsSync as F$}from"fs";import{homedir as QQ}from"os";function ZQ(){let $=N1;for(let Q=0;Q<6;Q++){if(F$(a($,"VERSION"))&&F$(a($,"autonomy/run.sh")))return $;let Z=a$($);if(Z===$)break;$=Z}return a(N1,"..","..","..")}function s$($){let Q=$;for(let Z=0;Z<6;Z++){if(F$(a(Q,"VERSION"))&&F$(a(Q,"autonomy/run.sh")))return Q;let z=a$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function t$(){return a(QQ(),".loki")}var N1,g;var C=L(()=>{N1=a$($Q(import.meta.url));g=ZQ()});import{readFileSync as zQ}from"fs";import{resolve as XQ,dirname as KQ}from"path";import{fileURLToPath as qQ}from"url";function R$(){if(Q$!==null)return Q$;let $="7.76.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=KQ(qQ(import.meta.url)),Z=s$(Q);Q$=zQ(XQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var r$=L(()=>{C()});var b1={};b(b1,{runOrThrow:()=>VQ,run:()=>F,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>i$});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([new Response(Z.stdout).text(),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 VQ($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new i$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=JQ($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function JQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,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 i$;var d=L(()=>{i$=class i$ 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 s($){return UQ?"":$}var UQ,O,N,_,_Z,I,R,h,V;var c=L(()=>{UQ=(process.env.NO_COLOR??"").length>0;O=s("\x1B[0;31m"),N=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),R=s("\x1B[1m"),h=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as _Q}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(_Q($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=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 Y$;var V$=L(()=>{d()});var e1={};b(e1,{runStatus:()=>cQ});import{existsSync as y,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as D,basename as CQ}from"path";import{homedir as bQ}from"os";function n1($){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 a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*x$/Q);if(X>x$)X=x$;let q=x$-X,K=N;if(z>=80)K=O;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${R}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function yQ(){if(await f("jq"))return!0;return process.stdout.write(`${O}Error: jq is required but not installed.${V}
|
|
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)
|
|
@@ -793,4 +793,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
793
793
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
794
794
|
`),process.stderr.write(t6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var qZ=await KZ(Bun.argv.slice(2));process.exit(qZ);
|
|
795
795
|
|
|
796
|
-
//# debugId=
|
|
796
|
+
//# debugId=D4E8DE98E1AB29DC64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/memory/consolidation.py
CHANGED
|
@@ -226,9 +226,9 @@ class ConsolidationPipeline:
|
|
|
226
226
|
# Re-read the target pattern fresh immediately before
|
|
227
227
|
# merging (BUG-MEM C1, lost-update). The whole-run
|
|
228
228
|
# snapshot at step 4 can be stale by now: a concurrent
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
# AFTER the snapshot. merge_with_existing() builds the
|
|
229
|
+
# storage.increment_pattern_usage() (atomic read-mutate-
|
|
230
|
+
# write under one exclusive lock) may have bumped
|
|
231
|
+
# usage_count/last_used AFTER the snapshot. merge_with_existing() builds the
|
|
232
232
|
# merged record from best_match.usage_count/last_used,
|
|
233
233
|
# so merging from the stale snapshot clobbers that bump.
|
|
234
234
|
# Re-reading narrows the window to this single write.
|
package/memory/engine.py
CHANGED
|
@@ -580,21 +580,13 @@ class MemoryEngine:
|
|
|
580
580
|
Args:
|
|
581
581
|
pattern_id: Pattern identifier
|
|
582
582
|
"""
|
|
583
|
-
#
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
#
|
|
589
|
-
|
|
590
|
-
# a null and 0 are equivalent here so `or` is safe.
|
|
591
|
-
pattern_data["usage_count"] = (pattern_data.get("usage_count") or 0) + 1
|
|
592
|
-
pattern_data["last_used"] = datetime.now(timezone.utc).isoformat()
|
|
593
|
-
|
|
594
|
-
# Write back via save_pattern which holds an exclusive lock during
|
|
595
|
-
# the full read-modify-write (upsert) cycle
|
|
596
|
-
pattern_obj = self._dict_to_pattern(pattern_data)
|
|
597
|
-
self.storage.save_pattern(pattern_obj)
|
|
583
|
+
# Delegate the entire read-modify-write to storage, which performs it
|
|
584
|
+
# under a single exclusive lock. Doing the read here (load_pattern's
|
|
585
|
+
# shared lock is released immediately) and writing a detached snapshot
|
|
586
|
+
# back via save_pattern (wholesale entry replacement) loses updates
|
|
587
|
+
# when two agents increment concurrently. A no-op if the pattern is
|
|
588
|
+
# missing.
|
|
589
|
+
self.storage.increment_pattern_usage(pattern_id)
|
|
598
590
|
|
|
599
591
|
# -------------------------------------------------------------------------
|
|
600
592
|
# Skill Operations
|
package/memory/ingest.py
CHANGED
|
@@ -26,6 +26,7 @@ Safety:
|
|
|
26
26
|
"""
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
|
+
import hashlib
|
|
29
30
|
import json
|
|
30
31
|
import os
|
|
31
32
|
import re
|
|
@@ -165,6 +166,52 @@ def _log_to_errors(memory_base: str, function_name: str, exc: BaseException) ->
|
|
|
165
166
|
return
|
|
166
167
|
|
|
167
168
|
|
|
169
|
+
def _deterministic_episode_id(stable_key: str) -> str:
|
|
170
|
+
"""Derive a stable, idempotent episode id from a stable key.
|
|
171
|
+
|
|
172
|
+
Re-ingesting the same session/summary must NOT create a new episode.
|
|
173
|
+
EpisodeTrace.create() mints a random uuid every call, so without a
|
|
174
|
+
deterministic id the index dedup (which keys on episode_id) counts
|
|
175
|
+
the same session twice, inflating episode_count / total_cost_usd /
|
|
176
|
+
total_tokens. Deriving the id from the stable key (e.g. the session
|
|
177
|
+
or task id) makes re-ingest land on the same filename + the same
|
|
178
|
+
index dedup bucket. Date is intentionally omitted so the id does not
|
|
179
|
+
drift across day boundaries for the same source.
|
|
180
|
+
"""
|
|
181
|
+
digest = hashlib.sha1(stable_key.encode("utf-8")).hexdigest()[:12]
|
|
182
|
+
return f"ep-{digest}"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _existing_episode_path(
|
|
186
|
+
storage: Any, memory_base: str, episode_id: str
|
|
187
|
+
) -> Optional[str]:
|
|
188
|
+
"""Return the on-disk path for an already-stored episode id, or None.
|
|
189
|
+
|
|
190
|
+
Used for the idempotency existence check before writing so a repeat
|
|
191
|
+
ingest of the same source is a no-op rather than an append-new.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
existing = storage.load_episode(episode_id)
|
|
195
|
+
except Exception:
|
|
196
|
+
return None
|
|
197
|
+
if not existing:
|
|
198
|
+
return None
|
|
199
|
+
# Mirror storage.save_episode's path layout; prefer the stored
|
|
200
|
+
# timestamp date, fall back to a directory scan if absent.
|
|
201
|
+
ts = existing.get("timestamp")
|
|
202
|
+
if isinstance(ts, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", ts[:10] or ""):
|
|
203
|
+
candidate = Path(memory_base) / "episodic" / ts[:10] / f"task-{episode_id}.json"
|
|
204
|
+
if candidate.is_file():
|
|
205
|
+
return str(candidate)
|
|
206
|
+
episodic_dir = Path(memory_base) / "episodic"
|
|
207
|
+
if episodic_dir.is_dir():
|
|
208
|
+
for date_dir in episodic_dir.iterdir():
|
|
209
|
+
candidate = date_dir / f"task-{episode_id}.json"
|
|
210
|
+
if candidate.is_file():
|
|
211
|
+
return str(candidate)
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
168
215
|
def _parse_transcript_line(line: str) -> Optional[Dict[str, Any]]:
|
|
169
216
|
"""Parse one JSONL line; return None on parse error."""
|
|
170
217
|
try:
|
|
@@ -385,12 +432,21 @@ def ingest_from_claude_transcript(
|
|
|
385
432
|
engine = MemoryEngine(storage=storage, base_path=memory_base)
|
|
386
433
|
engine.initialize()
|
|
387
434
|
|
|
435
|
+
# The transcript task_id is always stable (sessionId or filename
|
|
436
|
+
# stem), so derive a deterministic episode id from it and skip the
|
|
437
|
+
# write if this session was already ingested (idempotent re-run).
|
|
438
|
+
episode_id = _deterministic_episode_id(task_id)
|
|
439
|
+
already = _existing_episode_path(storage, memory_base, episode_id)
|
|
440
|
+
if already is not None:
|
|
441
|
+
return already
|
|
442
|
+
|
|
388
443
|
trace = EpisodeTrace.create(
|
|
389
444
|
task_id=task_id,
|
|
390
445
|
agent=agent,
|
|
391
446
|
phase=phase,
|
|
392
447
|
goal=goal,
|
|
393
448
|
)
|
|
449
|
+
trace.id = episode_id
|
|
394
450
|
trace.outcome = outcome
|
|
395
451
|
trace.duration_seconds = duration
|
|
396
452
|
# v7.7.18 council fix: apply BOTH scrubbers to file paths --
|
|
@@ -440,16 +496,30 @@ def ingest_from_summary(
|
|
|
440
496
|
engine = MemoryEngine(storage=storage, base_path=memory_base)
|
|
441
497
|
engine.initialize()
|
|
442
498
|
|
|
499
|
+
# Only a caller-supplied task_id is a stable key. The auto-generated
|
|
500
|
+
# timestamp fallback is unique per call by design, so it must NOT
|
|
501
|
+
# drive a deterministic id (that would alias unrelated captures).
|
|
502
|
+
caller_supplied_key = task_id is not None
|
|
443
503
|
if task_id is None:
|
|
444
504
|
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
|
|
445
505
|
task_id = f"mcp-capture-{ts}"
|
|
446
506
|
|
|
507
|
+
if caller_supplied_key:
|
|
508
|
+
episode_id = _deterministic_episode_id(task_id)
|
|
509
|
+
already = _existing_episode_path(storage, memory_base, episode_id)
|
|
510
|
+
if already is not None:
|
|
511
|
+
return already
|
|
512
|
+
else:
|
|
513
|
+
episode_id = None
|
|
514
|
+
|
|
447
515
|
trace = EpisodeTrace.create(
|
|
448
516
|
task_id=task_id,
|
|
449
517
|
agent=agent,
|
|
450
518
|
phase=phase,
|
|
451
519
|
goal=_scrub(goal)[:500],
|
|
452
520
|
)
|
|
521
|
+
if episode_id is not None:
|
|
522
|
+
trace.id = episode_id
|
|
453
523
|
trace.outcome = outcome if outcome in ("success", "failure", "partial") else "success"
|
|
454
524
|
trace.duration_seconds = max(0, int(duration_seconds))
|
|
455
525
|
trace.files_read = [_scrub_path(_scrub(p)) for p in (files_read or [])]
|
|
@@ -9,10 +9,20 @@ Storage: ~/.loki/knowledge/
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
|
+
import logging
|
|
12
13
|
import os
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from datetime import datetime, timezone
|
|
15
16
|
|
|
17
|
+
try:
|
|
18
|
+
import fcntl # POSIX-only; absent on Windows.
|
|
19
|
+
_HAS_FCNTL = True
|
|
20
|
+
except ImportError: # pragma: no cover - non-POSIX fallback
|
|
21
|
+
fcntl = None
|
|
22
|
+
_HAS_FCNTL = False
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
16
26
|
|
|
17
27
|
class OrganizationKnowledgeGraph:
|
|
18
28
|
"""Aggregates patterns and knowledge across multiple projects."""
|
|
@@ -79,11 +89,30 @@ class OrganizationKnowledgeGraph:
|
|
|
79
89
|
return unique
|
|
80
90
|
|
|
81
91
|
def save_patterns(self, patterns):
|
|
82
|
-
"""Save patterns to the knowledge store (appends to JSONL).
|
|
92
|
+
"""Save patterns to the knowledge store (appends to JSONL).
|
|
93
|
+
|
|
94
|
+
The append is guarded by an exclusive flock and a single flushed
|
|
95
|
+
write so concurrent writers cannot interleave partial lines.
|
|
96
|
+
stdio buffering breaks O_APPEND atomicity and a record longer
|
|
97
|
+
than PIPE_BUF can split mid-line under concurrency, so we both
|
|
98
|
+
hold the lock and emit one contiguous buffer before unlocking.
|
|
99
|
+
"""
|
|
83
100
|
self.ensure_dir()
|
|
101
|
+
# Serialize first so a serialization error cannot leave a partial
|
|
102
|
+
# line on disk while the lock is held.
|
|
103
|
+
buffer = "".join(json.dumps(p) + "\n" for p in patterns)
|
|
104
|
+
if not buffer:
|
|
105
|
+
return
|
|
84
106
|
with open(self.patterns_file, 'a') as f:
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
if _HAS_FCNTL:
|
|
108
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
109
|
+
try:
|
|
110
|
+
f.write(buffer)
|
|
111
|
+
f.flush()
|
|
112
|
+
os.fsync(f.fileno())
|
|
113
|
+
finally:
|
|
114
|
+
if _HAS_FCNTL:
|
|
115
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
87
116
|
|
|
88
117
|
def load_patterns(self, limit=100):
|
|
89
118
|
"""Load patterns from the knowledge store."""
|
|
@@ -98,6 +127,12 @@ class OrganizationKnowledgeGraph:
|
|
|
98
127
|
try:
|
|
99
128
|
patterns.append(json.loads(line))
|
|
100
129
|
except json.JSONDecodeError:
|
|
130
|
+
# Resilient: skip the bad row but surface it so silent
|
|
131
|
+
# data loss (e.g. a torn legacy line) is observable.
|
|
132
|
+
logger.warning(
|
|
133
|
+
"knowledge_graph: dropped unparseable patterns.jsonl line: %.120r",
|
|
134
|
+
line,
|
|
135
|
+
)
|
|
101
136
|
continue
|
|
102
137
|
if len(patterns) >= limit:
|
|
103
138
|
break
|
package/memory/storage.py
CHANGED
|
@@ -1519,6 +1519,53 @@ class MemoryStorage:
|
|
|
1519
1519
|
|
|
1520
1520
|
return False
|
|
1521
1521
|
|
|
1522
|
+
def increment_pattern_usage(self, pattern_id: str) -> bool:
|
|
1523
|
+
"""Atomically increment a semantic pattern's usage_count, keyed by id.
|
|
1524
|
+
|
|
1525
|
+
The entire read-mutate-write happens inside a single exclusive
|
|
1526
|
+
_file_lock so concurrent increments cannot lose updates. Mirrors the
|
|
1527
|
+
lock-spanning idiom of _persist_boost_semantic / save_pattern: fresh
|
|
1528
|
+
read of patterns.json under the lock -> bump the matching entry ->
|
|
1529
|
+
atomic write (which reuses the same reentrant lock).
|
|
1530
|
+
|
|
1531
|
+
Args:
|
|
1532
|
+
pattern_id: Pattern identifier to increment.
|
|
1533
|
+
|
|
1534
|
+
Returns:
|
|
1535
|
+
True if the pattern was found and incremented, False otherwise.
|
|
1536
|
+
"""
|
|
1537
|
+
patterns_path = self.base_path / "semantic" / "patterns.json"
|
|
1538
|
+
if not patterns_path.exists():
|
|
1539
|
+
return False
|
|
1540
|
+
|
|
1541
|
+
with self._file_lock(patterns_path, exclusive=True):
|
|
1542
|
+
if not patterns_path.exists():
|
|
1543
|
+
return False
|
|
1544
|
+
try:
|
|
1545
|
+
with open(patterns_path, "r", encoding="utf-8") as f:
|
|
1546
|
+
patterns_file = json.load(f)
|
|
1547
|
+
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
|
|
1548
|
+
return False
|
|
1549
|
+
if not patterns_file:
|
|
1550
|
+
return False
|
|
1551
|
+
|
|
1552
|
+
patterns = patterns_file.get("patterns", [])
|
|
1553
|
+
for pattern in patterns:
|
|
1554
|
+
if not isinstance(pattern, dict):
|
|
1555
|
+
continue
|
|
1556
|
+
if pattern.get("id") == pattern_id:
|
|
1557
|
+
# `or 0` guards an explicit null usage_count (corrupt or
|
|
1558
|
+
# hand-edited record); null and 0 are equivalent here.
|
|
1559
|
+
pattern["usage_count"] = (pattern.get("usage_count") or 0) + 1
|
|
1560
|
+
pattern["last_used"] = datetime.now(timezone.utc).isoformat()
|
|
1561
|
+
patterns_file["last_updated"] = datetime.now(
|
|
1562
|
+
timezone.utc
|
|
1563
|
+
).isoformat()
|
|
1564
|
+
self._atomic_write(patterns_path, patterns_file)
|
|
1565
|
+
return True
|
|
1566
|
+
|
|
1567
|
+
return False
|
|
1568
|
+
|
|
1522
1569
|
def batch_apply_decay(
|
|
1523
1570
|
self,
|
|
1524
1571
|
collection: str = "all",
|
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.76.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.76.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",
|