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.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +34 -8
- package/autonomy/completion-council.sh +70 -32
- package/autonomy/issue-parser.sh +4 -7
- package/autonomy/loki +238 -119
- package/autonomy/notification-checker.py +49 -23
- package/autonomy/run.sh +162 -79
- package/autonomy/sandbox.sh +91 -24
- package/bin/loki-mode.js +1 -2
- package/bin/postinstall.js +10 -4
- package/dashboard/__init__.py +1 -1
- package/dashboard/control.py +46 -36
- package/dashboard/database.py +21 -4
- package/dashboard/server.py +107 -78
- package/docs/BUG-AUDIT-v6.61.0.md +957 -0
- package/docs/INSTALLATION.md +2 -2
- package/events/bus.py +129 -28
- package/events/bus.ts +41 -27
- package/events/emit.sh +1 -1
- package/integrations/openclaw/README.md +139 -0
- package/integrations/openclaw/SKILL.md +88 -0
- package/integrations/openclaw/bridge/__init__.py +1 -0
- package/integrations/openclaw/bridge/__main__.py +88 -0
- package/integrations/openclaw/bridge/schema_map.py +180 -0
- package/integrations/openclaw/bridge/watcher.py +100 -0
- package/integrations/openclaw/scripts/format-progress.sh +80 -0
- package/integrations/openclaw/scripts/poll-status.sh +74 -0
- package/integrations/vibe-kanban.md +289 -0
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +96 -73
- package/memory/consolidation.py +21 -6
- package/memory/engine.py +53 -26
- package/memory/layers/index_layer.py +16 -3
- package/memory/layers/timeline_layer.py +16 -3
- package/memory/retrieval.py +4 -1
- package/memory/schemas.py +4 -2
- package/memory/storage.py +25 -4
- package/memory/token_economics.py +9 -2
- package/memory/vector_index.py +2 -2
- package/package.json +3 -1
- package/providers/cline.sh +5 -4
- package/providers/codex.sh +27 -5
- package/providers/gemini.sh +59 -23
- package/providers/loader.sh +3 -2
- package/skills/parallel-workflows.md +9 -7
- package/state/__init__.py +10 -0
- package/state/index.ts +18 -0
- package/state/manager.py +1801 -0
- package/state/manager.ts +1774 -0
- package/state/sqlite_backend.py +188 -0
- package/state/test_manager.py +703 -0
- package/state/test_manager.ts +366 -0
- package/templates/README.md +19 -4
- package/templates/dashboard.md +45 -0
- package/templates/data-pipeline.md +45 -0
- package/templates/game.md +48 -0
- package/templates/microservice.md +49 -0
- package/templates/npm-library.md +42 -0
- package/templates/rest-api.md +170 -33
- package/templates/slack-bot.md +48 -0
- package/templates/web-scraper.md +45 -0
- package/web-app/server.py +360 -191
- 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"
|