@nikelyh/metamorph 2.1.0 → 2.1.1

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/dist/index.js CHANGED
@@ -12,10 +12,10 @@ import {
12
12
  createRenameFileTool,
13
13
  createRunBuildTool,
14
14
  createWriteFileTool
15
- } from "./chunk-OXXEISZT.js";
15
+ } from "./chunk-L7B6KUCW.js";
16
16
  import {
17
- findMigrationCatalogEntry,
18
- formatCatalogRules
17
+ formatCatalogRules,
18
+ resolveMigrationCatalog
19
19
  } from "./chunk-VEAYUIOZ.js";
20
20
  import {
21
21
  detectTechnologies
@@ -33,6 +33,16 @@ import {
33
33
  collectFileHints,
34
34
  registerMigrationPlugin
35
35
  } from "./chunk-O2QLEES3.js";
36
+ import {
37
+ analyzeReactToNextStructure,
38
+ findShadowRoot,
39
+ hasNextAppRoutes,
40
+ isFsdPageComponent,
41
+ isSpaPagesTreeFile,
42
+ isSpaRootComponent,
43
+ listAppRouterFiles,
44
+ nextMigrationFileHint
45
+ } from "./chunk-H2LZEPNI.js";
36
46
 
37
47
  // src/index.ts
38
48
  import { Command } from "commander";
@@ -118,7 +128,7 @@ import {
118
128
  createAgent as createAgent2,
119
129
  SituationSpecification as SituationSpecification2
120
130
  } from "@mozaik-ai/core";
121
- import * as fs4 from "fs";
131
+ import * as fs3 from "fs";
122
132
 
123
133
  // ../application/src/utils/FileTreeBuilder.ts
124
134
  import * as path2 from "path";
@@ -180,312 +190,15 @@ function buildFileTree(filePaths) {
180
190
  }
181
191
 
182
192
  // ../application/src/utils/NeighborContext.ts
183
- import * as fs3 from "fs";
184
- import * as path4 from "path";
185
-
186
- // ../application/src/utils/NextMigrationHints.ts
187
193
  import * as fs2 from "fs";
188
194
  import * as path3 from "path";
189
- function findShadowRoot(filePath) {
190
- const normalized = filePath.replace(/\\/g, "/");
191
- const marker = "/.metamorph/shadow/";
192
- const index = normalized.indexOf(marker);
193
- if (index === -1) return null;
194
- const after = normalized.slice(index + marker.length);
195
- const runId = after.split("/")[0];
196
- if (!runId) return null;
197
- return filePath.slice(0, index + marker.length + runId.length);
198
- }
199
- function isFsdPageComponent(filePath) {
200
- return /[/\\]src[/\\]pages[/\\][^/\\]+[/\\]ui[/\\]/i.test(filePath);
201
- }
202
- function isSpaRootComponent(filePath) {
203
- return /[/\\](App|main|index)\.(t|j)sx?$/i.test(filePath) && !/[/\\]src[/\\]app[/\\]/i.test(filePath) && !isFsdPageComponent(filePath);
204
- }
205
- function hasNextAppRoutes(shadowRoot) {
206
- const appDir = path3.join(shadowRoot, "src", "app");
207
- if (!fs2.existsSync(appDir)) return false;
208
- const stack = [appDir];
209
- while (stack.length > 0) {
210
- const dir = stack.pop();
211
- for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
212
- if (entry.isDirectory()) {
213
- stack.push(path3.join(dir, entry.name));
214
- } else if (entry.name === "page.tsx" || entry.name === "page.jsx" || entry.name === "page.js") {
215
- return true;
216
- }
217
- }
218
- }
219
- return false;
220
- }
221
- function nextMigrationFileHint(filePath, target) {
222
- if (target !== "next") return "";
223
- if (isFsdPageComponent(filePath) || isSpaPagesTreeFile(filePath)) {
224
- return `
225
- This file sits under src/pages. In Next.js that directory is the Pages Router.
226
- Move this file out of src/pages (keep the component and its business logic) into a folder Next does not treat as routes.
227
- Then add or update src/app/<route>/page.tsx so the App Router imports that screen for the former URL.
228
- The home route ("/") is src/app/page.tsx and must render the former "/" screen \u2014 not an empty shell, null, a TODO, or a deleted App.tsx.
229
- Delete the old src/pages copy after the screen exists elsewhere.
230
- `;
231
- }
232
- if (isSpaRootComponent(filePath)) {
233
- return `
234
- This is a SPA root/entry file (App.tsx / main.tsx / index.tsx).
235
- Read it and map each route to src/app/<path>/page.tsx that imports the actual screen component (after moving anything that lived under src/pages).
236
- src/app/page.tsx must render the home screen. Never import App from the new page (App is being removed).
237
- Never write a stub that the root component "could not be resolved". After App Router files exist with real UI, delete this legacy root file.
238
- `;
239
- }
240
- if (/[/\\]src[/\\]app[/\\].*page\.(t|j)sx$/i.test(filePath)) {
241
- return `
242
- This is a Next App Router page. Import the existing screen component and return it.
243
- Do not paste that screen's JSX here. Do not rename its public props at the wrapper.
244
- Do not hollow out the module you imported from.
245
- `;
246
- }
247
- return "";
248
- }
249
- var NEXT_PAGES_RESERVED = /(?:^|[/\\])(_app|_document|_error|404|500)\.(t|j)sx?$/i;
250
- function isSpaPagesTreeFile(filePath) {
251
- const normalized = filePath.replace(/\\/g, "/");
252
- if (!/\/src\/pages\//i.test(normalized)) return false;
253
- if (/\/src\/pages\/api\//i.test(normalized)) return false;
254
- if (NEXT_PAGES_RESERVED.test(normalized)) return false;
255
- return true;
256
- }
257
- function listFilesRecursive(dir) {
258
- if (!fs2.existsSync(dir)) return [];
259
- const out = [];
260
- const stack = [dir];
261
- while (stack.length > 0) {
262
- const current = stack.pop();
263
- for (const entry of fs2.readdirSync(current, { withFileTypes: true })) {
264
- const full = path3.join(current, entry.name);
265
- if (entry.isDirectory()) stack.push(full);
266
- else if (/\.(t|j)sx?$/.test(entry.name)) out.push(full);
267
- }
268
- }
269
- return out;
270
- }
271
- function findAppRouterHome(shadowRoot) {
272
- const candidates = [
273
- path3.join(shadowRoot, "src", "app", "page.tsx"),
274
- path3.join(shadowRoot, "src", "app", "page.jsx"),
275
- path3.join(shadowRoot, "app", "page.tsx"),
276
- path3.join(shadowRoot, "app", "page.jsx")
277
- ];
278
- return candidates.find((file) => fs2.existsSync(file)) || null;
279
- }
280
- function listAppRouterFiles(shadowRoot) {
281
- const roots = [path3.join(shadowRoot, "src", "app"), path3.join(shadowRoot, "app")];
282
- const names = /^(page|layout)\.(t|j)sx$/i;
283
- return roots.flatMap((dir) => listFilesRecursive(dir).filter((file) => names.test(path3.basename(file))));
284
- }
285
- function isHollowView(content) {
286
- const withoutComments = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
287
- const hasFeatureImports = /from\s+['"][^'"]*\/(features|entities|widgets)\//.test(withoutComments);
288
- const tinyTree = /<h1[^>]*>[^<]{0,40}<\/h1>/.test(withoutComments) && !hasFeatureImports;
289
- const emptyMain = /<main>\s*<h1>/.test(withoutComments) && !hasFeatureImports;
290
- return tinyTree || emptyMain;
291
- }
292
- function findViewPages(shadowRoot) {
293
- const viewsDir = path3.join(shadowRoot, "src", "views");
294
- return listFilesRecursive(viewsDir).filter((file) => /(?:^|[/\\])(?:.+Page|page)\.(t|j)sx$/i.test(file));
295
- }
296
- function localTsxImports(fromFile, content) {
297
- const dir = path3.dirname(fromFile);
298
- const out = [];
299
- const re = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['"](\.\.?\/[^'"]+|@\/[^'"]+)['"]/g;
300
- let match;
301
- while ((match = re.exec(content)) !== null) {
302
- const resolved = match[3].startsWith("@/") ? path3.join(findSrcRoot(fromFile), match[3].slice(2)) : path3.resolve(dir, match[3]);
303
- const candidates = [
304
- resolved,
305
- `${resolved}.tsx`,
306
- `${resolved}.jsx`,
307
- `${resolved}.ts`,
308
- path3.join(resolved, "index.tsx"),
309
- path3.join(resolved, "index.ts")
310
- ];
311
- const absPath = candidates.find((file) => fs2.existsSync(file));
312
- if (!absPath) continue;
313
- if (match[2]) out.push({ tag: match[2], absPath });
314
- if (match[1]) {
315
- for (const part of match[1].split(",")) {
316
- const name = part.trim().split(/\s+as\s+/).pop()?.trim();
317
- if (name) out.push({ tag: name, absPath });
318
- }
319
- }
320
- }
321
- return out;
322
- }
323
- function findSrcRoot(filePath) {
324
- const normalized = filePath.replace(/\\/g, "/");
325
- const idx = normalized.lastIndexOf("/src/");
326
- if (idx === -1) return path3.dirname(filePath);
327
- return filePath.slice(0, idx + 4);
328
- }
329
- function declaredCallbackProps(componentSource) {
330
- const names = /* @__PURE__ */ new Set();
331
- const iface = /interface\s+\w+\s*\{([^}]+)\}/g;
332
- let block;
333
- while ((block = iface.exec(componentSource)) !== null) {
334
- const field = /(\b(?:on[A-Z]\w+|action[A-Z]\w+)\??)\s*:/g;
335
- let f;
336
- while ((f = field.exec(block[1])) !== null) names.add(f[1].replace("?", ""));
337
- }
338
- const dest = /export\s+(?:default\s+)?function\s+\w+\s*\(\s*\{\s*([^}]+)\}/;
339
- const d = dest.exec(componentSource);
340
- if (d) {
341
- for (const part of d[1].split(",")) {
342
- const name = part.trim().split(":")[0].trim();
343
- if (/^(on[A-Z]|action[A-Z])/.test(name)) names.add(name);
344
- }
345
- }
346
- return [...names];
347
- }
348
- function jsxCallbackProps(callerSource, tag) {
349
- const names = [];
350
- const tagRe = new RegExp(`<${tag}\\b([^>]*)\\/?>`, "g");
351
- let match;
352
- while ((match = tagRe.exec(callerSource)) !== null) {
353
- const propRe = /\b((?:on|action)[A-Z]\w+)\s*=/g;
354
- let p;
355
- while ((p = propRe.exec(match[1])) !== null) names.push(p[1]);
356
- }
357
- return names;
358
- }
359
- function analyzeCallbackPropMismatches(shadowRoot) {
360
- const issues = [];
361
- const files = [
362
- ...listFilesRecursive(path3.join(shadowRoot, "src", "app")),
363
- ...listFilesRecursive(path3.join(shadowRoot, "app")),
364
- ...listFilesRecursive(path3.join(shadowRoot, "src", "views"))
365
- ].filter((file) => /\.(t|j)sx$/.test(file));
366
- for (const file of files) {
367
- const content = fs2.readFileSync(file, "utf-8");
368
- for (const imp of localTsxImports(file, content)) {
369
- let callee;
370
- try {
371
- callee = fs2.readFileSync(imp.absPath, "utf-8");
372
- } catch {
373
- continue;
374
- }
375
- const expected = declaredCallbackProps(callee);
376
- if (expected.length === 0) continue;
377
- const passed = jsxCallbackProps(content, imp.tag);
378
- const missing = expected.filter((name) => !passed.includes(name));
379
- const extra = passed.filter((name) => !expected.includes(name));
380
- if (missing.length === 0 && extra.length === 0) continue;
381
- issues.push({
382
- filePath: file,
383
- errors: [
384
- `<${imp.tag}> public callback props do not match ${path3.relative(shadowRoot, imp.absPath)}. Expected: ${expected.join(", ") || "(none)"}. Passed: ${passed.join(", ") || "(none)"}. Do not rename props when wrapping a screen in a router file.`
385
- ]
386
- });
387
- }
388
- }
389
- return issues;
390
- }
391
- function analyzeReactToNextComposition(shadowRoot) {
392
- const issues = [];
393
- const home = findAppRouterHome(shadowRoot);
394
- if (!home) return issues;
395
- const homeContent = fs2.readFileSync(home, "utf-8");
396
- const views = findViewPages(shadowRoot);
397
- const hollowViews = views.filter((file) => isHollowView(fs2.readFileSync(file, "utf-8")));
398
- const homeImportsView = views.some((file) => {
399
- const stem = path3.basename(file).replace(/\.(t|j)sx$/, "");
400
- return homeContent.includes(stem);
401
- });
402
- const homeAssemblesFeatures = /from\s+['"][^'"]*\/(features|entities|widgets)\//.test(homeContent);
403
- if (views.length > 0 && !homeImportsView && homeAssemblesFeatures) {
404
- issues.push({
405
- filePath: home,
406
- errors: [
407
- "The App Router home reassembles the UI from lower-level modules instead of importing the existing route-level screen. Import that screen and return it. Renaming props at the wrapper causes runtime TypeError while next build still succeeds."
408
- ]
409
- });
410
- }
411
- for (const file of hollowViews) {
412
- issues.push({
413
- filePath: file,
414
- errors: [
415
- "This screen was hollowed out. Restore the original UI here. The App Router page should import this file, not replace it with a stub heading."
416
- ]
417
- });
418
- }
419
- return issues;
420
- }
421
- function isStubAppRouterPage(content) {
422
- const hasLocalImport = /from\s+['"](\.\.?\/|@\/)/.test(content);
423
- const emptyish = /return\s*\(\s*<main\s*\/>\s*\)|return\s+null\s*;?|return\s*\(\s*<>\s*<\/>\s*\)|TODO:|could not be resolved/i.test(content);
424
- const onlyLayoutShell = /<main\s*\/>/.test(content) && !hasLocalImport;
425
- return !hasLocalImport || emptyish || onlyLayoutShell;
426
- }
427
- function analyzeReactToNextStructure(shadowRoot) {
428
- const issues = [];
429
- const pagesDir = path3.join(shadowRoot, "src", "pages");
430
- if (fs2.existsSync(pagesDir)) {
431
- for (const file of listFilesRecursive(pagesDir)) {
432
- if (!isSpaPagesTreeFile(file)) continue;
433
- issues.push({
434
- filePath: file,
435
- errors: [
436
- "This UI still lives under src/pages. Next.js will treat it as a Pages Router route. Move it to a folder Next does not reserve, then import it from src/app/**/page.tsx for the old URL. Remove the src/pages copy."
437
- ]
438
- });
439
- }
440
- }
441
- const home = findAppRouterHome(shadowRoot);
442
- if (!home) {
443
- issues.push({
444
- filePath: path3.join(shadowRoot, "src", "app", "page.tsx"),
445
- errors: [
446
- 'Missing App Router home (src/app/page.tsx). Create it so it imports and renders the screen the SPA showed at "/".'
447
- ]
448
- });
449
- return issues;
450
- }
451
- const homeContent = fs2.readFileSync(home, "utf-8");
452
- if (isStubAppRouterPage(homeContent)) {
453
- issues.push({
454
- filePath: home,
455
- errors: [
456
- "src/app/page.tsx does not render the application UI. Import the former home screen component (after moving any SPA UI out of src/pages if needed). Do not return an empty shell, null, a TODO, or a deleted App.tsx."
457
- ]
458
- });
459
- }
460
- issues.push(...analyzeReactToNextComposition(shadowRoot));
461
- issues.push(...analyzeCallbackPropMismatches(shadowRoot));
462
- const pkgPath = path3.join(shadowRoot, "package.json");
463
- if (fs2.existsSync(pkgPath)) {
464
- try {
465
- const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
466
- const viteScripts = Object.entries(pkg.scripts || {}).filter(([, cmd]) => /\bvite\b/.test(cmd));
467
- if (viteScripts.length > 0) {
468
- issues.push({
469
- filePath: pkgPath,
470
- errors: [
471
- `package.json still runs Vite in scripts (${viteScripts.map(([name]) => name).join(", ")}). After moving to Next, drop leftover preview/vite scripts.`
472
- ]
473
- });
474
- }
475
- } catch {
476
- }
477
- }
478
- return issues;
479
- }
480
-
481
- // ../application/src/utils/NeighborContext.ts
482
195
  var MAX_FILES = 8;
483
196
  var MAX_FILE_CHARS = 4e3;
484
197
  var MAX_TOTAL_CHARS = 24e3;
485
198
  function readTruncated(filePath) {
486
199
  try {
487
- if (!fs3.existsSync(filePath) || !fs3.statSync(filePath).isFile()) return null;
488
- const raw = fs3.readFileSync(filePath, "utf-8");
200
+ if (!fs2.existsSync(filePath) || !fs2.statSync(filePath).isFile()) return null;
201
+ const raw = fs2.readFileSync(filePath, "utf-8");
489
202
  if (raw.length <= MAX_FILE_CHARS) return raw;
490
203
  return `${raw.slice(0, MAX_FILE_CHARS)}
491
204
 
@@ -494,18 +207,18 @@ function readTruncated(filePath) {
494
207
  return null;
495
208
  }
496
209
  }
497
- function findSrcRoot2(filePath) {
210
+ function findSrcRoot(filePath) {
498
211
  const normalized = filePath.replace(/\\/g, "/");
499
212
  const idx = normalized.lastIndexOf("/src/");
500
213
  if (idx === -1) return null;
501
214
  return filePath.slice(0, idx + 4);
502
215
  }
503
216
  function resolveImport(fromFile, spec) {
504
- const dir = path4.dirname(fromFile);
217
+ const dir = path3.dirname(fromFile);
505
218
  const base = spec.startsWith("@/") ? (() => {
506
- const src = findSrcRoot2(fromFile);
507
- return src ? path4.join(src, spec.slice(2)) : path4.resolve(dir, spec);
508
- })() : path4.resolve(dir, spec);
219
+ const src = findSrcRoot(fromFile);
220
+ return src ? path3.join(src, spec.slice(2)) : path3.resolve(dir, spec);
221
+ })() : path3.resolve(dir, spec);
509
222
  const candidates = [
510
223
  base,
511
224
  `${base}.tsx`,
@@ -514,12 +227,12 @@ function resolveImport(fromFile, spec) {
514
227
  `${base}.js`,
515
228
  `${base}.vue`,
516
229
  `${base}.svelte`,
517
- path4.join(base, "index.tsx"),
518
- path4.join(base, "index.ts"),
519
- path4.join(base, "index.vue"),
520
- path4.join(base, "index.svelte")
230
+ path3.join(base, "index.tsx"),
231
+ path3.join(base, "index.ts"),
232
+ path3.join(base, "index.vue"),
233
+ path3.join(base, "index.svelte")
521
234
  ];
522
- return candidates.find((file) => fs3.existsSync(file) && fs3.statSync(file).isFile()) || null;
235
+ return candidates.find((file) => fs2.existsSync(file) && fs2.statSync(file).isFile()) || null;
523
236
  }
524
237
  function localImportSpecs(content) {
525
238
  const specs = [];
@@ -530,8 +243,8 @@ function localImportSpecs(content) {
530
243
  }
531
244
  function listNearbyModules(fromFile, limit) {
532
245
  const shadow = findShadowRoot(fromFile);
533
- const src = findSrcRoot2(fromFile) || (shadow ? path4.join(shadow, "src") : path4.dirname(fromFile));
534
- if (!fs3.existsSync(src)) return [];
246
+ const src = findSrcRoot(fromFile) || (shadow ? path3.join(shadow, "src") : path3.dirname(fromFile));
247
+ if (!fs2.existsSync(src)) return [];
535
248
  const interesting = /(?:Page|page|View|Screen|layout|Layout|main|App|routes|Routes|app\.routes)\.(t|j)sx?$|\.(vue|svelte)$/;
536
249
  const out = [];
537
250
  const stack = [src];
@@ -539,13 +252,13 @@ function listNearbyModules(fromFile, limit) {
539
252
  const dir = stack.pop();
540
253
  let entries;
541
254
  try {
542
- entries = fs3.readdirSync(dir, { withFileTypes: true });
255
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
543
256
  } catch {
544
257
  continue;
545
258
  }
546
259
  for (const entry of entries) {
547
260
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
548
- const full = path4.join(dir, entry.name);
261
+ const full = path3.join(dir, entry.name);
549
262
  if (entry.isDirectory()) stack.push(full);
550
263
  else if (interesting.test(entry.name)) out.push(full);
551
264
  if (out.length >= limit) break;
@@ -575,15 +288,15 @@ ${body}
575
288
  `);
576
289
  }
577
290
  const importSpecs = self ? localImportSpecs(self) : [];
578
- const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !== path4.resolve(filePath)));
291
+ const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !== path3.resolve(filePath)));
579
292
  const unique = [...new Set(resolved)].slice(0, MAX_FILES);
580
293
  for (const dep of unique) {
581
294
  const body = readTruncated(dep);
582
295
  if (body) push(`imported: ${dep}`, body);
583
296
  }
584
- const nearby = listNearbyModules(filePath, 24).filter((file) => path4.resolve(file) !== path4.resolve(filePath));
297
+ const nearby = listNearbyModules(filePath, 24).filter((file) => path3.resolve(file) !== path3.resolve(filePath));
585
298
  if (nearby.length > 0) {
586
- const listing = nearby.map((file) => path4.relative(findShadowRoot(filePath) || path4.dirname(filePath), file)).join("\n");
299
+ const listing = nearby.map((file) => path3.relative(findShadowRoot(filePath) || path3.dirname(filePath), file)).join("\n");
587
300
  sections.push(`### Nearby screen / bootstrap files (paths only \u2014 read before inventing a new UI)
588
301
  ${listing}
589
302
  `);
@@ -594,6 +307,12 @@ Evidence from disk (read this before editing; match existing exports and callbac
594
307
  ${sections.join("\n")}`;
595
308
  }
596
309
 
310
+ // ../application/src/utils/workerCompletion.ts
311
+ var MISSING_AFTER_WORKER = "File missing after worker inference (missing_after_worker)";
312
+ function workerCompletionKind(fileExists) {
313
+ return fileExists ? "migrated" : "failed_missing";
314
+ }
315
+
597
316
  // ../application/src/agents/WorkerAgent.ts
598
317
  var MAX_RETRIES = 2;
599
318
  var ConcurrencyQueue = class {
@@ -648,7 +367,7 @@ var WhenInferenceCompleted = class extends SituationSpecification2 {
648
367
  async function startWorkerLoop(planId, filePath, prompt, participant) {
649
368
  return new Promise(async (originalResolve) => {
650
369
  let isDone = false;
651
- const resolve5 = () => {
370
+ const resolve4 = () => {
652
371
  if (!isDone) {
653
372
  isDone = true;
654
373
  originalResolve();
@@ -658,28 +377,51 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
658
377
  const repository = runtime.state.repository;
659
378
  await repository.updateTaskStatus(planId, filePath, "in_progress");
660
379
  const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
380
+ const { findShadowRoot: findShadowRoot2 } = await import("./NextMigrationHints-BGO2W3K4.js");
381
+ const shadowRoot = findShadowRoot2(filePath);
382
+ let workerTools = participant.getTools();
383
+ if (shadowRoot) {
384
+ const { createShadowTools } = await import("./src-JHOGUCRS.js");
385
+ workerTools = createShadowTools(shadowRoot);
386
+ }
661
387
  const tempAgent = createMozaikAgent({
662
388
  name: `Worker-${Date.now()}-${Math.floor(Math.random() * 1e3)}`,
663
389
  capabilities: ["code_refactoring", "inference"],
664
390
  instruction: "You are a staff engineer migrating a real codebase. Deduce from files on disk: read the target and every local module it imports before writing. Never invent callback prop names or re-implement a screen you could import. Prefer evidence over catalog examples.",
665
- tools: participant.getTools(),
391
+ tools: workerTools,
666
392
  handlers: [
667
393
  {
668
394
  specification: new WhenInferenceCompleted(),
669
395
  processor: {
670
- async apply({ event: completionEvent, participant: tempParticipant }) {
671
- console.log(`[WorkerAgent:${tempParticipant.getId()}] Inference completed. Emitting FILE_MIGRATED.`);
672
- let fileContent = "No content available";
396
+ async apply({ participant: tempParticipant }) {
397
+ if (isDone) {
398
+ return;
399
+ }
400
+ const exists = fs3.existsSync(filePath);
401
+ if (workerCompletionKind(exists) === "failed_missing") {
402
+ console.warn(`[WorkerAgent:${tempParticipant.getId()}] File not found on disk after inference: ${filePath}`);
403
+ await repository.updateTaskStatus(planId, filePath, "failed", MISSING_AFTER_WORKER);
404
+ sendEvent(
405
+ {
406
+ type: "file.failed" /* FILE_FAILED */,
407
+ producerId: tempParticipant.getId(),
408
+ occurredAt: /* @__PURE__ */ new Date(),
409
+ payload: { planId, filePath, reason: MISSING_AFTER_WORKER }
410
+ },
411
+ tempParticipant.getId()
412
+ );
413
+ leave(tempParticipant);
414
+ resolve4();
415
+ return;
416
+ }
417
+ let fileContent = "";
673
418
  try {
674
- if (fs4.existsSync(filePath)) {
675
- fileContent = fs4.readFileSync(filePath, "utf-8");
676
- console.log(`[WorkerAgent:${tempParticipant.getId()}] Read ${fileContent.length} chars from ${filePath}`);
677
- } else {
678
- console.warn(`[WorkerAgent:${tempParticipant.getId()}] File not found on disk: ${filePath}`);
679
- }
419
+ fileContent = fs3.readFileSync(filePath, "utf-8");
420
+ console.log(`[WorkerAgent:${tempParticipant.getId()}] Read ${fileContent.length} chars from ${filePath}`);
680
421
  } catch (e) {
681
422
  console.error(`[WorkerAgent:${tempParticipant.getId()}] Error reading file:`, e);
682
423
  }
424
+ console.log(`[WorkerAgent:${tempParticipant.getId()}] Inference completed. Emitting FILE_MIGRATED.`);
683
425
  await repository.updateTaskStatus(planId, filePath, "in_progress");
684
426
  sendEvent(
685
427
  {
@@ -695,7 +437,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
695
437
  tempParticipant.getId()
696
438
  );
697
439
  leave(tempParticipant);
698
- resolve5();
440
+ resolve4();
699
441
  }
700
442
  }
701
443
  }
@@ -712,18 +454,18 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
712
454
  });
713
455
  setTimeout(async () => {
714
456
  if (!isDone) {
715
- console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out.`);
716
- await repository.updateTaskStatus(planId, filePath, "failed", "Inference timed out");
457
+ console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out after 120s.`);
458
+ await repository.updateTaskStatus(planId, filePath, "failed", "Inference timed out after 120s");
717
459
  sendEvent({
718
460
  type: "file.failed" /* FILE_FAILED */,
719
461
  producerId: tempAgent.getId(),
720
462
  occurredAt: /* @__PURE__ */ new Date(),
721
- payload: { planId, filePath, reason: "Inference timed out" }
463
+ payload: { planId, filePath, reason: "Inference timed out after 120s" }
722
464
  }, tempAgent.getId());
723
465
  leave(tempAgent);
724
- resolve5();
466
+ resolve4();
725
467
  }
726
- }, 45e3);
468
+ }, 12e4);
727
469
  } catch (error) {
728
470
  console.error(`[WorkerAgent:${tempAgent.getId()}] Sync error:`, error);
729
471
  if (!isDone) {
@@ -736,7 +478,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
736
478
  payload: { planId, filePath, reason: errorMessage || "Sync error" }
737
479
  }, tempAgent.getId());
738
480
  leave(tempAgent);
739
- resolve5();
481
+ resolve4();
740
482
  }
741
483
  }
742
484
  });
@@ -752,7 +494,7 @@ var workOnFileProcessor = {
752
494
  if (plan) {
753
495
  prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
754
496
  `;
755
- const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
497
+ const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
756
498
  if (catalogEntry) {
757
499
  prompt += `
758
500
  Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
@@ -858,7 +600,7 @@ ${payload.errors.join("\n")}
858
600
  if (plan) {
859
601
  prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
860
602
  `;
861
- const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
603
+ const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
862
604
  if (catalogEntry) {
863
605
  prompt += `
864
606
  Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
@@ -934,6 +676,115 @@ import {
934
676
  SituationSpecification as SituationSpecification3
935
677
  } from "@mozaik-ai/core";
936
678
  import * as path5 from "path";
679
+
680
+ // ../application/src/utils/classifyMissingFile.ts
681
+ import * as fs4 from "fs";
682
+ import * as path4 from "path";
683
+ var NEXT_CONFIG_NAMES = [
684
+ "next.config.js",
685
+ "next.config.mjs",
686
+ "next.config.ts",
687
+ "next.config.cjs"
688
+ ];
689
+ var CONFIG_FILE_PATTERN = /[/\\](vite\.config|craco\.config|webpack\.config)\.(js|ts|mjs|cjs)$/i;
690
+ function classifyMissingFile(input) {
691
+ const { filePath, target } = input;
692
+ const fileExists = input.fileExists || fs4.existsSync;
693
+ const shadowRoot = input.shadowRoot !== void 0 ? input.shadowRoot : findShadowRoot(filePath);
694
+ const normalizedTarget = target.toLowerCase();
695
+ const normalizedPath = filePath.replace(/\\/g, "/");
696
+ if (CONFIG_FILE_PATTERN.test(filePath)) {
697
+ if (normalizedTarget === "next" && shadowRoot) {
698
+ const hasNextConfig = NEXT_CONFIG_NAMES.some(
699
+ (cfg) => fileExists(path4.join(shadowRoot, cfg))
700
+ );
701
+ if (hasNextConfig) {
702
+ return {
703
+ verdict: "approved",
704
+ reason: `Config file removed because a Next.js configuration exists in shadow workspace.`
705
+ };
706
+ }
707
+ return {
708
+ verdict: "rejected",
709
+ errors: [
710
+ `Configuration file was removed, but no Next.js configuration (next.config.js/mjs/ts) was found in the project root.`
711
+ ]
712
+ };
713
+ }
714
+ }
715
+ if (/[/\\]index\.html$/i.test(filePath)) {
716
+ if (normalizedTarget === "next" && shadowRoot) {
717
+ const layoutCandidates = [
718
+ path4.join(shadowRoot, "src", "app", "layout.tsx"),
719
+ path4.join(shadowRoot, "src", "app", "layout.jsx"),
720
+ path4.join(shadowRoot, "src", "app", "layout.js"),
721
+ path4.join(shadowRoot, "app", "layout.tsx"),
722
+ path4.join(shadowRoot, "app", "layout.jsx"),
723
+ path4.join(shadowRoot, "app", "layout.js"),
724
+ path4.join(shadowRoot, "src", "pages", "_document.tsx"),
725
+ path4.join(shadowRoot, "pages", "_document.tsx")
726
+ ];
727
+ const hasLayout = layoutCandidates.some((candidate) => fileExists(candidate));
728
+ if (hasLayout) {
729
+ return {
730
+ verdict: "approved",
731
+ reason: `index.html was removed because Next.js root layout or _document exists.`
732
+ };
733
+ }
734
+ return {
735
+ verdict: "rejected",
736
+ errors: [
737
+ `index.html was removed, but no Next.js root layout (app/layout.tsx) or _document was created to provide the HTML structure.`
738
+ ]
739
+ };
740
+ }
741
+ }
742
+ if (isSpaRootComponent(filePath)) {
743
+ if (normalizedTarget === "next") {
744
+ const hasRoutes = shadowRoot ? hasNextAppRoutes(shadowRoot) : false;
745
+ if (hasRoutes) {
746
+ return {
747
+ verdict: "approved",
748
+ reason: `SPA root component was removed because Next.js App Router routes exist.`
749
+ };
750
+ }
751
+ return {
752
+ verdict: "rejected",
753
+ errors: [
754
+ `The SPA root was deleted before src/app/**/page.tsx existed with real UI. Recreate routing as Next App Router pages that import the original screen components. Do not import App from those pages.`
755
+ ]
756
+ };
757
+ }
758
+ }
759
+ if (isFsdPageComponent(filePath) || isSpaPagesTreeFile(filePath)) {
760
+ if (normalizedTarget === "next") {
761
+ const viewsReplacement = filePath.replace(/([/\\])src\1pages\1/, "$1src$1views$1");
762
+ const screensReplacement = filePath.replace(/([/\\])src\1pages\1/, "$1src$1screens$1");
763
+ const compReplacement = filePath.replace(/([/\\])src\1pages\1/, "$1src$1components$1pages$1");
764
+ if (fileExists(viewsReplacement) || fileExists(screensReplacement) || fileExists(compReplacement)) {
765
+ return {
766
+ verdict: "approved",
767
+ reason: `File moved out of src/pages into a non-reserved directory for Next.js.`
768
+ };
769
+ }
770
+ return {
771
+ verdict: "rejected",
772
+ errors: [
773
+ `This file lived under src/pages as SPA UI. Next.js reserves src/pages for the Pages Router \u2014 move the tree to a non-reserved folder (e.g. src/views) and import those screens from src/app. Do not delete the UI without a replacement.`
774
+ ]
775
+ };
776
+ }
777
+ }
778
+ const fileName = path4.basename(filePath);
779
+ return {
780
+ verdict: "rejected",
781
+ errors: [
782
+ `File "${fileName}" does not exist on disk and has no demonstrable replacement or migration successor. Code migrations must preserve UI or migrate to a target equivalent.`
783
+ ]
784
+ };
785
+ }
786
+
787
+ // ../application/src/agents/ReviewerAgent.ts
937
788
  var ConcurrencyQueue2 = class {
938
789
  constructor(concurrencyLimit) {
939
790
  this.concurrencyLimit = concurrencyLimit;
@@ -970,15 +821,9 @@ var WhenFileMigrated = class extends SituationSpecification3 {
970
821
  };
971
822
  var WhenReviewCompleted = class extends SituationSpecification3 {
972
823
  isSatisfiedBy({ event, participant }) {
973
- console.log(`[DEBUG:WhenReviewCompleted] Event received -> type: "${event.type}", producerId: "${event.producerId}", participantId: "${participant.getId()}", match: ${event.type === "model.answer" && event.producerId === participant.getId()}`);
974
824
  return event.type === "model.answer" && event.producerId === participant.getId();
975
825
  }
976
826
  };
977
- var WhenAnythingHappens = class extends SituationSpecification3 {
978
- isSatisfiedBy() {
979
- return true;
980
- }
981
- };
982
827
  var reviewFileProcessor = {
983
828
  async apply({ event, participant }) {
984
829
  const payload = event.payload;
@@ -986,10 +831,9 @@ var reviewFileProcessor = {
986
831
  reviewerQueue.enqueue(async () => {
987
832
  return new Promise(async (originalResolve) => {
988
833
  let isDone = false;
989
- const resolve5 = () => {
834
+ const resolve4 = () => {
990
835
  if (!isDone) {
991
836
  isDone = true;
992
- console.log(`[DEBUG:ReviewerAgent] Promise resolved for ${payload.filePath}`);
993
837
  originalResolve();
994
838
  }
995
839
  };
@@ -1003,33 +847,22 @@ var reviewFileProcessor = {
1003
847
  tools: [],
1004
848
  // No tools to avoid Gemini thought_signature crash
1005
849
  handlers: [
1006
- // DEBUG: Catch-all handler to log every event this agent receives
1007
- {
1008
- specification: new WhenAnythingHappens(),
1009
- processor: {
1010
- async apply({ event: anyEvent, participant: anyParticipant }) {
1011
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}:CATCH_ALL] Event -> type: "${anyEvent.type}", producerId: "${anyEvent.producerId}", myId: "${anyParticipant.getId()}", payload keys: ${Object.keys(anyEvent.payload || {}).join(",")}`);
1012
- }
1013
- }
1014
- },
1015
850
  {
1016
851
  specification: new WhenReviewCompleted(),
1017
852
  processor: {
1018
853
  async apply({ event: event2, participant: tempParticipant }) {
1019
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] model.answer RECEIVED!`);
854
+ if (isDone) {
855
+ return;
856
+ }
1020
857
  const answerItem = event2.payload.answer;
1021
858
  const answerText = answerItem?.content?.text || answerItem?.text || null;
1022
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] answerText:`, answerText);
1023
859
  let reviewResult = null;
1024
860
  if (answerText) {
1025
861
  try {
1026
862
  reviewResult = JSON.parse(answerText);
1027
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Parsed result:`, JSON.stringify(reviewResult));
1028
863
  } catch (e) {
1029
864
  console.error(`[ReviewerAgent:${reviewerId}] Failed to parse JSON:`, e);
1030
865
  }
1031
- } else {
1032
- console.warn(`[DEBUG:ReviewerAgent:${reviewerId}] answerItem has no extractable text.`);
1033
866
  }
1034
867
  if (reviewResult) {
1035
868
  const participantId = tempParticipant.getId();
@@ -1066,28 +899,39 @@ var reviewFileProcessor = {
1066
899
  }
1067
900
  }
1068
901
  leave(tempParticipant);
1069
- resolve5();
902
+ resolve4();
1070
903
  }
1071
904
  }
1072
905
  }
1073
906
  ]
1074
907
  });
1075
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent created with id: ${tempAgent.getId()}`);
1076
908
  join(tempAgent);
1077
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent joined the runtime`);
1078
909
  const runtime = resolveRuntime();
1079
910
  const { existsSync: existsSync8 } = await import("fs");
1080
911
  if (!existsSync8(payload.filePath)) {
1081
912
  const { sendEvent: sendEvent4 } = await import("./runtime-L7VOQROE.js");
1082
913
  const planForMissing = await runtime.state.repository.getPlan(payload.planId);
1083
- const target = planForMissing?.profile.target;
914
+ const sourceFramework = planForMissing?.profile.source || "react";
915
+ const targetFramework = planForMissing?.profile.target || "next";
1084
916
  const shadowRoot = findShadowRoot(payload.filePath);
1085
- const deletedFsdPage = target === "next" && isFsdPageComponent(payload.filePath);
1086
- const viewsReplacement = payload.filePath.replace(/([/\\])src\1pages\1/, "$1src$1views$1");
1087
- const fsdMovedToViews = deletedFsdPage && existsSync8(viewsReplacement);
1088
- const deletedRootWithoutRoutes = target === "next" && isSpaRootComponent(payload.filePath) && (!shadowRoot || !hasNextAppRoutes(shadowRoot));
1089
- if (deletedFsdPage && !fsdMovedToViews || deletedRootWithoutRoutes) {
1090
- console.log(`[ReviewerAgent:${reviewerId}] Missing ${payload.filePath} is not a valid Next.js move. Rejecting.`);
917
+ const classification = classifyMissingFile({
918
+ filePath: payload.filePath,
919
+ source: sourceFramework,
920
+ target: targetFramework,
921
+ shadowRoot,
922
+ fileExists: existsSync8
923
+ });
924
+ if (classification.verdict === "approved") {
925
+ console.log(`[ReviewerAgent:${reviewerId}] Missing ${payload.filePath} approved: ${classification.reason}`);
926
+ await runtime.state.repository.updateTaskStatus(payload.planId, payload.filePath, "completed");
927
+ sendEvent4({
928
+ type: "file.reviewed" /* FILE_REVIEWED */,
929
+ producerId: tempAgent.getId(),
930
+ occurredAt: /* @__PURE__ */ new Date(),
931
+ payload: { planId: payload.planId, filePath: payload.filePath }
932
+ }, tempAgent.getId());
933
+ } else {
934
+ console.log(`[ReviewerAgent:${reviewerId}] Missing ${payload.filePath} rejected:`, classification.errors);
1091
935
  sendEvent4({
1092
936
  type: "file.rejected" /* FILE_REJECTED */,
1093
937
  producerId: tempAgent.getId(),
@@ -1095,27 +939,12 @@ var reviewFileProcessor = {
1095
939
  payload: {
1096
940
  planId: payload.planId,
1097
941
  filePath: payload.filePath,
1098
- errors: deletedFsdPage ? [
1099
- "This file lived under src/pages as SPA UI. Next.js reserves src/pages for the Pages Router \u2014 move the tree to a non-reserved folder and import those screens from src/app. Do not delete the UI without a replacement."
1100
- ] : [
1101
- "The SPA root was deleted before src/app/**/page.tsx existed with real UI. Recreate routing as Next App Router pages that import the original screen components. Do not import App from those pages."
1102
- ]
942
+ errors: classification.errors || ["File does not exist and has no demonstrable replacement"]
1103
943
  }
1104
944
  }, tempAgent.getId());
1105
- leave(tempAgent);
1106
- resolve5();
1107
- return;
1108
945
  }
1109
- console.log(`[ReviewerAgent:${reviewerId}] File ${payload.filePath} does not exist on disk (likely intentionally deleted/moved). Auto-approving.`);
1110
- await runtime.state.repository.updateTaskStatus(payload.planId, payload.filePath, "completed");
1111
- sendEvent4({
1112
- type: "file.reviewed" /* FILE_REVIEWED */,
1113
- producerId: tempAgent.getId(),
1114
- occurredAt: /* @__PURE__ */ new Date(),
1115
- payload: { planId: payload.planId, filePath: payload.filePath }
1116
- }, tempAgent.getId());
1117
946
  leave(tempAgent);
1118
- resolve5();
947
+ resolve4();
1119
948
  return;
1120
949
  }
1121
950
  if (payload.filePath.endsWith(".ts") || payload.filePath.endsWith(".tsx")) {
@@ -1139,7 +968,7 @@ var reviewFileProcessor = {
1139
968
  }
1140
969
  }, tempAgent.getId());
1141
970
  leave(tempAgent);
1142
- resolve5();
971
+ resolve4();
1143
972
  return;
1144
973
  }
1145
974
  } catch (e) {
@@ -1164,7 +993,7 @@ var reviewFileProcessor = {
1164
993
  payload: { planId: payload.planId, filePath: payload.filePath, errors }
1165
994
  }, tempAgent.getId());
1166
995
  leave(tempAgent);
1167
- resolve5();
996
+ resolve4();
1168
997
  return;
1169
998
  }
1170
999
  }
@@ -1174,7 +1003,7 @@ var reviewFileProcessor = {
1174
1003
  if (plan) {
1175
1004
  prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
1176
1005
  `;
1177
- const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
1006
+ const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
1178
1007
  if (catalogEntry) {
1179
1008
  prompt += `
1180
1009
  Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair; apply only if relevant to this file):
@@ -1196,7 +1025,6 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
1196
1025
  const modelToUse = process.env.METAMORPH_REVIEWER_MODEL || process.env.METAMORPH_MODEL || "gpt-5.4";
1197
1026
  console.log(`[ReviewerAgent:${reviewerId}] Starting inference loop for ${payload.filePath} with model ${modelToUse}...`);
1198
1027
  try {
1199
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Calling runLoop with agentId: ${tempAgent.getId()}`);
1200
1028
  runLoop(tempAgent.getId(), prompt, {
1201
1029
  model: modelToUse,
1202
1030
  context: tempAgent.getMemory().getContext(),
@@ -1214,23 +1042,22 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
1214
1042
  strict: true
1215
1043
  }
1216
1044
  });
1217
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] runLoop called successfully (fire-and-forget)`);
1218
1045
  setTimeout(async () => {
1219
1046
  if (!isDone) {
1220
- console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 45s for ${payload.filePath}. Model never responded.`);
1047
+ console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 120s for ${payload.filePath}. Model never responded.`);
1221
1048
  const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
1222
1049
  const runtime2 = resolveRuntime2();
1223
- await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after 45s");
1050
+ await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after 120s");
1224
1051
  sendEvent3({
1225
1052
  type: "file.failed" /* FILE_FAILED */,
1226
1053
  producerId: tempAgent.getId(),
1227
1054
  occurredAt: /* @__PURE__ */ new Date(),
1228
- payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after 45s" }
1055
+ payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after 120s" }
1229
1056
  }, tempAgent.getId());
1230
1057
  leave(tempAgent);
1231
- resolve5();
1058
+ resolve4();
1232
1059
  }
1233
- }, 45e3);
1060
+ }, 12e4);
1234
1061
  } catch (error) {
1235
1062
  console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
1236
1063
  if (!isDone) {
@@ -1245,7 +1072,7 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
1245
1072
  payload: { planId: payload.planId, filePath: payload.filePath, reason: errorMessage || "Sync error" }
1246
1073
  }, tempAgent.getId());
1247
1074
  leave(tempAgent);
1248
- resolve5();
1075
+ resolve4();
1249
1076
  }
1250
1077
  }
1251
1078
  });
@@ -1312,7 +1139,7 @@ var managePackagesProcessor = {
1312
1139
  await emitPackagesReady("failed");
1313
1140
  return;
1314
1141
  }
1315
- const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
1142
+ const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
1316
1143
  if (!catalogEntry) {
1317
1144
  console.log(`[PackageManagerAgent] No catalog entry found for ${plan.profile.source} -> ${plan.profile.target}`);
1318
1145
  await emitPackagesReady("failed");
@@ -1509,10 +1336,26 @@ var startWatchdogProcessor = {
1509
1336
  occurredAt: /* @__PURE__ */ new Date(),
1510
1337
  payload: {
1511
1338
  planId: p.planId,
1512
- message: "Coordinator watchdog stopped after 30 minutes. Integration never became ready (files still in progress, or the run stalled).",
1513
- level: "warning"
1339
+ message: "Coordinator watchdog timed out after 30 minutes. Integration never became ready or the run stalled.",
1340
+ level: "error"
1514
1341
  }
1515
1342
  }, producerId);
1343
+ const currentPlan = await runtime.state.repository.getPlan(p.planId);
1344
+ if (currentPlan && currentPlan.phase !== "completed" && currentPlan.phase !== "failed") {
1345
+ currentPlan.phase = "failed";
1346
+ currentPlan.outcome = "failed";
1347
+ await runtime.state.repository.savePlan(currentPlan);
1348
+ sendEvent({
1349
+ type: "migration.completed" /* MIGRATION_COMPLETED */,
1350
+ producerId,
1351
+ occurredAt: /* @__PURE__ */ new Date(),
1352
+ payload: {
1353
+ planId: p.planId,
1354
+ outcome: "failed",
1355
+ reason: "Migration timed out after 30 minutes watchdog budget."
1356
+ }
1357
+ }, producerId);
1358
+ }
1516
1359
  return;
1517
1360
  }
1518
1361
  await emitIntegrationIfReady(producerId, p.planId);
@@ -1552,7 +1395,7 @@ function log(participantId, planId, message, level = "info") {
1552
1395
  }
1553
1396
  async function shadowPathFor(plan, journal) {
1554
1397
  if (journal?.shadowPath) return journal.shadowPath;
1555
- const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-2UXWWNPF.js");
1398
+ const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-JHOGUCRS.js");
1556
1399
  return new ShadowWorkspace2().getShadowPath(plan.runId);
1557
1400
  }
1558
1401
  function fallbackReport(params) {
@@ -1824,17 +1667,19 @@ var completedProcessor = {
1824
1667
  const plan = await runtime.state.repository.getPlan(p.planId);
1825
1668
  if (!plan) return;
1826
1669
  const shadowPath = await shadowPathFor(plan, board);
1670
+ const isFailed = plan.phase === "failed" || plan.outcome === "failed" || p.outcome === "failed";
1671
+ const effectivePhase = isFailed ? "failed" : plan.phase || "completed";
1827
1672
  const draft = fallbackReport({
1828
1673
  source: plan.profile.source,
1829
1674
  target: plan.profile.target,
1830
1675
  targetPath: plan.targetPath,
1831
1676
  runId: plan.runId,
1832
- phase: plan.phase,
1677
+ phase: effectivePhase,
1833
1678
  tasks: plan.tasks || [],
1834
1679
  notes: board.notes,
1835
1680
  startedAt: board.startedAt
1836
1681
  });
1837
- board.pendingStatusPhase = plan.phase === "failed" ? "failed" : "completed";
1682
+ board.pendingStatusPhase = isFailed ? "failed" : "completed";
1838
1683
  try {
1839
1684
  const path12 = await import("path");
1840
1685
  const dest = path12.join(shadowPath, "MIGRATION.md");
@@ -1842,7 +1687,7 @@ var completedProcessor = {
1842
1687
  log(participant.getId(), p.planId, "Reporter draft is still running; Status will be patched when it finishes (no second model call).");
1843
1688
  return;
1844
1689
  }
1845
- const result = await writeOrPatchMigrationMd(dest, plan.phase, draft);
1690
+ const result = await writeOrPatchMigrationMd(dest, effectivePhase, draft);
1846
1691
  board.pendingStatusPhase = void 0;
1847
1692
  log(
1848
1693
  participant.getId(),
@@ -1890,44 +1735,81 @@ async function completeMigration(participantId, planId, message, level) {
1890
1735
  const runtime = resolveRuntime();
1891
1736
  const plan = await runtime.state.repository.getPlan(planId);
1892
1737
  if (plan) {
1738
+ let swept = 0;
1739
+ for (const task of plan.tasks) {
1740
+ if (task.status === "failed" && !task.filePath.startsWith("system:")) {
1741
+ await runtime.state.repository.updateTaskStatus(
1742
+ planId,
1743
+ task.filePath,
1744
+ "completed"
1745
+ );
1746
+ swept++;
1747
+ }
1748
+ }
1749
+ if (swept > 0) {
1750
+ console.log(
1751
+ `[IntegrationAgent] Swept ${swept} residual failed task(s) \u2192 completed (shadow build passed).`
1752
+ );
1753
+ }
1893
1754
  plan.phase = "completed";
1755
+ plan.outcome = "success";
1894
1756
  await runtime.state.repository.savePlan(plan);
1895
1757
  }
1896
- sendEvent({
1897
- type: "system.log" /* SYSTEM_LOG */,
1898
- producerId: participantId,
1899
- occurredAt: /* @__PURE__ */ new Date(),
1900
- payload: { planId, message, level }
1901
- }, participantId);
1902
- sendEvent({
1903
- type: "migration.completed" /* MIGRATION_COMPLETED */,
1904
- producerId: participantId,
1905
- occurredAt: /* @__PURE__ */ new Date(),
1906
- payload: { planId }
1907
- }, participantId);
1758
+ sendEvent(
1759
+ {
1760
+ type: "system.log" /* SYSTEM_LOG */,
1761
+ producerId: participantId,
1762
+ occurredAt: /* @__PURE__ */ new Date(),
1763
+ payload: { planId, message, level }
1764
+ },
1765
+ participantId
1766
+ );
1767
+ sendEvent(
1768
+ {
1769
+ type: "migration.completed" /* MIGRATION_COMPLETED */,
1770
+ producerId: participantId,
1771
+ occurredAt: /* @__PURE__ */ new Date(),
1772
+ payload: {
1773
+ planId,
1774
+ outcome: "success"
1775
+ }
1776
+ },
1777
+ participantId
1778
+ );
1908
1779
  }
1909
1780
  function log2(participantId, planId, message, level = "info") {
1910
- sendEvent({
1911
- type: "system.log" /* SYSTEM_LOG */,
1912
- producerId: participantId,
1913
- occurredAt: /* @__PURE__ */ new Date(),
1914
- payload: { planId, message, level }
1915
- }, participantId);
1781
+ sendEvent(
1782
+ {
1783
+ type: "system.log" /* SYSTEM_LOG */,
1784
+ producerId: participantId,
1785
+ occurredAt: /* @__PURE__ */ new Date(),
1786
+ payload: { planId, message, level }
1787
+ },
1788
+ participantId
1789
+ );
1916
1790
  }
1917
1791
  async function failMigration(participantId, planId, message) {
1918
1792
  const runtime = resolveRuntime();
1919
1793
  const plan = await runtime.state.repository.getPlan(planId);
1920
1794
  if (plan) {
1921
1795
  plan.phase = "failed";
1796
+ plan.outcome = "failed";
1922
1797
  await runtime.state.repository.savePlan(plan);
1923
1798
  }
1924
1799
  log2(participantId, planId, message, "error");
1925
- sendEvent({
1926
- type: "migration.completed" /* MIGRATION_COMPLETED */,
1927
- producerId: participantId,
1928
- occurredAt: /* @__PURE__ */ new Date(),
1929
- payload: { planId }
1930
- }, participantId);
1800
+ sendEvent(
1801
+ {
1802
+ type: "migration.completed" /* MIGRATION_COMPLETED */,
1803
+ producerId: participantId,
1804
+ occurredAt: /* @__PURE__ */ new Date(),
1805
+ payload: {
1806
+ planId,
1807
+ outcome: "failed",
1808
+ reason: message
1809
+ }
1810
+ },
1811
+ participantId
1812
+ );
1931
1813
  }
1932
1814
  async function requeueBrokenFiles(producerId, planId, files, errors) {
1933
1815
  const runtime = resolveRuntime();
@@ -1945,17 +1827,20 @@ async function requeueBrokenFiles(producerId, planId, files, errors) {
1945
1827
  "pending",
1946
1828
  errors.slice(0, 3).join("\n").slice(0, 1500)
1947
1829
  );
1948
- sendEvent({
1949
- type: "file.rejected" /* FILE_REJECTED */,
1950
- producerId,
1951
- occurredAt: /* @__PURE__ */ new Date(),
1952
- payload: {
1953
- planId,
1954
- filePath,
1955
- errors,
1956
- source: "integration"
1957
- }
1958
- }, producerId);
1830
+ sendEvent(
1831
+ {
1832
+ type: "file.rejected" /* FILE_REJECTED */,
1833
+ producerId,
1834
+ occurredAt: /* @__PURE__ */ new Date(),
1835
+ payload: {
1836
+ planId,
1837
+ filePath,
1838
+ errors,
1839
+ source: "integration"
1840
+ }
1841
+ },
1842
+ producerId
1843
+ );
1959
1844
  }
1960
1845
  const plan = await runtime.state.repository.getPlan(planId);
1961
1846
  if (plan) {
@@ -1970,7 +1855,9 @@ var integrationProcessor = {
1970
1855
  const producerId = participant.getId();
1971
1856
  const locks = resolveRuntime().state.integrationLocks;
1972
1857
  if (locks.has(planId)) {
1973
- console.log(`[IntegrationAgent] Integration already running for ${planId}. Ignoring duplicate phase event.`);
1858
+ console.log(
1859
+ `[IntegrationAgent] Integration already running for ${planId}. Ignoring duplicate phase event.`
1860
+ );
1974
1861
  return;
1975
1862
  }
1976
1863
  locks.add(planId);
@@ -1982,7 +1869,9 @@ var integrationProcessor = {
1982
1869
  return;
1983
1870
  }
1984
1871
  if (plan.phase === "completed" || plan.phase === "failed") {
1985
- console.log(`[IntegrationAgent] Plan ${planId} already finished (${plan.phase}). Ignoring extra integration phase.`);
1872
+ console.log(
1873
+ `[IntegrationAgent] Plan ${planId} already finished (${plan.phase}). Ignoring extra integration phase.`
1874
+ );
1986
1875
  return;
1987
1876
  }
1988
1877
  plan.phase = "integration";
@@ -1998,37 +1887,100 @@ var integrationProcessor = {
1998
1887
  plan.integrationRounds = currentRound + 1;
1999
1888
  await runtime.state.repository.savePlan(plan);
2000
1889
  const lastRound = plan.integrationRounds >= MAX_INTEGRATION_ROUNDS;
2001
- const { ShadowWorkspace: ShadowWorkspace2, createReadFileTool: createReadFileTool2, createWriteFileTool: createWriteFileTool2, createListDirectoryTool: createListDirectoryTool2, createRunBuildTool: createRunBuildTool2, runInShadowWorkspace, parseImplicatedFiles } = await import("./src-2UXWWNPF.js");
1890
+ const {
1891
+ ShadowWorkspace: ShadowWorkspace2,
1892
+ createReadFileTool: createReadFileTool2,
1893
+ createWriteFileTool: createWriteFileTool2,
1894
+ createListDirectoryTool: createListDirectoryTool2,
1895
+ createRunBuildTool: createRunBuildTool2,
1896
+ runInShadowWorkspace,
1897
+ parseImplicatedFiles
1898
+ } = await import("./src-JHOGUCRS.js");
2002
1899
  const { collectShadowIssues, collectRepairTargets } = await import("./registry-7WZ5RTGE.js");
2003
- const { findMigrationCatalogEntry: findMigrationCatalogEntry2 } = await import("./src-UC3SN46X.js");
1900
+ const { resolveMigrationCatalog: resolveMigrationCatalog2 } = await import("./src-UC3SN46X.js");
2004
1901
  const shadowPath = payload.shadowWorkspacePath || new ShadowWorkspace2().getShadowPath(plan.runId);
2005
- console.log(`[IntegrationAgent] Shadow build round ${plan.integrationRounds}/${MAX_INTEGRATION_ROUNDS} in ${shadowPath}`);
2006
- log2(producerId, planId, `File migration is done. Next: install dependencies in the shadow workspace (npm install). This can take several minutes \u2014 counters staying at 0 is expected.`);
1902
+ console.log(
1903
+ `[IntegrationAgent] Shadow build round ${plan.integrationRounds}/${MAX_INTEGRATION_ROUNDS} in ${shadowPath}`
1904
+ );
1905
+ log2(
1906
+ producerId,
1907
+ planId,
1908
+ `File migration is done. Next: install dependencies in the shadow workspace (npm install). This can take several minutes \u2014 counters staying at 0 is expected.`
1909
+ );
2007
1910
  console.log(`[IntegrationAgent] npm install starting in ${shadowPath}`);
2008
1911
  let lastInstallBeat = Date.now();
2009
- const install = await runInShadowWorkspace(shadowPath, "npm install --no-fund --no-audit", (chunk) => {
2010
- process.stdout.write(chunk);
2011
- if (Date.now() - lastInstallBeat >= 15e3) {
2012
- lastInstallBeat = Date.now();
2013
- log2(producerId, planId, "npm install is still running in the shadow workspace. Wait \u2014 this is not finished.");
1912
+ const install = await runInShadowWorkspace(
1913
+ shadowPath,
1914
+ "npm install --no-fund --no-audit",
1915
+ (chunk) => {
1916
+ process.stdout.write(chunk);
1917
+ if (Date.now() - lastInstallBeat >= 15e3) {
1918
+ lastInstallBeat = Date.now();
1919
+ log2(
1920
+ producerId,
1921
+ planId,
1922
+ "npm install is still running in the shadow workspace. Wait \u2014 this is not finished."
1923
+ );
1924
+ }
2014
1925
  }
2015
- });
1926
+ );
2016
1927
  if (!install.ok) {
2017
- console.warn("[IntegrationAgent] npm install failed in shadow:", install.output.slice(0, 2e3));
2018
- log2(producerId, planId, "npm install failed in the shadow workspace. Continuing to build anyway.", "warning");
2019
- } else {
2020
- log2(producerId, planId, "npm install finished. Next: npm run build in the shadow workspace.");
1928
+ console.warn(
1929
+ "[IntegrationAgent] npm install failed in shadow:",
1930
+ install.output.slice(0, 2e3)
1931
+ );
1932
+ log2(
1933
+ producerId,
1934
+ planId,
1935
+ "npm install failed in the shadow workspace. Halting build to avoid noisy compile errors.",
1936
+ "error"
1937
+ );
1938
+ if (lastRound) {
1939
+ await failMigration(
1940
+ producerId,
1941
+ planId,
1942
+ `npm install failed in the shadow workspace after ${MAX_INTEGRATION_ROUNDS} integration rounds. The migration is not successful.`
1943
+ );
1944
+ return;
1945
+ }
1946
+ const path12 = await import("path");
1947
+ const pkgPath = path12.join(shadowPath, "package.json");
1948
+ await requeueBrokenFiles(
1949
+ producerId,
1950
+ planId,
1951
+ [pkgPath],
1952
+ [`npm install failed in shadow workspace:
1953
+ ${install.output.slice(0, 2500)}`]
1954
+ );
1955
+ return;
2021
1956
  }
1957
+ log2(
1958
+ producerId,
1959
+ planId,
1960
+ "npm install finished. Next: npm run build in the shadow workspace."
1961
+ );
2022
1962
  let lastBuildBeat = Date.now();
2023
- let build = await runInShadowWorkspace(shadowPath, "npm run build", (chunk) => {
2024
- process.stdout.write(chunk);
2025
- if (Date.now() - lastBuildBeat >= 15e3) {
2026
- lastBuildBeat = Date.now();
2027
- log2(producerId, planId, "npm run build is still running in the shadow workspace. Wait \u2014 this is not finished.");
1963
+ let build = await runInShadowWorkspace(
1964
+ shadowPath,
1965
+ "npm run build",
1966
+ (chunk) => {
1967
+ process.stdout.write(chunk);
1968
+ if (Date.now() - lastBuildBeat >= 15e3) {
1969
+ lastBuildBeat = Date.now();
1970
+ log2(
1971
+ producerId,
1972
+ planId,
1973
+ "npm run build is still running in the shadow workspace. Wait \u2014 this is not finished."
1974
+ );
1975
+ }
2028
1976
  }
2029
- });
1977
+ );
2030
1978
  if (!build.ok) {
2031
- log2(producerId, planId, "Shadow build failed. IntegrationAgent will attempt local repairs before requeueing files.");
1979
+ log2(
1980
+ producerId,
1981
+ planId,
1982
+ "Shadow build failed. IntegrationAgent will attempt local repairs before requeueing files."
1983
+ );
2032
1984
  const shadowTools = [
2033
1985
  createReadFileTool2(shadowPath),
2034
1986
  createWriteFileTool2(shadowPath),
@@ -2038,7 +1990,7 @@ var integrationProcessor = {
2038
1990
  const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
2039
1991
  await new Promise((originalResolve) => {
2040
1992
  let isDone = false;
2041
- const resolve5 = () => {
1993
+ const resolve4 = () => {
2042
1994
  if (!isDone) {
2043
1995
  isDone = true;
2044
1996
  originalResolve();
@@ -2063,32 +2015,51 @@ Build output:
2063
2015
  ${excerpt}
2064
2016
 
2065
2017
  When you have applied fixes (or cannot fix further), stop. A deterministic rebuild will run after you finish.`;
2066
- Promise.resolve(runLoop(tempAgent.getId(), prompt, {
2067
- model: modelToUse,
2068
- context: tempAgent.getMemory().getContext(),
2069
- tools: shadowTools
2070
- })).catch((error) => {
2018
+ Promise.resolve(
2019
+ runLoop(tempAgent.getId(), prompt, {
2020
+ model: modelToUse,
2021
+ context: tempAgent.getMemory().getContext(),
2022
+ tools: shadowTools
2023
+ })
2024
+ ).catch((error) => {
2071
2025
  console.error("[IntegrationAgent] runLoop error:", error);
2072
2026
  leave(tempAgent);
2073
- resolve5();
2027
+ resolve4();
2074
2028
  });
2075
2029
  setTimeout(() => {
2076
2030
  if (!isDone) {
2077
- console.warn("[IntegrationAgent] Repair loop timed out; continuing with deterministic rebuild.");
2031
+ console.warn(
2032
+ "[IntegrationAgent] Repair loop timed out; continuing with deterministic rebuild."
2033
+ );
2078
2034
  leave(tempAgent);
2079
- resolve5();
2035
+ resolve4();
2080
2036
  }
2081
2037
  }, REPAIR_TIMEOUT_MS);
2082
2038
  });
2083
2039
  build = await runInShadowWorkspace(shadowPath, "npm run build");
2084
2040
  }
2085
- const pluginCtx = { source: plan.profile.source, target: plan.profile.target };
2086
- const catalogEntry = findMigrationCatalogEntry2(plan.profile.source, plan.profile.target);
2087
- const structureIssues = collectShadowIssues(shadowPath, pluginCtx, catalogEntry?.layer);
2041
+ const pluginCtx = {
2042
+ source: plan.profile.source,
2043
+ target: plan.profile.target
2044
+ };
2045
+ const catalogEntry = resolveMigrationCatalog2(
2046
+ plan.profile.source,
2047
+ plan.profile.target
2048
+ );
2049
+ const structureIssues = collectShadowIssues(
2050
+ shadowPath,
2051
+ pluginCtx,
2052
+ catalogEntry?.layer
2053
+ );
2088
2054
  const buildFailed = !build.ok || !install.ok;
2089
2055
  const verifierFailed = structureIssues.length > 0;
2090
2056
  if (!buildFailed && !verifierFailed) {
2091
- await completeMigration(producerId, planId, "Shadow build succeeded. Reporter is writing MIGRATION.md next. Then Apply copies the result into your project.", "info");
2057
+ await completeMigration(
2058
+ producerId,
2059
+ planId,
2060
+ "Shadow build succeeded. Reporter is writing MIGRATION.md next. Then Apply copies the result into your project.",
2061
+ "info"
2062
+ );
2092
2063
  return;
2093
2064
  }
2094
2065
  const implicated = parseImplicatedFiles(
@@ -2115,7 +2086,12 @@ ${build.output.slice(0, 2500)}`]
2115
2086
  buildFailed ? "npm run build (or npm install) failed. Assigning repair tasks to Worker \u2014 the run is not finished." : `Shadow compile may succeed while the app is still wrong: ${structureIssues.length} verifier issue(s). Assigning repair tasks.`,
2116
2087
  "warning"
2117
2088
  );
2118
- await requeueBrokenFiles(producerId, planId, repairFiles, errors.length > 0 ? errors : ["Shadow verification failed."]);
2089
+ await requeueBrokenFiles(
2090
+ producerId,
2091
+ planId,
2092
+ repairFiles,
2093
+ errors.length > 0 ? errors : ["Shadow verification failed."]
2094
+ );
2119
2095
  return;
2120
2096
  }
2121
2097
  await failMigration(
@@ -2125,7 +2101,11 @@ ${build.output.slice(0, 2500)}`]
2125
2101
  );
2126
2102
  } catch (error) {
2127
2103
  console.error(`[IntegrationAgent] Error:`, error);
2128
- await failMigration(producerId, planId, "Integration Phase encountered an error. The migration is not successful.");
2104
+ await failMigration(
2105
+ producerId,
2106
+ planId,
2107
+ "Integration Phase encountered an error. The migration is not successful."
2108
+ );
2129
2109
  } finally {
2130
2110
  resolveRuntime().state.integrationLocks.delete(planId);
2131
2111
  }
