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
@@ -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.60.0
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 16+
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 fcntl
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, Set, Callable
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=EventType(data['type']) if data.get('type') else EventType.STATE,
79
- source=EventSource(data['source']) if data.get('source') else EventSource.CLI,
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 to avoid reprocessing
119
- self._processed_ids: Set[str] = self._load_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) -> Set[str]:
127
- """Load set of already processed event IDs."""
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
- return set(data.get('ids', [])[-1000:])
157
+ ids = data.get('ids', [])[-1000:]
158
+ return OrderedDict.fromkeys(ids)
134
159
  except (json.JSONDecodeError, IOError):
135
160
  pass
136
- return set()
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.add(event_id)
165
+ self._processed_ids[event_id] = None
141
166
 
142
- # Prune to last 1000 (always, regardless of disk write outcome)
143
- if len(self._processed_ids) > 1000:
144
- pruned = list(self._processed_ids)[-1000:]
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
- with open(self.processed_file, 'w') as f:
149
- fcntl.flock(f.fileno(), fcntl.LOCK_EX)
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
- json.dump({'ids': list(self._processed_ids)}, f)
152
- finally:
153
- fcntl.flock(f.fileno(), fcntl.LOCK_UN)
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 -- set is already pruned in memory above
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
- with open(event_file, 'w') as f:
172
- fcntl.flock(f.fileno(), fcntl.LOCK_EX)
173
- try:
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
- finally:
176
- fcntl.flock(f.fileno(), fcntl.LOCK_UN)
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 = datetime.now(timezone.utc).isoformat()
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
- let fd: number | null = null;
79
- try {
80
- // Acquire lock by creating lockfile exclusively (fails if already exists)
81
- fd = fs.openSync(lockfile, "wx");
82
- fs.closeSync(fd);
83
- fd = null;
84
- // Write the actual data
85
- fs.writeFileSync(filepath, data);
86
- } catch (e: unknown) {
87
- // If lock acquisition failed (file exists), retry once after brief delay
88
- if (e instanceof Error && "code" in e && (e as NodeJS.ErrnoException).code === "EEXIST") {
89
- // Another process holds the lock; wait briefly and retry
90
- const start = Date.now();
91
- while (fs.existsSync(lockfile) && Date.now() - start < 1000) {
92
- // Busy-wait up to 1 second
93
- }
94
- try {
95
- fd = fs.openSync(lockfile, "wx");
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: NodeJS.Timer | null;
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
@@ -12,7 +12,7 @@
12
12
  # Environment:
13
13
  # LOKI_DIR - Path to .loki directory (default: .loki)
14
14
 
15
- set -uo pipefail
15
+ set -euo pipefail
16
16
 
17
17
  # Configuration
18
18
  LOKI_DIR="${LOKI_DIR:-.loki}"
@@ -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()