loki-mode 6.15.0 → 6.15.1
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/bmad-adapter.py +91 -1
- package/autonomy/loki +2 -2
- package/autonomy/run.sh +31 -1
- package/dashboard/__init__.py +1 -1
- 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.15.
|
|
6
|
+
# Loki Mode v6.15.1
|
|
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.15.
|
|
270
|
+
**v6.15.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.15.
|
|
1
|
+
6.15.1
|
package/autonomy/bmad-adapter.py
CHANGED
|
@@ -147,6 +147,7 @@ class BmadArtifacts:
|
|
|
147
147
|
self.prd_path: Optional[Path] = None
|
|
148
148
|
self.architecture_path: Optional[Path] = None
|
|
149
149
|
self.epics_path: Optional[Path] = None
|
|
150
|
+
self.sprint_status_path: Optional[Path] = None
|
|
150
151
|
self.output_dir: Optional[Path] = None
|
|
151
152
|
self.errors: List[str] = []
|
|
152
153
|
self._discover()
|
|
@@ -202,6 +203,11 @@ class BmadArtifacts:
|
|
|
202
203
|
if epics_path.exists():
|
|
203
204
|
self.epics_path = epics_path
|
|
204
205
|
|
|
206
|
+
# Find sprint-status.yml (optional)
|
|
207
|
+
sprint_path = self.output_dir / "sprint-status.yml"
|
|
208
|
+
if sprint_path.exists():
|
|
209
|
+
self.sprint_status_path = sprint_path
|
|
210
|
+
|
|
205
211
|
@property
|
|
206
212
|
def is_valid(self) -> bool:
|
|
207
213
|
"""True if at least a PRD was found."""
|
|
@@ -213,6 +219,7 @@ class BmadArtifacts:
|
|
|
213
219
|
"prd": str(self.prd_path) if self.prd_path else None,
|
|
214
220
|
"architecture": str(self.architecture_path) if self.architecture_path else None,
|
|
215
221
|
"epics": str(self.epics_path) if self.epics_path else None,
|
|
222
|
+
"sprint_status": str(self.sprint_status_path) if self.sprint_status_path else None,
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
|
|
@@ -438,6 +445,73 @@ def parse_epics(epics_path: Path) -> List[Dict[str, Any]]:
|
|
|
438
445
|
return epics
|
|
439
446
|
|
|
440
447
|
|
|
448
|
+
# -- Sprint Status Parsing (stdlib-only YAML) ---------------------------------
|
|
449
|
+
|
|
450
|
+
def parse_sprint_status(path: Path) -> set:
|
|
451
|
+
"""Parse sprint-status.yml and return a set of completed story names.
|
|
452
|
+
|
|
453
|
+
Uses a simple line-by-line parser for the specific BMAD sprint-status
|
|
454
|
+
format (no PyYAML dependency). Recognizes stories with status
|
|
455
|
+
'completed' or 'done' (case-insensitive).
|
|
456
|
+
|
|
457
|
+
Expected format:
|
|
458
|
+
epics:
|
|
459
|
+
- name: "Epic Name"
|
|
460
|
+
status: in-progress
|
|
461
|
+
stories:
|
|
462
|
+
- name: "Story title"
|
|
463
|
+
status: completed
|
|
464
|
+
"""
|
|
465
|
+
text = _safe_read(path)
|
|
466
|
+
completed: set = set()
|
|
467
|
+
current_name: Optional[str] = None
|
|
468
|
+
in_stories = False
|
|
469
|
+
|
|
470
|
+
for line in text.split("\n"):
|
|
471
|
+
stripped = line.strip()
|
|
472
|
+
if not stripped or stripped.startswith("#"):
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
# Detect stories: block start
|
|
476
|
+
if stripped == "stories:":
|
|
477
|
+
in_stories = True
|
|
478
|
+
current_name = None
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
# Detect epics: block start (reset stories context)
|
|
482
|
+
if stripped == "epics:":
|
|
483
|
+
in_stories = False
|
|
484
|
+
current_name = None
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
# Top-level list item under epics resets stories context
|
|
488
|
+
# (indentation: " - name:" for epics vs " - name:" for stories)
|
|
489
|
+
indent = len(line) - len(line.lstrip())
|
|
490
|
+
|
|
491
|
+
if in_stories:
|
|
492
|
+
# Story name line: " - name: ..." or " name: ..."
|
|
493
|
+
name_match = re.match(r'^-?\s*name:\s*["\']?(.*?)["\']?\s*$', stripped)
|
|
494
|
+
if name_match:
|
|
495
|
+
current_name = name_match.group(1).strip()
|
|
496
|
+
continue
|
|
497
|
+
|
|
498
|
+
# Story status line
|
|
499
|
+
status_match = re.match(r'^status:\s*["\']?(.*?)["\']?\s*$', stripped)
|
|
500
|
+
if status_match:
|
|
501
|
+
status = status_match.group(1).strip().lower()
|
|
502
|
+
if status in ("completed", "done") and current_name:
|
|
503
|
+
completed.add(current_name)
|
|
504
|
+
current_name = None
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
# A new epic-level item resets context
|
|
508
|
+
if indent <= 4 and stripped.startswith("- name:"):
|
|
509
|
+
in_stories = False
|
|
510
|
+
current_name = None
|
|
511
|
+
|
|
512
|
+
return completed
|
|
513
|
+
|
|
514
|
+
|
|
441
515
|
# -- Architecture Summary -----------------------------------------------------
|
|
442
516
|
|
|
443
517
|
def summarize_architecture(arch_path: Path) -> str:
|
|
@@ -593,6 +667,7 @@ def write_outputs(
|
|
|
593
667
|
arch_summary: Optional[str],
|
|
594
668
|
tasks_json: Optional[List[Dict[str, Any]]],
|
|
595
669
|
validation_report: Optional[List[Dict[str, str]]],
|
|
670
|
+
completed_stories: Optional[set] = None,
|
|
596
671
|
) -> List[str]:
|
|
597
672
|
"""Write all output files to the specified directory.
|
|
598
673
|
|
|
@@ -633,6 +708,12 @@ def write_outputs(
|
|
|
633
708
|
_write_atomic(val_path, "\n".join(val_lines) + "\n")
|
|
634
709
|
written.append(str(val_path))
|
|
635
710
|
|
|
711
|
+
# bmad-completed-stories.json
|
|
712
|
+
if completed_stories:
|
|
713
|
+
completed_path = output_dir / "bmad-completed-stories.json"
|
|
714
|
+
_write_atomic(completed_path, json.dumps(sorted(completed_stories), indent=2))
|
|
715
|
+
written.append(str(completed_path))
|
|
716
|
+
|
|
636
717
|
return written
|
|
637
718
|
|
|
638
719
|
|
|
@@ -674,6 +755,11 @@ def run(
|
|
|
674
755
|
if artifacts.epics_path:
|
|
675
756
|
epics_data = parse_epics(artifacts.epics_path)
|
|
676
757
|
|
|
758
|
+
# 4b. Parse sprint status (optional)
|
|
759
|
+
completed_stories: Optional[set] = None
|
|
760
|
+
if artifacts.sprint_status_path:
|
|
761
|
+
completed_stories = parse_sprint_status(artifacts.sprint_status_path)
|
|
762
|
+
|
|
677
763
|
# 5. Build combined metadata
|
|
678
764
|
combined_metadata: Dict[str, Any] = {
|
|
679
765
|
"project_classification": classification,
|
|
@@ -714,6 +800,7 @@ def run(
|
|
|
714
800
|
arch_summary=arch_summary,
|
|
715
801
|
tasks_json=epics_data,
|
|
716
802
|
validation_report=validation_report,
|
|
803
|
+
completed_stories=completed_stories,
|
|
717
804
|
)
|
|
718
805
|
|
|
719
806
|
print(f"BMAD adapter: processed {artifacts.prd_path}")
|
|
@@ -722,7 +809,10 @@ def run(
|
|
|
722
809
|
print(f" Classification: {classification.get('project_type', 'unknown')} / {classification.get('complexity', 'unknown')}")
|
|
723
810
|
print(f" Artifacts: PRD={'found' if artifacts.prd_path else 'MISSING'}, "
|
|
724
811
|
f"Architecture={'found' if artifacts.architecture_path else 'missing'}, "
|
|
725
|
-
f"Epics={'found' if artifacts.epics_path else 'missing'}"
|
|
812
|
+
f"Epics={'found' if artifacts.epics_path else 'missing'}, "
|
|
813
|
+
f"SprintStatus={'found' if artifacts.sprint_status_path else 'missing'}")
|
|
814
|
+
if completed_stories:
|
|
815
|
+
print(f" Completed stories (will skip): {len(completed_stories)}")
|
|
726
816
|
print(f" Output files written to {abs_output_dir}/:")
|
|
727
817
|
for path in written:
|
|
728
818
|
print(f" - {Path(path).name}")
|
package/autonomy/loki
CHANGED
|
@@ -756,7 +756,7 @@ cmd_start() {
|
|
|
756
756
|
|
|
757
757
|
# Run the BMAD adapter to normalize artifacts
|
|
758
758
|
echo -e "${CYAN}Running BMAD adapter...${NC}"
|
|
759
|
-
local adapter_script="$
|
|
759
|
+
local adapter_script="$(dirname "$(resolve_script_path "$0")")/bmad-adapter.py"
|
|
760
760
|
if [[ ! -f "$adapter_script" ]]; then
|
|
761
761
|
echo -e "${RED}Error: BMAD adapter not found at $adapter_script${NC}"
|
|
762
762
|
echo "Please ensure autonomy/bmad-adapter.py exists."
|
|
@@ -808,7 +808,7 @@ cmd_start() {
|
|
|
808
808
|
|
|
809
809
|
# Run the OpenSpec adapter to normalize artifacts
|
|
810
810
|
echo -e "${CYAN}Running OpenSpec adapter...${NC}"
|
|
811
|
-
local adapter_script="$
|
|
811
|
+
local adapter_script="$(dirname "$(resolve_script_path "$0")")/openspec-adapter.py"
|
|
812
812
|
if [[ ! -f "$adapter_script" ]]; then
|
|
813
813
|
echo -e "${RED}Error: OpenSpec adapter not found at $adapter_script${NC}"
|
|
814
814
|
echo "Please ensure autonomy/openspec-adapter.py exists."
|
package/autonomy/run.sh
CHANGED
|
@@ -7879,6 +7879,7 @@ import sys
|
|
|
7879
7879
|
|
|
7880
7880
|
bmad_tasks_path = ".loki/bmad-tasks.json"
|
|
7881
7881
|
pending_path = ".loki/queue/pending.json"
|
|
7882
|
+
completed_stories_path = ".loki/bmad-completed-stories.json"
|
|
7882
7883
|
|
|
7883
7884
|
try:
|
|
7884
7885
|
with open(bmad_tasks_path, "r") as f:
|
|
@@ -7887,6 +7888,17 @@ except (json.JSONDecodeError, FileNotFoundError) as e:
|
|
|
7887
7888
|
print(f"Warning: Could not read BMAD tasks: {e}", file=sys.stderr)
|
|
7888
7889
|
sys.exit(0)
|
|
7889
7890
|
|
|
7891
|
+
# Load completed stories from sprint-status (if available)
|
|
7892
|
+
completed_stories = set()
|
|
7893
|
+
if os.path.exists(completed_stories_path):
|
|
7894
|
+
try:
|
|
7895
|
+
with open(completed_stories_path, "r") as f:
|
|
7896
|
+
completed_list = json.load(f)
|
|
7897
|
+
if isinstance(completed_list, list):
|
|
7898
|
+
completed_stories = {s.lower() for s in completed_list if isinstance(s, str)}
|
|
7899
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
7900
|
+
pass
|
|
7901
|
+
|
|
7890
7902
|
# Extract stories from BMAD structure
|
|
7891
7903
|
# Supports both flat list and nested epic/story format
|
|
7892
7904
|
stories = []
|
|
@@ -7914,6 +7926,21 @@ if not stories:
|
|
|
7914
7926
|
print("No BMAD stories found to queue", file=sys.stderr)
|
|
7915
7927
|
sys.exit(0)
|
|
7916
7928
|
|
|
7929
|
+
# Filter out completed stories from sprint-status
|
|
7930
|
+
skipped_count = 0
|
|
7931
|
+
if completed_stories:
|
|
7932
|
+
filtered = []
|
|
7933
|
+
for story in stories:
|
|
7934
|
+
if isinstance(story, dict):
|
|
7935
|
+
title = story.get("title", story.get("name", "")).lower()
|
|
7936
|
+
if title and title in completed_stories:
|
|
7937
|
+
skipped_count += 1
|
|
7938
|
+
continue
|
|
7939
|
+
filtered.append(story)
|
|
7940
|
+
stories = filtered
|
|
7941
|
+
if skipped_count > 0:
|
|
7942
|
+
print(f"Skipped {skipped_count} completed stories (from sprint-status.yml)", file=sys.stderr)
|
|
7943
|
+
|
|
7917
7944
|
# Load existing pending tasks (if any)
|
|
7918
7945
|
existing = []
|
|
7919
7946
|
if os.path.exists(pending_path):
|
|
@@ -7954,7 +7981,10 @@ for i, story in enumerate(stories):
|
|
|
7954
7981
|
with open(pending_path, "w") as f:
|
|
7955
7982
|
json.dump(existing, f, indent=2)
|
|
7956
7983
|
|
|
7957
|
-
|
|
7984
|
+
msg = f"Added {len(stories)} BMAD stories to task queue"
|
|
7985
|
+
if skipped_count > 0:
|
|
7986
|
+
msg += f" (skipped {skipped_count} completed)"
|
|
7987
|
+
print(msg)
|
|
7958
7988
|
BMAD_QUEUE_EOF
|
|
7959
7989
|
|
|
7960
7990
|
if [[ $? -ne 0 ]]; then
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED