loki-mode 5.58.2 → 6.0.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/autonomy/run.sh CHANGED
@@ -177,9 +177,9 @@ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
177
177
  # Solution: Copy ourselves to /tmp and run from there. The original can be safely edited.
178
178
  #===============================================================================
179
179
  if [[ -z "${LOKI_RUNNING_FROM_TEMP:-}" ]] && [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
180
- TEMP_SCRIPT="/tmp/loki-run-$$.sh"
180
+ TEMP_SCRIPT=$(mktemp /tmp/loki-run-XXXXXX.sh)
181
181
  cp "${BASH_SOURCE[0]}" "$TEMP_SCRIPT"
182
- chmod +x "$TEMP_SCRIPT"
182
+ chmod 700 "$TEMP_SCRIPT"
183
183
  export LOKI_RUNNING_FROM_TEMP=1
184
184
  export LOKI_ORIGINAL_SCRIPT_DIR="$SCRIPT_DIR"
185
185
  export LOKI_ORIGINAL_PROJECT_DIR="$PROJECT_DIR"
@@ -471,6 +471,47 @@ parse_yaml_with_yq() {
471
471
  # Load config file before setting defaults
472
472
  load_config_file
473
473
 
474
+ # Load JSON settings from loki config set (v6.0.0)
475
+ _load_json_settings() {
476
+ local settings_file="${TARGET_DIR:-.}/.loki/config/settings.json"
477
+ [ -f "$settings_file" ] || return 0
478
+ eval "$(_LOKI_SETTINGS_FILE="$settings_file" python3 -c "
479
+ import json, sys, os, shlex
480
+
481
+ def get_nested(d, key):
482
+ \"\"\"Resolve dotted keys through nested dicts (model.planning -> data['model']['planning'])\"\"\"
483
+ parts = key.split('.')
484
+ cur = d
485
+ for p in parts:
486
+ if isinstance(cur, dict):
487
+ cur = cur.get(p)
488
+ else:
489
+ return None
490
+ return cur
491
+
492
+ try:
493
+ with open(os.environ['_LOKI_SETTINGS_FILE']) as f:
494
+ data = json.load(f)
495
+ except Exception:
496
+ sys.exit(0)
497
+ mapping = {
498
+ 'maxTier': 'LOKI_MAX_TIER',
499
+ 'model.planning': 'LOKI_MODEL_PLANNING',
500
+ 'model.development': 'LOKI_MODEL_DEVELOPMENT',
501
+ 'model.fast': 'LOKI_MODEL_FAST',
502
+ 'notify.slack': 'LOKI_SLACK_WEBHOOK',
503
+ 'notify.discord': 'LOKI_DISCORD_WEBHOOK',
504
+ }
505
+ for key, env_var in mapping.items():
506
+ # Try nested dict lookup first, then flat key, then underscore variant
507
+ val = get_nested(data, key) or data.get(key) or data.get(key.replace('.', '_'))
508
+ if val and isinstance(val, str):
509
+ safe_val = shlex.quote(val)
510
+ print(f'[ -z \"\${{{env_var}:-}}\" ] && export {env_var}={safe_val}')
511
+ " 2>/dev/null)" 2>/dev/null || true
512
+ }
513
+ _LOKI_SETTINGS_FILE="${TARGET_DIR:-.}/.loki/config/settings.json" _load_json_settings
514
+
474
515
  # Configuration
475
516
  MAX_RETRIES=${LOKI_MAX_RETRIES:-50}
476
517
  BASE_WAIT=${LOKI_BASE_WAIT:-60}
@@ -1305,10 +1346,25 @@ get_rarv_phase_name() {
1305
1346
  }
1306
1347
 
1307
1348
  # Get provider-specific tier parameter based on current tier
1308
- # Uses provider config variables for the tier mapping
1349
+ # v6.0.0: Delegates to resolve_model_for_tier() if available (dynamic resolution).
1350
+ # Falls back to static mapping for backward compatibility.
1309
1351
  get_provider_tier_param() {
1310
1352
  local tier="${1:-$CURRENT_TIER}"
1311
1353
 
1354
+ # v6.0.0: Use dynamic resolution if provider has resolve_model_for_tier
1355
+ if type resolve_model_for_tier &>/dev/null; then
1356
+ local resolved
1357
+ resolved=$(resolve_model_for_tier "$tier")
1358
+ # For Claude, extract short name (opus/sonnet/haiku) from full model ID
1359
+ if [ "${PROVIDER_NAME:-claude}" = "claude" ]; then
1360
+ echo "$resolved" | sed 's/claude-\([a-z]*\).*/\1/'
1361
+ else
1362
+ echo "$resolved"
1363
+ fi
1364
+ return
1365
+ fi
1366
+
1367
+ # Legacy fallback: static tier mapping
1312
1368
  case "${PROVIDER_NAME:-claude}" in
1313
1369
  claude)
1314
1370
  case "$tier" in
@@ -1340,6 +1396,57 @@ get_provider_tier_param() {
1340
1396
  esac
1341
1397
  }
1342
1398
 
1399
+ #===============================================================================
1400
+ # Provider Spawn Timeout (v6.0.0)
1401
+ # Wraps provider invocation with timeout + retries.
1402
+ # Default: 120s timeout, 2 retries.
1403
+ #===============================================================================
1404
+
1405
+ PROVIDER_SPAWN_TIMEOUT=${LOKI_SPAWN_TIMEOUT:-120}
1406
+ PROVIDER_SPAWN_RETRIES=${LOKI_SPAWN_RETRIES:-2}
1407
+
1408
+ # Invoke a command with timeout and retry logic
1409
+ # Usage: invoke_with_timeout <timeout_seconds> <retries> <command...>
1410
+ invoke_with_timeout() {
1411
+ local timeout="$1"
1412
+ local max_retries="$2"
1413
+ shift 2
1414
+
1415
+ local attempt=0
1416
+ while [ $attempt -le $max_retries ]; do
1417
+ if [ $attempt -gt 0 ]; then
1418
+ log_warn "Provider spawn retry $attempt/$max_retries..."
1419
+ fi
1420
+
1421
+ local exit_code=0
1422
+ # Use timeout command if available (GNU coreutils or macOS)
1423
+ if command -v timeout &>/dev/null; then
1424
+ timeout "$timeout" "$@"
1425
+ exit_code=$?
1426
+ elif command -v gtimeout &>/dev/null; then
1427
+ gtimeout "$timeout" "$@"
1428
+ exit_code=$?
1429
+ else
1430
+ # Fallback: no timeout wrapper, run directly
1431
+ log_warn "timeout/gtimeout not available - running without timeout enforcement"
1432
+ "$@"
1433
+ exit_code=$?
1434
+ fi
1435
+
1436
+ # Exit code 124 = timeout
1437
+ if [ $exit_code -eq 124 ]; then
1438
+ log_warn "Provider spawn timed out after ${timeout}s (attempt $((attempt+1))/$((max_retries+1)))"
1439
+ ((attempt++))
1440
+ continue
1441
+ fi
1442
+
1443
+ return $exit_code
1444
+ done
1445
+
1446
+ log_error "Provider spawn failed after $((max_retries+1)) attempts (timeout=${timeout}s)"
1447
+ return 124
1448
+ }
1449
+
1343
1450
  #===============================================================================
1344
1451
  # GitHub Integration Functions (v4.1.0)
1345
1452
  #===============================================================================
@@ -5125,6 +5232,141 @@ AGG_SCRIPT
5125
5232
  return 0
5126
5233
  }
5127
5234
 
5235
+ #===============================================================================
5236
+ # Adversarial Testing (v6.0.0) - For Standard+ complexity tiers
5237
+ # Spawns an adversarial agent that tries to break the implementation.
5238
+ # Only runs when complexity >= standard (6+ agents).
5239
+ #===============================================================================
5240
+
5241
+ run_adversarial_testing() {
5242
+ local loki_dir="${TARGET_DIR:-.}/.loki"
5243
+ local adversarial_dir="$loki_dir/quality/adversarial"
5244
+ local test_id
5245
+ test_id="adversarial-$(date -u +%Y%m%dT%H%M%SZ)-${ITERATION_COUNT:-0}"
5246
+ mkdir -p "$adversarial_dir/$test_id"
5247
+
5248
+ # Only run for Standard+ complexity
5249
+ local complexity="${LOKI_COMPLEXITY:-auto}"
5250
+ if [ "$complexity" = "simple" ]; then
5251
+ log_debug "Adversarial testing skipped: simple complexity tier"
5252
+ return 0
5253
+ fi
5254
+
5255
+ # Check if adversarial testing is disabled
5256
+ if [ "${LOKI_ADVERSARIAL_TESTING:-true}" = "false" ]; then
5257
+ log_debug "Adversarial testing disabled via LOKI_ADVERSARIAL_TESTING=false"
5258
+ return 0
5259
+ fi
5260
+
5261
+ log_header "ADVERSARIAL TESTING: $test_id"
5262
+
5263
+ # Get diff for adversarial analysis
5264
+ local diff_content
5265
+ diff_content=$(git -C "${TARGET_DIR:-.}" diff HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --cached 2>/dev/null || echo "")
5266
+ if [ -z "$diff_content" ]; then
5267
+ log_info "Adversarial testing: No diff to test, skipping"
5268
+ return 0
5269
+ fi
5270
+
5271
+ local changed_files
5272
+ changed_files=$(git -C "${TARGET_DIR:-.}" diff --name-only HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --name-only --cached 2>/dev/null || echo "")
5273
+
5274
+ # Write analysis files
5275
+ local diff_file="$adversarial_dir/$test_id/diff.txt"
5276
+ local files_file="$adversarial_dir/$test_id/files.txt"
5277
+ echo "$diff_content" > "$diff_file"
5278
+ echo "$changed_files" > "$files_file"
5279
+
5280
+ # Build adversarial prompt
5281
+ local adversarial_prompt="You are an ADVERSARIAL TESTER. Your goal is to BREAK the implementation.
5282
+
5283
+ CHANGED FILES:
5284
+ $(cat "$files_file")
5285
+
5286
+ DIFF:
5287
+ $(head -500 "$diff_file")
5288
+
5289
+ YOUR MISSION:
5290
+ 1. Find edge cases that will cause crashes or incorrect behavior
5291
+ 2. Identify inputs that bypass validation
5292
+ 3. Find race conditions or concurrency issues
5293
+ 4. Discover security vulnerabilities (injection, auth bypass, SSRF)
5294
+ 5. Find resource exhaustion vectors (unbounded loops, memory leaks)
5295
+ 6. Identify error handling gaps (missing try/catch, unchecked returns)
5296
+
5297
+ OUTPUT FORMAT (STRICT):
5298
+ ATTACK_VECTORS:
5299
+ - [severity] [category] description | reproduction steps
5300
+ Severity: Critical, High, Medium, Low
5301
+ Category: crash, security, correctness, performance, resource
5302
+
5303
+ SUGGESTED_TESTS:
5304
+ - Test description that would catch this issue
5305
+
5306
+ OVERALL_RISK: HIGH or MEDIUM or LOW"
5307
+
5308
+ local result_file="$adversarial_dir/$test_id/result.txt"
5309
+
5310
+ # Run adversarial agent
5311
+ log_info "Spawning adversarial agent..."
5312
+ case "${PROVIDER_NAME:-claude}" in
5313
+ claude)
5314
+ if command -v claude &>/dev/null; then
5315
+ claude --dangerously-skip-permissions -p "$adversarial_prompt" \
5316
+ --output-format text > "$result_file" 2>/dev/null || true
5317
+ fi
5318
+ ;;
5319
+ codex)
5320
+ if command -v codex &>/dev/null; then
5321
+ codex exec --full-auto "$adversarial_prompt" \
5322
+ > "$result_file" 2>/dev/null || true
5323
+ fi
5324
+ ;;
5325
+ gemini)
5326
+ if command -v gemini &>/dev/null; then
5327
+ invoke_gemini_capture "$adversarial_prompt" \
5328
+ > "$result_file" 2>/dev/null || true
5329
+ fi
5330
+ ;;
5331
+ *)
5332
+ echo "ATTACK_VECTORS: None (unknown provider)" > "$result_file"
5333
+ echo "OVERALL_RISK: LOW" >> "$result_file"
5334
+ ;;
5335
+ esac
5336
+
5337
+ if [ ! -s "$result_file" ]; then
5338
+ log_warn "Adversarial agent produced no output"
5339
+ return 0
5340
+ fi
5341
+
5342
+ # Parse risk level
5343
+ local risk_level
5344
+ risk_level=$(grep -i "OVERALL_RISK:" "$result_file" | head -1 | sed 's/.*OVERALL_RISK:[[:space:]]*//' | awk '{print toupper($1)}')
5345
+
5346
+ # Count critical/high attack vectors
5347
+ local critical_count high_count
5348
+ critical_count=$(grep -ci "\[critical\]" "$result_file" 2>/dev/null || echo "0")
5349
+ high_count=$(grep -ci "\[high\]" "$result_file" 2>/dev/null || echo "0")
5350
+
5351
+ log_info "Adversarial testing complete: risk=$risk_level, critical=$critical_count, high=$high_count"
5352
+
5353
+ emit_event_json "adversarial_test_complete" \
5354
+ "test_id=$test_id" \
5355
+ "risk_level=${risk_level:-UNKNOWN}" \
5356
+ "critical_count=$critical_count" \
5357
+ "high_count=$high_count" \
5358
+ "iteration=$ITERATION_COUNT"
5359
+
5360
+ # Block on critical findings
5361
+ if [ "$critical_count" -gt 0 ]; then
5362
+ log_error "ADVERSARIAL TEST BLOCKED: $critical_count critical attack vectors found"
5363
+ log_error "Details: $adversarial_dir/$test_id/result.txt"
5364
+ return 1
5365
+ fi
5366
+
5367
+ return 0
5368
+ }
5369
+
5128
5370
  load_solutions_context() {
5129
5371
  # Load relevant structured solutions for the current task context
5130
5372
  local context="$1"
@@ -6444,6 +6686,81 @@ except Exception as e:
6444
6686
  PYEOF
6445
6687
  }
