loki-mode 6.15.0 → 6.16.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.15.0
6
+ # Loki Mode v6.16.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.15.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
270
+ **v6.16.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.15.0
1
+ 6.16.0
@@ -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="${SCRIPT_DIR:-$(dirname "$0")}/bmad-adapter.py"
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="${SCRIPT_DIR:-$(dirname "$0")}/openspec-adapter.py"
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
- print(f"Added {len(stories)} BMAD stories to task queue")
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.15.0"
10
+ __version__ = "6.16.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: