loki-mode 6.27.2 → 6.29.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/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/bmad-adapter.py +306 -7
- package/autonomy/loki +350 -231
- package/autonomy/run.sh +47 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/templates/dashboard.md +42 -0
- package/templates/data-pipeline.md +42 -0
- package/templates/game.md +42 -0
- package/templates/microservice.md +43 -0
- package/templates/npm-library.md +43 -0
- package/templates/slack-bot.md +42 -0
- package/templates/web-scraper.md +42 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.autonomi.dev/)
|
|
11
11
|
[](https://hub.docker.com/r/asklokesh/loki-mode)
|
|
12
12
|
|
|
13
|
-
**Current Version: v6.
|
|
13
|
+
**Current Version: v6.28.0**
|
|
14
14
|
|
|
15
15
|
### Traction
|
|
16
16
|
|
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.29.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.29.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.29.0
|
package/autonomy/bmad-adapter.py
CHANGED
|
@@ -306,6 +306,42 @@ def normalize_prd(prd_path: Path) -> Tuple[Dict[str, Any], str]:
|
|
|
306
306
|
|
|
307
307
|
# -- Epic/Story Extraction ----------------------------------------------------
|
|
308
308
|
|
|
309
|
+
def _extract_phase_label(text: str) -> Tuple[str, int]:
|
|
310
|
+
"""Extract MVP/phase label from an epic title or list entry.
|
|
311
|
+
|
|
312
|
+
Returns (priority_label, sort_weight):
|
|
313
|
+
- "(MVP)" -> ("mvp", 1)
|
|
314
|
+
- "(Phase 2)" or "post-MVP" -> ("phase2", 2)
|
|
315
|
+
- "(Phase 3)" or "deferred" -> ("phase3", 3)
|
|
316
|
+
- No label -> ("medium", 2)
|
|
317
|
+
"""
|
|
318
|
+
lower = text.lower()
|
|
319
|
+
if "(mvp)" in lower or "mvp" in lower.split():
|
|
320
|
+
return "mvp", 1
|
|
321
|
+
if re.search(r"\(phase\s*2\)", lower) or "post-mvp" in lower:
|
|
322
|
+
return "phase2", 2
|
|
323
|
+
if re.search(r"\(phase\s*3\)", lower) or "deferred" in lower:
|
|
324
|
+
return "phase3", 3
|
|
325
|
+
return "medium", 2
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _build_epic_phase_map(body: str) -> Dict[int, Tuple[str, int]]:
|
|
329
|
+
"""Scan the Epic List section for phase labels by epic number.
|
|
330
|
+
|
|
331
|
+
Looks for lines like: "- Epic 1: Core Task Board (MVP)"
|
|
332
|
+
Returns {epic_number: (priority_label, sort_weight)}.
|
|
333
|
+
"""
|
|
334
|
+
phase_map: Dict[int, Tuple[str, int]] = {}
|
|
335
|
+
for m in re.finditer(r"Epic\s+(\d+)\s*:\s*(.+)", body):
|
|
336
|
+
epic_num = int(m.group(1))
|
|
337
|
+
full_text = m.group(2).strip()
|
|
338
|
+
label, weight = _extract_phase_label(full_text)
|
|
339
|
+
# Only store if we actually found a label (not default)
|
|
340
|
+
if label != "medium" or epic_num not in phase_map:
|
|
341
|
+
phase_map[epic_num] = (label, weight)
|
|
342
|
+
return phase_map
|
|
343
|
+
|
|
344
|
+
|
|
309
345
|
def parse_epics(epics_path: Path) -> List[Dict[str, Any]]:
|
|
310
346
|
"""Parse epics.md into structured JSON.
|
|
311
347
|
|
|
@@ -313,11 +349,15 @@ def parse_epics(epics_path: Path) -> List[Dict[str, Any]]:
|
|
|
313
349
|
[
|
|
314
350
|
{
|
|
315
351
|
"epic": "Epic 1: Core Task Board",
|
|
352
|
+
"priority": "mvp",
|
|
353
|
+
"priority_weight": 1,
|
|
316
354
|
"description": "...",
|
|
317
355
|
"stories": [
|
|
318
356
|
{
|
|
319
357
|
"id": "1.1",
|
|
320
358
|
"title": "Task CRUD",
|
|
359
|
+
"priority": "mvp",
|
|
360
|
+
"priority_weight": 1,
|
|
321
361
|
"as_a": "team member",
|
|
322
362
|
"i_want": "create, edit, and delete tasks",
|
|
323
363
|
"so_that": "I can track my work items.",
|
|
@@ -330,6 +370,9 @@ def parse_epics(epics_path: Path) -> List[Dict[str, Any]]:
|
|
|
330
370
|
text = _safe_read(epics_path)
|
|
331
371
|
_, body = parse_frontmatter(text)
|
|
332
372
|
|
|
373
|
+
# Pre-scan for phase labels in Epic List section and headings
|
|
374
|
+
epic_phase_map = _build_epic_phase_map(body)
|
|
375
|
+
|
|
333
376
|
epics: List[Dict[str, Any]] = []
|
|
334
377
|
current_epic: Optional[Dict[str, Any]] = None
|
|
335
378
|
current_story: Optional[Dict[str, Any]] = None
|
|
@@ -349,7 +392,7 @@ def parse_epics(epics_path: Path) -> List[Dict[str, Any]]:
|
|
|
349
392
|
stripped = line.strip()
|
|
350
393
|
|
|
351
394
|
# Epic heading: ## Epic N: Title
|
|
352
|
-
epic_match = re.match(r"^##\s+(Epic\s
|
|
395
|
+
epic_match = re.match(r"^##\s+(Epic\s+(\d+).*)", stripped)
|
|
353
396
|
if epic_match:
|
|
354
397
|
# Flush any pending acceptance criteria
|
|
355
398
|
_flush_acceptance()
|
|
@@ -357,8 +400,18 @@ def parse_epics(epics_path: Path) -> List[Dict[str, Any]]:
|
|
|
357
400
|
current_story = None
|
|
358
401
|
|
|
359
402
|
epic_title = epic_match.group(1).strip()
|
|
403
|
+
epic_num = int(epic_match.group(2))
|
|
404
|
+
|
|
405
|
+
# Get phase from pre-scanned map, fallback to heading text
|
|
406
|
+
if epic_num in epic_phase_map:
|
|
407
|
+
priority, weight = epic_phase_map[epic_num]
|
|
408
|
+
else:
|
|
409
|
+
priority, weight = _extract_phase_label(epic_title)
|
|
410
|
+
|
|
360
411
|
current_epic = {
|
|
361
412
|
"epic": epic_title,
|
|
413
|
+
"priority": priority,
|
|
414
|
+
"priority_weight": weight,
|
|
362
415
|
"description": "",
|
|
363
416
|
"stories": [],
|
|
364
417
|
}
|
|
@@ -373,9 +426,14 @@ def parse_epics(epics_path: Path) -> List[Dict[str, Any]]:
|
|
|
373
426
|
|
|
374
427
|
story_id = story_match.group(1)
|
|
375
428
|
story_title = story_match.group(2).strip()
|
|
429
|
+
# Inherit priority from parent epic
|
|
430
|
+
epic_priority = current_epic.get("priority", "medium")
|
|
431
|
+
epic_weight = current_epic.get("priority_weight", 2)
|
|
376
432
|
current_story = {
|
|
377
433
|
"id": story_id,
|
|
378
434
|
"title": story_title,
|
|
435
|
+
"priority": epic_priority,
|
|
436
|
+
"priority_weight": epic_weight,
|
|
379
437
|
"as_a": "",
|
|
380
438
|
"i_want": "",
|
|
381
439
|
"so_that": "",
|
|
@@ -512,6 +570,153 @@ def parse_sprint_status(path: Path) -> set:
|
|
|
512
570
|
return completed
|
|
513
571
|
|
|
514
572
|
|
|
573
|
+
# -- Sprint Status Write-Back -------------------------------------------------
|
|
574
|
+
|
|
575
|
+
def write_sprint_status(path: Path, completed_stories: set) -> bool:
|
|
576
|
+
"""Update sprint-status.yml to mark completed stories.
|
|
577
|
+
|
|
578
|
+
Reads the existing file, finds story entries by name, and updates
|
|
579
|
+
their status to 'completed'. Writes back atomically.
|
|
580
|
+
|
|
581
|
+
Returns True if any updates were made.
|
|
582
|
+
"""
|
|
583
|
+
if not path.exists():
|
|
584
|
+
return False
|
|
585
|
+
|
|
586
|
+
text = _safe_read(path)
|
|
587
|
+
lines = text.split("\n")
|
|
588
|
+
updated = False
|
|
589
|
+
current_name: Optional[str] = None
|
|
590
|
+
|
|
591
|
+
# Normalize completed story names for matching
|
|
592
|
+
completed_lower = {s.lower() for s in completed_stories}
|
|
593
|
+
|
|
594
|
+
new_lines: List[str] = []
|
|
595
|
+
i = 0
|
|
596
|
+
while i < len(lines):
|
|
597
|
+
line = lines[i]
|
|
598
|
+
stripped = line.strip()
|
|
599
|
+
|
|
600
|
+
# Track story name
|
|
601
|
+
name_match = re.match(r'^(\s*)-?\s*name:\s*["\']?(.*?)["\']?\s*$', stripped)
|
|
602
|
+
if name_match:
|
|
603
|
+
current_name = name_match.group(2).strip()
|
|
604
|
+
new_lines.append(line)
|
|
605
|
+
i += 1
|
|
606
|
+
continue
|
|
607
|
+
|
|
608
|
+
# Update status line for matching stories
|
|
609
|
+
status_match = re.match(r'^(\s*)status:\s*["\']?(.*?)["\']?\s*$', stripped)
|
|
610
|
+
if status_match and current_name and current_name.lower() in completed_lower:
|
|
611
|
+
indent = status_match.group(1)
|
|
612
|
+
old_status = status_match.group(2).strip().lower()
|
|
613
|
+
if old_status not in ("completed", "done"):
|
|
614
|
+
# Preserve indentation, update status
|
|
615
|
+
new_lines.append(f"{indent}status: completed")
|
|
616
|
+
updated = True
|
|
617
|
+
current_name = None
|
|
618
|
+
i += 1
|
|
619
|
+
continue
|
|
620
|
+
|
|
621
|
+
new_lines.append(line)
|
|
622
|
+
i += 1
|
|
623
|
+
|
|
624
|
+
if updated:
|
|
625
|
+
_write_atomic(path, "\n".join(new_lines))
|
|
626
|
+
|
|
627
|
+
return updated
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def update_epics_checkboxes(epics_path: Path, completed_stories: set) -> bool:
|
|
631
|
+
"""Update epics.md to check off completed stories.
|
|
632
|
+
|
|
633
|
+
For each completed story, adds or updates a checkbox marker
|
|
634
|
+
under the story heading: `- [x] Completed` or `- [ ] Pending`.
|
|
635
|
+
|
|
636
|
+
Returns True if any updates were made.
|
|
637
|
+
"""
|
|
638
|
+
if not epics_path.exists():
|
|
639
|
+
return False
|
|
640
|
+
|
|
641
|
+
text = _safe_read(epics_path)
|
|
642
|
+
lines = text.split("\n")
|
|
643
|
+
|
|
644
|
+
# Normalize completed story IDs and titles for matching
|
|
645
|
+
completed_ids = set()
|
|
646
|
+
completed_titles = set()
|
|
647
|
+
for s in completed_stories:
|
|
648
|
+
completed_ids.add(s.lower())
|
|
649
|
+
completed_titles.add(s.lower())
|
|
650
|
+
|
|
651
|
+
new_lines: List[str] = []
|
|
652
|
+
updated = False
|
|
653
|
+
i = 0
|
|
654
|
+
while i < len(lines):
|
|
655
|
+
line = lines[i]
|
|
656
|
+
stripped = line.strip()
|
|
657
|
+
|
|
658
|
+
# Story heading: ### Story N.M: Title
|
|
659
|
+
story_match = re.match(r"^###\s+Story\s+(\d+\.\d+):\s*(.*)", stripped)
|
|
660
|
+
if story_match:
|
|
661
|
+
story_id = story_match.group(1)
|
|
662
|
+
story_title = story_match.group(2).strip()
|
|
663
|
+
is_completed = (
|
|
664
|
+
story_id in completed_ids
|
|
665
|
+
or story_title.lower() in completed_titles
|
|
666
|
+
or f"Story {story_id}: {story_title}".lower() in completed_titles
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
new_lines.append(line)
|
|
670
|
+
i += 1
|
|
671
|
+
|
|
672
|
+
# Check if next non-empty line is already a checkbox
|
|
673
|
+
# Skip blank lines between heading and checkbox
|
|
674
|
+
blank_count = 0
|
|
675
|
+
while i < len(lines) and not lines[i].strip():
|
|
676
|
+
new_lines.append(lines[i])
|
|
677
|
+
blank_count += 1
|
|
678
|
+
i += 1
|
|
679
|
+
|
|
680
|
+
if i < len(lines):
|
|
681
|
+
next_stripped = lines[i].strip()
|
|
682
|
+
checkbox_match = re.match(r"^-\s+\[([ xX])\]\s+(Completed|Pending)", next_stripped)
|
|
683
|
+
if checkbox_match:
|
|
684
|
+
# Update existing checkbox
|
|
685
|
+
if is_completed and checkbox_match.group(1) == " ":
|
|
686
|
+
new_lines.append("- [x] Completed")
|
|
687
|
+
updated = True
|
|
688
|
+
i += 1
|
|
689
|
+
continue
|
|
690
|
+
elif not is_completed and checkbox_match.group(1) in ("x", "X"):
|
|
691
|
+
new_lines.append("- [ ] Pending")
|
|
692
|
+
updated = True
|
|
693
|
+
i += 1
|
|
694
|
+
continue
|
|
695
|
+
else:
|
|
696
|
+
# Already correct state
|
|
697
|
+
new_lines.append(lines[i])
|
|
698
|
+
i += 1
|
|
699
|
+
continue
|
|
700
|
+
else:
|
|
701
|
+
# No existing checkbox -- insert one if story is completed
|
|
702
|
+
if is_completed:
|
|
703
|
+
new_lines.append("")
|
|
704
|
+
new_lines.append("- [x] Completed")
|
|
705
|
+
new_lines.append("")
|
|
706
|
+
updated = True
|
|
707
|
+
# Continue processing current line (don't skip it)
|
|
708
|
+
continue
|
|
709
|
+
continue
|
|
710
|
+
|
|
711
|
+
new_lines.append(line)
|
|
712
|
+
i += 1
|
|
713
|
+
|
|
714
|
+
if updated:
|
|
715
|
+
_write_atomic(epics_path, "\n".join(new_lines))
|
|
716
|
+
|
|
717
|
+
return updated
|
|
718
|
+
|
|
719
|
+
|
|
515
720
|
# -- Architecture Summary -----------------------------------------------------
|
|
516
721
|
|
|
517
722
|
def summarize_architecture(arch_path: Path) -> str:
|
|
@@ -820,6 +1025,75 @@ def run(
|
|
|
820
1025
|
return 0
|
|
821
1026
|
|
|
822
1027
|
|
|
1028
|
+
def write_back(
|
|
1029
|
+
project_path: str,
|
|
1030
|
+
completed_story: Optional[str] = None,
|
|
1031
|
+
completed_stories_file: Optional[str] = None,
|
|
1032
|
+
) -> int:
|
|
1033
|
+
"""Write-back mode: update sprint-status.yml and epics.md checkboxes.
|
|
1034
|
+
|
|
1035
|
+
Reads completed stories from either --completed-story arg or
|
|
1036
|
+
.loki/bmad-completed-stories.json, then updates the BMAD source files.
|
|
1037
|
+
|
|
1038
|
+
Returns exit code (0 = success, 1 = errors).
|
|
1039
|
+
"""
|
|
1040
|
+
artifacts = BmadArtifacts(project_path)
|
|
1041
|
+
if not artifacts.is_valid:
|
|
1042
|
+
print("ERROR: Not a valid BMAD project", file=sys.stderr)
|
|
1043
|
+
return 1
|
|
1044
|
+
|
|
1045
|
+
# Collect completed stories
|
|
1046
|
+
completed: set = set()
|
|
1047
|
+
|
|
1048
|
+
if completed_story:
|
|
1049
|
+
completed.add(completed_story)
|
|
1050
|
+
|
|
1051
|
+
if completed_stories_file:
|
|
1052
|
+
cpath = Path(completed_stories_file)
|
|
1053
|
+
if cpath.exists():
|
|
1054
|
+
try:
|
|
1055
|
+
data = json.loads(cpath.read_text(encoding="utf-8"))
|
|
1056
|
+
if isinstance(data, list):
|
|
1057
|
+
completed.update(s for s in data if isinstance(s, str))
|
|
1058
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
1059
|
+
print(f"Warning: Could not read completed stories file: {e}", file=sys.stderr)
|
|
1060
|
+
|
|
1061
|
+
if not completed:
|
|
1062
|
+
print("No completed stories to write back", file=sys.stderr)
|
|
1063
|
+
return 0
|
|
1064
|
+
|
|
1065
|
+
updates = 0
|
|
1066
|
+
|
|
1067
|
+
# Update sprint-status.yml
|
|
1068
|
+
if artifacts.sprint_status_path:
|
|
1069
|
+
try:
|
|
1070
|
+
if write_sprint_status(artifacts.sprint_status_path, completed):
|
|
1071
|
+
print(f"Updated sprint-status.yml with {len(completed)} completed stories")
|
|
1072
|
+
updates += 1
|
|
1073
|
+
else:
|
|
1074
|
+
print("sprint-status.yml: no changes needed")
|
|
1075
|
+
except Exception as e:
|
|
1076
|
+
print(f"Warning: Failed to update sprint-status.yml: {e}", file=sys.stderr)
|
|
1077
|
+
|
|
1078
|
+
# Update epics.md checkboxes
|
|
1079
|
+
if artifacts.epics_path:
|
|
1080
|
+
try:
|
|
1081
|
+
if update_epics_checkboxes(artifacts.epics_path, completed):
|
|
1082
|
+
print(f"Updated epics.md checkboxes for {len(completed)} completed stories")
|
|
1083
|
+
updates += 1
|
|
1084
|
+
else:
|
|
1085
|
+
print("epics.md: no changes needed")
|
|
1086
|
+
except Exception as e:
|
|
1087
|
+
print(f"Warning: Failed to update epics.md: {e}", file=sys.stderr)
|
|
1088
|
+
|
|
1089
|
+
if updates > 0:
|
|
1090
|
+
print(f"Write-back complete: {updates} file(s) updated")
|
|
1091
|
+
else:
|
|
1092
|
+
print("Write-back complete: no files updated")
|
|
1093
|
+
|
|
1094
|
+
return 0
|
|
1095
|
+
|
|
1096
|
+
|
|
823
1097
|
def main() -> None:
|
|
824
1098
|
parser = argparse.ArgumentParser(
|
|
825
1099
|
description="BMAD Artifact Adapter for Loki Mode",
|
|
@@ -830,6 +1104,7 @@ def main() -> None:
|
|
|
830
1104
|
" python3 bmad-adapter.py ./my-project --json\n"
|
|
831
1105
|
" python3 bmad-adapter.py ./my-project --validate\n"
|
|
832
1106
|
" python3 bmad-adapter.py ./my-project --output-dir .loki/ --validate\n"
|
|
1107
|
+
" python3 bmad-adapter.py ./my-project --write-back --completed-story 'Task CRUD'\n"
|
|
833
1108
|
),
|
|
834
1109
|
)
|
|
835
1110
|
parser.add_argument(
|
|
@@ -852,14 +1127,38 @@ def main() -> None:
|
|
|
852
1127
|
action="store_true",
|
|
853
1128
|
help="Run artifact chain validation",
|
|
854
1129
|
)
|
|
1130
|
+
parser.add_argument(
|
|
1131
|
+
"--write-back",
|
|
1132
|
+
action="store_true",
|
|
1133
|
+
dest="write_back_mode",
|
|
1134
|
+
help="Update sprint-status.yml and epics.md with completed stories",
|
|
1135
|
+
)
|
|
1136
|
+
parser.add_argument(
|
|
1137
|
+
"--completed-story",
|
|
1138
|
+
default=None,
|
|
1139
|
+
help="Name/title of a single completed story (for --write-back)",
|
|
1140
|
+
)
|
|
1141
|
+
parser.add_argument(
|
|
1142
|
+
"--completed-stories-file",
|
|
1143
|
+
default=None,
|
|
1144
|
+
help="Path to JSON file with list of completed story names (for --write-back)",
|
|
1145
|
+
)
|
|
855
1146
|
|
|
856
1147
|
args = parser.parse_args()
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1148
|
+
|
|
1149
|
+
if args.write_back_mode:
|
|
1150
|
+
exit_code = write_back(
|
|
1151
|
+
project_path=args.project_path,
|
|
1152
|
+
completed_story=args.completed_story,
|
|
1153
|
+
completed_stories_file=args.completed_stories_file,
|
|
1154
|
+
)
|
|
1155
|
+
else:
|
|
1156
|
+
exit_code = run(
|
|
1157
|
+
project_path=args.project_path,
|
|
1158
|
+
output_dir=args.output_dir,
|
|
1159
|
+
as_json=args.as_json,
|
|
1160
|
+
validate=args.validate,
|
|
1161
|
+
)
|
|
863
1162
|
sys.exit(exit_code)
|
|
864
1163
|
|
|
865
1164
|
|