loki-mode 6.8.0 → 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/autonomy/loki +37 -19
- 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/src/observability/spans.js +9 -9
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/autonomy/loki
CHANGED
|
@@ -1932,29 +1932,47 @@ cmd_provider_models() {
|
|
|
1932
1932
|
for tier in planning development fast; do
|
|
1933
1933
|
local tier_upper
|
|
1934
1934
|
tier_upper=$(echo "$tier" | tr '[:lower:]' '[:upper:]')
|
|
1935
|
-
local provider_var="LOKI_${provider_upper}_MODEL_${tier_upper}"
|
|
1936
|
-
local generic_var="LOKI_MODEL_${tier_upper}"
|
|
1937
1935
|
local source="default"
|
|
1938
1936
|
|
|
1939
|
-
#
|
|
1940
|
-
|
|
1941
|
-
if [
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
if [ -n "$gval" ]; then
|
|
1947
|
-
source="$generic_var"
|
|
1937
|
+
# Single-model providers (aider, cline) only chain through their own env var
|
|
1938
|
+
# and LOKI_MODEL_DEVELOPMENT -- they ignore per-tier and generic tier vars
|
|
1939
|
+
if [ "$provider" = "aider" ]; then
|
|
1940
|
+
if [ -n "${LOKI_AIDER_MODEL+x}" ]; then
|
|
1941
|
+
source="LOKI_AIDER_MODEL"
|
|
1942
|
+
elif [ -n "${LOKI_MODEL_DEVELOPMENT+x}" ]; then
|
|
1943
|
+
source="LOKI_MODEL_DEVELOPMENT"
|
|
1948
1944
|
fi
|
|
1949
|
-
|
|
1945
|
+
elif [ "$provider" = "cline" ]; then
|
|
1946
|
+
if [ -n "${LOKI_CLINE_MODEL+x}" ]; then
|
|
1947
|
+
source="LOKI_CLINE_MODEL"
|
|
1948
|
+
elif [ -n "${LOKI_MODEL_DEVELOPMENT+x}" ]; then
|
|
1949
|
+
source="LOKI_MODEL_DEVELOPMENT"
|
|
1950
|
+
fi
|
|
1951
|
+
elif [ "$provider" = "codex" ]; then
|
|
1952
|
+
# Codex uses single LOKI_CODEX_MODEL or generic tier vars
|
|
1953
|
+
if [ -n "${LOKI_CODEX_MODEL+x}" ]; then
|
|
1954
|
+
source="LOKI_CODEX_MODEL"
|
|
1955
|
+
else
|
|
1956
|
+
local generic_var="LOKI_MODEL_${tier_upper}"
|
|
1957
|
+
eval "local gval=\${$generic_var+x}"
|
|
1958
|
+
if [ -n "$gval" ]; then
|
|
1959
|
+
source="$generic_var"
|
|
1960
|
+
fi
|
|
1961
|
+
fi
|
|
1962
|
+
else
|
|
1963
|
+
# Multi-tier providers (claude, gemini): check provider-specific per-tier, then generic
|
|
1964
|
+
local provider_var="LOKI_${provider_upper}_MODEL_${tier_upper}"
|
|
1965
|
+
local generic_var="LOKI_MODEL_${tier_upper}"
|
|
1950
1966
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1967
|
+
eval "local pval=\${$provider_var+x}"
|
|
1968
|
+
if [ -n "$pval" ]; then
|
|
1969
|
+
source="$provider_var"
|
|
1970
|
+
else
|
|
1971
|
+
eval "local gval=\${$generic_var+x}"
|
|
1972
|
+
if [ -n "$gval" ]; then
|
|
1973
|
+
source="$generic_var"
|
|
1974
|
+
fi
|
|
1975
|
+
fi
|
|
1958
1976
|
fi
|
|
1959
1977
|
|
|
1960
1978
|
local value
|
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:
|