6446
6688
 
6689
+ #===============================================================================
6690
+ # Knowledge Graph Integration (v6.0.0)
6691
+ # Enrich prompts with cross-project patterns and store new learnings.
6692
+ #===============================================================================
6693
+
6694
+ # Enrich prompt context with relevant cross-project patterns
6695
+ enrich_from_knowledge_graph() {
6696
+ local context="$1"
6697
+ local max_patterns="${2:-5}"
6698
+
6699
+ _LOKI_KG_CONTEXT="$context" _LOKI_KG_MAX="$max_patterns" \
6700
+ _LOKI_PROJECT_DIR="$PROJECT_DIR" \
6701
+ python3 << 'PYEOF' 2>/dev/null || echo ""
6702
+ import sys
6703
+ import os
6704
+ import json
6705
+
6706
+ project_dir = os.environ.get('_LOKI_PROJECT_DIR', '')
6707
+ context = os.environ.get('_LOKI_KG_CONTEXT', '')
6708
+ max_results = int(os.environ.get('_LOKI_KG_MAX', '5'))
6709
+
6710
+ if not project_dir:
6711
+ sys.exit(0)
6712
+ sys.path.insert(0, project_dir)
6713
+ try:
6714
+ from memory.knowledge_graph import OrganizationKnowledgeGraph
6715
+ kg = OrganizationKnowledgeGraph()
6716
+ patterns = kg.query_patterns(context, max_results=max_results)
6717
+ if patterns:
6718
+ output = "\n## Cross-Project Knowledge (from knowledge graph)\n"
6719
+ for p in patterns:
6720
+ name = p.get('name', p.get('pattern', 'unnamed'))
6721
+ category = p.get('category', '')
6722
+ desc = p.get('description', '')
6723
+ output += f"- **{name}** ({category}): {desc}\n"
6724
+ print(output)
6725
+ except Exception:
6726
+ pass
6727
+ PYEOF
6728
+ }
6729
+
6730
+ # Store new patterns to the knowledge graph after successful iterations
6731
+ store_to_knowledge_graph() {
6732
+ local target_dir="${TARGET_DIR:-.}"
6733
+
6734
+ _LOKI_PROJECT_DIR="$PROJECT_DIR" _LOKI_TARGET_DIR="$target_dir" \
6735
+ python3 << 'PYEOF' 2>/dev/null || true
6736
+ import sys
6737
+ import os
6738
+
6739
+ project_dir = os.environ.get('_LOKI_PROJECT_DIR', '')
6740
+ target_dir = os.environ.get('_LOKI_TARGET_DIR', '.')
6741
+
6742
+ sys.path.insert(0, project_dir)
6743
+ try:
6744
+ from memory.knowledge_graph import OrganizationKnowledgeGraph
6745
+ from pathlib import Path
6746
+
6747
+ kg = OrganizationKnowledgeGraph()
6748
+ project_dirs = [Path(target_dir)]
6749
+
6750
+ # Extract and store patterns
6751
+ patterns = kg.extract_patterns(project_dirs)
6752
+ if patterns:
6753
+ patterns = kg.deduplicate_patterns(patterns)
6754
+ kg.save_patterns(patterns)
6755
+
6756
+ # Rebuild graph
6757
+ kg.build_graph(project_dirs)
6758
+ kg.save_graph()
6759
+ except Exception:
6760
+ pass
6761
+ PYEOF
6762
+ }
6763
+
6447
6764
  #===============================================================================
