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
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v6.
|
|
5
|
+
**Version:** v6.62.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -54,7 +54,7 @@ npm install -g loki-mode
|
|
|
54
54
|
|
|
55
55
|
Installs the `loki` CLI and automatically sets up the skill for Claude Code, Codex CLI, and Gemini CLI via the postinstall script.
|
|
56
56
|
|
|
57
|
-
**Prerequisites:** Node.js
|
|
57
|
+
**Prerequisites:** Node.js 18+
|
|
58
58
|
|
|
59
59
|
**What it does:**
|
|
60
60
|
- Installs the `loki` CLI binary to your PATH
|
package/events/bus.py
CHANGED
|
@@ -9,12 +9,12 @@ import json
|
|
|
9
9
|
import os
|
|
10
10
|
import time
|
|
11
11
|
import uuid
|
|
12
|
-
import
|
|
12
|
+
from collections import OrderedDict
|
|
13
13
|
from dataclasses import dataclass, asdict
|
|
14
14
|
from datetime import datetime, timezone
|
|
15
15
|
from enum import Enum
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Generator, List, Optional,
|
|
17
|
+
from typing import Generator, List, Optional, Callable
|
|
18
18
|
import threading
|
|
19
19
|
|
|
20
20
|
|
|
@@ -72,11 +72,35 @@ class LokiEvent:
|
|
|
72
72
|
|
|
73
73
|
@classmethod
|
|
74
74
|
def from_dict(cls, data: dict) -> 'LokiEvent':
|
|
75
|
-
"""Create event from dictionary.
|
|
75
|
+
"""Create event from dictionary.
|
|
76
|
+
|
|
77
|
+
Handles compound types like 'session_start' by splitting on underscore
|
|
78
|
+
and using the first token as the event type.
|
|
79
|
+
"""
|
|
80
|
+
raw_type = data.get('type', '')
|
|
81
|
+
raw_source = data.get('source', '')
|
|
82
|
+
|
|
83
|
+
# Parse event type, handling compound values like "session_start"
|
|
84
|
+
try:
|
|
85
|
+
event_type = EventType(raw_type) if raw_type else EventType.STATE
|
|
86
|
+
except ValueError:
|
|
87
|
+
# Try splitting compound type on underscore
|
|
88
|
+
first_token = raw_type.split('_')[0] if raw_type else ''
|
|
89
|
+
try:
|
|
90
|
+
event_type = EventType(first_token)
|
|
91
|
+
except ValueError:
|
|
92
|
+
event_type = EventType.STATE
|
|
93
|
+
|
|
94
|
+
# Parse event source with fallback
|
|
95
|
+
try:
|
|
96
|
+
event_source = EventSource(raw_source) if raw_source else EventSource.CLI
|
|
97
|
+
except ValueError:
|
|
98
|
+
event_source = EventSource.CLI
|
|
99
|
+
|
|
76
100
|
return cls(
|
|
77
101
|
id=data.get('id', ''),
|
|
78
|
-
type=
|
|
79
|
-
source=
|
|
102
|
+
type=event_type,
|
|
103
|
+
source=event_source,
|
|
80
104
|
timestamp=data.get('timestamp', ''),
|
|
81
105
|
payload=data.get('payload', {}),
|
|
82
106
|
version=data.get('version', '1.0')
|
|
@@ -115,44 +139,54 @@ class EventBus:
|
|
|
115
139
|
self.pending_dir.mkdir(parents=True, exist_ok=True)
|
|
116
140
|
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
117
141
|
|
|
118
|
-
# Track processed event IDs
|
|
119
|
-
self._processed_ids:
|
|
142
|
+
# Track processed event IDs with deterministic FIFO eviction order
|
|
143
|
+
self._processed_ids: OrderedDict = self._load_processed_ids()
|
|
120
144
|
|
|
121
145
|
# Subscribers for callback-based subscription
|
|
122
146
|
self._subscribers: List[tuple] = [] # [(types, callback), ...]
|
|
123
147
|
self._running = False
|
|
124
148
|
self._thread: Optional[threading.Thread] = None
|
|
125
149
|
|
|
126
|
-
def _load_processed_ids(self) ->
|
|
127
|
-
"""Load
|
|
150
|
+
def _load_processed_ids(self) -> OrderedDict:
|
|
151
|
+
"""Load ordered dict of already processed event IDs."""
|
|
128
152
|
try:
|
|
129
153
|
if self.processed_file.exists():
|
|
130
154
|
with open(self.processed_file, 'r') as f:
|
|
131
155
|
data = json.load(f)
|
|
132
156
|
# Keep only last 1000 IDs to prevent unbounded growth
|
|
133
|
-
|
|
157
|
+
ids = data.get('ids', [])[-1000:]
|
|
158
|
+
return OrderedDict.fromkeys(ids)
|
|
134
159
|
except (json.JSONDecodeError, IOError):
|
|
135
160
|
pass
|
|
136
|
-
return
|
|
161
|
+
return OrderedDict()
|
|
137
162
|
|
|
138
163
|
def _save_processed_id(self, event_id: str) -> None:
|
|
139
164
|
"""Save event ID as processed."""
|
|
140
|
-
self._processed_ids
|
|
165
|
+
self._processed_ids[event_id] = None
|
|
141
166
|
|
|
142
|
-
# Prune to last 1000
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
self._processed_ids = set(pruned)
|
|
167
|
+
# Prune to last 1000 using FIFO eviction (deterministic order)
|
|
168
|
+
while len(self._processed_ids) > 1000:
|
|
169
|
+
self._processed_ids.popitem(last=False)
|
|
146
170
|
|
|
171
|
+
lockfile = self.processed_file.with_suffix('.json.lock')
|
|
147
172
|
try:
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
# Use lockfile approach (cross-platform, compatible with TS)
|
|
174
|
+
fd = os.open(str(lockfile), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
175
|
+
os.close(fd)
|
|
176
|
+
try:
|
|
177
|
+
with open(self.processed_file, 'w') as f:
|
|
178
|
+
json.dump({'ids': list(self._processed_ids.keys())}, f)
|
|
179
|
+
finally:
|
|
150
180
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
lockfile.unlink()
|
|
182
|
+
except OSError:
|
|
183
|
+
pass
|
|
184
|
+
except FileExistsError:
|
|
185
|
+
# Another process holds the lock; skip this disk write.
|
|
186
|
+
# In-memory state is already updated.
|
|
187
|
+
pass
|
|
154
188
|
except IOError:
|
|
155
|
-
# Disk write failed --
|
|
189
|
+
# Disk write failed -- in-memory state is already updated
|
|
156
190
|
pass
|
|
157
191
|
|
|
158
192
|
def emit(self, event: LokiEvent) -> str:
|
|
@@ -166,14 +200,25 @@ class EventBus:
|
|
|
166
200
|
The event ID
|
|
167
201
|
"""
|
|
168
202
|
event_file = self.pending_dir / f"{event.timestamp.replace(':', '-')}_{event.id}.json"
|
|
203
|
+
lockfile = event_file.with_suffix('.json.lock')
|
|
169
204
|
|
|
170
205
|
try:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
206
|
+
# Use lockfile approach (cross-platform, compatible with TS)
|
|
207
|
+
fd = os.open(str(lockfile), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
208
|
+
os.close(fd)
|
|
209
|
+
try:
|
|
210
|
+
with open(event_file, 'w') as f:
|
|
174
211
|
json.dump(event.to_dict(), f, indent=2)
|
|
175
|
-
|
|
176
|
-
|
|
212
|
+
finally:
|
|
213
|
+
try:
|
|
214
|
+
lockfile.unlink()
|
|
215
|
+
except OSError:
|
|
216
|
+
pass
|
|
217
|
+
except FileExistsError:
|
|
218
|
+
# Lock held by another process; write without lock as fallback
|
|
219
|
+
# Event files are unique per ID so collision is unlikely
|
|
220
|
+
with open(event_file, 'w') as f:
|
|
221
|
+
json.dump(event.to_dict(), f, indent=2)
|
|
177
222
|
except IOError as e:
|
|
178
223
|
raise RuntimeError(f"Failed to emit event: {e}")
|
|
179
224
|
|
|
@@ -286,8 +331,11 @@ class EventBus:
|
|
|
286
331
|
if timeout and (time.time() - start_time) > timeout:
|
|
287
332
|
break
|
|
288
333
|
|
|
334
|
+
# Set last_check BEFORE fetching to avoid missing events that
|
|
335
|
+
# arrive between fetch and timestamp update
|
|
336
|
+
next_check = datetime.now(timezone.utc).isoformat()
|
|
289
337
|
events = self.get_pending_events(types=types, since=last_check)
|
|
290
|
-
last_check =
|
|
338
|
+
last_check = next_check
|
|
291
339
|
|
|
292
340
|
for event in events:
|
|
293
341
|
yield event
|
|
@@ -410,6 +458,59 @@ class EventBus:
|
|
|
410
458
|
pass
|
|
411
459
|
return count
|
|
412
460
|
|
|
461
|
+
def import_from_jsonl(self, limit: int = 100) -> int:
|
|
462
|
+
"""Bridge: read events from events.jsonl and import as pending events.
|
|
463
|
+
|
|
464
|
+
This bridges the gap between the append-only events.jsonl log
|
|
465
|
+
(written by emit.sh and run.sh) and the pending/ directory
|
|
466
|
+
consumed by subscribers.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
limit: Maximum number of recent events to import
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Number of events imported
|
|
473
|
+
"""
|
|
474
|
+
jsonl_file = self.loki_dir / 'events.jsonl'
|
|
475
|
+
if not jsonl_file.exists():
|
|
476
|
+
return 0
|
|
477
|
+
|
|
478
|
+
imported = 0
|
|
479
|
+
try:
|
|
480
|
+
with open(jsonl_file, 'r') as f:
|
|
481
|
+
# Read from end of file to get most recent events
|
|
482
|
+
lines = f.readlines()
|
|
483
|
+
recent = lines[-limit:] if len(lines) > limit else lines
|
|
484
|
+
|
|
485
|
+
for line in recent:
|
|
486
|
+
line = line.strip()
|
|
487
|
+
if not line:
|
|
488
|
+
continue
|
|
489
|
+
try:
|
|
490
|
+
data = json.loads(line)
|
|
491
|
+
event = LokiEvent.from_dict(data)
|
|
492
|
+
|
|
493
|
+
# Skip if already processed
|
|
494
|
+
if event.id in self._processed_ids:
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
# Check if pending file already exists
|
|
498
|
+
existing = list(self.pending_dir.glob(f'*_{event.id}.json'))
|
|
499
|
+
if existing:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
# Write as pending event
|
|
503
|
+
event_file = self.pending_dir / f"{event.timestamp.replace(':', '-')}_{event.id}.json"
|
|
504
|
+
with open(event_file, 'w') as f:
|
|
505
|
+
json.dump(event.to_dict(), f, indent=2)
|
|
506
|
+
imported += 1
|
|
507
|
+
except (json.JSONDecodeError, KeyError):
|
|
508
|
+
continue
|
|
509
|
+
except IOError:
|
|
510
|
+
pass
|
|
511
|
+
|
|
512
|
+
return imported
|
|
513
|
+
|
|
413
514
|
def clear_archive(self, older_than_days: int = 7) -> int:
|
|
414
515
|
"""
|
|
415
516
|
Clear archived events older than specified days.
|
package/events/bus.ts
CHANGED
|
@@ -69,40 +69,54 @@ function getTimestamp(): string {
|
|
|
69
69
|
return new Date().toISOString();
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Sleep helper that does not busy-wait.
|
|
74
|
+
*/
|
|
75
|
+
function sleepSync(ms: number): void {
|
|
76
|
+
const end = Date.now() + ms;
|
|
77
|
+
while (Date.now() < end) {
|
|
78
|
+
// Intentional short spin -- only used for brief lock retries (< 200ms)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
/**
|
|
73
83
|
* Write a file with an exclusive lockfile to prevent concurrent corruption.
|
|
74
84
|
* Creates a .lock file, writes data, then removes the lock.
|
|
85
|
+
* If the lock cannot be acquired after retries, the write is skipped
|
|
86
|
+
* and a warning is logged to stderr.
|
|
75
87
|
*/
|
|
76
88
|
function writeFileWithLock(filepath: string, data: string): void {
|
|
77
89
|
const lockfile = filepath + ".lock";
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
fs.closeSync(fd);
|
|
97
|
-
fd = null;
|
|
98
|
-
fs.writeFileSync(filepath, data);
|
|
99
|
-
} catch {
|
|
100
|
-
// Fall back to unlocked write
|
|
101
|
-
fs.writeFileSync(filepath, data);
|
|
90
|
+
const maxRetries = 5;
|
|
91
|
+
const retryDelayMs = 200;
|
|
92
|
+
let acquired = false;
|
|
93
|
+
|
|
94
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
95
|
+
try {
|
|
96
|
+
const fd = fs.openSync(lockfile, "wx");
|
|
97
|
+
fs.closeSync(fd);
|
|
98
|
+
acquired = true;
|
|
99
|
+
break;
|
|
100
|
+
} catch (e: unknown) {
|
|
101
|
+
if (e instanceof Error && "code" in e && (e as NodeJS.ErrnoException).code === "EEXIST") {
|
|
102
|
+
// Lock held by another process; wait and retry
|
|
103
|
+
if (attempt < maxRetries - 1) {
|
|
104
|
+
sleepSync(retryDelayMs);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
throw e;
|
|
102
108
|
}
|
|
103
|
-
} else {
|
|
104
|
-
throw e;
|
|
105
109
|
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!acquired) {
|
|
113
|
+
// Lock could not be acquired after retries -- skip write to avoid corruption
|
|
114
|
+
process.stderr.write(`[loki-events] WARNING: could not acquire lock for ${filepath}, skipping write\n`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
fs.writeFileSync(filepath, data);
|
|
106
120
|
} finally {
|
|
107
121
|
try {
|
|
108
122
|
fs.unlinkSync(lockfile);
|
|
@@ -124,7 +138,7 @@ export class EventBus {
|
|
|
124
138
|
private processedIds: Set<string>;
|
|
125
139
|
private subscribers: Array<{ types: EventType[] | null; callback: EventCallback }>;
|
|
126
140
|
private running: boolean;
|
|
127
|
-
private pollInterval:
|
|
141
|
+
private pollInterval: ReturnType<typeof setInterval> | null;
|
|
128
142
|
|
|
129
143
|
constructor(lokiDir: string = ".loki") {
|
|
130
144
|
this.lokiDir = lokiDir;
|
package/events/emit.sh
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Loki Mode - OpenClaw Integration
|
|
2
|
+
|
|
3
|
+
Run Loki Mode autonomous SDLC sessions from any OpenClaw channel (Slack, Discord, Teams, web).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
1. Install Loki Mode CLI:
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g loki-mode
|
|
10
|
+
# or
|
|
11
|
+
brew install asklokesh/tap/loki-mode
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
2. Copy skill to OpenClaw workspace:
|
|
15
|
+
```bash
|
|
16
|
+
cp -r integrations/openclaw/ ~/.openclaw/workspace/skills/loki-mode/
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. Configure API keys in the OpenClaw environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY).
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
From any connected channel, the agent will invoke Loki Mode when:
|
|
24
|
+
- You ask it to "build" or "implement" something from a PRD
|
|
25
|
+
- You say "loki mode" with a project reference
|
|
26
|
+
- You provide requirements for autonomous development
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Channel (Slack/Discord/Web)
|
|
32
|
+
|
|
|
33
|
+
v
|
|
34
|
+
OpenClaw Gateway --> Agent routes to loki-mode skill
|
|
35
|
+
|
|
|
36
|
+
v
|
|
37
|
+
loki start --bg --yes <prd> (background process)
|
|
38
|
+
|
|
|
39
|
+
v
|
|
40
|
+
Poll loop: loki status --json (every 30s)
|
|
41
|
+
|
|
|
42
|
+
v
|
|
43
|
+
Progress messages back to channel
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Helper Scripts
|
|
47
|
+
|
|
48
|
+
Two helper scripts are provided in `scripts/` for structured status polling and formatting:
|
|
49
|
+
|
|
50
|
+
- `poll-status.sh [workdir]` -- Calls `loki status --json` and enriches the output with budget and council data from `.loki/` flat files. Returns a single JSON object.
|
|
51
|
+
- `format-progress.sh` -- Reads the JSON output from `poll-status.sh` via stdin and produces a human-readable multi-line progress message suitable for channel posting.
|
|
52
|
+
|
|
53
|
+
Example pipeline:
|
|
54
|
+
```bash
|
|
55
|
+
./scripts/poll-status.sh /path/to/project | ./scripts/format-progress.sh
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
| Env Var | Description | Default |
|
|
61
|
+
|---------|-------------|---------|
|
|
62
|
+
| LOKI_PROVIDER | AI provider (claude/codex/gemini) | claude |
|
|
63
|
+
| LOKI_BUDGET_LIMIT | Cost limit in USD | unlimited |
|
|
64
|
+
| LOKI_MAX_PARALLEL_AGENTS | Max concurrent agents | 10 |
|
|
65
|
+
| LOKI_COMPLEXITY | Force complexity tier (simple/standard/complex) | auto |
|
|
66
|
+
| LOKI_DASHBOARD_PORT | Dashboard HTTP port | 57374 |
|
|
67
|
+
|
|
68
|
+
## Phase 2: Bridge Daemon (Foundation)
|
|
69
|
+
|
|
70
|
+
The bridge daemon watches Loki's `.loki/` flat-file events in real time and translates them into OpenClaw gateway messages. This replaces the polling-based approach (Phase 1) with event-driven communication.
|
|
71
|
+
|
|
72
|
+
### What exists now
|
|
73
|
+
|
|
74
|
+
| Component | Path | Status |
|
|
75
|
+
|-----------|------|--------|
|
|
76
|
+
| Event schema mapping | `bridge/schema_map.py` | Implemented -- maps 15 Loki event types to OpenClaw `sessions_send` messages |
|
|
77
|
+
| File watcher | `bridge/watcher.py` | Implemented -- polls `events/pending/` and `dashboard-state.json` |
|
|
78
|
+
| CLI entry point | `bridge/__main__.py` | Implemented -- prints mapped events as JSON to stdout |
|
|
79
|
+
|
|
80
|
+
### Running the bridge (stdout mode)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# From the project root where .loki/ exists
|
|
84
|
+
python -m integrations.openclaw.bridge --loki-dir .loki
|
|
85
|
+
|
|
86
|
+
# With custom poll interval
|
|
87
|
+
python -m integrations.openclaw.bridge --loki-dir .loki --poll-interval 0.5
|
|
88
|
+
|
|
89
|
+
# The --gateway flag is accepted but not yet functional
|
|
90
|
+
python -m integrations.openclaw.bridge --loki-dir .loki --gateway ws://127.0.0.1:18789
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Events are printed to stdout as JSON, one per line:
|
|
94
|
+
```json
|
|
95
|
+
{"method": "sessions_send", "params": {"message": "Loki Mode started. Provider: claude. PRD: my-app.md", "source": "loki-bridge", "event_type": "session.start", "timestamp": "2026-02-12T00:00:00Z", "loki_event_id": "a1b2c3d4"}}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### What is NOT yet implemented
|
|
99
|
+
|
|
100
|
+
- **WebSocket gateway client** -- the `--gateway` flag is parsed but the actual WebSocket connection to OpenClaw is not built yet. Events go to stdout only.
|
|
101
|
+
- **Reconnection logic** -- automatic reconnect on gateway disconnect.
|
|
102
|
+
- **Event filtering** -- ability to select which event types to forward.
|
|
103
|
+
- **Authentication** -- token-based auth for the OpenClaw gateway.
|
|
104
|
+
- **Backpressure** -- buffering when the gateway is slow or unavailable.
|
|
105
|
+
- **Async I/O** -- the watcher uses synchronous polling; a future version may use asyncio or inotify/kqueue for lower latency.
|
|
106
|
+
- **Two-way communication** -- sending OpenClaw commands back to Loki (pause, stop, etc.).
|
|
107
|
+
|
|
108
|
+
### Event schema mapping
|
|
109
|
+
|
|
110
|
+
The bridge normalizes two Loki event formats into a canonical dot-notation key:
|
|
111
|
+
|
|
112
|
+
1. **Individual JSON files** (`events/pending/*.json` from `events/emit.sh`): `type` + `payload.action` -> `"session.start"`
|
|
113
|
+
2. **JSONL entries** (`events.jsonl` from `emit_event_json`): underscore-compound `type` -> `"session_start"` -> `"session.start"`
|
|
114
|
+
|
|
115
|
+
Currently mapped event types: `session.start`, `session.stop`, `session.end`, `phase.change`, `iteration.start`, `iteration.complete`, `task.complete`, `error.failed`, `council.vote`, `council.verdict`, `budget.exceeded`, `budget.warning`, `code_review.start`, `code_review.complete`, `watchdog.alert`.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Status JSON Schema
|
|
120
|
+
|
|
121
|
+
The enriched JSON from `poll-status.sh` contains:
|
|
122
|
+
|
|
123
|
+
| Field | Type | Description |
|
|
124
|
+
|-------|------|-------------|
|
|
125
|
+
| status | string | inactive, running, paused, stopped, completed, unknown |
|
|
126
|
+
| phase | string/null | BOOTSTRAP, DISCOVERY, ARCHITECTURE, DEVELOPMENT, QA, DEPLOYMENT |
|
|
127
|
+
| iteration | number | Current iteration count |
|
|
128
|
+
| tasks_completed | number | Tasks finished successfully |
|
|
129
|
+
| tasks_total | number | Total tasks discovered |
|
|
130
|
+
| tasks_failed | number | Tasks that errored |
|
|
131
|
+
| tasks_pending | number | Tasks not yet started |
|
|
132
|
+
| elapsed_minutes | number | Minutes since session start |
|
|
133
|
+
| provider | string | Active AI provider |
|
|
134
|
+
| version | string | Loki Mode version |
|
|
135
|
+
| pid | number/null | Session process ID |
|
|
136
|
+
| dashboard_url | string/null | Dashboard URL if running |
|
|
137
|
+
| budget_used | number/null | USD spent so far |
|
|
138
|
+
| budget_limit | number/null | USD budget cap |
|
|
139
|
+
| council_verdict | string/null | Completion council decision |
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loki-mode
|
|
3
|
+
description: "Launch Loki Mode autonomous SDLC agent. Handles PRD-to-deployment with minimal human intervention. Invoke for multi-phase development tasks, bug fixing campaigns, or full product builds."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Loki Mode - OpenClaw Skill
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
- User asks to "build", "implement", or "develop" a feature from a PRD
|
|
10
|
+
- User provides a requirements document and wants autonomous execution
|
|
11
|
+
- User says "loki mode" or references autonomous development
|
|
12
|
+
- User wants to run a full SDLC cycle on a codebase
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
- `loki` CLI installed on the host (via `npm install -g loki-mode` or Homebrew)
|
|
16
|
+
- One of: Claude Code, Codex CLI, or Gemini CLI installed
|
|
17
|
+
- Corresponding API key set (ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY)
|
|
18
|
+
|
|
19
|
+
## How to invoke
|
|
20
|
+
|
|
21
|
+
### Start a session
|
|
22
|
+
Use the bash tool with background mode:
|
|
23
|
+
```
|
|
24
|
+
bash(command: "loki start <prd-path> --bg --yes --no-dashboard", pty: true, background: true, workdir: "<project-dir>")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Key flags:
|
|
28
|
+
- `--bg`: Background mode (session outlives the tool call)
|
|
29
|
+
- `--yes`: Skip confirmation prompts
|
|
30
|
+
- `--no-dashboard`: Avoid port conflicts in sandboxed environments
|
|
31
|
+
- `--provider <claude|codex|gemini>`: Select AI provider (default: claude)
|
|
32
|
+
- `--budget <amount>`: Set cost limit in USD (auto-pause when exceeded)
|
|
33
|
+
|
|
34
|
+
### Monitor progress
|
|
35
|
+
Poll status every 30 seconds:
|
|
36
|
+
```
|
|
37
|
+
bash(command: "loki status --json", workdir: "<project-dir>")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The JSON output contains:
|
|
41
|
+
- `version`: Loki Mode version string
|
|
42
|
+
- `status`: inactive, running, paused, stopped, completed, unknown
|
|
43
|
+
- `phase`: Current SDLC phase (e.g., BOOTSTRAP, DISCOVERY, ARCHITECTURE, DEVELOPMENT, QA, DEPLOYMENT)
|
|
44
|
+
- `iteration`: Current iteration number
|
|
45
|
+
- `provider`: Which AI provider is active (claude, codex, gemini)
|
|
46
|
+
- `pid`: Process ID of the running session (null if not running)
|
|
47
|
+
- `elapsed_time`: Seconds since session start
|
|
48
|
+
- `dashboard_url`: URL of the web dashboard (null if disabled)
|
|
49
|
+
- `task_counts`: Object with `total`, `completed`, `failed`, `pending` counts
|
|
50
|
+
|
|
51
|
+
For budget tracking (not in JSON output), read the budget file directly:
|
|
52
|
+
```
|
|
53
|
+
bash(command: "cat .loki/metrics/budget.json 2>/dev/null || echo '{}'", workdir: "<project-dir>")
|
|
54
|
+
```
|
|
55
|
+
Budget JSON fields: `budget_limit`, `budget_used`
|
|
56
|
+
|
|
57
|
+
### Report progress to channel
|
|
58
|
+
After each poll, summarize changes:
|
|
59
|
+
- Phase transitions ("Moved from ARCHITECTURE to DEVELOPMENT")
|
|
60
|
+
- Task completion counts ("12/20 tasks complete, 0 failed")
|
|
61
|
+
- Elapsed time ("Running for 45 minutes")
|
|
62
|
+
- Error states that need attention (failed tasks > 0, status is unknown)
|
|
63
|
+
|
|
64
|
+
If budget tracking is active, include cost in updates:
|
|
65
|
+
- "Estimated cost: $4.50 / $50.00 budget"
|
|
66
|
+
|
|
67
|
+
### Control commands
|
|
68
|
+
- Pause: `bash(command: "loki pause", workdir: "<project-dir>")`
|
|
69
|
+
- Resume: `bash(command: "loki resume", workdir: "<project-dir>")`
|
|
70
|
+
- Stop: `bash(command: "loki stop", workdir: "<project-dir>")`
|
|
71
|
+
- Status: `bash(command: "loki status", workdir: "<project-dir>")`
|
|
72
|
+
- Logs: `bash(command: "loki logs --tail 50", workdir: "<project-dir>")`
|
|
73
|
+
|
|
74
|
+
### Session complete
|
|
75
|
+
When status becomes "stopped" or "completed":
|
|
76
|
+
1. Run `loki status --json` for final summary
|
|
77
|
+
2. Run `git log --oneline -20` to show commits made
|
|
78
|
+
3. Report final task counts, elapsed time, and duration
|
|
79
|
+
4. If council verdict exists, include it: `cat .loki/council/report.md`
|
|
80
|
+
|
|
81
|
+
## Critical rules
|
|
82
|
+
- ALWAYS use --bg flag (session must outlive the tool call)
|
|
83
|
+
- ALWAYS use --yes flag (no confirmation prompts in non-interactive channels)
|
|
84
|
+
- NEVER run loki in the OpenClaw workspace directory itself
|
|
85
|
+
- Poll status rather than watching stdout (background mode detaches)
|
|
86
|
+
- If session crashes, check `loki logs` before restarting
|
|
87
|
+
- Respect budget limits -- include cost in every progress update when tracking is active
|
|
88
|
+
- The --no-dashboard flag is recommended to avoid port conflicts in sandboxed environments
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Loki-OpenClaw Bridge -- foundation package
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Loki-OpenClaw Bridge Daemon entry point.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m integrations.openclaw.bridge --loki-dir .loki
|
|
5
|
+
python -m integrations.openclaw.bridge --loki-dir .loki --gateway ws://127.0.0.1:18789
|
|
6
|
+
|
|
7
|
+
NOTE: This is a foundation/skeleton. The WebSocket gateway client is not yet
|
|
8
|
+
implemented. Currently logs mapped events to stdout as JSON for testing.
|
|
9
|
+
The --gateway flag is accepted but has no effect until the WebSocket client
|
|
10
|
+
is built in a future phase.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import signal
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from .schema_map import map_event
|
|
19
|
+
from .watcher import LokiFileWatcher
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _on_event(event: dict) -> None:
|
|
23
|
+
"""Handle a raw Loki event: map it and print as JSON to stdout."""
|
|
24
|
+
mapped = map_event(event)
|
|
25
|
+
if mapped is not None:
|
|
26
|
+
try:
|
|
27
|
+
print(json.dumps(mapped), flush=True)
|
|
28
|
+
except (TypeError, ValueError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> None:
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
prog="loki-openclaw-bridge",
|
|
35
|
+
description="Watch Loki Mode events and translate to OpenClaw gateway format.",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--loki-dir",
|
|
39
|
+
required=True,
|
|
40
|
+
help="Path to the .loki directory to watch.",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--gateway",
|
|
44
|
+
default=None,
|
|
45
|
+
help=(
|
|
46
|
+
"WebSocket URL for the OpenClaw gateway (e.g. ws://127.0.0.1:18789). "
|
|
47
|
+
"Not yet implemented -- events are printed to stdout regardless."
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--poll-interval",
|
|
52
|
+
type=float,
|
|
53
|
+
default=1.0,
|
|
54
|
+
help="Seconds between directory polls (default: 1.0).",
|
|
55
|
+
)
|
|
56
|
+
args = parser.parse_args()
|
|
57
|
+
|
|
58
|
+
if args.gateway:
|
|
59
|
+
print(
|
|
60
|
+
f"[bridge] --gateway {args.gateway} accepted but WebSocket client "
|
|
61
|
+
"is not yet implemented. Events will be printed to stdout.",
|
|
62
|
+
file=sys.stderr,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
watcher = LokiFileWatcher(
|
|
66
|
+
loki_dir=args.loki_dir,
|
|
67
|
+
on_event=_on_event,
|
|
68
|
+
poll_interval=args.poll_interval,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Graceful shutdown on Ctrl+C and SIGTERM
|
|
72
|
+
def _shutdown(signum, frame):
|
|
73
|
+
print("\n[bridge] Shutting down.", file=sys.stderr)
|
|
74
|
+
watcher.stop()
|
|
75
|
+
|
|
76
|
+
signal.signal(signal.SIGINT, _shutdown)
|
|
77
|
+
signal.signal(signal.SIGTERM, _shutdown)
|
|
78
|
+
|
|
79
|
+
print(
|
|
80
|
+
f"[bridge] Watching {args.loki_dir} (poll every {args.poll_interval}s). "
|
|
81
|
+
"Press Ctrl+C to stop.",
|
|
82
|
+
file=sys.stderr,
|
|
83
|
+
)
|
|
84
|
+
watcher.start()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|