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 +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +223 -14
- package/dashboard/__init__.py +1 -1
- package/dashboard/migration_engine.py +108 -76
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
266
|
+
**v5.59.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
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
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
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
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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
|
package/dashboard/__init__.py
CHANGED
|
@@ -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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
return
|
|
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
|
-
|
|
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.
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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 = "
|
|
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":
|
|
740
|
-
"target":
|
|
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 = "
|
|
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":
|
|
885
|
-
"source_path":
|
|
886
|
-
"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:
|
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED