loki-mode 7.45.0 → 7.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: 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, 11 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.45.0
6
+ # Loki Mode v7.45.1
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -407,4 +407,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
407
407
 
408
408
  ---
409
409
 
410
- **v7.45.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
410
+ **v7.45.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.45.0
1
+ 7.45.1
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.45.0"
10
+ __version__ = "7.45.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -9,9 +9,11 @@ from __future__ import annotations
9
9
 
10
10
  import json
11
11
  import os
12
+ import tempfile
13
+ from contextlib import contextmanager
12
14
  from datetime import datetime, timezone
13
15
  from pathlib import Path
14
- from typing import Optional
16
+ from typing import Iterator, Optional
15
17
  import hashlib
16
18
 
17
19
 
@@ -25,6 +27,62 @@ def _ensure_registry_dir() -> None:
25
27
  REGISTRY_DIR.mkdir(parents=True, exist_ok=True)
26
28
 
27
29
 
30
+ @contextmanager
31
+ def _registry_lock() -> Iterator[None]:
32
+ """
33
+ Best-effort advisory lock around a read-modify-write of the registry.
34
+
35
+ Two concurrent writers (e.g. two `loki docker start` in different repos, or
36
+ a docker run racing a host `loki start`) would otherwise both load the old
37
+ registry, mutate, and save, dropping one writer's entry (lost update). This
38
+ serializes the leaf mutators so they take turns.
39
+
40
+ Degrades gracefully: if fcntl is unavailable (Windows) or the lock cannot
41
+ be acquired for any reason, execution proceeds without a lock rather than
42
+ blocking a build. The atomic write in _save_registry still guarantees no
43
+ reader ever sees a torn file; only the lost-update protection is
44
+ best-effort.
45
+
46
+ The lock path is derived from the current REGISTRY_DIR at call time (not a
47
+ module-level constant) so tests that monkeypatch REGISTRY_DIR stay
48
+ hermetic. Not reentrant: do not nest this around another leaf mutator (the
49
+ leaf mutators do not call one another).
50
+ """
51
+ _ensure_registry_dir()
52
+ lock_fd = None
53
+ locked = False
54
+ try:
55
+ import fcntl # POSIX only; absent on Windows
56
+
57
+ lock_path = REGISTRY_DIR / ".registry.lock"
58
+ try:
59
+ lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o644)
60
+ fcntl.flock(lock_fd, fcntl.LOCK_EX)
61
+ locked = True
62
+ except OSError:
63
+ # Could not open or lock the file; proceed without the lock.
64
+ locked = False
65
+ except ImportError:
66
+ # fcntl not available (e.g. Windows); proceed without the lock.
67
+ lock_fd = None
68
+
69
+ try:
70
+ yield
71
+ finally:
72
+ if lock_fd is not None:
73
+ try:
74
+ if locked:
75
+ import fcntl
76
+
77
+ fcntl.flock(lock_fd, fcntl.LOCK_UN)
78
+ except (OSError, ImportError):
79
+ pass
80
+ try:
81
+ os.close(lock_fd)
82
+ except OSError:
83
+ pass
84
+
85
+
28
86
  def _load_registry() -> dict:
29
87
  """Load the project registry from disk."""
30
88
  _ensure_registry_dir()
@@ -38,10 +96,39 @@ def _load_registry() -> dict:
38
96
 
39
97
 
40
98
  def _save_registry(registry: dict) -> None:
41
- """Save the project registry to disk."""
99
+ """
100
+ Save the project registry to disk atomically.
101
+
102
+ Writes to a temp file in the SAME directory as REGISTRY_FILE (so os.replace
103
+ is an atomic rename on the same filesystem), flushes and fsyncs it, then
104
+ os.replace()s it over the destination. Every reader therefore sees either
105
+ the complete old file or the complete new file, never a half-written (torn)
106
+ one. The temp file is removed on any error path so partial files never
107
+ leak.
108
+
109
+ Note: atomic write alone eliminates torn reads but does not by itself
110
+ prevent lost updates under true simultaneity. The leaf mutators wrap their
111
+ load->mutate->save in _registry_lock() to serialize concurrent writers and
112
+ reduce that window; when locking is unavailable the degradation is honest
113
+ (torn reads still impossible, lost-update still possible).
114
+ """
42
115
  _ensure_registry_dir()
43
- with open(REGISTRY_FILE, "w") as f:
44
- json.dump(registry, f, indent=2, default=str)
116
+ tmp_fd, tmp_path = tempfile.mkstemp(
117
+ dir=str(REGISTRY_DIR), prefix=".projects.", suffix=".tmp"
118
+ )
119
+ try:
120
+ with os.fdopen(tmp_fd, "w") as f:
121
+ json.dump(registry, f, indent=2, default=str)
122
+ f.flush()
123
+ os.fsync(f.fileno())
124
+ os.replace(tmp_path, str(REGISTRY_FILE))
125
+ except BaseException:
126
+ # Clean up the temp file on any failure so we never leak partial files.
127
+ try:
128
+ os.unlink(tmp_path)
129
+ except OSError:
130
+ pass
131
+ raise
45
132
 
46
133
 
47
134
  def _generate_project_id(path: str) -> str:
@@ -70,34 +157,38 @@ def register_project(
70
157
  if not os.path.isdir(path):
71
158
  raise ValueError(f"Path does not exist: {path}")
72
159
 
73
- registry = _load_registry()
74
160
  project_id = _generate_project_id(path)
75
161
 
76
- # Check if already registered
77
- if project_id in registry["projects"]:
78
- # Update existing entry
79
- project = registry["projects"][project_id]
80
- if name:
81
- project["name"] = name
82
- if alias:
83
- project["alias"] = alias
84
- project["updated_at"] = datetime.now(timezone.utc).isoformat()
85
- else:
86
- # Create new entry
87
- project = {
88
- "id": project_id,
89
- "path": path,
90
- "name": name or os.path.basename(path),
91
- "alias": alias,
92
- "registered_at": datetime.now(timezone.utc).isoformat(),
93
- "updated_at": datetime.now(timezone.utc).isoformat(),
94
- "last_accessed": None,
95
- "has_loki_dir": os.path.isdir(os.path.join(path, ".loki")),
96
- "status": "active",
97
- }
98
- registry["projects"][project_id] = project
162
+ # Lock the load->mutate->save so concurrent registrations serialize and do
163
+ # not lost-update each other (the multi-repo `loki docker` happy path).
164
+ with _registry_lock():
165
+ registry = _load_registry()
99
166
 
100
- _save_registry(registry)
167
+ # Check if already registered
168
+ if project_id in registry["projects"]:
169
+ # Update existing entry
170
+ project = registry["projects"][project_id]
171
+ if name:
172
+ project["name"] = name
173
+ if alias:
174
+ project["alias"] = alias
175
+ project["updated_at"] = datetime.now(timezone.utc).isoformat()
176
+ else:
177
+ # Create new entry
178
+ project = {
179
+ "id": project_id,
180
+ "path": path,
181
+ "name": name or os.path.basename(path),
182
+ "alias": alias,
183
+ "registered_at": datetime.now(timezone.utc).isoformat(),
184
+ "updated_at": datetime.now(timezone.utc).isoformat(),
185
+ "last_accessed": None,
186
+ "has_loki_dir": os.path.isdir(os.path.join(path, ".loki")),
187
+ "status": "active",
188
+ }
189
+ registry["projects"][project_id] = project
190
+
191
+ _save_registry(registry)
101
192
  return project
102
193
 
103
194
 
@@ -111,19 +202,20 @@ def unregister_project(identifier: str) -> bool:
111
202
  Returns:
112
203
  True if removed, False if not found
113
204
  """
114
- registry = _load_registry()
115
-
116
- # Find by ID, path, or alias
117
- project_id = None
118
- for pid, project in registry["projects"].items():
119
- if pid == identifier or project["path"] == identifier or project.get("alias") == identifier:
120
- project_id = pid
121
- break
122
-
123
- if project_id:
124
- del registry["projects"][project_id]
125
- _save_registry(registry)
126
- return True
205
+ with _registry_lock():
206
+ registry = _load_registry()
207
+
208
+ # Find by ID, path, or alias
209
+ project_id = None
210
+ for pid, project in registry["projects"].items():
211
+ if pid == identifier or project["path"] == identifier or project.get("alias") == identifier:
212
+ project_id = pid
213
+ break
214
+
215
+ if project_id:
216
+ del registry["projects"][project_id]
217
+ _save_registry(registry)
218
+ return True
127
219
  return False
128
220
 
129
221
 
@@ -179,13 +271,14 @@ def update_last_accessed(identifier: str) -> Optional[dict]:
179
271
  Returns:
180
272
  Updated project entry or None
181
273
  """
182
- registry = _load_registry()
183
-
184
- for pid, project in registry["projects"].items():
185
- if pid == identifier or project["path"] == identifier or project.get("alias") == identifier:
186
- project["last_accessed"] = datetime.now(timezone.utc).isoformat()
187
- _save_registry(registry)
188
- return project
274
+ with _registry_lock():
275
+ registry = _load_registry()
276
+
277
+ for pid, project in registry["projects"].items():
278
+ if pid == identifier or project["path"] == identifier or project.get("alias") == identifier:
279
+ project["last_accessed"] = datetime.now(timezone.utc).isoformat()
280
+ _save_registry(registry)
281
+ return project
189
282
  return None
190
283
 
191
284
 
@@ -207,19 +300,20 @@ def mark_project_stopped(identifier: str) -> Optional[dict]:
207
300
  Idempotent: marking an already-stopped project is a no-op that still
208
301
  returns the entry.
209
302
  """
210
- registry = _load_registry()
211
-
212
- for pid_key, project in registry["projects"].items():
213
- if (
214
- pid_key == identifier
215
- or project["path"] == identifier
216
- or project.get("alias") == identifier
217
- ):
218
- project["status"] = "stopped"
219
- project["pid"] = None
220
- project["updated_at"] = datetime.now(timezone.utc).isoformat()
221
- _save_registry(registry)
222
- return project
303
+ with _registry_lock():
304
+ registry = _load_registry()
305
+
306
+ for pid_key, project in registry["projects"].items():
307
+ if (
308
+ pid_key == identifier
309
+ or project["path"] == identifier
310
+ or project.get("alias") == identifier
311
+ ):
312
+ project["status"] = "stopped"
313
+ project["pid"] = None
314
+ project["updated_at"] = datetime.now(timezone.utc).isoformat()
315
+ _save_registry(registry)
316
+ return project
223
317
  return None
224
318
 
225
319
 
@@ -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.45.0
5
+ **Version:** v7.45.1
6
6
 
7
7
  ---
8
8
 
@@ -389,7 +389,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
389
389
  # Run Loki Mode in Docker (Claude provider, API-key auth)
390
390
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
391
391
  -v $(pwd):/workspace -w /workspace \
392
- asklokesh/loki-mode:7.45.0 start ./my-spec.md
392
+ asklokesh/loki-mode:7.45.1 start ./my-spec.md
393
393
  ```
394
394
 
395
395
  ##### docker compose + .env (no host install)
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.45.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,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[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ 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 a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
2
+ var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.45.1";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,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[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ 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 a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
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)
@@ -789,4 +789,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
789
789
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
790
790
  `),process.stderr.write(o6),2}}p1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var ZZ=await QZ(Bun.argv.slice(2));process.exit(ZZ);
791
791
 
792
- //# debugId=7FCBCE9F1C748AE964756E2164756E21
792
+ //# debugId=91DA611FDFEA183B64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.45.0'
60
+ __version__ = '7.45.1'
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.45.0",
4
+ "version": "7.45.1",
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 11 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.45.0",
5
+ "version": "7.45.1",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 11 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",