loki-mode 7.68.0 → 7.69.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/app-runner.sh +14 -2
- package/autonomy/completion-council.sh +34 -3
- package/autonomy/loki +52 -38
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +32 -21
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +66 -66
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +12 -0
- package/memory/engine.py +25 -5
- package/memory/token_economics.py +9 -0
- package/memory/vector_index.py +13 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -1358,14 +1358,17 @@ async def loki_start_project(prd_content: str = "", prd_path: str = "") -> str:
|
|
|
1358
1358
|
try:
|
|
1359
1359
|
resolved = validate_path(prd_path, allowed_dirs=['.'])
|
|
1360
1360
|
except PathTraversalError as e:
|
|
1361
|
+
_emit_tool_event_async('loki_start_project', 'complete', result_status='error', error=str(e))
|
|
1361
1362
|
return json.dumps({"error": str(e)})
|
|
1362
1363
|
if os.path.exists(resolved) and os.path.isfile(resolved):
|
|
1363
1364
|
with open(resolved, 'r', encoding='utf-8') as f:
|
|
1364
1365
|
content = f.read()
|
|
1365
1366
|
else:
|
|
1367
|
+
_emit_tool_event_async('loki_start_project', 'complete', result_status='error', error='PRD file not found')
|
|
1366
1368
|
return json.dumps({"error": f"PRD file not found: {prd_path}"})
|
|
1367
1369
|
|
|
1368
1370
|
if not content:
|
|
1371
|
+
_emit_tool_event_async('loki_start_project', 'complete', result_status='error', error='No PRD content or path provided')
|
|
1369
1372
|
return json.dumps({"error": "No PRD content or path provided"})
|
|
1370
1373
|
|
|
1371
1374
|
# Initialize project state using safe path operations
|
|
@@ -1390,6 +1393,7 @@ async def loki_start_project(prd_content: str = "", prd_path: str = "") -> str:
|
|
|
1390
1393
|
_emit_tool_event_async('loki_start_project', 'complete', result_status='success')
|
|
1391
1394
|
return json.dumps({"success": True, **project})
|
|
1392
1395
|
except PathTraversalError as e:
|
|
1396
|
+
_emit_tool_event_async('loki_start_project', 'complete', result_status='error', error='Access denied')
|
|
1393
1397
|
return json.dumps({"error": f"Access denied: {e}"})
|
|
1394
1398
|
except Exception as e:
|
|
1395
1399
|
logger.error(f"Start project failed: {e}")
|
|
@@ -1440,6 +1444,7 @@ async def loki_project_status() -> str:
|
|
|
1440
1444
|
_emit_tool_event_async('loki_project_status', 'complete', result_status='success')
|
|
1441
1445
|
return json.dumps(status, default=str)
|
|
1442
1446
|
except PathTraversalError:
|
|
1447
|
+
_emit_tool_event_async('loki_project_status', 'complete', result_status='error', error='Access denied')
|
|
1443
1448
|
return json.dumps({"error": "Access denied"})
|
|
1444
1449
|
except Exception as e:
|
|
1445
1450
|
logger.error(f"Project status failed: {e}")
|
|
@@ -1478,6 +1483,7 @@ async def loki_agent_metrics() -> str:
|
|
|
1478
1483
|
_emit_tool_event_async('loki_agent_metrics', 'complete', result_status='success')
|
|
1479
1484
|
return json.dumps(metrics, default=str)
|
|
1480
1485
|
except PathTraversalError:
|
|
1486
|
+
_emit_tool_event_async('loki_agent_metrics', 'complete', result_status='error', error='Access denied')
|
|
1481
1487
|
return json.dumps({"error": "Access denied"})
|
|
1482
1488
|
except Exception as e:
|
|
1483
1489
|
logger.error(f"Agent metrics failed: {e}")
|
|
@@ -1500,6 +1506,7 @@ async def loki_checkpoint_restore(checkpoint_id: str = "") -> str:
|
|
|
1500
1506
|
try:
|
|
1501
1507
|
cp_dir = safe_path_join('.loki', 'state', 'checkpoints')
|
|
1502
1508
|
if not os.path.isdir(cp_dir):
|
|
1509
|
+
_emit_tool_event_async('loki_checkpoint_restore', 'complete', result_status='success')
|
|
1503
1510
|
return json.dumps({"checkpoints": [], "message": "No checkpoints directory"})
|
|
1504
1511
|
|
|
1505
1512
|
checkpoints = []
|
|
@@ -1518,6 +1525,7 @@ async def loki_checkpoint_restore(checkpoint_id: str = "") -> str:
|
|
|
1518
1525
|
# Find and restore specific checkpoint
|
|
1519
1526
|
target = next((c for c in checkpoints if c["id"] == checkpoint_id), None)
|
|
1520
1527
|
if not target:
|
|
1528
|
+
_emit_tool_event_async('loki_checkpoint_restore', 'complete', result_status='error', error='Checkpoint not found')
|
|
1521
1529
|
return json.dumps({"error": f"Checkpoint not found: {checkpoint_id}"})
|
|
1522
1530
|
|
|
1523
1531
|
# Write checkpoint state as current state, stripping the injected "id" field
|
|
@@ -1529,6 +1537,7 @@ async def loki_checkpoint_restore(checkpoint_id: str = "") -> str:
|
|
|
1529
1537
|
_emit_tool_event_async('loki_checkpoint_restore', 'complete', result_status='success')
|
|
1530
1538
|
return json.dumps({"restored": True, "checkpoint_id": checkpoint_id})
|
|
1531
1539
|
except PathTraversalError:
|
|
1540
|
+
_emit_tool_event_async('loki_checkpoint_restore', 'complete', result_status='error', error='Access denied')
|
|
1532
1541
|
return json.dumps({"error": "Access denied"})
|
|
1533
1542
|
except Exception as e:
|
|
1534
1543
|
logger.error(f"Checkpoint restore failed: {e}")
|
|
@@ -1569,6 +1578,7 @@ async def loki_quality_report() -> str:
|
|
|
1569
1578
|
_emit_tool_event_async('loki_quality_report', 'complete', result_status='success')
|
|
1570
1579
|
return json.dumps(report, default=str)
|
|
1571
1580
|
except PathTraversalError:
|
|
1581
|
+
_emit_tool_event_async('loki_quality_report', 'complete', result_status='error', error='Access denied')
|
|
1572
1582
|
return json.dumps({"error": "Access denied"})
|
|
1573
1583
|
except Exception as e:
|
|
1574
1584
|
logger.error(f"Quality report failed: {e}")
|
|
@@ -2057,6 +2067,7 @@ async def mem_get(
|
|
|
2057
2067
|
|
|
2058
2068
|
id_list = [i.strip() for i in ids.split(",") if i.strip()]
|
|
2059
2069
|
if not id_list:
|
|
2070
|
+
_emit_tool_event_async('mem_get', 'complete', result_status='error', error='No IDs provided')
|
|
2060
2071
|
return json.dumps({"entries": {}, "error": "No IDs provided"})
|
|
2061
2072
|
|
|
2062
2073
|
# Cap at 20 to prevent abuse
|
|
@@ -2401,6 +2412,7 @@ async def loki_learnings(limit: int = 50) -> str:
|
|
|
2401
2412
|
try:
|
|
2402
2413
|
path = safe_path_join('.loki', 'state', 'relevant-learnings.json')
|
|
2403
2414
|
if not os.path.exists(path):
|
|
2415
|
+
_emit_tool_event_async('loki_learnings', 'complete', result_status='success')
|
|
2404
2416
|
return json.dumps({"version": 1, "learnings": [], "total": 0})
|
|
2405
2417
|
try:
|
|
2406
2418
|
with safe_open(path, 'r') as f:
|
package/memory/engine.py
CHANGED
|
@@ -244,7 +244,11 @@ class MemoryEngine:
|
|
|
244
244
|
for pattern in patterns_data.get("patterns", []):
|
|
245
245
|
if not isinstance(pattern, dict):
|
|
246
246
|
continue
|
|
247
|
-
|
|
247
|
+
# `or []` guards an explicit null source_episodes (corrupt or
|
|
248
|
+
# hand-edited record): .get(..., []) returns None on a stored
|
|
249
|
+
# null, and set.update(None) raises TypeError, crashing the whole
|
|
250
|
+
# cleanup pass. A null and an empty list are equivalent here.
|
|
251
|
+
referenced_ids.update(pattern.get("source_episodes") or [])
|
|
248
252
|
|
|
249
253
|
# Scan episodic directories
|
|
250
254
|
episodic_path = Path(self.base_path) / "episodic"
|
|
@@ -376,17 +380,28 @@ class MemoryEngine:
|
|
|
376
380
|
"last_accessed": now,
|
|
377
381
|
"relevance_score": 0.5,
|
|
378
382
|
})
|
|
379
|
-
|
|
383
|
+
# total_memories counts memories (episodes), not topics, to
|
|
384
|
+
# match the canonical rebuild_index semantics
|
|
385
|
+
# (total_memories += 1 per episode at line ~860). Previously
|
|
386
|
+
# this only incremented when a NEW topic was created, so N
|
|
387
|
+
# episodes sharing one phase reported total_memories=1 until
|
|
388
|
+
# the user ran rebuild_index, which then jumped it to N
|
|
389
|
+
# (two functions in this file disagreeing on the same field,
|
|
390
|
+
# surfaced by get_stats). Increment per distinct episode.
|
|
391
|
+
if episode_id:
|
|
392
|
+
index["total_memories"] = index.get("total_memories", 0) + 1
|
|
380
393
|
else:
|
|
381
394
|
# Only count a given episode once. On resume/checkpoint the same
|
|
382
395
|
# trace id can be re-saved; without this guard episode_count,
|
|
383
|
-
# total_cost_usd, and
|
|
384
|
-
# even though episode_ids is already
|
|
396
|
+
# total_cost_usd, total_tokens, and total_memories would inflate
|
|
397
|
+
# on every re-save even though episode_ids is already
|
|
398
|
+
# de-duplicated.
|
|
385
399
|
if episode_id and episode_id not in found.get("episode_ids", []):
|
|
386
400
|
found.setdefault("episode_ids", []).append(episode_id)
|
|
387
401
|
found["episode_count"] = found.get("episode_count", 0) + 1
|
|
388
402
|
found["total_cost_usd"] = float(found.get("total_cost_usd", 0) or 0) + cost
|
|
389
403
|
found["total_tokens"] = int(found.get("total_tokens", 0) or 0) + tokens
|
|
404
|
+
index["total_memories"] = index.get("total_memories", 0) + 1
|
|
390
405
|
merged = set(found.get("files_touched", []) or []) | set(files[:20])
|
|
391
406
|
found["files_touched"] = sorted(merged)[:50]
|
|
392
407
|
found["last_accessed"] = now
|
|
@@ -878,7 +893,12 @@ class MemoryEngine:
|
|
|
878
893
|
}
|
|
879
894
|
|
|
880
895
|
topics[phase]["token_count"] += tokens
|
|
881
|
-
|
|
896
|
+
# `or ""` on BOTH sides: an episode with a missing or
|
|
897
|
+
# explicit-null timestamp leaves last_accessed=None (set
|
|
898
|
+
# from data.get("timestamp") above), and `str > None`
|
|
899
|
+
# raises TypeError, crashing rebuild_index. A single
|
|
900
|
+
# such episode is enough to break the whole rebuild.
|
|
901
|
+
if (data.get("timestamp") or "") > (topics[phase].get("last_accessed") or ""):
|
|
882
902
|
topics[phase]["last_accessed"] = data.get("timestamp")
|
|
883
903
|
|
|
884
904
|
# Index semantic patterns
|
|
@@ -256,6 +256,15 @@ def optimize_context(
|
|
|
256
256
|
relevance = memory.get("_weighted_score", 0.5)
|
|
257
257
|
else:
|
|
258
258
|
relevance = memory.get("_score", 0.5)
|
|
259
|
+
# Guard against an explicit null score in a corrupt/hand-edited
|
|
260
|
+
# record: .get(key, default) returns None when the key is present
|
|
261
|
+
# but null, and the `relevance > 1.0` comparison below would then
|
|
262
|
+
# raise "'>' not supported between instances of 'NoneType' and
|
|
263
|
+
# 'float'". Use an is-None check (not `or`) so a legitimate stored
|
|
264
|
+
# 0.0 relevance is preserved, mirroring the confidence guard above
|
|
265
|
+
# and retrieval.py's own base_score guard.
|
|
266
|
+
if relevance is None:
|
|
267
|
+
relevance = 0.5
|
|
259
268
|
if relevance > 1.0:
|
|
260
269
|
# Normalize high scores
|
|
261
270
|
relevance = min(1.0, relevance / 10.0)
|
package/memory/vector_index.py
CHANGED
|
@@ -42,7 +42,20 @@ class VectorIndex:
|
|
|
42
42
|
Args:
|
|
43
43
|
dimension: The dimensionality of vectors. Default is 384
|
|
44
44
|
which matches MiniLM embedding size.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ImportError: If numpy is not installed. Every operation in this
|
|
48
|
+
index (add, search, save, load) relies on numpy, so fail
|
|
49
|
+
fast with a clear message here rather than letting a later
|
|
50
|
+
call crash with an opaque ``AttributeError: 'NoneType'
|
|
51
|
+
object has no attribute ...`` once ``np`` is None. The
|
|
52
|
+
module-level NUMPY_AVAILABLE flag is what gates this check.
|
|
45
53
|
"""
|
|
54
|
+
if not NUMPY_AVAILABLE:
|
|
55
|
+
raise ImportError(
|
|
56
|
+
"numpy is required for the vector index. "
|
|
57
|
+
"Install it with: pip install numpy"
|
|
58
|
+
)
|
|
46
59
|
self.dimension = dimension
|
|
47
60
|
self.embeddings: List[np.ndarray] = []
|
|
48
61
|
self.ids: List[str] = []
|
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.69.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.69.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",
|