6448
6765
  # Save/Load Wrapper State
6449
6766
  #===============================================================================
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "5.58.2"
10
+ __version__ = "6.0.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -126,6 +126,10 @@ class MigrationManifest:
126
126
  feature_list_path: str = ""
127
127
  migration_plan_path: str = ""
128
128
  checkpoints: list[str] = field(default_factory=list)
129
+ status: str = "pending"
130
+ progress_pct: int = 0
131
+ updated_at: str = ""
132
+ source_path: str = ""
129
133
 
130
134
 
131
135
  # ---------------------------------------------------------------------------
@@ -149,7 +153,7 @@ def _atomic_write(path: Path, content: str) -> None:
149
153
  os.fsync(fd)
150
154
  finally:
151
155
  os.close(fd)
152
- os.rename(tmp_path, str(path))
156
+ os.replace(tmp_path, str(path))
153
157
  except OSError as exc:
154
158
  logger.error("Failed to write %s: %s", path, exc)
155
159
  # Clean up temp file on failure
@@ -312,20 +316,23 @@ class MigrationPipeline:
312
316
  return phase_data.get("status", "pending")
313
317
 
314
318
  def start_phase(self, phase: str) -> None:
315
- """Start a phase (transition from pending to in_progress). Idempotent if already in_progress."""
319
+ """Start a phase (transition to in_progress).
320
+
321
+ Idempotent if already in_progress. Also allows restarting completed or
322
+ failed phases (e.g., when using --resume --phase <phase>).
323
+ """
316
324
  if phase not in PHASE_ORDER:
317
325
  raise ValueError(f"Unknown phase: {phase}")
318
326
  with self._lock:
319
327
  manifest = self._load_manifest_unlocked()
320
- current_status = manifest.phases[phase]["status"]
328
+ if phase not in manifest.phases:
329
+ manifest.phases[phase] = {"status": "pending", "started_at": "", "completed_at": ""}
330
+ current_status = manifest.phases[phase].get("status", "pending")
321
331
  if current_status == "in_progress":
322
332
  return # Already started, idempotent
323
- if current_status != "pending":
324
- raise RuntimeError(
325
- f"Cannot start phase '{phase}': status is '{current_status}', expected 'pending'"
326
- )
327
333
  manifest.phases[phase]["status"] = "in_progress"
328
334
  manifest.phases[phase]["started_at"] = datetime.now(timezone.utc).isoformat()
335
+ manifest.phases[phase]["completed_at"] = ""
329
336
  self._save_manifest_unlocked(manifest)
330
337
 
331
338
  def _check_phase_gate_unlocked(self, from_phase: str, to_phase: str) -> tuple[bool, str]:
@@ -402,55 +409,13 @@ class MigrationPipeline:
402
409
  def check_phase_gate(self, from_phase: str, to_phase: str) -> tuple[bool, str]:
403
410
  """Validate whether transition from from_phase to to_phase is allowed.
404
411
 
412
+ Thread-safe wrapper that delegates to _check_phase_gate_unlocked under lock.
413
+
405
414
  Returns:
406
415
  Tuple of (allowed, reason). If allowed is False, reason explains why.
407
416
  """
408
- if from_phase not in PHASE_ORDER or to_phase not in PHASE_ORDER:
409
- return False, f"Unknown phase: {from_phase} or {to_phase}"
410
-
411
- from_idx = PHASE_ORDER.index(from_phase)
412
- to_idx = PHASE_ORDER.index(to_phase)
413
- if to_idx != from_idx + 1:
414
- return False, f"Cannot jump from {from_phase} to {to_phase}"
415
-
416
- # Gate: understand -> guardrail
417
- if from_phase == "understand" and to_phase == "guardrail":
418
- docs_dir = self.migration_dir / "docs"
419
- has_docs = any(docs_dir.iterdir()) if docs_dir.exists() else False
420
- if not has_docs:
421
- return False, "Phase gate failed: no documentation generated in docs/"
422
- seams_path = self.migration_dir / "seams.json"
423
- if not seams_path.exists():
424
- return False, "Phase gate failed: seams.json does not exist"
425
- return True, "Gate passed: docs generated and seams.json exists"
426
-
427
- # Gate: guardrail -> migrate
428
- if from_phase == "guardrail" and to_phase == "migrate":
429
- try:
430
- features = self.load_features()
431
- except FileNotFoundError:
432
- return False, "Phase gate failed: features.json not found"
433
- if not features:
434
- return False, "No features defined"
435
- failing = [f for f in features if not f.passes]
436
- if failing:
437
- ids = ", ".join(f.id for f in failing[:5])
438
- return False, f"Phase gate failed: {len(failing)} characterization tests not passing ({ids})"
439
- return True, "Gate passed: all characterization tests pass"
440
-
441
- # Gate: migrate -> verify
442
- if from_phase == "migrate" and to_phase == "verify":
443
- try:
444
- plan = self.load_plan()
445
- except FileNotFoundError:
446
- return False, "Phase gate failed: migration-plan.json not found"
447
- incomplete = [s for s in plan.steps if s.status != "completed"]
448
- if incomplete:
449
- ids = ", ".join(s.id for s in incomplete[:5])
450
- return False, f"Phase gate failed: {len(incomplete)} steps not completed ({ids})"
451
- return True, "Gate passed: all migration steps completed"
452
-
453
- return True, "Gate passed"
417
+ with self._lock:
418
+ return self._check_phase_gate_unlocked(from_phase, to_phase)
454
419
 
