loki-mode 6.8.1 → 6.9.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/dashboard/__init__.py +1 -1
- package/dashboard/server.py +153 -2
- package/dashboard/static/index.html +328 -54
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.
|
|
6
|
+
# Loki Mode v6.9.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -267,4 +267,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
267
267
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
268
268
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
269
269
|
|
|
270
|
-
**v6.
|
|
270
|
+
**v6.9.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.9.0
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -794,6 +794,99 @@ async def delete_project(
|
|
|
794
794
|
})
|
|
795
795
|
|
|
796
796
|
|
|
797
|
+
def _parse_task_markdown(content: str, task_id: str) -> dict:
|
|
798
|
+
"""Parse a markdown task file into a structured task dict."""
|
|
799
|
+
|
|
800
|
+
task = {
|
|
801
|
+
"id": task_id,
|
|
802
|
+
"title": task_id,
|
|
803
|
+
"description": "",
|
|
804
|
+
"status": "pending",
|
|
805
|
+
"priority": "medium",
|
|
806
|
+
"type": "task",
|
|
807
|
+
"position": 0,
|
|
808
|
+
"specification": "",
|
|
809
|
+
"acceptance_criteria": [],
|
|
810
|
+
"context_files": [],
|
|
811
|
+
"metadata": {},
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
lines = content.split("\n")
|
|
815
|
+
|
|
816
|
+
# Extract title from first heading
|
|
817
|
+
for line in lines:
|
|
818
|
+
if line.startswith("# "):
|
|
819
|
+
task["title"] = line[2:].strip()
|
|
820
|
+
break
|
|
821
|
+
|
|
822
|
+
# Parse sections
|
|
823
|
+
current_section = None
|
|
824
|
+
section_lines = []
|
|
825
|
+
|
|
826
|
+
for line in lines:
|
|
827
|
+
if line.startswith("## "):
|
|
828
|
+
# Save previous section
|
|
829
|
+
if current_section:
|
|
830
|
+
_apply_task_section(task, current_section, section_lines)
|
|
831
|
+
current_section = line[3:].strip().lower()
|
|
832
|
+
section_lines = []
|
|
833
|
+
elif current_section is not None:
|
|
834
|
+
section_lines.append(line)
|
|
835
|
+
|
|
836
|
+
# Save last section
|
|
837
|
+
if current_section:
|
|
838
|
+
_apply_task_section(task, current_section, section_lines)
|
|
839
|
+
|
|
840
|
+
# Store full markdown for detail view
|
|
841
|
+
task["full_content"] = content
|
|
842
|
+
|
|
843
|
+
return task
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _apply_task_section(task: dict, section: str, lines: list):
|
|
847
|
+
"""Apply parsed markdown section to task dict."""
|
|
848
|
+
text = "\n".join(lines).strip()
|
|
849
|
+
|
|
850
|
+
if section == "metadata":
|
|
851
|
+
for line in lines:
|
|
852
|
+
line = line.strip()
|
|
853
|
+
if line.startswith("- "):
|
|
854
|
+
parts = line[2:].split(":", 1)
|
|
855
|
+
if len(parts) == 2:
|
|
856
|
+
key = parts[0].strip().lower().replace(" ", "_")
|
|
857
|
+
val = parts[1].strip()
|
|
858
|
+
task["metadata"][key] = val
|
|
859
|
+
if key == "priority":
|
|
860
|
+
task["priority"] = val.lower()
|
|
861
|
+
elif key == "team":
|
|
862
|
+
task["type"] = val
|
|
863
|
+
elif section == "specification":
|
|
864
|
+
task["specification"] = text
|
|
865
|
+
if not task["description"]:
|
|
866
|
+
# Use first paragraph as description
|
|
867
|
+
for para in text.split("\n\n"):
|
|
868
|
+
if para.strip():
|
|
869
|
+
task["description"] = para.strip()[:200]
|
|
870
|
+
break
|
|
871
|
+
elif section in ("acceptance criteria", "acceptance_criteria"):
|
|
872
|
+
criteria = []
|
|
873
|
+
for line in lines:
|
|
874
|
+
line = line.strip()
|
|
875
|
+
if line and (line[0].isdigit() or line.startswith("- ")):
|
|
876
|
+
# Strip leading number/bullet
|
|
877
|
+
clean = line.lstrip("0123456789.-) ").strip()
|
|
878
|
+
if clean:
|
|
879
|
+
criteria.append(clean)
|
|
880
|
+
task["acceptance_criteria"] = criteria
|
|
881
|
+
elif section in ("context files", "context files to read", "context_files"):
|
|
882
|
+
files = []
|
|
883
|
+
for line in lines:
|
|
884
|
+
line = line.strip()
|
|
885
|
+
if line.startswith("- "):
|
|
886
|
+
files.append(line[2:].strip())
|
|
887
|
+
task["context_files"] = files
|
|
888
|
+
|
|
889
|
+
|
|
797
890
|
# Task endpoints - reads from .loki/dashboard-state.json
|
|
798
891
|
@app.get("/api/tasks")
|
|
799
892
|
async def list_tasks(
|
|
@@ -868,6 +961,29 @@ async def list_tasks(
|
|
|
868
961
|
except (json.JSONDecodeError, KeyError):
|
|
869
962
|
pass
|
|
870
963
|
|
|
964
|
+
# Read markdown task files from queue subdirectories
|
|
965
|
+
for subdir, q_status in [
|
|
966
|
+
("pending", "pending"),
|
|
967
|
+
("active", "in_progress"),
|
|
968
|
+
("review", "review"),
|
|
969
|
+
("done", "done"),
|
|
970
|
+
]:
|
|
971
|
+
dir_path = queue_dir / subdir
|
|
972
|
+
if not dir_path.is_dir():
|
|
973
|
+
continue
|
|
974
|
+
for md_file in sorted(dir_path.glob("*.md")):
|
|
975
|
+
tid = md_file.stem
|
|
976
|
+
if any(t["id"] == tid for t in all_tasks):
|
|
977
|
+
continue
|
|
978
|
+
try:
|
|
979
|
+
content = md_file.read_text(errors="replace")
|
|
980
|
+
parsed = _parse_task_markdown(content, tid)
|
|
981
|
+
parsed["status"] = q_status
|
|
982
|
+
parsed["position"] = len([t for t in all_tasks if t["status"] == q_status])
|
|
983
|
+
all_tasks.append(parsed)
|
|
984
|
+
except Exception:
|
|
985
|
+
pass
|
|
986
|
+
|
|
871
987
|
# Apply status filter if provided
|
|
872
988
|
if status:
|
|
873
989
|
all_tasks = [t for t in all_tasks if t["status"] == status]
|
|
@@ -2799,9 +2915,10 @@ def _sanitize_checkpoint_id(checkpoint_id: str) -> str:
|
|
|
2799
2915
|
|
|
2800
2916
|
@app.get("/api/checkpoints")
|
|
2801
2917
|
async def list_checkpoints(limit: int = Query(default=20, ge=1, le=200)):
|
|
2802
|
-
"""List recent checkpoints from index.jsonl."""
|
|
2918
|
+
"""List recent checkpoints from index.jsonl, enriched with metadata when available."""
|
|
2803
2919
|
loki_dir = _get_loki_dir()
|
|
2804
2920
|
index_file = loki_dir / "state" / "checkpoints" / "index.jsonl"
|
|
2921
|
+
checkpoints_dir = loki_dir / "state" / "checkpoints"
|
|
2805
2922
|
checkpoints = []
|
|
2806
2923
|
|
|
2807
2924
|
if index_file.exists():
|
|
@@ -2809,7 +2926,41 @@ async def list_checkpoints(limit: int = Query(default=20, ge=1, le=200)):
|
|
|
2809
2926
|
for line in index_file.read_text().strip().split("\n"):
|
|
2810
2927
|
if line.strip():
|
|
2811
2928
|
try:
|
|
2812
|
-
|
|
2929
|
+
raw = json.loads(line)
|
|
2930
|
+
# Normalize field names from run.sh index format
|
|
2931
|
+
# Index writes: {id, ts, iter, task, sha}
|
|
2932
|
+
# Frontend expects: {id, created_at, git_sha, message, iteration, ...}
|
|
2933
|
+
cp = {
|
|
2934
|
+
"id": raw.get("id", ""),
|
|
2935
|
+
"created_at": raw.get("ts", raw.get("created_at", raw.get("timestamp", ""))),
|
|
2936
|
+
"git_sha": raw.get("sha", raw.get("git_sha", "")),
|
|
2937
|
+
"message": raw.get("task", raw.get("message", raw.get("task_description", ""))),
|
|
2938
|
+
"iteration": raw.get("iter", raw.get("iteration")),
|
|
2939
|
+
}
|
|
2940
|
+
# Sanitize checkpoint id before using in path construction
|
|
2941
|
+
cp_id = cp["id"]
|
|
2942
|
+
if not cp_id or not _SAFE_ID_RE.match(cp_id):
|
|
2943
|
+
continue
|
|
2944
|
+
# Enrich from metadata.json if available
|
|
2945
|
+
meta_file = checkpoints_dir / cp_id / "metadata.json"
|
|
2946
|
+
if meta_file.exists():
|
|
2947
|
+
try:
|
|
2948
|
+
meta = json.loads(meta_file.read_text())
|
|
2949
|
+
cp["git_branch"] = meta.get("git_branch", "")
|
|
2950
|
+
cp["provider"] = meta.get("provider", "")
|
|
2951
|
+
cp["phase"] = meta.get("phase", "")
|
|
2952
|
+
# Count files in checkpoint dir
|
|
2953
|
+
cp_dir = checkpoints_dir / cp_id
|
|
2954
|
+
cp["files_count"] = sum(1 for f in cp_dir.rglob("*") if f.is_file() and f.name != "metadata.json")
|
|
2955
|
+
if not cp["message"]:
|
|
2956
|
+
cp["message"] = meta.get("task_description", "")
|
|
2957
|
+
if not cp["git_sha"]:
|
|
2958
|
+
cp["git_sha"] = meta.get("git_sha", "")
|
|
2959
|
+
if not cp["created_at"]:
|
|
2960
|
+
cp["created_at"] = meta.get("timestamp", "")
|
|
2961
|
+
except (json.JSONDecodeError, IOError):
|
|
2962
|
+
pass
|
|
2963
|
+
checkpoints.append(cp)
|
|
2813
2964
|
except json.JSONDecodeError:
|
|
2814
2965
|
pass
|
|
2815
2966
|
except Exception:
|