loki-mode 7.5.10 → 7.5.12
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/README.md +29 -4
- package/SKILL.md +17 -14
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +81 -6
- package/autonomy/lib/lock.sh +147 -0
- package/autonomy/loki +22 -0
- package/autonomy/run.sh +332 -69
- package/dashboard/__init__.py +1 -1
- package/dashboard/database.py +26 -0
- package/dashboard/models.py +8 -0
- package/dashboard/server.py +125 -4
- package/dashboard/static/index.html +361 -172
- package/docs/COMPARISON.md +6 -6
- package/docs/INSTALLATION.md +32 -22
- package/docs/cursor-comparison.md +6 -6
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
package/dashboard/server.py
CHANGED
|
@@ -164,6 +164,78 @@ def _sanitize_text_field(value: str) -> str:
|
|
|
164
164
|
return cleaned
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
def _decode_task_json_list(raw: Optional[str]) -> list:
|
|
168
|
+
"""Decode a JSON-encoded list column on the Task model.
|
|
169
|
+
|
|
170
|
+
v7.5.12 enrichment columns (acceptance_criteria, notes, logs) are stored
|
|
171
|
+
as JSON-encoded text. Returns [] for NULL / empty / malformed values so
|
|
172
|
+
the API response is always shape-stable.
|
|
173
|
+
"""
|
|
174
|
+
if not raw:
|
|
175
|
+
return []
|
|
176
|
+
try:
|
|
177
|
+
parsed = json.loads(raw)
|
|
178
|
+
except (TypeError, ValueError):
|
|
179
|
+
return []
|
|
180
|
+
return parsed if isinstance(parsed, list) else []
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _encode_task_json_list(value: Any) -> Optional[str]:
|
|
184
|
+
"""Encode a list (or list-of-pydantic-models) as JSON for storage.
|
|
185
|
+
|
|
186
|
+
Pydantic models are dumped via .model_dump(mode='json') so nested
|
|
187
|
+
datetimes serialize as ISO strings. Plain dicts go through a
|
|
188
|
+
datetime-aware encoder fallback (PUT requests reach here as dicts
|
|
189
|
+
via model_dump(exclude_unset=True), which leaves datetimes raw).
|
|
190
|
+
Returns None for empty/None input so we don't write empty strings
|
|
191
|
+
into the column.
|
|
192
|
+
"""
|
|
193
|
+
if value is None:
|
|
194
|
+
return None
|
|
195
|
+
if not isinstance(value, list) or not value:
|
|
196
|
+
return None
|
|
197
|
+
out = []
|
|
198
|
+
for item in value:
|
|
199
|
+
if hasattr(item, "model_dump"):
|
|
200
|
+
out.append(item.model_dump(mode="json"))
|
|
201
|
+
else:
|
|
202
|
+
out.append(item)
|
|
203
|
+
return json.dumps(out, default=_json_default)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _json_default(obj: Any) -> Any:
|
|
207
|
+
"""JSON encoder fallback for datetime / date objects."""
|
|
208
|
+
if isinstance(obj, datetime):
|
|
209
|
+
return obj.isoformat()
|
|
210
|
+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _task_response_from_db(task: Any) -> "TaskResponse":
|
|
214
|
+
"""Build a TaskResponse from a Task ORM row, decoding JSON columns."""
|
|
215
|
+
payload = {
|
|
216
|
+
"id": task.id,
|
|
217
|
+
"project_id": task.project_id,
|
|
218
|
+
"title": task.title,
|
|
219
|
+
"description": task.description,
|
|
220
|
+
"status": task.status,
|
|
221
|
+
"priority": task.priority,
|
|
222
|
+
"position": task.position,
|
|
223
|
+
"assigned_agent_id": task.assigned_agent_id,
|
|
224
|
+
"parent_task_id": task.parent_task_id,
|
|
225
|
+
"estimated_duration": task.estimated_duration,
|
|
226
|
+
"actual_duration": task.actual_duration,
|
|
227
|
+
"created_at": task.created_at,
|
|
228
|
+
"updated_at": task.updated_at,
|
|
229
|
+
"completed_at": task.completed_at,
|
|
230
|
+
"acceptance_criteria": _decode_task_json_list(
|
|
231
|
+
getattr(task, "acceptance_criteria", None)
|
|
232
|
+
),
|
|
233
|
+
"notes": _decode_task_json_list(getattr(task, "notes", None)),
|
|
234
|
+
"logs": _decode_task_json_list(getattr(task, "logs", None)),
|
|
235
|
+
}
|
|
236
|
+
return TaskResponse.model_validate(payload)
|
|
237
|
+
|
|
238
|
+
|
|
167
239
|
class ProjectCreate(BaseModel):
|
|
168
240
|
"""Schema for creating a project."""
|
|
169
241
|
name: str = Field(..., min_length=1, max_length=255)
|
|
@@ -200,6 +272,26 @@ class ProjectResponse(BaseModel):
|
|
|
200
272
|
completed_task_count: int = 0
|
|
201
273
|
|
|
202
274
|
|
|
275
|
+
class TaskNote(BaseModel):
|
|
276
|
+
"""A single note attached to a task (v7.5.12)."""
|
|
277
|
+
timestamp: datetime
|
|
278
|
+
author: str = "system"
|
|
279
|
+
body: str
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class TaskLog(BaseModel):
|
|
283
|
+
"""A single log entry attached to a task (v7.5.12).
|
|
284
|
+
|
|
285
|
+
Written by the runner after each RARV phase (REASON, ACT, REFLECT,
|
|
286
|
+
VERIFY) so the dashboard can show per-iteration progress.
|
|
287
|
+
"""
|
|
288
|
+
timestamp: datetime
|
|
289
|
+
iteration: Optional[int] = None
|
|
290
|
+
level: str = "info" # info | warn | error
|
|
291
|
+
phase: Optional[str] = None # REASON | ACT | REFLECT | VERIFY | ...
|
|
292
|
+
message: str
|
|
293
|
+
|
|
294
|
+
|
|
203
295
|
class TaskCreate(BaseModel):
|
|
204
296
|
"""Schema for creating a task."""
|
|
205
297
|
project_id: int
|
|
@@ -210,6 +302,10 @@ class TaskCreate(BaseModel):
|
|
|
210
302
|
position: int = 0
|
|
211
303
|
parent_task_id: Optional[int] = None
|
|
212
304
|
estimated_duration: Optional[int] = None
|
|
305
|
+
# v7.5.12 enrichment (additive, all optional, default to empty list).
|
|
306
|
+
acceptance_criteria: list[str] = Field(default_factory=list)
|
|
307
|
+
notes: list[TaskNote] = Field(default_factory=list)
|
|
308
|
+
logs: list[TaskLog] = Field(default_factory=list)
|
|
213
309
|
|
|
214
310
|
@field_validator("title")
|
|
215
311
|
@classmethod
|
|
@@ -227,6 +323,11 @@ class TaskUpdate(BaseModel):
|
|
|
227
323
|
assigned_agent_id: Optional[int] = None
|
|
228
324
|
estimated_duration: Optional[int] = None
|
|
229
325
|
actual_duration: Optional[int] = None
|
|
326
|
+
# v7.5.12 enrichment. Clients PUT a full replacement list when supplied;
|
|
327
|
+
# omitted fields are left untouched (Pydantic exclude_unset semantics).
|
|
328
|
+
acceptance_criteria: Optional[list[str]] = None
|
|
329
|
+
notes: Optional[list[TaskNote]] = None
|
|
330
|
+
logs: Optional[list[TaskLog]] = None
|
|
230
331
|
|
|
231
332
|
|
|
232
333
|
class TaskMove(BaseModel):
|
|
@@ -253,6 +354,12 @@ class TaskResponse(BaseModel):
|
|
|
253
354
|
created_at: datetime
|
|
254
355
|
updated_at: datetime
|
|
255
356
|
completed_at: Optional[datetime]
|
|
357
|
+
# v7.5.12 enrichment. Always present in the response (default to []) so
|
|
358
|
+
# frontend code can rely on the shape; legacy DB rows with NULL columns
|
|
359
|
+
# surface as empty lists via _decode_task_json_list().
|
|
360
|
+
acceptance_criteria: list[str] = Field(default_factory=list)
|
|
361
|
+
notes: list[TaskNote] = Field(default_factory=list)
|
|
362
|
+
logs: list[TaskLog] = Field(default_factory=list)
|
|
256
363
|
|
|
257
364
|
|
|
258
365
|
class SessionInfo(BaseModel):
|
|
@@ -1428,6 +1535,12 @@ async def list_tasks(
|
|
|
1428
1535
|
task_entry["project"] = item["project"]
|
|
1429
1536
|
if item.get("source"):
|
|
1430
1537
|
task_entry["source"] = item["source"]
|
|
1538
|
+
# v7.5.12: pass-through enrichment fields so
|
|
1539
|
+
# the dashboard can render notes + per-phase logs.
|
|
1540
|
+
if isinstance(item.get("notes"), list):
|
|
1541
|
+
task_entry["notes"] = item["notes"]
|
|
1542
|
+
if isinstance(item.get("logs"), list):
|
|
1543
|
+
task_entry["logs"] = item["logs"]
|
|
1431
1544
|
all_tasks.append(task_entry)
|
|
1432
1545
|
except (json.JSONDecodeError, KeyError):
|
|
1433
1546
|
pass
|
|
@@ -1519,6 +1632,9 @@ async def create_task(
|
|
|
1519
1632
|
position=task.position,
|
|
1520
1633
|
parent_task_id=task.parent_task_id,
|
|
1521
1634
|
estimated_duration=task.estimated_duration,
|
|
1635
|
+
acceptance_criteria=_encode_task_json_list(task.acceptance_criteria),
|
|
1636
|
+
notes=_encode_task_json_list(task.notes),
|
|
1637
|
+
logs=_encode_task_json_list(task.logs),
|
|
1522
1638
|
)
|
|
1523
1639
|
db.add(db_task)
|
|
1524
1640
|
await db.flush()
|
|
@@ -1535,7 +1651,7 @@ async def create_task(
|
|
|
1535
1651
|
},
|
|
1536
1652
|
})
|
|
1537
1653
|
|
|
1538
|
-
return
|
|
1654
|
+
return _task_response_from_db(db_task)
|
|
1539
1655
|
|
|
1540
1656
|
|
|
1541
1657
|
@app.get("/api/tasks/{task_id}", response_model=TaskResponse)
|
|
@@ -1552,7 +1668,7 @@ async def get_task(
|
|
|
1552
1668
|
if not task:
|
|
1553
1669
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
1554
1670
|
|
|
1555
|
-
return
|
|
1671
|
+
return _task_response_from_db(task)
|
|
1556
1672
|
|
|
1557
1673
|
|
|
1558
1674
|
@app.put("/api/tasks/{task_id}", response_model=TaskResponse, dependencies=[Depends(auth.require_scope("control"))])
|
|
@@ -1579,6 +1695,11 @@ async def update_task(
|
|
|
1579
1695
|
else:
|
|
1580
1696
|
update_data["completed_at"] = None
|
|
1581
1697
|
|
|
1698
|
+
# v7.5.12: encode enrichment list columns as JSON before persisting.
|
|
1699
|
+
for _enrich_col in ("acceptance_criteria", "notes", "logs"):
|
|
1700
|
+
if _enrich_col in update_data:
|
|
1701
|
+
update_data[_enrich_col] = _encode_task_json_list(update_data[_enrich_col])
|
|
1702
|
+
|
|
1582
1703
|
for field, value in update_data.items():
|
|
1583
1704
|
setattr(task, field, value)
|
|
1584
1705
|
|
|
@@ -1596,7 +1717,7 @@ async def update_task(
|
|
|
1596
1717
|
},
|
|
1597
1718
|
})
|
|
1598
1719
|
|
|
1599
|
-
return
|
|
1720
|
+
return _task_response_from_db(task)
|
|
1600
1721
|
|
|
1601
1722
|
|
|
1602
1723
|
@app.delete("/api/tasks/{task_id}", status_code=204, dependencies=[Depends(auth.require_scope("control"))])
|
|
@@ -1696,7 +1817,7 @@ async def move_task(
|
|
|
1696
1817
|
},
|
|
1697
1818
|
})
|
|
1698
1819
|
|
|
1699
|
-
return
|
|
1820
|
+
return _task_response_from_db(task)
|
|
1700
1821
|
|
|
1701
1822
|
|
|
1702
1823
|
# WebSocket endpoint
|