loki-mode 5.58.2 → 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 +221 -12
- package/dashboard/__init__.py +1 -1
- package/dashboard/migration_engine.py +68 -60
- 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."
|
|
@@ -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
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
return
|
|
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
|
|
|
@@ -705,7 +672,7 @@ class MigrationPipeline:
|
|
|
705
672
|
if status == "completed":
|
|
706
673
|
current_phase = phase
|
|
707
674
|
completed_phases.append(phase)
|
|
708
|
-
overall_status = "
|
|
675
|
+
overall_status = "in_progress" # partial completion
|
|
709
676
|
|
|
710
677
|
# Feature stats
|
|
711
678
|
features_total = 0
|
|
@@ -754,19 +721,47 @@ class MigrationPipeline:
|
|
|
754
721
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
755
722
|
last_checkpoint_data = {"tag": last_tag, "step_id": "", "timestamp": ""}
|
|
756
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
|
+
|
|
757
751
|
return {
|
|
758
752
|
"migration_id": self.migration_id,
|
|
759
753
|
"status": overall_status,
|
|
760
754
|
"current_phase": current_phase,
|
|
761
755
|
"phases": manifest.phases,
|
|
762
756
|
"completed_phases": completed_phases,
|
|
763
|
-
"source":
|
|
764
|
-
"target":
|
|
757
|
+
"source": source_path,
|
|
758
|
+
"target": target_name,
|
|
765
759
|
"current_step": current_step,
|
|
766
760
|
"features": {"passing": features_passing, "total": features_total},
|
|
767
761
|
"steps": {"current": current_step_index, "completed": steps_completed, "total": steps_total},
|
|
768
762
|
"last_checkpoint": last_checkpoint_data,
|
|
769
763
|
"checkpoints_count": len(manifest.checkpoints),
|
|
764
|
+
"seams": seams_data,
|
|
770
765
|
}
|
|
771
766
|
|
|
772
767
|
def generate_plan_summary(self) -> str:
|
|
@@ -893,21 +888,34 @@ def list_migrations() -> list[dict[str, Any]]:
|
|
|
893
888
|
# Determine overall status from phases (clean string, no parenthesized phase)
|
|
894
889
|
phases = data.get("phases", {})
|
|
895
890
|
status = "pending"
|
|
891
|
+
all_completed = True
|
|
896
892
|
for phase in PHASE_ORDER:
|
|
897
893
|
phase_status = phases.get(phase, {}).get("status", "pending")
|
|
894
|
+
if phase_status == "failed":
|
|
895
|
+
status = "failed"
|
|
896
|
+
all_completed = False
|
|
897
|
+
break
|
|
898
898
|
if phase_status == "in_progress":
|
|
899
899
|
status = "in_progress"
|
|
900
|
+
all_completed = False
|
|
900
901
|
break
|
|
901
902
|
if phase_status == "completed":
|
|
902
|
-
status = "
|
|
903
|
+
status = "in_progress" # partial completion
|
|
904
|
+
else:
|
|
905
|
+
all_completed = False
|
|
906
|
+
if all_completed:
|
|
907
|
+
status = "completed"
|
|
903
908
|
|
|
904
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)
|
|
905
913
|
results.append({
|
|
906
914
|
"id": data.get("id", entry.name),
|
|
907
915
|
"created_at": data.get("created_at", ""),
|
|
908
|
-
"source":
|
|
909
|
-
"source_path":
|
|
910
|
-
"target":
|
|
916
|
+
"source": source_path,
|
|
917
|
+
"source_path": source_path,
|
|
918
|
+
"target": target_name,
|
|
911
919
|
"status": status,
|
|
912
920
|
})
|
|
913
921
|
except (json.JSONDecodeError, OSError) as exc:
|
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED