loki-mode 6.28.0 → 6.30.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 CHANGED
@@ -10,7 +10,7 @@
10
10
  [![Autonomi](https://img.shields.io/badge/Autonomi-autonomi.dev-5B4EEA)](https://www.autonomi.dev/)
11
11
  [![Docker Pulls](https://img.shields.io/docker/pulls/asklokesh/loki-mode)](https://hub.docker.com/r/asklokesh/loki-mode)
12
12
 
13
- **Current Version: v6.28.0**
13
+ **Current Version: v6.30.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.28.0
6
+ # Loki Mode v6.30.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.28.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
270
+ **v6.30.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.28.0
1
+ 6.30.0
@@ -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+\d+.*)", stripped)
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
- exit_code = run(
858
- project_path=args.project_path,
859
- output_dir=args.output_dir,
860
- as_json=args.as_json,
861
- validate=args.validate,
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
 
package/autonomy/loki CHANGED
@@ -442,6 +442,7 @@ show_help() {
442
442
  echo " ci [opts] CI/CD quality gate integration (--pr, --report, --github-comment)"
443
443
  echo " test [opts] AI-powered test generation (--file, --dir, --changed, --dry-run)"
444
444
  echo " report [opts] Session report generator (--format text|markdown|html, --output)"
445
+ echo " share [opts] Share session report as GitHub Gist (--private, --format)"
445
446
  echo " version Show version"
446
447
  echo " help Show this help"
447
448
  echo ""
@@ -9557,6 +9558,9 @@ main() {
9557
9558
  report)
9558
9559
  cmd_report "$@"
9559
9560
  ;;
9561
+ share)
9562
+ cmd_share "$@"
9563
+ ;;
9560
9564
  version|--version|-v)
9561
9565
  cmd_version
9562
9566
  ;;
@@ -17233,4 +17237,118 @@ REPORT_SCRIPT
17233
17237
  fi
17234
17238
  }
17235
17239
 
17240
+ cmd_share() {
17241
+ local format="markdown"
17242
+ local visibility="--public"
17243
+
17244
+ while [[ $# -gt 0 ]]; do
17245
+ case "$1" in
17246
+ --help|-h)
17247
+ echo -e "${BOLD}loki share${NC} - Share session report as a GitHub Gist"
17248
+ echo ""
17249
+ echo "Usage: loki share [options]"
17250
+ echo ""
17251
+ echo "Generates a session report and uploads it as a GitHub Gist,"
17252
+ echo "returning a shareable URL."
17253
+ echo ""
17254
+ echo "Options:"
17255
+ echo " --private Create a secret gist (default: public)"
17256
+ echo " --format <fmt> Report format: text, markdown, html (default: markdown)"
17257
+ echo " --help, -h Show this help"
17258
+ echo ""
17259
+ echo "Examples:"
17260
+ echo " loki share # Public markdown gist"
17261
+ echo " loki share --private # Secret gist"
17262
+ echo " loki share --format html # Public HTML gist"
17263
+ echo " loki share --private --format text # Secret text gist"
17264
+ echo ""
17265
+ echo "Requires: gh CLI (https://cli.github.com)"
17266
+ exit 0
17267
+ ;;
17268
+ --private) visibility=""; shift ;;
17269
+ --format) format="${2:-markdown}"; shift 2 ;;
17270
+ --format=*) format="${1#*=}"; shift ;;
17271
+ *) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki share --help' for usage."; exit 1 ;;
17272
+ esac
17273
+ done
17274
+
17275
+ # Validate format
17276
+ case "$format" in
17277
+ text|markdown|md|html) ;;
17278
+ *) echo -e "${RED}Unknown format: $format${NC}"; echo "Supported: text, markdown, html"; exit 1 ;;
17279
+ esac
17280
+ [ "$format" = "md" ] && format="markdown"
17281
+
17282
+ # Check gh CLI is installed
17283
+ if ! command -v gh &>/dev/null; then
17284
+ echo -e "${RED}gh CLI not found${NC}"
17285
+ echo ""
17286
+ echo "Install the GitHub CLI to use 'loki share':"
17287
+ echo " brew install gh # macOS"
17288
+ echo " sudo apt install gh # Ubuntu/Debian"
17289
+ echo " https://cli.github.com # Other platforms"
17290
+ exit 1
17291
+ fi
17292
+
17293
+ # Check gh is authenticated
17294
+ if ! gh auth status &>/dev/null 2>&1; then
17295
+ echo -e "${RED}GitHub CLI not authenticated${NC}"
17296
+ echo ""
17297
+ echo "Run 'gh auth login' to authenticate, then try again."
17298
+ exit 1
17299
+ fi
17300
+
17301
+ # Check .loki directory exists
17302
+ local loki_dir="${LOKI_DIR:-.loki}"
17303
+ if [ ! -d "$loki_dir" ]; then
17304
+ echo -e "${RED}No .loki/ directory found${NC}"
17305
+ echo "Run 'loki start' first to create a session."
17306
+ exit 1
17307
+ fi
17308
+
17309
+ # Determine file extension for gist
17310
+ local ext="md"
17311
+ case "$format" in
17312
+ text) ext="txt" ;;
17313
+ markdown) ext="md" ;;
17314
+ html) ext="html" ;;
17315
+ esac
17316
+
17317
+ # Generate report to temp file
17318
+ local tmpfile
17319
+ tmpfile=$(mktemp "/tmp/loki-share-XXXXXX.$ext")
17320
+
17321
+ echo "Generating session report..."
17322
+ if ! loki report --format "$format" > "$tmpfile" 2>/dev/null; then
17323
+ echo -e "${RED}Failed to generate session report${NC}"
17324
+ rm -f "$tmpfile"
17325
+ exit 1
17326
+ fi
17327
+
17328
+ # Verify report is not empty
17329
+ if [ ! -s "$tmpfile" ]; then
17330
+ echo -e "${RED}Generated report is empty${NC}"
17331
+ rm -f "$tmpfile"
17332
+ exit 1
17333
+ fi
17334
+
17335
+ # Upload as gist
17336
+ echo "Uploading session report..."
17337
+ local gist_desc="Loki Mode session report ($(date +%Y-%m-%d))"
17338
+ local gist_url
17339
+ gist_url=$(gh gist create "$tmpfile" --desc "$gist_desc" $visibility 2>&1)
17340
+ local gist_exit=$?
17341
+
17342
+ # Cleanup temp file
17343
+ rm -f "$tmpfile"
17344
+
17345
+ if [ $gist_exit -ne 0 ]; then
17346
+ echo -e "${RED}Failed to create gist${NC}"
17347
+ echo "$gist_url"
17348
+ exit 1
17349
+ fi
17350
+
17351
+ echo -e "${GREEN}Shared: ${gist_url}${NC}"
17352
+ }
17353
+
17236
17354
  main "$@"
package/autonomy/run.sh CHANGED
@@ -3629,6 +3629,11 @@ except:
3629
3629
 
3630
3630
  # Clear current-task.json
3631
3631
  echo "{}" > .loki/queue/current-task.json
3632
+
3633
+ # Write-back completed BMAD stories to source artifacts (v6.29.0)
3634
+ if [ "$exit_code" = "0" ]; then
3635
+ bmad_write_back
3636
+ fi
3632
3637
  }
3633
3638
 
3634
3639
  start_status_monitor() {
@@ -8198,6 +8203,9 @@ if not stories:
8198
8203
  print("No BMAD stories found to queue", file=sys.stderr)
8199
8204
  sys.exit(0)
8200
8205
 
8206
+ # Sort stories by priority_weight (MVP=1 first, then phase2=2, then phase3=3)
8207
+ stories.sort(key=lambda s: s.get("priority_weight", 2) if isinstance(s, dict) else 2)
8208
+
8201
8209
  # Filter out completed stories from sprint-status
8202
8210
  skipped_count = 0
8203
8211
  if completed_stories:
@@ -8269,6 +8277,45 @@ BMAD_QUEUE_EOF
8269
8277
  log_info "BMAD queue population complete"
8270
8278
  }
8271
8279
 
8280
+ # Write-back completed BMAD stories to sprint-status.yml and epics.md
8281
+ # Called after each iteration to sync completion state back to BMAD artifacts
8282
+ bmad_write_back() {
8283
+ # Skip if not a BMAD project
8284
+ local bmad_project="${BMAD_PROJECT_PATH:-}"
8285
+ if [[ -z "$bmad_project" ]]; then
8286
+ return 0
8287
+ fi
8288
+
8289
+ # Skip if no completed stories file
8290
+ local completed_file=".loki/bmad-completed-stories.json"
8291
+ if [[ ! -f "$completed_file" ]]; then
8292
+ return 0
8293
+ fi
8294
+
8295
+ # Skip if completed stories file is empty or just []
8296
+ local story_count
8297
+ story_count=$(python3 -c "import json; data=json.load(open('$completed_file')); print(len(data))" 2>/dev/null || echo "0")
8298
+ if [[ "$story_count" -eq 0 ]]; then
8299
+ return 0
8300
+ fi
8301
+
8302
+ # Find the adapter script
8303
+ local adapter_script="${SCRIPT_DIR}/bmad-adapter.py"
8304
+ if [[ ! -f "$adapter_script" ]]; then
8305
+ log_warn "BMAD adapter not found, skipping write-back"
8306
+ return 0
8307
+ fi
8308
+
8309
+ # Run write-back (warn on failure, never crash)
8310
+ if python3 "$adapter_script" "$bmad_project" \
8311
+ --write-back \
8312
+ --completed-stories-file "$completed_file" 2>/dev/null; then
8313
+ log_info "BMAD write-back: synced completed stories to source artifacts"
8314
+ else
8315
+ log_warn "BMAD write-back failed (non-fatal)"
8316
+ fi
8317
+ }
8318
+
8272
8319
  #===============================================================================
8273
8320
  # OpenSpec Task Queue Population
8274
8321
  #===============================================================================
package/completions/_loki CHANGED
@@ -70,6 +70,12 @@ function _loki {
70
70
  issue)
71
71
  _loki_issue
72
72
  ;;
73
+ share)
74
+ _arguments \
75
+ '--private[Create a secret gist]' \
76
+ '--format[Report format]:format:(text markdown html)' \
77
+ '--help[Show help]'
78
+ ;;
73
79
  completions)
74
80
  _arguments '1:shell:(bash zsh)'
75
81
  ;;
@@ -109,6 +115,7 @@ function _loki_commands {
109
115
  'voice:Voice input commands'
110
116
  'doctor:Check system prerequisites'
111
117
  'onboard:Analyze repo and generate CLAUDE.md'
118
+ 'share:Share session report as GitHub Gist'
112
119
  'version:Show version'
113
120
  'completions:Output shell completions'
114
121
  'help:Show help'
@@ -5,7 +5,7 @@ _loki_completion() {
5
5
  _init_completion || return
6
6
 
7
7
  # Main subcommands (must match autonomy/loki main case statement)
8
- local main_commands="start quick demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise voice secrets doctor watchdog audit metrics syslog onboard version completions help"
8
+ local main_commands="start quick demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise voice secrets doctor watchdog audit metrics syslog onboard share version completions help"
9
9
 
10
10
  # 1. If we are on the first argument (subcommand)
11
11
  if [[ $cword -eq 1 ]]; then
@@ -144,6 +144,17 @@ _loki_completion() {
144
144
  _filedir -d
145
145
  ;;
146
146
 
147
+ share)
148
+ if [[ "$cur" == -* ]]; then
149
+ COMPREPLY=( $(compgen -W "--private --format --help" -- "$cur") )
150
+ return 0
151
+ fi
152
+ if [[ "$prev" == "--format" ]]; then
153
+ COMPREPLY=( $(compgen -W "text markdown html" -- "$cur") )
154
+ return 0
155
+ fi
156
+ ;;
157
+
147
158
  completions)
148
159
  COMPREPLY=( $(compgen -W "bash zsh" -- "$cur") )
149
160
  ;;
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.28.0"
10
+ __version__ = "6.30.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v6.28.0
5
+ **Version:** v6.30.0
6
6
 
7
7
  ---
8
8
 
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.28.0'
60
+ __version__ = '6.30.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "6.28.0",
3
+ "version": "6.30.0",
4
4
  "description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "agent",