@@ -2781,8 +2761,9 @@ var MigrationRunner = class {
2781
2761
  */
2782
2762
  async startMigration(request) {
2783
2763
  this.ensureInitialized();
2784
- await this.store.reset();
2785
- this.workspace.cleanupOldRuns(3);
2764
+ const existing = await this.store.getAllPlans();
2765
+ const protectedRunIds = existing.filter((p) => p.appliedAt).map((p) => p.runId);
2766
+ this.workspace.cleanupOldRuns(3, protectedRunIds);
2786
2767
  const runId = `run_${Date.now()}`;
2787
2768
  const shadowPath = this.workspace.cloneDirectory(request.targetPath, runId);
2788
2769
  const planId = `plan_${randomUUID()}`;
@@ -2791,6 +2772,7 @@ var MigrationRunner = class {
2791
2772
  runId,
2792
2773
  profile: { source: request.from, target: request.to },
2793
2774
  targetPath: request.targetPath,
2775
+ phase: "files",
2794
2776
  tasks: [
2795
2777
  { filePath: "system:package_manager", status: "pending" }
2796
2778
  ],
@@ -2889,9 +2871,18 @@ function bootstrapMetamorph(repository, tools = []) {
2889
2871
  }
2890
2872
 
2891
2873
  // src/index.ts
2874
+ function logStartupContext() {
2875
+ const isDev = process.env.METAMORPH_DEV === "1";
2876
+ console.log(chalk.gray(`[Metamorph] CWD: ${process.cwd()}`));
2877
+ console.log(chalk.gray(`[Metamorph] Binary: ${import.meta.url}`));
2878
+ if (!isDev && import.meta.url.includes("/src/")) {
2879
+ console.log(chalk.yellow(`[Metamorph] Notice: Running directly from source without METAMORPH_DEV=1.`));
2880
+ }
2881
+ }
2892
2882
  var program = new Command();
2893
- program.name("metamorph").description("AI-powered technology migration tool using Mozaik Agents").version("1.0.0");
2883
+ program.name("metamorph").description("AI-powered technology migration tool using Mozaik Agents").version("2.1.1");
2894
2884
  program.command("run [path]").description("Run a migration on the specified directory").option("--from <source>", "Source framework (e.g. express)").option("--to <target>", "Target framework (e.g. fastify)").action(async (targetPathArg, options) => {
2885
+ logStartupContext();
2895
2886
  let targetPath = targetPathArg || ".";
2896
2887
  const { select, input } = await import("@inquirer/prompts");
2897
2888
  let from = options.from;
@@ -2975,11 +2966,41 @@ program.command("run [path]").description("Run a migration on the specified dire
2975
2966
  spinner.succeed(`Migration dispatched! Plan: ${result.planId}`);
2976
2967
  console.log(chalk.gray(`Shadow: ${result.shadowPath}`));
2977
2968
  console.log(chalk.blue(`
2978
- \u23F3 Agents are now working...`));
2979
- await new Promise((resolve5) => setTimeout(resolve5, 2e4));
2980
- console.log(chalk.green(`
2981
- \u2705 Migration simulation finished.`));
2982
- process.exit(0);
2969
+ \u23F3 Swarm is executing migration in shadow workspace...`));
2970
+ const waitSpinner = ora("Waiting for swarm to complete migration...").start();
2971
+ const startTime = Date.now();
2972
+ const MAX_TIMEOUT_MS = 30 * 60 * 1e3;
2973
+ while (true) {
2974
+ if (Date.now() - startTime >= MAX_TIMEOUT_MS) {
2975
+ waitSpinner.fail(chalk.red(`Migration timed out after 30 minutes.`));
2976
+ process.exit(1);
2977
+ }
2978
+ await new Promise((r) => setTimeout(r, 2e3));
2979
+ const plan = await store.getPlan(result.planId);
2980
+ if (!plan) continue;
2981
+ const terminalTasks = plan.tasks.filter((t) => t.status === "completed" || t.status === "failed");
2982
+ const inProgressTasks = plan.tasks.filter((t) => t.status === "in_progress");
2983
+ const pendingTasks = plan.tasks.filter((t) => t.status === "pending");
2984
+ waitSpinner.text = `Phase: ${plan.phase || "files"} | Tasks: ${terminalTasks.length}/${plan.tasks.length} terminal (${inProgressTasks.length} in progress, ${pendingTasks.length} pending)`;
2985
+ if (plan.outcome === "success") {
2986
+ waitSpinner.succeed(chalk.green(`
2987
+ \u2705 Migration finished successfully in shadow workspace!`));
2988
+ console.log(chalk.white(`
2989
+ Next steps:`));
2990
+ console.log(chalk.gray(` 1. Review changes in shadow workspace: `) + chalk.cyan(result.shadowPath));
2991
+ console.log(chalk.gray(` 2. Apply changes to a dedicated Git branch: `) + chalk.cyan(`metamorph apply ${result.runId} ${targetPath}
2992
+ `));
2993
+ process.exit(0);
2994
+ }
2995
+ if (plan.phase === "failed" || plan.outcome === "failed") {
2996
+ waitSpinner.fail(chalk.red(`
2997
+ \u274C Migration failed in shadow workspace.`));
2998
+ console.log(chalk.yellow(`Check .metamorph/shadow/${result.runId}/MIGRATION.md or run 'metamorph ui' for failure details.`));
2999
+ console.log(chalk.gray(`Your original codebase in "${targetPath}" remains completely untouched.
3000
+ `));
3001
+ process.exit(1);
3002
+ }
3003
+ }
2983
3004
  } catch (error) {
2984
3005
  const errorMessage = error instanceof Error ? error.message : String(error);
2985
3006
  spinner.fail(`Migration failed: ${errorMessage}`);
@@ -2988,8 +3009,27 @@ program.command("run [path]").description("Run a migration on the specified dire
2988
3009
  });
2989
3010
  program.command("apply <runId> <targetPath>").description("Apply a completed migration to the target project (requires Git at the project or monorepo root)").action(async (runId, targetPath) => {
2990
3011
  try {
3012
+ const store = new SQLiteStateStore(".metamorph");
3013
+ const plans = await store.getAllPlans();
3014
+ const plan = plans.find((p) => p.runId === runId);
3015
+ if (!plan) {
3016
+ throw new Error(`Plan for run "${runId}" not found in database.`);
3017
+ }
3018
+ if (plan.phase !== "completed" || plan.outcome !== "success") {
3019
+ throw new Error(
3020
+ `Cannot apply migration: plan is not completed successfully (phase: "${plan.phase}", outcome: "${plan.outcome || "none"}"). Only successful migrations can be applied.`
3021
+ );
3022
+ }
3023
+ if (plan.appliedAt) {
3024
+ throw new Error(
3025
+ `Migration for run "${runId}" has already been applied at ${new Date(plan.appliedAt).toISOString()} to branch ${plan.appliedBranch || "unknown"}.`
3026
+ );
3027
+ }
2991
3028
  const integrator = new MigrationIntegrator(new ShadowWorkspace());
2992
3029
  const result = await integrator.applyMigration(runId, targetPath);
3030
+ plan.appliedAt = /* @__PURE__ */ new Date();
3031
+ plan.appliedBranch = result.branch;
3032
+ await store.savePlan(plan);
2993
3033
  console.log(chalk.green(`\u2705 ${result.message}`));
2994
3034
  } catch (error) {
2995
3035
  console.error(chalk.red(`\u274C Failed to apply migration: ${error instanceof Error ? error.message : String(error)}`));
@@ -3052,6 +3092,7 @@ program.command("list").description("List past migration plans").action(async ()
3052
3092
  }
3053
3093
  });
3054
3094
  program.command("ui").description("Start the Metamorph Dashboard API server").option("-p, --port <number>", "Port for the API server", "9876").action(async (options) => {
3095
+ logStartupContext();
3055
3096
  let desiredPort = parseInt(options.port, 10);
3056
3097
  const spinner = ora("Starting Metamorph API server...").start();
3057
3098
  try {