455
420
  def advance_phase(self, phase: str) -> PhaseResult:
456
421
  """Mark the current phase as complete and start the next one.
@@ -492,6 +457,8 @@ class MigrationPipeline:
492
457
 
493
458
  # Start next phase if there is one
494
459
  if next_phase is not None:
460
+ if next_phase not in manifest.phases:
461
+ manifest.phases[next_phase] = {"status": "pending", "started_at": "", "completed_at": ""}
495
462
  manifest.phases[next_phase]["status"] = "in_progress"
496
463
  manifest.phases[next_phase]["started_at"] = now
497
464
 
@@ -698,14 +665,16 @@ class MigrationPipeline:
698
665
  completed_phases: list[str] = []
699
666
  for phase in PHASE_ORDER:
700
667
  status = manifest.phases.get(phase, {}).get("status", "pending")
701
- if status == "in_progress":
668
+ if status == "completed":
669
+ completed_phases.append(phase)
670
+ current_phase = phase
671
+ overall_status = "in_progress" # partial completion
672
+ elif status == "in_progress":
702
673
  current_phase = phase
703
674
  overall_status = "in_progress"
704
- break
705
- if status == "completed":
675
+ elif status == "failed":
706
676
  current_phase = phase
707
- completed_phases.append(phase)
708
- overall_status = "completed"
677
+ overall_status = "failed"
709
678
 
710
679
  # Feature stats
711
680
  features_total = 0
@@ -754,19 +723,47 @@ class MigrationPipeline:
754
723
  except (FileNotFoundError, json.JSONDecodeError):
755
724
  last_checkpoint_data = {"tag": last_tag, "step_id": "", "timestamp": ""}
756
725
 
726
+ # Check if all phases are completed
727
+ if len(completed_phases) == len(PHASE_ORDER):
728
+ overall_status = "completed"
729
+
730
+ # Seam stats
731
+ seams_data: Optional[dict[str, Any]] = None
732
+ try:
733
+ seams = self.load_seams()
734
+ seams_high = sum(1 for s in seams if getattr(s, "priority", "medium") == "high")
735
+ seams_medium = sum(1 for s in seams if getattr(s, "priority", "medium") == "medium")
736
+ seams_low = sum(1 for s in seams if getattr(s, "priority", "medium") == "low")
737
+ seams_data = {"total": len(seams), "high": seams_high, "medium": seams_medium, "low": seams_low}
738
+ except (FileNotFoundError, json.JSONDecodeError, TypeError):
739
+ pass
740
+
741
+ # Flatten source/target to strings for UI consumption
742
+ source_path = ""
743
+ target_name = ""
744
+ if isinstance(manifest.source_info, dict):
745
+ source_path = manifest.source_info.get("path", "")
746
+ elif isinstance(manifest.source_info, str):
747
+ source_path = manifest.source_info
748
+ if isinstance(manifest.target_info, dict):
749
+ target_name = manifest.target_info.get("target", "")
750
+ elif isinstance(manifest.target_info, str):
751
+ target_name = manifest.target_info
752
+
757
753
  return {
758
754
  "migration_id": self.migration_id,
759
755
  "status": overall_status,
760
756
  "current_phase": current_phase,
761
757
  "phases": manifest.phases,
762
758
  "completed_phases": completed_phases,
763
- "source": manifest.source_info,
764
- "target": manifest.target_info,
759
+ "source": source_path,
760
+ "target": target_name,
765
761
  "current_step": current_step,
766
762
  "features": {"passing": features_passing, "total": features_total},
767
763
  "steps": {"current": current_step_index, "completed": steps_completed, "total": steps_total},
768
764
  "last_checkpoint": last_checkpoint_data,
769
765
  "checkpoints_count": len(manifest.checkpoints),
766
+ "seams": seams_data,
770
767
  }
771
768
 
772
769
  def generate_plan_summary(self) -> str:
@@ -893,21 +890,34 @@ def list_migrations() -> list[dict[str, Any]]:
893
890
  # Determine overall status from phases (clean string, no parenthesized phase)
894
891
  phases = data.get("phases", {})
895
892
  status = "pending"
893
+ all_completed = True
896
894
  for phase in PHASE_ORDER:
897
895
  phase_status = phases.get(phase, {}).get("status", "pending")
896
+ if phase_status == "failed":
897
+ status = "failed"
898
+ all_completed = False
899
+ break
898
900
  if phase_status == "in_progress":
899
901
  status = "in_progress"
902
+ all_completed = False
900
903
  break
901
904
  if phase_status == "completed":
902
- status = "completed"
905
+ status = "in_progress" # partial completion
906
+ else:
907
+ all_completed = False
908
+ if all_completed:
909
+ status = "completed"
903
910
 
904
911
  source_info = data.get("source_info", {})
912
+ source_path = source_info.get("path", "") if isinstance(source_info, dict) else str(source_info)
913
+ target_info = data.get("target_info", {})
914
+ target_name = target_info.get("target", "") if isinstance(target_info, dict) else str(target_info)
905
915
  results.append({
906
916
  "id": data.get("id", entry.name),
907
917
  "created_at": data.get("created_at", ""),
908
- "source": source_info,
909
- "source_path": source_info.get("path", ""),
910
- "target": data.get("target_info", {}).get("target", ""),
918
+ "source": source_path,
919
+ "source_path": source_path,
920
+ "target": target_name,
911
921
  "status": status,
912
922
  })
913
923
  except (json.JSONDecodeError, OSError) as exc:
@@ -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:** v5.58.2
5
+ **Version:** v6.0.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__ = '5.58.2'
60
+ __version__ = '6.0.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "5.58.2",
3
+ "version": "6.0.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",