loki-mode 5.58.1 → 5.59.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 v5.58.1
6
+ # Loki Mode v5.59.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -263,4 +263,4 @@ The following features are documented in skill modules but not yet fully automat
263
263
  | Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
264
264
  | Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
265
265
 
266
- **v5.58.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
266
+ **v5.59.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.58.1
1
+ 5.59.0
package/autonomy/loki CHANGED
@@ -4839,6 +4839,8 @@ cmd_migrate_help() {
4839
4839
  echo " --resume Resume from last checkpoint"
4840
4840
  echo " --multi-repo <glob> Multiple repository paths (glob pattern)"
4841
4841
  echo " --export-report Export migration report to file"
4842
+ echo " --no-dashboard Disable web dashboard during migration"
4843
+ echo " --no-docs Skip migration_docs/ generation"
4842
4844
  echo " --list List all migrations"
4843
4845
  echo " --status [migration-id] Show migration status"
4844
4846
  echo ""
@@ -4994,6 +4996,8 @@ cmd_migrate_start() {
4994
4996
  local resume="$8"
4995
4997
  local multi_repo="$9"
4996
4998
  local export_report="${10}"
4999
+ local no_dashboard="${11:-false}"
5000
+ local no_docs="${12:-false}"
4997
5001
 
4998
5002
  local migrations_dir="${HOME}/.loki/migrations"
4999
5003
  local migration_id=""
@@ -5118,9 +5122,18 @@ if manifests:
5118
5122
  echo ""
5119
5123
  fi
5120
5124
 
5121
- # Create migration via engine
5122
- local create_result
5123
- create_result=$(PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
5125
+ local migration_dir
5126
+ if [ -n "$migration_id" ]; then
5127
+ # Resume: load existing migration directory
5128
+ migration_dir="${migrations_dir}/${migration_id}"
5129
+ if [ ! -d "$migration_dir" ]; then
5130
+ echo -e "${RED}Error: Migration directory not found: ${migration_dir}${NC}"
5131
+ return 1
5132
+ fi
5133
+ else
5134
+ # Create new migration via engine
5135
+ local create_result
5136
+ create_result=$(PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
5124
5137
  import json, sys
5125
5138
  sys.path.insert(0, '.')
5126
5139
  from dashboard.migration_engine import MigrationPipeline
@@ -5128,13 +5141,13 @@ pipeline = MigrationPipeline(sys.argv[1], sys.argv[2])
5128
5141
  manifest = pipeline.create_manifest()
5129
5142
  print(json.dumps({'id': manifest.id, 'dir': str(pipeline.migration_dir)}))
5130
5143
  " "$codebase_path" "$target" 2>&1) || {
5131
- echo -e "${RED}Error: Failed to create migration${NC}"
5132
- echo "$create_result"
5133
- return 1
5134
- }
5135
- migration_id=$(echo "$create_result" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
5136
- local migration_dir
5137
- migration_dir=$(echo "$create_result" | python3 -c "import json,sys; print(json.load(sys.stdin)['dir'])")
5144
+ echo -e "${RED}Error: Failed to create migration${NC}"
5145
+ echo "$create_result"
5146
+ return 1
5147
+ }
5148
+ migration_id=$(echo "$create_result" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
5149
+ migration_dir=$(echo "$create_result" | python3 -c "import json,sys; print(json.load(sys.stdin)['dir'])")
5150
+ fi
5138
5151
 
5139
5152
  # Security scan (first step of understand phase)
5140
5153
  echo -e "${BOLD}[Security] Scanning for secrets...${NC}"
@@ -5190,6 +5203,79 @@ print(json.dumps({'id': manifest.id, 'dir': str(pipeline.migration_dir)}))
5190
5203
  [ -n "$compliance" ] && echo -e " Compliance: ${compliance}"
5191
5204
  echo ""
5192
5205
 
5206
+ # Dashboard cleanup trap for error paths
5207
+ local migrate_dashboard_pid=""
5208
+ _migrate_cleanup_dashboard() {
5209
+ if [ -n "$migrate_dashboard_pid" ]; then
5210
+ kill "$migrate_dashboard_pid" 2>/dev/null || true
5211
+ wait "$migrate_dashboard_pid" 2>/dev/null || true
5212
+ rm -f "${codebase_path}/.loki/dashboard/dashboard.pid" 2>/dev/null || true
5213
+ fi
5214
+ }
5215
+ trap '_migrate_cleanup_dashboard' RETURN
5216
+
5217
+ # Start dashboard for migration monitoring (unless --no-dashboard)
5218
+ if [ "$no_dashboard" != "true" ]; then
5219
+ local dashboard_port="${LOKI_DASHBOARD_PORT:-57374}"
5220
+ local skill_dir="${SKILL_DIR:-.}"
5221
+ local dashboard_venv="$HOME/.loki/dashboard-venv"
5222
+ local python_cmd="python3"
5223
+ local url_scheme="http"
5224
+
5225
+ # Use venv python if available
5226
+ if [ -x "${dashboard_venv}/bin/python3" ]; then
5227
+ python_cmd="${dashboard_venv}/bin/python3"
5228
+ fi
5229
+
5230
+ # Check if dashboard deps are available
5231
+ if "$python_cmd" -c "import fastapi" 2>/dev/null; then
5232
+ # Find available port (increment until a free port is found)
5233
+ local port_attempts=0
5234
+ while lsof -i :"$dashboard_port" &>/dev/null && [ $port_attempts -lt 10 ]; do
5235
+ dashboard_port=$((dashboard_port + 1))
5236
+ port_attempts=$((port_attempts + 1))
5237
+ done
5238
+
5239
+ if [ $port_attempts -lt 10 ]; then
5240
+ # Determine TLS
5241
+ if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
5242
+ url_scheme="https"
5243
+ fi
5244
+
5245
+ if ! mkdir -p "${codebase_path}/.loki/dashboard/logs" 2>/dev/null; then
5246
+ echo -e " ${YELLOW}Dashboard skipped (cannot create log directory)${NC}"
5247
+ else
5248
+ local log_file="${codebase_path}/.loki/dashboard/logs/dashboard.log"
5249
+
5250
+ LOKI_DASHBOARD_PORT="$dashboard_port" \
5251
+ LOKI_DASHBOARD_HOST="127.0.0.1" \
5252
+ LOKI_PROJECT_PATH="$codebase_path" \
5253
+ LOKI_SKILL_DIR="$skill_dir" \
5254
+ LOKI_TLS_CERT="${LOKI_TLS_CERT:-}" \
5255
+ LOKI_TLS_KEY="${LOKI_TLS_KEY:-}" \
5256
+ PYTHONPATH="$skill_dir" \
5257
+ nohup "$python_cmd" -m dashboard.server > "$log_file" 2>&1 &
5258
+ migrate_dashboard_pid=$!
5259
+
5260
+ echo "$migrate_dashboard_pid" > "${codebase_path}/.loki/dashboard/dashboard.pid"
5261
+
5262
+ sleep 2
5263
+ if kill -0 "$migrate_dashboard_pid" 2>/dev/null; then
5264
+ echo -e " Dashboard: ${CYAN}${url_scheme}://127.0.0.1:${dashboard_port}/${NC}"
5265
+ if [[ "$OSTYPE" == "darwin"* ]]; then
5266
+ open "${url_scheme}://127.0.0.1:${dashboard_port}/" 2>/dev/null || true
5267
+ fi
5268
+ echo ""
5269
+ else
5270
+ echo -e " ${YELLOW}Dashboard failed to start (check logs)${NC}"
5271
+ migrate_dashboard_pid=""
5272
+ echo ""
5273
+ fi
5274
+ fi
5275
+ fi
5276
+ fi
5277
+ fi
5278
+
5193
5279
  # Determine which phases to run
5194
5280
  local phases_to_run=()
5195
5281
  if [ -n "$phase" ]; then
@@ -5238,7 +5324,7 @@ Tasks:
5238
5324
  1. Analyze the full codebase structure (languages, frameworks, dependencies, architecture)
5239
5325
  2. Create the docs directory: mkdir -p ${migration_dir}/docs
5240
5326
  3. Write analysis documentation to ${migration_dir}/docs/analysis.md
5241
- 3. Identify migration seams (logical boundaries for incremental migration) and write them to ${migration_dir}/seams.json as a JSON array of objects with fields: id, name, description, files (array of file paths), dependencies (array of seam ids), priority (high/medium/low)
5327
+ 4. Identify migration seams (logical boundaries for incremental migration) and write them to ${migration_dir}/seams.json as a JSON array of objects with fields: id (string, e.g. 'seam-01'), name (string), description (string), type (string: 'module'/'api'/'config'/'adapter'), files (array of file paths), dependencies (array of seam ids), priority (string: 'high'/'medium'/'low')
5242
5328
 
5243
5329
  You MUST create both files. The migration cannot proceed without them.
5244
5330
  Write the analysis doc first, then the seams.json."
@@ -5254,7 +5340,7 @@ Read ${migration_dir}/docs/analysis.md and ${migration_dir}/seams.json for conte
5254
5340
 
5255
5341
  Tasks:
5256
5342
  1. Identify existing tests and create characterization tests that capture current behavior
5257
- 2. Write ${migration_dir}/features.json as a JSON array of objects with fields: id, name, description, test_command (shell command to verify), passes (boolean, set to true for existing passing behavior)
5343
+ 2. Write ${migration_dir}/features.json as a JSON array of objects with fields: id (string, e.g. 'F01'), category (string, e.g. 'core'), description (string), characterization_test (string, shell command to verify), passes (boolean, set to true for existing passing behavior), risk (string: 'low'/'medium'/'high')
5258
5344
  3. Create a git checkpoint: cd ${codebase_path} && git stash || true
5259
5345
 
5260
5346
  All features in features.json must have passes: true for the gate to pass."
@@ -5269,7 +5355,7 @@ Migration dir: ${migration_dir}
5269
5355
  Read ${migration_dir}/docs/analysis.md, ${migration_dir}/seams.json, and ${migration_dir}/features.json for context.
5270
5356
 
5271
5357
  Tasks:
5272
- 1. Create a migration plan and write it to ${migration_dir}/migration-plan.json with fields: target, source_path, steps (array of objects with: id, name, description, status set to 'completed' after you do the step)
5358
+ 1. Create a migration plan and write it to ${migration_dir}/migration-plan.json as a JSON object with fields: version (integer, default 1), strategy (string: 'incremental' or 'big_bang'), steps (array of objects with: id (string), description (string), type (string: 'refactor'/'rewrite'/'config'/'test'), status (string, set to 'completed' after you do the step))
5273
5359
  2. Execute the actual code migration transforms in ${codebase_path} -- convert code from the current framework/language to ${target}
5274
5360
  3. Update each step status to 'completed' as you finish it
5275
5361
  4. Work incrementally seam by seam from ${migration_dir}/seams.json
@@ -5383,6 +5469,119 @@ pipeline.advance_phase(sys.argv[2])
5383
5469
  echo ""
5384
5470
  done
5385
5471
 
5472
+ # Generate migration_docs/ in the codebase (unless --no-docs or --plan-only)
5473
+ if [ "$plan_only" != "true" ] && [ "$no_docs" != "true" ]; then
5474
+ echo -e "${CYAN}[Phase: document]${NC} Generating migration documentation..."
5475
+
5476
+ local doc_prompt="You are generating comprehensive migration documentation for a completed codebase migration.
5477
+
5478
+ Target: ${target}
5479
+ Codebase: ${codebase_path}
5480
+ Migration dir: ${migration_dir}
5481
+
5482
+ Read the following files for context:
5483
+ - ${migration_dir}/docs/analysis.md (original codebase analysis)
5484
+ - ${migration_dir}/docs/verification-report.md (verification results)
5485
+ - ${migration_dir}/features.json (feature tracking)
5486
+ - ${migration_dir}/seams.json (migration boundaries)
5487
+ - ${migration_dir}/migration-plan.json (migration steps)
5488
+
5489
+ Create a directory called migration_docs/ in the codebase root (${codebase_path}/migration_docs/) with the following files:
5490
+
5491
+ 1. **README.md** - Executive summary of the migration:
5492
+ - What the codebase was before (source language/framework/architecture)
5493
+ - What it is now (target: ${target})
5494
+ - High-level summary of changes made
5495
+ - Link to other docs in migration_docs/
5496
+
5497
+ 2. **USAGE.md** - How to use the migrated codebase:
5498
+ - Getting started / quick start
5499
+ - Key commands and workflows
5500
+ - Configuration options
5501
+ - Environment variables
5502
+
5503
+ 3. **INSTALLATION.md** - Installation and setup:
5504
+ - Prerequisites and dependencies
5505
+ - Step-by-step installation
5506
+ - Build instructions
5507
+ - Development environment setup
5508
+
5509
+ 4. **TESTING.md** - Testing the migrated codebase:
5510
+ - How to run tests
5511
+ - Test coverage status
5512
+ - Known test gaps
5513
+ - How to add new tests
5514
+
5515
+ 5. **WHAT-CHANGED.md** - Detailed migration changelog:
5516
+ - Files added, modified, and removed
5517
+ - Architecture changes
5518
+ - API changes (if any)
5519
+ - Configuration changes
5520
+ - Dependency changes
5521
+
5522
+ 6. **WHAT-WORKS.md** - Current status:
5523
+ - Features confirmed working (from verification)
5524
+ - Features that need manual testing
5525
+ - Known limitations
5526
+
5527
+ 7. **WHAT-PENDING.md** - Remaining work:
5528
+ - Items flagged during verification
5529
+ - Manual steps required
5530
+ - Recommended follow-up tasks
5531
+ - Security items to address
5532
+
5533
+ 8. **DEPLOYMENT.md** - Deployment guide (if applicable):
5534
+ - How to deploy the migrated application
5535
+ - CI/CD considerations
5536
+ - Infrastructure changes needed
5537
+ - Rollback instructions
5538
+
5539
+ IMPORTANT RULES:
5540
+ - Only document what is TRUE and VERIFIED - never fabricate features or capabilities
5541
+ - Reference actual files and test results from the migration artifacts
5542
+ - If something was not tested or verified, say so explicitly
5543
+ - Mark uncertain items with 'NEEDS VERIFICATION' tags
5544
+ - Do NOT use emojis anywhere in the documentation"
5545
+
5546
+ local doc_exit=0
5547
+ local provider_name="${LOKI_PROVIDER:-claude}"
5548
+ case "$provider_name" in
5549
+ claude)
5550
+ (cd "$codebase_path" && claude --dangerously-skip-permissions -p "$doc_prompt" --output-format stream-json --verbose 2>&1) | \
5551
+ while IFS= read -r line; do
5552
+ if echo "$line" | python3 -c "
5553
+ import sys, json
5554
+ try:
5555
+ d = json.loads(sys.stdin.read())
5556
+ if d.get('type') == 'assistant':
5557
+ for item in d.get('message', {}).get('content', []):
5558
+ if item.get('type') == 'text':
5559
+ print(item.get('text', ''), end='')
5560
+ except Exception: pass
5561
+ " 2>/dev/null; then
5562
+ true
5563
+ fi
5564
+ done
5565
+ doc_exit=${PIPESTATUS[0]}
5566
+ ;;
5567
+ codex)
5568
+ (cd "$codebase_path" && codex exec --full-auto "$doc_prompt" 2>&1) || doc_exit=$?
5569
+ ;;
5570
+ gemini)
5571
+ (cd "$codebase_path" && gemini --approval-mode=yolo "$doc_prompt" 2>&1) || doc_exit=$?
5572
+ ;;
5573
+ esac
5574
+
5575
+ if [ "$doc_exit" -eq 0 ] && [ -d "${codebase_path}/migration_docs" ]; then
5576
+ local doc_count
5577
+ doc_count=$(find "${codebase_path}/migration_docs" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
5578
+ echo -e "${GREEN} [Phase: document] Complete (${doc_count} docs generated)${NC}"
5579
+ else
5580
+ echo -e "${YELLOW} [Phase: document] Warning: Documentation may be incomplete${NC}"
5581
+ fi
5582
+ echo ""
5583
+ fi
5584
+
5386
5585
  # Plan-only: generate plan file and stop
5387
5586
  if [ "$plan_only" = "true" ]; then
5388
5587
  echo -e "${BOLD}Migration plan generated.${NC}"
@@ -5471,6 +5670,8 @@ cmd_migrate() {
5471
5670
  local do_list="false"
5472
5671
  local do_status="false"
5473
5672
  local status_id=""
5673
+ local no_dashboard="false"
5674
+ local no_docs="false"
5474
5675
 
5475
5676
  if [ $# -eq 0 ]; then
5476
5677
  cmd_migrate_help
@@ -5575,6 +5776,14 @@ cmd_migrate() {
5575
5776
  export_report="true"
5576
5777
  shift
5577
5778
  ;;
5779
+ --no-dashboard)
5780
+ no_dashboard="true"
5781
+ shift
5782
+ ;;
5783
+ --no-docs)
5784
+ no_docs="true"
5785
+ shift
5786
+ ;;
5578
5787
  -*)
5579
5788
  echo -e "${RED}Unknown option: $1${NC}"
5580
5789
  echo "Run 'loki migrate --help' for usage."
@@ -5618,7 +5827,7 @@ cmd_migrate() {
5618
5827
  return 1
5619
5828
  fi
5620
5829
 
5621
- cmd_migrate_start "$codebase_path" "$target" "$plan_only" "$phase" "$parallel" "$compliance" "$dry_run" "$do_resume" "$multi_repo" "$export_report"
5830
+ cmd_migrate_start "$codebase_path" "$target" "$plan_only" "$phase" "$parallel" "$compliance" "$dry_run" "$do_resume" "$multi_repo" "$export_report" "$no_dashboard" "$no_docs"
5622
5831
  }
5623
5832
 
5624
5833
  # Main command dispatcher
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "5.58.1"
10
+ __version__ = "5.59.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -14,7 +14,7 @@ import re
14
14
  import subprocess
15
15
  import tempfile
16
16
  import threading
17
- from dataclasses import asdict, dataclass, field
17
+ from dataclasses import asdict, dataclass, field, fields
18
18
  from datetime import datetime, timezone
19
19
  from pathlib import Path
20
20
  from typing import Any, Optional
@@ -38,8 +38,8 @@ class Feature:
38
38
  """Individual feature tracked during migration."""
39
39
 
40
40
  id: str
41
- category: str
42
- description: str
41
+ category: str = ""
42
+ description: str = ""
43
43
  verification_steps: list[str] = field(default_factory=list)
44
44
  passes: bool = False
45
45
  characterization_test: str = ""
@@ -52,8 +52,8 @@ class MigrationStep:
52
52
  """Single step in a migration plan."""
53
53
 
54
54
  id: str
55
- description: str
56
- type: str # e.g. "refactor", "rewrite", "config", "test"
55
+ description: str = ""
56
+ type: str = "" # e.g. "refactor", "rewrite", "config", "test"
57
57
  files: list[str] = field(default_factory=list)
58
58
  tests_required: list[str] = field(default_factory=list)
59
59
  estimated_tokens: int = 0
@@ -81,9 +81,14 @@ class SeamInfo:
81
81
  """Detected seam (boundary/interface) in the codebase."""
82
82
 
83
83
  id: str
84
- type: str # e.g. "api", "module", "database", "config"
85
- location: str
86
- description: str
84
+ description: str = ""
85
+ type: str = "" # e.g. "api", "module", "database", "config"
86
+ location: str = ""
87
+ name: str = ""
88
+ priority: str = "medium"
89
+ files: list[str] = field(default_factory=list)
90
+ dependencies: list[str] = field(default_factory=list)
91
+ complexity: str = ""
87
92
  confidence: float = 0.0
88
93
  suggested_interface: str = ""
89
94
 
@@ -121,6 +126,10 @@ class MigrationManifest:
121
126
  feature_list_path: str = ""
122
127
  migration_plan_path: str = ""
123
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 = ""
124
133
 
125
134
 
126
135
  # ---------------------------------------------------------------------------
@@ -144,7 +153,7 @@ def _atomic_write(path: Path, content: str) -> None:
144
153
  os.fsync(fd)
145
154
  finally:
146
155
  os.close(fd)
147
- os.rename(tmp_path, str(path))
156
+ os.replace(tmp_path, str(path))
148
157
  except OSError as exc:
149
158
  logger.error("Failed to write %s: %s", path, exc)
150
159
  # Clean up temp file on failure
@@ -307,20 +316,23 @@ class MigrationPipeline:
307
316
  return phase_data.get("status", "pending")
308
317
 
309
318
  def start_phase(self, phase: str) -> None:
310
- """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
+ """
311
324
  if phase not in PHASE_ORDER:
312
325
  raise ValueError(f"Unknown phase: {phase}")
313
326
  with self._lock:
314
327
  manifest = self._load_manifest_unlocked()
315
- 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")
316
331
  if current_status == "in_progress":
317
332
  return # Already started, idempotent
318
- if current_status != "pending":
319
- raise RuntimeError(
320
- f"Cannot start phase '{phase}': status is '{current_status}', expected 'pending'"
321
- )
322
333
  manifest.phases[phase]["status"] = "in_progress"
323
334
  manifest.phases[phase]["started_at"] = datetime.now(timezone.utc).isoformat()
335
+ manifest.phases[phase]["completed_at"] = ""
324
336
  self._save_manifest_unlocked(manifest)
325
337
 
326
338
  def _check_phase_gate_unlocked(self, from_phase: str, to_phase: str) -> tuple[bool, str]:
@@ -353,7 +365,12 @@ class MigrationPipeline:
353
365
  features_path = self.migration_dir / "features.json"
354
366
  try:
355
367
  data = json.loads(features_path.read_text(encoding="utf-8"))
356
- features = [Feature(**f) for f in data]
368
+ # Handle both flat list and {"features": [...]} wrapper
369
+ if isinstance(data, dict):
370
+ data = data.get("features", [])
371
+ # Filter to known Feature fields to tolerate extra keys
372
+ _feature_fields = {f.name for f in fields(Feature)}
373
+ features = [Feature(**{k: v for k, v in f.items() if k in _feature_fields}) for f in data]
357
374
  except FileNotFoundError:
358
375
  return False, "Phase gate failed: features.json not found"
359
376
  except (json.JSONDecodeError, TypeError) as exc:
@@ -372,9 +389,11 @@ class MigrationPipeline:
372
389
  try:
373
390
  data = json.loads(plan_path.read_text(encoding="utf-8"))
374
391
  steps_data = data.get("steps", [])
375
- plan_data = {k: v for k, v in data.items() if k != "steps"}
392
+ _plan_fields = {f.name for f in fields(MigrationPlan)}
393
+ _step_fields = {f.name for f in fields(MigrationStep)}
394
+ plan_data = {k: v for k, v in data.items() if k in _plan_fields and k != "steps"}
376
395
  plan = MigrationPlan(**plan_data)
377
- plan.steps = [MigrationStep(**s) for s in steps_data]
396
+ plan.steps = [MigrationStep(**{k: v for k, v in s.items() if k in _step_fields}) for s in steps_data]
378
397
  except FileNotFoundError:
379
398
  return False, "Phase gate failed: migration-plan.json not found"
380
399
  except (json.JSONDecodeError, TypeError) as exc:
@@ -390,55 +409,13 @@ class MigrationPipeline:
390
409
  def check_phase_gate(self, from_phase: str, to_phase: str) -> tuple[bool, str]:
391
410
  """Validate whether transition from from_phase to to_phase is allowed.
392
411
 
412
+ Thread-safe wrapper that delegates to _check_phase_gate_unlocked under lock.
413
+
393
414
  Returns:
394
415
  Tuple of (allowed, reason). If allowed is False, reason explains why.
395
416
  """
396
- if from_phase not in PHASE_ORDER or to_phase not in PHASE_ORDER:
397
- return False, f"Unknown phase: {from_phase} or {to_phase}"
398
-
399
- from_idx = PHASE_ORDER.index(from_phase)
400
- to_idx = PHASE_ORDER.index(to_phase)
401
- if to_idx != from_idx + 1:
402
- return False, f"Cannot jump from {from_phase} to {to_phase}"
403
-
404
- # Gate: understand -> guardrail
405
- if from_phase == "understand" and to_phase == "guardrail":
406
- docs_dir = self.migration_dir / "docs"
407
- has_docs = any(docs_dir.iterdir()) if docs_dir.exists() else False
408
- if not has_docs:
409
- return False, "Phase gate failed: no documentation generated in docs/"
410
- seams_path = self.migration_dir / "seams.json"
411
- if not seams_path.exists():
412
- return False, "Phase gate failed: seams.json does not exist"
413
- return True, "Gate passed: docs generated and seams.json exists"
414
-
415
- # Gate: guardrail -> migrate
416
- if from_phase == "guardrail" and to_phase == "migrate":
417
- try:
418
- features = self.load_features()
419
- except FileNotFoundError:
420
- return False, "Phase gate failed: features.json not found"
421
- if not features:
422
- return False, "No features defined"
423
- failing = [f for f in features if not f.passes]
424
- if failing:
425
- ids = ", ".join(f.id for f in failing[:5])
426
- return False, f"Phase gate failed: {len(failing)} characterization tests not passing ({ids})"
427
- return True, "Gate passed: all characterization tests pass"
428
-
429
- # Gate: migrate -> verify
430
- if from_phase == "migrate" and to_phase == "verify":
431
- try:
432
- plan = self.load_plan()
433
- except FileNotFoundError:
434
- return False, "Phase gate failed: migration-plan.json not found"
435
- incomplete = [s for s in plan.steps if s.status != "completed"]
436
- if incomplete:
437
- ids = ", ".join(s.id for s in incomplete[:5])
438
- return False, f"Phase gate failed: {len(incomplete)} steps not completed ({ids})"
439
- return True, "Gate passed: all migration steps completed"
440
-
441
- return True, "Gate passed"
417
+ with self._lock:
418
+ return self._check_phase_gate_unlocked(from_phase, to_phase)
442
419
 
443
420
  def advance_phase(self, phase: str) -> PhaseResult:
444
421
  """Mark the current phase as complete and start the next one.
@@ -480,6 +457,8 @@ class MigrationPipeline:
480
457
 
481
458
  # Start next phase if there is one
482
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": ""}
483
462
  manifest.phases[next_phase]["status"] = "in_progress"
484
463
  manifest.phases[next_phase]["started_at"] = now
485
464
 
@@ -501,7 +480,12 @@ class MigrationPipeline:
501
480
  with self._lock:
502
481
  try:
503
482
  data = json.loads(features_path.read_text(encoding="utf-8"))
504
- return [Feature(**f) for f in data]
483
+ # Handle both flat list and {"features": [...]} wrapper
484
+ if isinstance(data, dict):
485
+ data = data.get("features", [])
486
+ # Filter to known Feature fields to tolerate extra keys
487
+ _feature_fields = {f.name for f in fields(Feature)}
488
+ return [Feature(**{k: v for k, v in f.items() if k in _feature_fields}) for f in data]
505
489
  except FileNotFoundError:
506
490
  logger.warning("Features file not found: %s", features_path)
507
491
  raise
@@ -528,9 +512,12 @@ class MigrationPipeline:
528
512
  try:
529
513
  data = json.loads(plan_path.read_text(encoding="utf-8"))
530
514
  # Reconstruct nested MigrationStep objects
531
- steps_data = data.pop("steps", [])
532
- plan = MigrationPlan(**data)
533
- plan.steps = [MigrationStep(**s) for s in steps_data]
515
+ steps_data = data.get("steps", [])
516
+ _plan_fields = {f.name for f in fields(MigrationPlan)}
517
+ _step_fields = {f.name for f in fields(MigrationStep)}
518
+ plan_data = {k: v for k, v in data.items() if k in _plan_fields and k != "steps"}
519
+ plan = MigrationPlan(**plan_data)
520
+ plan.steps = [MigrationStep(**{k: v for k, v in s.items() if k in _step_fields}) for s in steps_data]
534
521
  return plan
535
522
  except FileNotFoundError:
536
523
  logger.warning("Plan file not found: %s", plan_path)
@@ -555,7 +542,11 @@ class MigrationPipeline:
555
542
  with self._lock:
556
543
  try:
557
544
  data = json.loads(seams_path.read_text(encoding="utf-8"))
558
- return [SeamInfo(**s) for s in data]
545
+ # Handle both flat list and {"seams": [...]} wrapper
546
+ if isinstance(data, dict):
547
+ data = data.get("seams", [])
548
+ _seam_fields = {f.name for f in fields(SeamInfo)}
549
+ return [SeamInfo(**{k: v for k, v in s.items() if k in _seam_fields}) for s in data]
559
550
  except FileNotFoundError:
560
551
  logger.warning("Seams file not found: %s", seams_path)
561
552
  raise
@@ -681,7 +672,7 @@ class MigrationPipeline:
681
672
  if status == "completed":
682
673
  current_phase = phase
683
674
  completed_phases.append(phase)
684
- overall_status = "completed"
675
+ overall_status = "in_progress" # partial completion
685
676
 
686
677
  # Feature stats
687
678
  features_total = 0
@@ -730,19 +721,47 @@ class MigrationPipeline:
730
721
  except (FileNotFoundError, json.JSONDecodeError):
731
722
  last_checkpoint_data = {"tag": last_tag, "step_id": "", "timestamp": ""}
732
723
 
724
+ # Check if all phases are completed
725
+ if len(completed_phases) == len(PHASE_ORDER):
726
+ overall_status = "completed"
727
+
728
+ # Seam stats
729
+ seams_data: Optional[dict[str, Any]] = None
730
+ try:
731
+ seams = self.load_seams()
732
+ seams_high = sum(1 for s in seams if getattr(s, "priority", "medium") == "high")
733
+ seams_medium = sum(1 for s in seams if getattr(s, "priority", "medium") == "medium")
734
+ seams_low = sum(1 for s in seams if getattr(s, "priority", "medium") == "low")
735
+ seams_data = {"total": len(seams), "high": seams_high, "medium": seams_medium, "low": seams_low}
736
+ except (FileNotFoundError, json.JSONDecodeError, TypeError):
737
+ pass
738
+
739
+ # Flatten source/target to strings for UI consumption
740
+ source_path = ""
741
+ target_name = ""
742
+ if isinstance(manifest.source_info, dict):
743
+ source_path = manifest.source_info.get("path", "")
744
+ elif isinstance(manifest.source_info, str):
745
+ source_path = manifest.source_info
746
+ if isinstance(manifest.target_info, dict):
747
+ target_name = manifest.target_info.get("target", "")
748
+ elif isinstance(manifest.target_info, str):
749
+ target_name = manifest.target_info
750
+
733
751
  return {
734
752
  "migration_id": self.migration_id,
735
753
  "status": overall_status,
736
754
  "current_phase": current_phase,
737
755
  "phases": manifest.phases,
738
756
  "completed_phases": completed_phases,
739
- "source": manifest.source_info,
740
- "target": manifest.target_info,
757
+ "source": source_path,
758
+ "target": target_name,
741
759
  "current_step": current_step,
742
760
  "features": {"passing": features_passing, "total": features_total},
743
761
  "steps": {"current": current_step_index, "completed": steps_completed, "total": steps_total},
744
762
  "last_checkpoint": last_checkpoint_data,
745
763
  "checkpoints_count": len(manifest.checkpoints),
764
+ "seams": seams_data,
746
765
  }
747
766
 
748
767
  def generate_plan_summary(self) -> str:
@@ -869,21 +888,34 @@ def list_migrations() -> list[dict[str, Any]]:
869
888
  # Determine overall status from phases (clean string, no parenthesized phase)
870
889
  phases = data.get("phases", {})
871
890
  status = "pending"
891
+ all_completed = True
872
892
  for phase in PHASE_ORDER:
873
893
  phase_status = phases.get(phase, {}).get("status", "pending")
894
+ if phase_status == "failed":
895
+ status = "failed"
896
+ all_completed = False
897
+ break
874
898
  if phase_status == "in_progress":
875
899
  status = "in_progress"
900
+ all_completed = False
876
901
  break
877
902
  if phase_status == "completed":
878
- status = "completed"
903
+ status = "in_progress" # partial completion
904
+ else:
905
+ all_completed = False
906
+ if all_completed:
907
+ status = "completed"
879
908
 
880
909
  source_info = data.get("source_info", {})
910
+ source_path = source_info.get("path", "") if isinstance(source_info, dict) else str(source_info)
911
+ target_info = data.get("target_info", {})
912
+ target_name = target_info.get("target", "") if isinstance(target_info, dict) else str(target_info)
881
913
  results.append({
882
914
  "id": data.get("id", entry.name),
883
915
  "created_at": data.get("created_at", ""),
884
- "source": source_info,
885
- "source_path": source_info.get("path", ""),
886
- "target": data.get("target_info", {}).get("target", ""),
916
+ "source": source_path,
917
+ "source_path": source_path,
918
+ "target": target_name,
887
919
  "status": status,
888
920
  })
889
921
  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.1
5
+ **Version:** v5.59.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.1'
60
+ __version__ = '5.59.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "5.58.1",
3
+ "version": "5.59.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",