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
@@ -0,0 +1,180 @@
1
+ """Event schema mapping between Loki Mode and OpenClaw gateway.
2
+
3
+ Loki Mode emits events in two formats:
4
+
5
+ 1. Individual JSON files in .loki/events/pending/ (via events/emit.sh):
6
+ {"id", "type", "source", "timestamp", "payload": {"action", ...}}
7
+ Here "type" is a category (e.g. "session") and payload.action is the verb.
8
+
9
+ 2. JSONL entries in .loki/events.jsonl (via emit_event_json in run.sh):
10
+ {"timestamp", "type", "data": {...}}
11
+ Here "type" is already compound (e.g. "session_start", "phase_change").
12
+
13
+ This module normalizes both into a canonical "type.action" key for lookup,
14
+ then maps to OpenClaw gateway message format.
15
+ """
16
+
17
+ # Mapping table: canonical event key -> OpenClaw message definition.
18
+ #
19
+ # Canonical keys use dot notation: "session.start", "phase.change", etc.
20
+ # The map_event() function normalizes both Loki formats into these keys.
21
+ LOKI_TO_OPENCLAW = {
22
+ "session.start": {
23
+ "openclaw_method": "sessions_send",
24
+ "template": "Loki Mode started. Provider: {provider}. PRD: {prd_path}",
25
+ },
26
+ "session.stop": {
27
+ "openclaw_method": "sessions_send",
28
+ "template": "Loki Mode stopped. Reason: {reason}.",
29
+ },
30
+ "session.end": {
31
+ "openclaw_method": "sessions_send",
32
+ "template": "Session ended. Result: {result}. Reason: {reason}",
33
+ },
34
+ "phase.change": {
35
+ "openclaw_method": "sessions_send",
36
+ "template": "Phase transition: {from_phase} -> {to_phase} (iteration {iteration})",
37
+ },
38
+ "iteration.start": {
39
+ "openclaw_method": "sessions_send",
40
+ "template": "Iteration {iteration} started. Phase: {phase}",
41
+ },
42
+ "iteration.complete": {
43
+ "openclaw_method": "sessions_send",
44
+ "template": "Iteration {iteration} complete. Phase: {phase}",
45
+ },
46
+ "task.complete": {
47
+ "openclaw_method": "sessions_send",
48
+ "template": "Task completed: {task_id} - {action}",
49
+ },
50
+ "error.failed": {
51
+ "openclaw_method": "sessions_send",
52
+ "template": "[ERROR] {error}. Command: {command}",
53
+ },
54
+ "council.vote": {
55
+ "openclaw_method": "sessions_send",
56
+ "template": "Council vote: {verdict} ({votes_for}/{votes_total} votes)",
57
+ },
58
+ "council.verdict": {
59
+ "openclaw_method": "sessions_send",
60
+ "template": "Council verdict: {verdict} ({votes_for}/{votes_total} votes)",
61
+ },
62
+ "budget.exceeded": {
63
+ "openclaw_method": "sessions_send",
64
+ "template": "[BUDGET] Limit exceeded. Current: ${current_cost}. Limit: ${budget_limit}",
65
+ },
66
+ "budget.warning": {
67
+ "openclaw_method": "sessions_send",
68
+ "template": "[BUDGET] Current spend: ${current_cost}. Limit: ${budget_limit}",
69
+ },
70
+ "code_review.start": {
71
+ "openclaw_method": "sessions_send",
72
+ "template": "Code review started. Files: {file_count}",
73
+ },
74
+ "code_review.complete": {
75
+ "openclaw_method": "sessions_send",
76
+ "template": "Code review complete. Result: {result}",
77
+ },
78
+ "watchdog.alert": {
79
+ "openclaw_method": "sessions_send",
80
+ "template": "[WATCHDOG] {reason}",
81
+ },
82
+ }
83
+
84
+ # Underscore-separated type names from emit_event_json (e.g. "session_start")
85
+ # are normalized to dot notation by replacing the first underscore with a dot.
86
+ # This dict handles cases where the underscore split is ambiguous.
87
+ _UNDERSCORE_OVERRIDES = {
88
+ "phase_change": "phase.change",
89
+ "iteration_start": "iteration.start",
90
+ "iteration_complete": "iteration.complete",
91
+ "session_start": "session.start",
92
+ "session_end": "session.end",
93
+ "budget_exceeded": "budget.exceeded",
94
+ "code_review_start": "code_review.start",
95
+ "code_review_complete": "code_review.complete",
96
+ "council_vote": "council.vote",
97
+ "watchdog_alert": "watchdog.alert",
98
+ }
99
+
100
+
101
+ def _normalize_event_key(loki_event: dict) -> str:
102
+ """Derive canonical dot-notation key from either Loki event format.
103
+
104
+ Format 1 (emit.sh individual files):
105
+ type="session", payload.action="start" -> "session.start"
106
+
107
+ Format 2 (events.jsonl from emit_event_json):
108
+ type="session_start" -> "session.start"
109
+ """
110
+ event_type = loki_event.get("type", "")
111
+
112
+ # Check override table first (handles underscore-compound types)
113
+ if event_type in _UNDERSCORE_OVERRIDES:
114
+ return _UNDERSCORE_OVERRIDES[event_type]
115
+
116
+ # Format 1: type + payload.action
117
+ payload = loki_event.get("payload") or loki_event.get("data") or {}
118
+ if isinstance(payload, dict):
119
+ action = payload.get("action", "")
120
+ if action:
121
+ return f"{event_type}.{action}"
122
+
123
+ # Fallback: replace first underscore with dot
124
+ if "_" in event_type:
125
+ parts = event_type.split("_", 1)
126
+ return f"{parts[0]}.{parts[1]}"
127
+
128
+ return event_type
129
+
130
+
131
+ def map_event(loki_event: dict): # -> Optional[dict]
132
+ """Map a Loki event to OpenClaw gateway message format.
133
+
134
+ Accepts either Loki event format (individual JSON file or JSONL entry).
135
+
136
+ Args:
137
+ loki_event: Dict from either format. Common fields:
138
+ - type: Event type string
139
+ - timestamp: ISO timestamp
140
+ For emit.sh format: id, source, payload
141
+ For JSONL format: data (dict of key-value pairs)
142
+
143
+ Returns:
144
+ OpenClaw message dict with "method" and "params", or None if the
145
+ event type is not mapped.
146
+ """
147
+ canonical_key = _normalize_event_key(loki_event)
148
+ mapping = LOKI_TO_OPENCLAW.get(canonical_key)
149
+ if not mapping:
150
+ return None
151
+
152
+ # Extract payload from either format
153
+ payload = loki_event.get("payload") or loki_event.get("data") or {}
154
+ if isinstance(payload, str):
155
+ # JSONL format sometimes has data as a plain string
156
+ payload = {"data": payload}
157
+
158
+ try:
159
+ message = mapping["template"].format_map(_SafeFormatDict(payload))
160
+ except (KeyError, ValueError):
161
+ message = f"{canonical_key}: {payload}"
162
+
163
+ return {
164
+ "method": mapping["openclaw_method"],
165
+ "params": {
166
+ "message": message,
167
+ "source": "loki-bridge",
168
+ "event_type": canonical_key,
169
+ "timestamp": loki_event.get("timestamp", ""),
170
+ "loki_event_id": loki_event.get("id", ""),
171
+ },
172
+ }
173
+
174
+
175
+ class _SafeFormatDict(dict):
176
+ """Dict subclass that returns placeholder text for missing keys
177
+ instead of raising KeyError during str.format_map()."""
178
+
179
+ def __missing__(self, key: str) -> str:
180
+ return f"<{key}>"
@@ -0,0 +1,100 @@
1
+ """File watcher for Loki Mode event directories.
2
+
3
+ Polls .loki/events/pending/ for new JSON event files and monitors
4
+ .loki/dashboard-state.json for changes. Calls a user-supplied callback
5
+ with each new event dict.
6
+
7
+ Uses stdlib only -- no external dependencies, no threads.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import time
13
+
14
+
15
+ class LokiFileWatcher:
16
+ """Synchronous polling watcher for Loki Mode flat-file events.
17
+
18
+ Args:
19
+ loki_dir: Path to the .loki directory (e.g. ".loki" or "/tmp/project/.loki").
20
+ on_event: Callback receiving a single dict (the parsed event).
21
+ poll_interval: Seconds between directory polls (default 1.0).
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ loki_dir: str,
27
+ on_event,
28
+ poll_interval: float = 1.0,
29
+ ):
30
+ self.loki_dir = os.path.abspath(loki_dir)
31
+ self.pending_dir = os.path.join(self.loki_dir, "events", "pending")
32
+ self.state_file = os.path.join(self.loki_dir, "dashboard-state.json")
33
+ self.on_event = on_event
34
+ self.poll_interval = poll_interval
35
+
36
+ self._processed: set[str] = set()
37
+ self._last_state_mtime: float = 0.0
38
+ self._running = False
39
+
40
+ def start(self) -> None:
41
+ """Run the poll loop. Blocks until stop() is called or interrupted."""
42
+ self._running = True
43
+ self._ensure_dirs()
44
+
45
+ while self._running:
46
+ self._poll_pending()
47
+ self._poll_state()
48
+ time.sleep(self.poll_interval)
49
+
50
+ def stop(self) -> None:
51
+ """Signal the poll loop to exit after the current iteration."""
52
+ self._running = False
53
+
54
+ def _ensure_dirs(self) -> None:
55
+ """Create watched directories if they do not exist."""
56
+ os.makedirs(self.pending_dir, exist_ok=True)
57
+
58
+ def _poll_pending(self) -> None:
59
+ """Scan pending directory for new JSON event files."""
60
+ try:
61
+ entries = os.listdir(self.pending_dir)
62
+ except OSError:
63
+ return
64
+
65
+ for name in sorted(entries):
66
+ if not name.endswith(".json") or name in self._processed:
67
+ continue
68
+ filepath = os.path.join(self.pending_dir, name)
69
+ event = self._read_json(filepath)
70
+ if event is not None:
71
+ self.on_event(event)
72
+ self._processed.add(name)
73
+
74
+ def _poll_state(self) -> None:
75
+ """Check dashboard-state.json for modifications."""
76
+ try:
77
+ mtime = os.path.getmtime(self.state_file)
78
+ except OSError:
79
+ return
80
+
81
+ if mtime <= self._last_state_mtime:
82
+ return
83
+
84
+ self._last_state_mtime = mtime
85
+ state = self._read_json(self.state_file)
86
+ if state is not None:
87
+ self.on_event({
88
+ "type": "dashboard_state",
89
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
90
+ "payload": state,
91
+ })
92
+
93
+ @staticmethod
94
+ def _read_json(path: str): # -> Optional[dict]
95
+ """Read and parse a JSON file, returning None on any error."""
96
+ try:
97
+ with open(path, "r", encoding="utf-8") as f:
98
+ return json.load(f)
99
+ except (OSError, json.JSONDecodeError, ValueError):
100
+ return None
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # Format loki status into human-readable progress message
3
+ # Reads JSON from stdin (output of poll-status.sh), outputs formatted text
4
+ # suitable for posting to Slack, Discord, or web channels.
5
+ #
6
+ # Usage: ./poll-status.sh /path/to/project | ./format-progress.sh
7
+
8
+ set -euo pipefail
9
+
10
+ python3 -c "
11
+ import json, sys
12
+
13
+ try:
14
+ s = json.load(sys.stdin)
15
+
16
+ # Check for error
17
+ if 'error' in s:
18
+ print('Loki Mode [ERROR] - ' + s['error'])
19
+ suggestion = s.get('suggestion')
20
+ if suggestion:
21
+ print('Suggestion: ' + suggestion)
22
+ sys.exit(0)
23
+
24
+ status = s.get('status', 'unknown').upper()
25
+ phase = s.get('phase') or 'N/A'
26
+ iteration = s.get('iteration', 0)
27
+ completed = s.get('tasks_completed', 0)
28
+ total = s.get('tasks_total', 0)
29
+ failed = s.get('tasks_failed', 0)
30
+ pending = s.get('tasks_pending', 0)
31
+ budget_used = s.get('budget_used')
32
+ budget_limit = s.get('budget_limit')
33
+ elapsed = s.get('elapsed_minutes', 0)
34
+ provider = s.get('provider', 'unknown')
35
+ verdict = s.get('council_verdict')
36
+ version = s.get('version', '')
37
+
38
+ lines = []
39
+
40
+ # Header line
41
+ header = 'Loki Mode [' + status + '] - ' + phase + ' (iteration ' + str(iteration) + ')'
42
+ if version:
43
+ header += ' | v' + version
44
+ lines.append(header)
45
+
46
+ # Task progress
47
+ task_line = 'Tasks: ' + str(completed) + '/' + str(total) + ' complete'
48
+ if failed:
49
+ task_line += ', ' + str(failed) + ' failed'
50
+ if pending:
51
+ task_line += ', ' + str(pending) + ' pending'
52
+ lines.append(task_line)
53
+
54
+ # Cost and timing
55
+ info_parts = []
56
+ if budget_used is not None:
57
+ cost_str = '\$' + '{:.2f}'.format(budget_used)
58
+ if budget_limit:
59
+ cost_str += ' / \$' + '{:.2f}'.format(budget_limit) + ' budget'
60
+ info_parts.append('Cost: ' + cost_str)
61
+ info_parts.append('Time: ' + '{:.0f}'.format(elapsed) + 'm')
62
+ info_parts.append('Provider: ' + provider)
63
+ lines.append(' | '.join(info_parts))
64
+
65
+ # Council verdict (if available)
66
+ if verdict:
67
+ lines.append('Council: ' + verdict)
68
+
69
+ # Warnings
70
+ if failed > 0:
71
+ lines.append('WARNING: ' + str(failed) + ' task(s) failed -- check loki logs')
72
+ if status == 'UNKNOWN':
73
+ lines.append('WARNING: Session status unknown -- process may have crashed')
74
+
75
+ print('\\n'.join(lines))
76
+ except json.JSONDecodeError:
77
+ print('Loki Mode [ERROR] - Failed to parse status JSON')
78
+ except Exception as e:
79
+ print('Loki Mode [ERROR] - ' + str(e))
80
+ "
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env bash
2
+ # Poll loki status and output structured progress for OpenClaw channel routing
3
+ # Usage: poll-status.sh [workdir]
4
+ #
5
+ # Reads loki status --json output and enriches it with budget data
6
+ # from .loki/metrics/budget.json and council verdict from .loki/council/state.json.
7
+ # Outputs a single JSON object suitable for channel message formatting.
8
+
9
+ set -euo pipefail
10
+
11
+ WORKDIR="${1:-.}"
12
+ cd "$WORKDIR" || { echo '{"error": "Cannot access workdir: '"$WORKDIR"'"}'; exit 1; }
13
+
14
+ # Get base status from loki CLI
15
+ STATUS_JSON=$(loki status --json 2>/dev/null)
16
+ if [ $? -ne 0 ]; then
17
+ echo '{"error": "loki status failed", "suggestion": "Is loki installed and a session running?"}'
18
+ exit 1
19
+ fi
20
+
21
+ # Enrich with budget and council data (not included in loki status --json)
22
+ python3 -c "
23
+ import json, sys, os
24
+
25
+ try:
26
+ s = json.load(sys.stdin)
27
+ tc = s.get('task_counts', {})
28
+
29
+ output = {
30
+ 'status': s.get('status', 'unknown'),
31
+ 'phase': s.get('phase'),
32
+ 'iteration': s.get('iteration', 0),
33
+ 'tasks_completed': tc.get('completed', 0),
34
+ 'tasks_total': tc.get('total', 0),
35
+ 'tasks_failed': tc.get('failed', 0),
36
+ 'tasks_pending': tc.get('pending', 0),
37
+ 'elapsed_minutes': round(s.get('elapsed_time', 0) / 60, 1),
38
+ 'provider': s.get('provider', 'claude'),
39
+ 'version': s.get('version', 'unknown'),
40
+ 'pid': s.get('pid'),
41
+ 'dashboard_url': s.get('dashboard_url'),
42
+ }
43
+
44
+ # Read budget data from flat file (not in loki status --json)
45
+ budget_file = os.path.join('.loki', 'metrics', 'budget.json')
46
+ if os.path.isfile(budget_file):
47
+ try:
48
+ with open(budget_file) as f:
49
+ budget = json.load(f)
50
+ output['budget_used'] = round(budget.get('budget_used', 0), 2)
51
+ output['budget_limit'] = budget.get('budget_limit')
52
+ except Exception:
53
+ output['budget_used'] = None
54
+ output['budget_limit'] = None
55
+ else:
56
+ output['budget_used'] = None
57
+ output['budget_limit'] = None
58
+
59
+ # Read council verdict from flat file (not in loki status --json)
60
+ council_file = os.path.join('.loki', 'council', 'state.json')
61
+ if os.path.isfile(council_file):
62
+ try:
63
+ with open(council_file) as f:
64
+ council = json.load(f)
65
+ output['council_verdict'] = council.get('verdict')
66
+ except Exception:
67
+ output['council_verdict'] = None
68
+ else:
69
+ output['council_verdict'] = None
70
+
71
+ print(json.dumps(output, indent=2))
72
+ except Exception as e:
73
+ print(json.dumps({'error': str(e)}))
74
+ " <<< "$STATUS_JSON"