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,289 @@
|
|
|
1
|
+
# Vibe Kanban Integration
|
|
2
|
+
|
|
3
|
+
Loki Mode can optionally integrate with [Vibe Kanban](https://github.com/BloopAI/vibe-kanban) to provide a visual dashboard for monitoring autonomous execution.
|
|
4
|
+
|
|
5
|
+
## Why Use Vibe Kanban with Loki Mode?
|
|
6
|
+
|
|
7
|
+
| Feature | Loki Mode Alone | + Vibe Kanban |
|
|
8
|
+
|---------|-----------------|---------------|
|
|
9
|
+
| Task visualization | File-based queues | Visual kanban board |
|
|
10
|
+
| Progress monitoring | Log files | Real-time dashboard |
|
|
11
|
+
| Manual intervention | Edit queue files | Drag-and-drop tasks |
|
|
12
|
+
| Code review | Automated 3-reviewer | + Visual diff review |
|
|
13
|
+
| Parallel agents | Background subagents | Isolated git worktrees |
|
|
14
|
+
|
|
15
|
+
## Quick Start Guide
|
|
16
|
+
|
|
17
|
+
### Step 1: Start Vibe Kanban
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx vibe-kanban
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This will:
|
|
24
|
+
- Start the Vibe Kanban server
|
|
25
|
+
- Automatically open the UI in your browser
|
|
26
|
+
- Keep the server running (leave this terminal open)
|
|
27
|
+
|
|
28
|
+
### Step 2: Run Loki Mode
|
|
29
|
+
|
|
30
|
+
Open a NEW terminal in your project directory:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Option A: Using Autonomy Runner (Recommended)
|
|
34
|
+
./autonomy/run.sh ./prd.md
|
|
35
|
+
|
|
36
|
+
# Option B: Manual Mode via Claude Code
|
|
37
|
+
claude --dangerously-skip-permissions
|
|
38
|
+
# Then: "Loki Mode with PRD at ./prd.md"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Step 3: Sync Tasks to Vibe Kanban (One Script)
|
|
42
|
+
|
|
43
|
+
Open another terminal in the same project directory:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# One-time sync
|
|
47
|
+
./scripts/sync-to-vibe-kanban.sh
|
|
48
|
+
|
|
49
|
+
# Or continuous sync (watches for changes)
|
|
50
|
+
./scripts/vibe-sync-watcher.sh
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You should see output like:
|
|
54
|
+
```
|
|
55
|
+
[INFO] Project: your-project-name
|
|
56
|
+
[INFO] Path: /Users/username/git/your-project
|
|
57
|
+
[INFO] Database: /Users/username/Library/Application Support/ai.bloop.vibe-kanban/db.sqlite
|
|
58
|
+
[INFO] Project ID: A1B2C3D4E5F6...
|
|
59
|
+
[INFO] Phase: DEVELOPMENT
|
|
60
|
+
[INFO] pending: 5 tasks
|
|
61
|
+
[INFO] in-progress: 3 tasks
|
|
62
|
+
[INFO] Synced 8 tasks to Vibe Kanban
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Step 4: View Tasks in Vibe Kanban
|
|
66
|
+
|
|
67
|
+
Tasks appear immediately in Vibe Kanban (no refresh needed). All synced tasks have `[Loki]` prefix for identification.
|
|
68
|
+
|
|
69
|
+
## Setup
|
|
70
|
+
|
|
71
|
+
### 1. Install Vibe Kanban
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx vibe-kanban
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2. Enable Integration in Loki Mode
|
|
78
|
+
|
|
79
|
+
Set environment variable before running:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export LOKI_VIBE_KANBAN=true
|
|
83
|
+
./scripts/loki-wrapper.sh ./docs/requirements.md
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or create `.loki/config/integrations.yaml`:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
vibe-kanban:
|
|
90
|
+
enabled: true
|
|
91
|
+
sync_interval: 30 # seconds
|
|
92
|
+
export_path: ~/.vibe-kanban/loki-tasks/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## How It Works
|
|
96
|
+
|
|
97
|
+
### Direct SQLite Sync (v2.37.1+)
|
|
98
|
+
|
|
99
|
+
The sync script writes directly to Vibe Kanban's SQLite database:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Loki Mode (.loki/queue/) sync-to-vibe-kanban.sh Vibe Kanban (SQLite)
|
|
103
|
+
│ │ │
|
|
104
|
+
├─ pending.json ────────────►├─────────────────────────►│ todo
|
|
105
|
+
├─ in-progress.json ────────►├─────────────────────────►│ inprogress
|
|
106
|
+
├─ completed.json ──────────►├─────────────────────────►│ done
|
|
107
|
+
└─ failed.json ─────────────►├─────────────────────────►│ cancelled
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Database Location:**
|
|
111
|
+
- macOS: `~/Library/Application Support/ai.bloop.vibe-kanban/db.sqlite`
|
|
112
|
+
- Linux: `~/.local/share/ai.bloop.vibe-kanban/db.sqlite` or `~/.config/ai.bloop.vibe-kanban/db.sqlite`
|
|
113
|
+
|
|
114
|
+
### Status Mapping
|
|
115
|
+
|
|
116
|
+
| Loki Status | Vibe Kanban Status |
|
|
117
|
+
|-------------|-------------------|
|
|
118
|
+
| pending | todo |
|
|
119
|
+
| in-progress | inprogress |
|
|
120
|
+
| completed | done |
|
|
121
|
+
| failed | cancelled |
|
|
122
|
+
|
|
123
|
+
### Task Identification
|
|
124
|
+
|
|
125
|
+
All synced tasks use `[Loki]` prefix in title for safe identification. On each sync:
|
|
126
|
+
1. Delete all `[Loki]` tasks for the project
|
|
127
|
+
2. Re-insert current tasks from queue files
|
|
128
|
+
|
|
129
|
+
This ensures clean sync without duplicates.
|
|
130
|
+
|
|
131
|
+
## Export Script
|
|
132
|
+
|
|
133
|
+
Add this to export Loki Mode tasks to Vibe Kanban:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
#!/bin/bash
|
|
137
|
+
# scripts/export-to-vibe-kanban.sh
|
|
138
|
+
|
|
139
|
+
LOKI_DIR=".loki"
|
|
140
|
+
EXPORT_DIR="${VIBE_KANBAN_DIR:-~/.vibe-kanban/loki-tasks}"
|
|
141
|
+
|
|
142
|
+
mkdir -p "$EXPORT_DIR"
|
|
143
|
+
|
|
144
|
+
# Export pending tasks
|
|
145
|
+
if [ -f "$LOKI_DIR/queue/pending.json" ]; then
|
|
146
|
+
python3 << EOF
|
|
147
|
+
import json
|
|
148
|
+
import os
|
|
149
|
+
|
|
150
|
+
with open("$LOKI_DIR/queue/pending.json") as f:
|
|
151
|
+
tasks = json.load(f)
|
|
152
|
+
|
|
153
|
+
export_dir = os.path.expanduser("$EXPORT_DIR")
|
|
154
|
+
|
|
155
|
+
for task in tasks:
|
|
156
|
+
vibe_task = {
|
|
157
|
+
"id": f"loki-{task['id']}",
|
|
158
|
+
"title": task.get('payload', {}).get('description', task['type']),
|
|
159
|
+
"description": json.dumps(task.get('payload', {}), indent=2),
|
|
160
|
+
"status": "todo",
|
|
161
|
+
"agent": "claude-code",
|
|
162
|
+
"tags": [task['type'], f"priority-{task.get('priority', 5)}"],
|
|
163
|
+
"metadata": {
|
|
164
|
+
"lokiTaskId": task['id'],
|
|
165
|
+
"lokiType": task['type'],
|
|
166
|
+
"createdAt": task.get('createdAt', '')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
with open(f"{export_dir}/{task['id']}.json", 'w') as out:
|
|
171
|
+
json.dump(vibe_task, out, indent=2)
|
|
172
|
+
|
|
173
|
+
print(f"Exported {len(tasks)} tasks to {export_dir}")
|
|
174
|
+
EOF
|
|
175
|
+
fi
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Real-Time Sync (Advanced)
|
|
179
|
+
|
|
180
|
+
For real-time sync, run the watcher alongside Loki Mode:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
#!/bin/bash
|
|
184
|
+
# scripts/vibe-sync-watcher.sh
|
|
185
|
+
|
|
186
|
+
LOKI_DIR=".loki"
|
|
187
|
+
|
|
188
|
+
# Watch for queue changes and sync
|
|
189
|
+
while true; do
|
|
190
|
+
# Use fswatch on macOS, inotifywait on Linux
|
|
191
|
+
if command -v fswatch &> /dev/null; then
|
|
192
|
+
fswatch -1 "$LOKI_DIR/queue/"
|
|
193
|
+
else
|
|
194
|
+
inotifywait -e modify,create "$LOKI_DIR/queue/" 2>/dev/null
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
./scripts/export-to-vibe-kanban.sh
|
|
198
|
+
sleep 2
|
|
199
|
+
done
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Benefits of Combined Usage
|
|
203
|
+
|
|
204
|
+
### 1. Visual Progress Tracking
|
|
205
|
+
See all active Loki agents as tasks moving across your kanban board.
|
|
206
|
+
|
|
207
|
+
### 2. Safe Isolation
|
|
208
|
+
Vibe Kanban runs each agent in isolated git worktrees, perfect for Loki's parallel development.
|
|
209
|
+
|
|
210
|
+
### 3. Human-in-the-Loop Option
|
|
211
|
+
Pause autonomous execution, review changes visually, then resume.
|
|
212
|
+
|
|
213
|
+
### 4. Multi-Project Dashboard
|
|
214
|
+
If running Loki Mode on multiple projects, see all in one Vibe Kanban instance.
|
|
215
|
+
|
|
216
|
+
## Comparison: When to Use What
|
|
217
|
+
|
|
218
|
+
| Scenario | Recommendation |
|
|
219
|
+
|----------|----------------|
|
|
220
|
+
| Fully autonomous, no monitoring | Loki Mode + Wrapper only |
|
|
221
|
+
| Need visual progress dashboard | Add Vibe Kanban |
|
|
222
|
+
| Want manual task prioritization | Use Vibe Kanban to reorder |
|
|
223
|
+
| Code review before merge | Use Vibe Kanban's diff viewer |
|
|
224
|
+
| Multiple concurrent PRDs | Vibe Kanban for project switching |
|
|
225
|
+
|
|
226
|
+
## Troubleshooting
|
|
227
|
+
|
|
228
|
+
### Issue: "Exported 0 tasks total"
|
|
229
|
+
|
|
230
|
+
**Cause:** No tasks in `.loki/queue/` yet.
|
|
231
|
+
|
|
232
|
+
**Solutions:**
|
|
233
|
+
1. Make sure Loki Mode is actually running and has created tasks
|
|
234
|
+
2. Check if `.loki/queue/` directory exists: `ls -la .loki/queue/`
|
|
235
|
+
3. Verify queue files have content: `cat .loki/queue/pending.json`
|
|
236
|
+
4. If running manual mode, Loki creates tasks as it works - give it time to start
|
|
237
|
+
|
|
238
|
+
### Issue: "AttributeError: 'str' object has no attribute 'get'"
|
|
239
|
+
|
|
240
|
+
**Cause:** Task payload was a string instead of expected JSON object (fixed in v2.35.1).
|
|
241
|
+
|
|
242
|
+
**Solution:** Update to latest version or apply the fix from PR #9.
|
|
243
|
+
|
|
244
|
+
### Issue: "STATUS.txt does not exist"
|
|
245
|
+
|
|
246
|
+
**Cause:** `.loki/STATUS.txt` is only created when using the autonomy runner.
|
|
247
|
+
|
|
248
|
+
**Solutions:**
|
|
249
|
+
- Use autonomy runner: `./autonomy/run.sh ./prd.md` instead of manual Claude Code
|
|
250
|
+
- Or check task queues directly: `ls -la .loki/queue/`
|
|
251
|
+
- Monitor orchestrator state: `cat .loki/state/orchestrator.json | jq`
|
|
252
|
+
|
|
253
|
+
### Issue: "Tasks not appearing in Vibe Kanban"
|
|
254
|
+
|
|
255
|
+
**Checklist:**
|
|
256
|
+
1. Is Vibe Kanban running? Check http://127.0.0.1:53380
|
|
257
|
+
2. Did you run the export script? `./scripts/export-to-vibe-kanban.sh`
|
|
258
|
+
3. Check export directory has files: `ls ~/.vibe-kanban/loki-tasks/`
|
|
259
|
+
4. Refresh the Vibe Kanban browser window
|
|
260
|
+
5. Check Vibe Kanban is configured to watch that directory
|
|
261
|
+
|
|
262
|
+
### Issue: "No real-time updates"
|
|
263
|
+
|
|
264
|
+
**Explanation:** The export script runs on-demand, not automatically.
|
|
265
|
+
|
|
266
|
+
**Solutions:**
|
|
267
|
+
1. Run `./scripts/vibe-sync-watcher.sh` for automatic sync
|
|
268
|
+
2. Or manually run export script periodically: `watch -n 10 ./scripts/export-to-vibe-kanban.sh`
|
|
269
|
+
3. Or refresh manually when you want to check progress
|
|
270
|
+
|
|
271
|
+
## Expected Workflow
|
|
272
|
+
|
|
273
|
+
**Important:** This is a manual integration, not automatic. Here's what to expect:
|
|
274
|
+
|
|
275
|
+
1. Start Vibe Kanban (terminal 1, keeps running)
|
|
276
|
+
2. Start Loki Mode (terminal 2, keeps running)
|
|
277
|
+
3. Wait for Loki to create some tasks
|
|
278
|
+
4. Run export script (terminal 3, one-time or via watcher)
|
|
279
|
+
5. Refresh Vibe Kanban in browser to see tasks
|
|
280
|
+
|
|
281
|
+
**Loki does NOT automatically push to Vibe Kanban.** You must run the export script.
|
|
282
|
+
|
|
283
|
+
## Future Integration Ideas
|
|
284
|
+
|
|
285
|
+
- [ ] Bidirectional sync (Vibe → Loki)
|
|
286
|
+
- [ ] Automatic background sync without watcher script
|
|
287
|
+
- [ ] Vibe Kanban MCP server for agent communication
|
|
288
|
+
- [ ] Shared agent profiles between tools
|
|
289
|
+
- [ ] Unified logging dashboard
|
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -592,6 +592,10 @@ async def loki_memory_store_pattern(
|
|
|
592
592
|
Returns:
|
|
593
593
|
Pattern ID if successful
|
|
594
594
|
"""
|
|
595
|
+
# Validate confidence range
|
|
596
|
+
if not (0.0 <= confidence <= 1.0):
|
|
597
|
+
return json.dumps({"success": False, "error": "confidence must be between 0.0 and 1.0"})
|
|
598
|
+
|
|
595
599
|
_emit_tool_event_async(
|
|
596
600
|
'loki_memory_store_pattern', 'start',
|
|
597
601
|
parameters={'pattern': pattern, 'category': category, 'confidence': confidence}
|
|
@@ -646,8 +650,10 @@ async def loki_task_queue_list() -> str:
|
|
|
646
650
|
if manager and STATE_MANAGER_AVAILABLE:
|
|
647
651
|
queue = manager.get_state("state/task-queue.json")
|
|
648
652
|
if queue:
|
|
653
|
+
# Strip internal fields before returning
|
|
654
|
+
response = {k: v for k, v in queue.items() if k not in ("_next_id", "version")}
|
|
649
655
|
_emit_tool_event_async('loki_task_queue_list', 'complete', result_status='success')
|
|
650
|
-
return json.dumps(
|
|
656
|
+
return json.dumps(response, default=str)
|
|
651
657
|
# If no queue found via StateManager, return empty
|
|
652
658
|
result = json.dumps({"tasks": [], "message": "No task queue found"})
|
|
653
659
|
_emit_tool_event_async('loki_task_queue_list', 'complete', result_status='success')
|
|
@@ -663,8 +669,10 @@ async def loki_task_queue_list() -> str:
|
|
|
663
669
|
with safe_open(queue_path, 'r') as f:
|
|
664
670
|
queue = json.load(f)
|
|
665
671
|
|
|
672
|
+
# Strip internal fields before returning
|
|
673
|
+
response = {k: v for k, v in queue.items() if k not in ("_next_id", "version")}
|
|
666
674
|
_emit_tool_event_async('loki_task_queue_list', 'complete', result_status='success')
|
|
667
|
-
return json.dumps(
|
|
675
|
+
return json.dumps(response, default=str)
|
|
668
676
|
except PathTraversalError as e:
|
|
669
677
|
logger.error(f"Path traversal attempt blocked: {e}")
|
|
670
678
|
_emit_tool_event_async('loki_task_queue_list', 'complete', result_status='error', error='Access denied')
|
|
@@ -694,6 +702,14 @@ async def loki_task_queue_add(
|
|
|
694
702
|
Returns:
|
|
695
703
|
Task ID if successful
|
|
696
704
|
"""
|
|
705
|
+
# Validate priority and phase enums
|
|
706
|
+
valid_priorities = {"low", "medium", "high", "critical"}
|
|
707
|
+
valid_phases = {"discovery", "architecture", "development", "testing", "deployment"}
|
|
708
|
+
if priority not in valid_priorities:
|
|
709
|
+
return json.dumps({"success": False, "error": f"priority must be one of: {', '.join(sorted(valid_priorities))}"})
|
|
710
|
+
if phase not in valid_phases:
|
|
711
|
+
return json.dumps({"success": False, "error": f"phase must be one of: {', '.join(sorted(valid_phases))}"})
|
|
712
|
+
|
|
697
713
|
_emit_tool_event_async(
|
|
698
714
|
'loki_task_queue_add', 'start',
|
|
699
715
|
parameters={'title': title, 'priority': priority, 'phase': phase}
|
|
@@ -716,7 +732,17 @@ async def loki_task_queue_add(
|
|
|
716
732
|
queue = {"tasks": [], "version": "1.0"}
|
|
717
733
|
|
|
718
734
|
# Create new task with monotonic counter to avoid ID collisions after deletions
|
|
719
|
-
|
|
735
|
+
# When _next_id is missing, scan existing IDs to find the max and use max+1
|
|
736
|
+
if "_next_id" not in queue:
|
|
737
|
+
existing_ids = []
|
|
738
|
+
for t in queue.get("tasks", []):
|
|
739
|
+
try:
|
|
740
|
+
existing_ids.append(int(t["id"].replace("task-", "")))
|
|
741
|
+
except (ValueError, KeyError):
|
|
742
|
+
pass
|
|
743
|
+
next_id = (max(existing_ids) + 1) if existing_ids else 1
|
|
744
|
+
else:
|
|
745
|
+
next_id = queue["_next_id"]
|
|
720
746
|
task_id = f"task-{next_id:04d}"
|
|
721
747
|
queue["_next_id"] = next_id + 1
|
|
722
748
|
task = {
|
|
@@ -768,6 +794,14 @@ async def loki_task_queue_update(
|
|
|
768
794
|
Returns:
|
|
769
795
|
Updated task if successful
|
|
770
796
|
"""
|
|
797
|
+
# Validate status and priority enums when provided
|
|
798
|
+
valid_statuses = {"pending", "in_progress", "completed", "blocked"}
|
|
799
|
+
valid_priorities = {"low", "medium", "high", "critical"}
|
|
800
|
+
if status is not None and status not in valid_statuses:
|
|
801
|
+
return json.dumps({"success": False, "error": f"status must be one of: {', '.join(sorted(valid_statuses))}"})
|
|
802
|
+
if priority is not None and priority not in valid_priorities:
|
|
803
|
+
return json.dumps({"success": False, "error": f"priority must be one of: {', '.join(sorted(valid_priorities))}"})
|
|
804
|
+
|
|
771
805
|
_emit_tool_event_async(
|
|
772
806
|
'loki_task_queue_update', 'start',
|
|
773
807
|
parameters={'task_id': task_id, 'status': status, 'priority': priority}
|
|
@@ -793,9 +827,9 @@ async def loki_task_queue_update(
|
|
|
793
827
|
# Find and update task
|
|
794
828
|
for task in queue["tasks"]:
|
|
795
829
|
if task["id"] == task_id:
|
|
796
|
-
if status:
|
|
830
|
+
if status is not None:
|
|
797
831
|
task["status"] = status
|
|
798
|
-
if priority:
|
|
832
|
+
if priority is not None:
|
|
799
833
|
task["priority"] = priority
|
|
800
834
|
task["updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
801
835
|
|
|
@@ -850,7 +884,7 @@ async def loki_state_get() -> str:
|
|
|
850
884
|
state["autonomy_state"] = autonomy_data
|
|
851
885
|
else:
|
|
852
886
|
# Fallback to direct file read
|
|
853
|
-
state_path = safe_path_join('.loki', '
|
|
887
|
+
state_path = safe_path_join('.loki', 'autonomy-state.json')
|
|
854
888
|
if os.path.exists(state_path):
|
|
855
889
|
with safe_open(state_path, 'r') as f:
|
|
856
890
|
state["autonomy_state"] = json.load(f)
|
|
@@ -1056,8 +1090,15 @@ async def loki_start_project(prd_content: str = "", prd_path: str = "") -> str:
|
|
|
1056
1090
|
if not content:
|
|
1057
1091
|
return json.dumps({"error": "No PRD content or path provided"})
|
|
1058
1092
|
|
|
1059
|
-
# Initialize project state
|
|
1060
|
-
|
|
1093
|
+
# Initialize project state using safe path operations
|
|
1094
|
+
state_dir = safe_path_join('.loki', 'state')
|
|
1095
|
+
safe_makedirs(state_dir, exist_ok=True)
|
|
1096
|
+
|
|
1097
|
+
# Persist PRD content so downstream tools can access it
|
|
1098
|
+
prd_dest = safe_path_join('.loki', 'state', 'prd.md')
|
|
1099
|
+
with safe_open(prd_dest, 'w') as f:
|
|
1100
|
+
f.write(content)
|
|
1101
|
+
|
|
1061
1102
|
project = {
|
|
1062
1103
|
"status": "initialized",
|
|
1063
1104
|
"prd_length": len(content),
|
|
@@ -1201,10 +1242,11 @@ async def loki_checkpoint_restore(checkpoint_id: str = "") -> str:
|
|
|
1201
1242
|
if not target:
|
|
1202
1243
|
return json.dumps({"error": f"Checkpoint not found: {checkpoint_id}"})
|
|
1203
1244
|
|
|
1204
|
-
# Write checkpoint state as current state
|
|
1245
|
+
# Write checkpoint state as current state, stripping the injected "id" field
|
|
1246
|
+
restored_state = {k: v for k, v in target.items() if k != "id"}
|
|
1205
1247
|
state_path = safe_path_join('.loki', 'state', 'orchestrator.json')
|
|
1206
1248
|
with safe_open(state_path, 'w') as f:
|
|
1207
|
-
json.dump(
|
|
1249
|
+
json.dump(restored_state, f, indent=2)
|
|
1208
1250
|
|
|
1209
1251
|
_emit_tool_event_async('loki_checkpoint_restore', 'complete', result_status='success')
|
|
1210
1252
|
return json.dumps({"restored": True, "checkpoint_id": checkpoint_id})
|
|
@@ -1310,7 +1352,10 @@ async def loki_code_search(
|
|
|
1310
1352
|
file_filter: Filter by file path substring (e.g., "autonomy/", "dashboard/") (optional)
|
|
1311
1353
|
type_filter: Filter by chunk type: "function", "class", "header", "section", "file" (optional)
|
|
1312
1354
|
"""
|
|
1313
|
-
_emit_tool_event_async('loki_code_search', 'start',
|
|
1355
|
+
_emit_tool_event_async('loki_code_search', 'start',
|
|
1356
|
+
parameters={'query': query, 'n_results': n_results,
|
|
1357
|
+
'language': language, 'file_filter': file_filter,
|
|
1358
|
+
'type_filter': type_filter})
|
|
1314
1359
|
|
|
1315
1360
|
collection = _get_chroma_collection()
|
|
1316
1361
|
if collection is None:
|
|
@@ -1362,7 +1407,7 @@ async def loki_code_search(
|
|
|
1362
1407
|
"name": meta.get("name", ""),
|
|
1363
1408
|
"type": meta.get("type", ""),
|
|
1364
1409
|
"language": meta.get("language", ""),
|
|
1365
|
-
"relevance": round(1 - dist, 4), #
|
|
1410
|
+
"relevance": round(max(0.0, 1.0 - dist / 2.0), 4), # L2 distance to similarity
|
|
1366
1411
|
"preview": preview,
|
|
1367
1412
|
})
|
|
1368
1413
|
|
|
@@ -1390,6 +1435,17 @@ async def loki_code_search_stats() -> str:
|
|
|
1390
1435
|
|
|
1391
1436
|
try:
|
|
1392
1437
|
count = collection.count()
|
|
1438
|
+
|
|
1439
|
+
# Short-circuit on empty collection to avoid limit=0 error
|
|
1440
|
+
if count == 0:
|
|
1441
|
+
return json.dumps({
|
|
1442
|
+
"total_chunks": 0,
|
|
1443
|
+
"unique_files": 0,
|
|
1444
|
+
"by_language": {},
|
|
1445
|
+
"by_type": {},
|
|
1446
|
+
"reindex_command": "python3.12 tools/index-codebase.py --reset",
|
|
1447
|
+
})
|
|
1448
|
+
|
|
1393
1449
|
results = collection.get(limit=count, include=["metadatas"])
|
|
1394
1450
|
|
|
1395
1451
|
langs = {}
|
|
@@ -1448,19 +1504,13 @@ async def mem_search(
|
|
|
1448
1504
|
_emit_tool_event_async('mem_search', 'complete', result_status='success')
|
|
1449
1505
|
return result
|
|
1450
1506
|
|
|
1451
|
-
#
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
from memory.retrieval import MemoryRetrieval
|
|
1459
|
-
from memory.storage import MemoryStorage
|
|
1460
|
-
storage = MemoryStorage(base_path)
|
|
1461
|
-
retriever = MemoryRetrieval(storage)
|
|
1462
|
-
context = {"goal": query, "task_type": "exploration"}
|
|
1463
|
-
results = retriever.retrieve_task_aware(context, top_k=limit)
|
|
1507
|
+
# Use retrieval-based search
|
|
1508
|
+
from memory.retrieval import MemoryRetrieval
|
|
1509
|
+
from memory.storage import MemoryStorage
|
|
1510
|
+
storage = MemoryStorage(base_path)
|
|
1511
|
+
retriever = MemoryRetrieval(storage)
|
|
1512
|
+
context = {"goal": query, "task_type": "exploration"}
|
|
1513
|
+
results = retriever.retrieve_task_aware(context, top_k=limit)
|
|
1464
1514
|
|
|
1465
1515
|
# Compact results for token efficiency
|
|
1466
1516
|
compact = []
|
|
@@ -1530,48 +1580,25 @@ async def mem_timeline(
|
|
|
1530
1580
|
from datetime import timedelta
|
|
1531
1581
|
cutoff = datetime.now(timezone.utc) - timedelta(hours=since_hours)
|
|
1532
1582
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
"outcome": ep.get("outcome"),
|
|
1553
|
-
"duration_seconds": ep.get("duration_seconds"),
|
|
1554
|
-
"files_modified": ep.get("files_modified", [])[:5],
|
|
1555
|
-
})
|
|
1556
|
-
|
|
1557
|
-
except (ImportError, Exception):
|
|
1558
|
-
from memory.storage import MemoryStorage
|
|
1559
|
-
storage = MemoryStorage(base_path)
|
|
1560
|
-
timeline = storage.get_timeline()
|
|
1561
|
-
actions = timeline.get("recent_actions", [])[:limit]
|
|
1562
|
-
|
|
1563
|
-
episode_ids = storage.list_episodes(since=cutoff, limit=limit)
|
|
1564
|
-
episodes = []
|
|
1565
|
-
for eid in episode_ids:
|
|
1566
|
-
ep = storage.load_episode(eid)
|
|
1567
|
-
if ep:
|
|
1568
|
-
episodes.append({
|
|
1569
|
-
"id": ep.get("id"),
|
|
1570
|
-
"timestamp": ep.get("timestamp"),
|
|
1571
|
-
"phase": ep.get("phase"),
|
|
1572
|
-
"goal": (ep.get("goal", "") or "")[:150],
|
|
1573
|
-
"outcome": ep.get("outcome"),
|
|
1574
|
-
})
|
|
1583
|
+
from memory.storage import MemoryStorage
|
|
1584
|
+
storage = MemoryStorage(base_path)
|
|
1585
|
+
timeline = storage.get_timeline()
|
|
1586
|
+
actions = timeline.get("recent_actions", [])[:limit]
|
|
1587
|
+
|
|
1588
|
+
episode_ids = storage.list_episodes(since=cutoff, limit=limit)
|
|
1589
|
+
episodes = []
|
|
1590
|
+
for eid in episode_ids:
|
|
1591
|
+
ep = storage.load_episode(eid)
|
|
1592
|
+
if ep:
|
|
1593
|
+
episodes.append({
|
|
1594
|
+
"id": ep.get("id"),
|
|
1595
|
+
"timestamp": ep.get("timestamp"),
|
|
1596
|
+
"phase": ep.get("phase"),
|
|
1597
|
+
"goal": (ep.get("goal", "") or "")[:150],
|
|
1598
|
+
"outcome": ep.get("outcome"),
|
|
1599
|
+
"duration_seconds": ep.get("duration_seconds"),
|
|
1600
|
+
"files_modified": ep.get("files_modified", [])[:5],
|
|
1601
|
+
})
|
|
1575
1602
|
|
|
1576
1603
|
result = json.dumps({
|
|
1577
1604
|
"actions": actions,
|
|
@@ -1624,12 +1651,8 @@ async def mem_get(
|
|
|
1624
1651
|
# Cap at 20 to prevent abuse
|
|
1625
1652
|
id_list = id_list[:20]
|
|
1626
1653
|
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
storage = SQLiteMemoryStorage(base_path)
|
|
1630
|
-
except (ImportError, Exception):
|
|
1631
|
-
from memory.storage import MemoryStorage
|
|
1632
|
-
storage = MemoryStorage(base_path)
|
|
1654
|
+
from memory.storage import MemoryStorage
|
|
1655
|
+
storage = MemoryStorage(base_path)
|
|
1633
1656
|
|
|
1634
1657
|
entries = {}
|
|
1635
1658
|
for mem_id in id_list:
|
package/memory/consolidation.py
CHANGED
|
@@ -225,6 +225,9 @@ class ConsolidationPipeline:
|
|
|
225
225
|
if not merged:
|
|
226
226
|
self.storage.save_pattern(anti_pattern)
|
|
227
227
|
all_patterns.append(anti_pattern)
|
|
228
|
+
# Add to existing_patterns so subsequent anti-patterns in this
|
|
229
|
+
# run are checked against it, preventing current-run duplicates.
|
|
230
|
+
existing_patterns.append(anti_pattern)
|
|
228
231
|
result.anti_patterns_created += 1
|
|
229
232
|
|
|
230
233
|
# 7. Create Zettelkasten links
|
|
@@ -294,6 +297,7 @@ class ConsolidationPipeline:
|
|
|
294
297
|
label=self._generate_cluster_label([episode])
|
|
295
298
|
)
|
|
296
299
|
used.add(i)
|
|
300
|
+
member_indices = [i]
|
|
297
301
|
|
|
298
302
|
# Find similar episodes
|
|
299
303
|
for j, other_episode in enumerate(episodes):
|
|
@@ -304,10 +308,11 @@ class ConsolidationPipeline:
|
|
|
304
308
|
if similarity >= threshold:
|
|
305
309
|
cluster.episodes.append(other_episode)
|
|
306
310
|
used.add(j)
|
|
311
|
+
member_indices.append(j)
|
|
307
312
|
|
|
308
|
-
# Update centroid
|
|
313
|
+
# Update centroid using tracked indices (avoids O(n) list.index())
|
|
309
314
|
if len(cluster.episodes) > 1:
|
|
310
|
-
cluster_embeddings = [embeddings[
|
|
315
|
+
cluster_embeddings = [embeddings[idx] for idx in member_indices]
|
|
311
316
|
cluster.centroid = np.mean(cluster_embeddings, axis=0)
|
|
312
317
|
cluster.label = self._generate_cluster_label(cluster.episodes)
|
|
313
318
|
|
|
@@ -408,13 +413,23 @@ class ConsolidationPipeline:
|
|
|
408
413
|
"""Convert episode to text for embedding."""
|
|
409
414
|
parts = [episode.goal]
|
|
410
415
|
|
|
411
|
-
# Add action summaries
|
|
416
|
+
# Add action summaries (handle both ActionEntry objects and dicts)
|
|
412
417
|
for action in episode.action_log[:5]: # Limit to first 5 actions
|
|
413
|
-
|
|
418
|
+
if isinstance(action, dict):
|
|
419
|
+
tool = action.get("tool", action.get("action", ""))
|
|
420
|
+
inp = action.get("input", action.get("target", ""))
|
|
421
|
+
else:
|
|
422
|
+
tool = action.tool
|
|
423
|
+
inp = action.input
|
|
424
|
+
parts.append(f"{tool}: {str(inp)[:100]}")
|
|
414
425
|
|
|
415
|
-
# Add error types
|
|
426
|
+
# Add error types (handle both ErrorEntry objects and dicts)
|
|
416
427
|
for error in episode.errors_encountered:
|
|
417
|
-
|
|
428
|
+
if isinstance(error, dict):
|
|
429
|
+
err_type = error.get("error_type", error.get("type", ""))
|
|
430
|
+
else:
|
|
431
|
+
err_type = error.error_type
|
|
432
|
+
parts.append(f"Error: {err_type}")
|
|
418
433
|
|
|
419
434
|
return " ".join(parts)
|
|
420
435
|
|