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 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.8.0
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.8.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
270
+ **v6.9.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.8.0
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
- # Check provider-specific var
1940
- eval "local pval=\${$provider_var+x}"
1941
- if [ -n "$pval" ]; then
1942
- source="$provider_var"
1943
- else
1944
- # Check generic var
1945
- eval "local gval=\${$generic_var+x}"
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
- fi
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
- # Special cases: codex uses single LOKI_CODEX_MODEL, aider/cline too
1952
- if [ "$provider" = "codex" ] && [ -n "${LOKI_CODEX_MODEL+x}" ] && [ "$source" = "default" ]; then
1953
- source="LOKI_CODEX_MODEL"
1954
- elif [ "$provider" = "aider" ] && [ -n "${LOKI_AIDER_MODEL+x}" ] && [ "$source" = "default" ]; then
1955
- source="LOKI_AIDER_MODEL"
1956
- elif [ "$provider" = "cline" ] && [ -n "${LOKI_CLINE_MODEL+x}" ] && [ "$source" = "default" ]; then
1957
- source="LOKI_CLINE_MODEL"
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.8.0"
10
+ __version__ = "6.9.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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
- checkpoints.append(json.loads(line))
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: