loki-mode 6.60.0 → 6.62.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.
Files changed (64) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/app-runner.sh +34 -8
  4. package/autonomy/completion-council.sh +70 -32
  5. package/autonomy/issue-parser.sh +4 -7
  6. package/autonomy/loki +238 -119
  7. package/autonomy/notification-checker.py +49 -23
  8. package/autonomy/run.sh +162 -79
  9. package/autonomy/sandbox.sh +91 -24
  10. package/bin/loki-mode.js +1 -2
  11. package/bin/postinstall.js +10 -4
  12. package/dashboard/__init__.py +1 -1
  13. package/dashboard/control.py +46 -36
  14. package/dashboard/database.py +21 -4
  15. package/dashboard/server.py +107 -78
  16. package/docs/BUG-AUDIT-v6.61.0.md +957 -0
  17. package/docs/INSTALLATION.md +2 -2
  18. package/events/bus.py +129 -28
  19. package/events/bus.ts +41 -27
  20. package/events/emit.sh +1 -1
  21. package/integrations/openclaw/README.md +139 -0
  22. package/integrations/openclaw/SKILL.md +88 -0
  23. package/integrations/openclaw/bridge/__init__.py +1 -0
  24. package/integrations/openclaw/bridge/__main__.py +88 -0
  25. package/integrations/openclaw/bridge/schema_map.py +180 -0
  26. package/integrations/openclaw/bridge/watcher.py +100 -0
  27. package/integrations/openclaw/scripts/format-progress.sh +80 -0
  28. package/integrations/openclaw/scripts/poll-status.sh +74 -0
  29. package/integrations/vibe-kanban.md +289 -0
  30. package/mcp/__init__.py +1 -1
  31. package/mcp/server.py +96 -73
  32. package/memory/consolidation.py +21 -6
  33. package/memory/engine.py +53 -26
  34. package/memory/layers/index_layer.py +16 -3
  35. package/memory/layers/timeline_layer.py +16 -3
  36. package/memory/retrieval.py +4 -1
  37. package/memory/schemas.py +4 -2
  38. package/memory/storage.py +25 -4
  39. package/memory/token_economics.py +9 -2
  40. package/memory/vector_index.py +2 -2
  41. package/package.json +3 -1
  42. package/providers/cline.sh +5 -4
  43. package/providers/codex.sh +27 -5
  44. package/providers/gemini.sh +59 -23
  45. package/providers/loader.sh +3 -2
  46. package/skills/parallel-workflows.md +9 -7
  47. package/state/__init__.py +10 -0
  48. package/state/index.ts +18 -0
  49. package/state/manager.py +1801 -0
  50. package/state/manager.ts +1774 -0
  51. package/state/sqlite_backend.py +188 -0
  52. package/state/test_manager.py +703 -0
  53. package/state/test_manager.ts +366 -0
  54. package/templates/README.md +19 -4
  55. package/templates/dashboard.md +45 -0
  56. package/templates/data-pipeline.md +45 -0
  57. package/templates/game.md +48 -0
  58. package/templates/microservice.md +49 -0
  59. package/templates/npm-library.md +42 -0
  60. package/templates/rest-api.md +170 -33
  61. package/templates/slack-bot.md +48 -0
  62. package/templates/web-scraper.md +45 -0
  63. package/web-app/server.py +360 -191
  64. package/templates/saas-app.md +0 -42
@@ -13,8 +13,10 @@ Usage:
13
13
  import argparse
14
14
  import json
15
15
  import os
16
+ import random
16
17
  import re
17
18
  import time
19
+ from collections import deque
18
20
  from datetime import datetime, timezone
19
21
  from pathlib import Path
20
22
 
@@ -131,8 +133,10 @@ def save_notifications(notif_dir, notifications):
131
133
 
132
134
  def make_notification(trigger_id, severity, message, iteration, data=None):
133
135
  """Create a notification entry."""
136
+ # Include iteration and random suffix to prevent ID collision within same second
137
+ rand_suffix = f"{random.randint(0, 0xFFFF):04x}"
134
138
  return {
135
- "id": f"notif-{int(time.time())}-{trigger_id}",
139
+ "id": f"notif-{int(time.time())}-i{iteration}-{rand_suffix}-{trigger_id}",
136
140
  "trigger_id": trigger_id,
137
141
  "severity": severity,
138
142
  "message": message,
@@ -208,11 +212,11 @@ def check_file_access(trigger, loki_dir, iteration, notifications):
208
212
  if not patterns:
209
213
  return None
210
214
 
211
- # Read last 200 lines of events
215
+ # Read last 200 lines of events using deque to avoid loading entire file
212
216
  results = []
213
217
  try:
214
- lines = events_file.read_text().strip().split("\n")
215
- recent = lines[-200:] if len(lines) > 200 else lines
218
+ with open(events_file, 'r') as f:
219
+ recent = deque(f, maxlen=200)
216
220
  for line in recent:
217
221
  try:
218
222
  event = json.loads(line)
@@ -237,11 +241,12 @@ def check_file_access(trigger, loki_dir, iteration, notifications):
237
241
 
238
242
 
239
243
  def check_quality_gate(trigger, loki_dir, iteration, notifications):
240
- """Check for quality gate failures."""
244
+ """Check for quality gate failures. Reports ALL failed gates, not just first."""
241
245
  state_file = Path(loki_dir) / "dashboard-state.json"
242
246
  if not state_file.exists():
243
- return None
247
+ return []
244
248
 
249
+ results = []
245
250
  try:
246
251
  state = json.loads(state_file.read_text())
247
252
  gates = state.get("qualityGates", {})
@@ -249,37 +254,54 @@ def check_quality_gate(trigger, loki_dir, iteration, notifications):
249
254
  for gate_name, gate_data in gates.items():
250
255
  if isinstance(gate_data, dict) and gate_data.get("status") == "failed":
251
256
  msg = trigger["message"].format(gate=gate_name)
252
- return make_notification(
257
+ results.append(make_notification(
253
258
  trigger["id"], trigger["severity"], msg, iteration,
254
259
  {"gate": gate_name},
255
- )
260
+ ))
256
261
  except (json.JSONDecodeError, OSError, KeyError):
257
262
  pass
258
- return None
263
+ return results if results else None
259
264
 
260
265
 
261
266
  def check_stagnation(trigger, loki_dir, iteration, notifications):
262
- """Check for stuck iterations (no git diff changes)."""
267
+ """Check for stuck iterations (no git diff changes).
268
+
269
+ convergence.log format (pipe-delimited):
270
+ timestamp|iteration|files_changed|CONSECUTIVE_NO_CHANGE|DONE_SIGNALS
271
+
272
+ We parse field 4 (CONSECUTIVE_NO_CHANGE, 0-indexed field 3) from the
273
+ most recent line to determine if stagnation exceeds the threshold.
274
+ """
263
275
  council_file = Path(loki_dir) / "council" / "convergence.log"
264
276
  if not council_file.exists():
265
277
  return None
266
278
 
267
279
  try:
268
- lines = council_file.read_text().strip().split("\n")
280
+ # Read only the tail of the file to avoid loading everything
281
+ with open(council_file, 'r') as f:
282
+ lines = deque(f, maxlen=20)
283
+
269
284
  max_no_progress = trigger.get("max_no_progress", 3)
270
- # Count recent consecutive "no_change" entries
271
- consecutive = 0
285
+
286
+ # Parse the most recent line's CONSECUTIVE_NO_CHANGE field
272
287
  for line in reversed(lines):
273
- if "no_change" in line or "converged" in line:
274
- consecutive += 1
275
- else:
288
+ line = line.strip()
289
+ if not line:
290
+ continue
291
+ fields = line.split('|')
292
+ if len(fields) >= 4:
293
+ try:
294
+ consecutive = int(fields[3])
295
+ except (ValueError, IndexError):
296
+ consecutive = 0
297
+ if consecutive >= max_no_progress:
298
+ msg = trigger["message"].format(count=consecutive)
299
+ return make_notification(
300
+ trigger["id"], trigger["severity"], msg, iteration,
301
+ {"consecutive_no_progress": consecutive},
302
+ )
303
+ # Only check the most recent entry
276
304
  break
277
- if consecutive >= max_no_progress:
278
- msg = trigger["message"].format(count=consecutive)
279
- return make_notification(
280
- trigger["id"], trigger["severity"], msg, iteration,
281
- {"consecutive_no_progress": consecutive},
282
- )
283
305
  except OSError:
284
306
  pass
285
307
  return None
@@ -351,7 +373,11 @@ def check_triggers(loki_dir, iteration):
351
373
 
352
374
  result = checker(trigger, loki_dir, iteration, notifications)
353
375
  if result:
354
- new_notifications.append(result)
376
+ # Checkers may return a single notification or a list of notifications
377
+ if isinstance(result, list):
378
+ new_notifications.extend(result)
379
+ else:
380
+ new_notifications.append(result)
355
381
 
356
382
  if new_notifications:
357
383
  notifications.extend(new_notifications)