@nikelyh/metamorph 2.1.0 → 2.1.2

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,14 +12,17 @@ import {
12
12
  createRenameFileTool,
13
13
  createRunBuildTool,
14
14
  createWriteFileTool
15
- } from "./chunk-OXXEISZT.js";
15
+ } from "./chunk-APRESSUR.js";
16
16
  import {
17
- findMigrationCatalogEntry,
18
- formatCatalogRules
19
- } from "./chunk-VEAYUIOZ.js";
17
+ formatCatalogRules,
18
+ resolveMigrationCatalog,
19
+ resolvePackageManagerCommands
20
+ } from "./chunk-WLHSZWET.js";
20
21
  import {
21
- detectTechnologies
22
- } from "./chunk-3QN2Q75J.js";
22
+ detectMonorepo,
23
+ inspectManifest,
24
+ inspectProject
25
+ } from "./chunk-XLD7NXYU.js";
23
26
  import {
24
27
  MetamorphState,
25
28
  initializeRuntime,
@@ -33,6 +36,16 @@ import {
33
36
  collectFileHints,
34
37
  registerMigrationPlugin
35
38
  } from "./chunk-O2QLEES3.js";
39
+ import {
40
+ analyzeReactToNextStructure,
41
+ findShadowRoot,
42
+ hasNextAppRoutes,
43
+ isFsdPageComponent,
44
+ isSpaPagesTreeFile,
45
+ isSpaRootComponent,
46
+ listAppRouterFiles,
47
+ nextMigrationFileHint
48
+ } from "./chunk-H2LZEPNI.js";
36
49
 
37
50
  // src/index.ts
38
51
  import { Command } from "commander";
@@ -118,7 +131,7 @@ import {
118
131
  createAgent as createAgent2,
119
132
  SituationSpecification as SituationSpecification2
120
133
  } from "@mozaik-ai/core";
121
- import * as fs4 from "fs";
134
+ import * as fs3 from "fs";
122
135
 
123
136
  // ../application/src/utils/FileTreeBuilder.ts
124
137
  import * as path2 from "path";
@@ -180,312 +193,15 @@ function buildFileTree(filePaths) {
180
193
  }
181
194
 
182
195
  // ../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
196
  import * as fs2 from "fs";
188
197
  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
198
  var MAX_FILES = 8;
483
199
  var MAX_FILE_CHARS = 4e3;
484
200
  var MAX_TOTAL_CHARS = 24e3;
485
201
  function readTruncated(filePath) {
486
202
  try {
487
- if (!fs3.existsSync(filePath) || !fs3.statSync(filePath).isFile()) return null;
488
- const raw = fs3.readFileSync(filePath, "utf-8");
203
+ if (!fs2.existsSync(filePath) || !fs2.statSync(filePath).isFile()) return null;
204
+ const raw = fs2.readFileSync(filePath, "utf-8");
489
205
  if (raw.length <= MAX_FILE_CHARS) return raw;
490
206
  return `${raw.slice(0, MAX_FILE_CHARS)}
491
207
 
@@ -494,18 +210,18 @@ function readTruncated(filePath) {
494
210
  return null;
495
211
  }
496
212
  }
497
- function findSrcRoot2(filePath) {
213
+ function findSrcRoot(filePath) {
498
214
  const normalized = filePath.replace(/\\/g, "/");
499
215
  const idx = normalized.lastIndexOf("/src/");
500
216
  if (idx === -1) return null;
501
217
  return filePath.slice(0, idx + 4);
502
218
  }
503
219
  function resolveImport(fromFile, spec) {
504
- const dir = path4.dirname(fromFile);
220
+ const dir = path3.dirname(fromFile);
505
221
  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);
222
+ const src = findSrcRoot(fromFile);
223
+ return src ? path3.join(src, spec.slice(2)) : path3.resolve(dir, spec);
224
+ })() : path3.resolve(dir, spec);
509
225
  const candidates = [
510
226
  base,
511
227
  `${base}.tsx`,
@@ -514,12 +230,12 @@ function resolveImport(fromFile, spec) {
514
230
  `${base}.js`,
515
231
  `${base}.vue`,
516
232
  `${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")
233
+ path3.join(base, "index.tsx"),
234
+ path3.join(base, "index.ts"),
235
+ path3.join(base, "index.vue"),
236
+ path3.join(base, "index.svelte")
521
237
  ];
522
- return candidates.find((file) => fs3.existsSync(file) && fs3.statSync(file).isFile()) || null;
238
+ return candidates.find((file) => fs2.existsSync(file) && fs2.statSync(file).isFile()) || null;
523
239
  }
524
240
  function localImportSpecs(content) {
525
241
  const specs = [];
@@ -530,8 +246,8 @@ function localImportSpecs(content) {
530
246
  }
531
247
  function listNearbyModules(fromFile, limit) {
532
248
  const shadow = findShadowRoot(fromFile);
533
- const src = findSrcRoot2(fromFile) || (shadow ? path4.join(shadow, "src") : path4.dirname(fromFile));
534
- if (!fs3.existsSync(src)) return [];
249
+ const src = findSrcRoot(fromFile) || (shadow ? path3.join(shadow, "src") : path3.dirname(fromFile));
250
+ if (!fs2.existsSync(src)) return [];
535
251
  const interesting = /(?:Page|page|View|Screen|layout|Layout|main|App|routes|Routes|app\.routes)\.(t|j)sx?$|\.(vue|svelte)$/;
536
252
  const out = [];
537
253
  const stack = [src];
@@ -539,13 +255,13 @@ function listNearbyModules(fromFile, limit) {
539
255
  const dir = stack.pop();
540
256
  let entries;
541
257
  try {
542
- entries = fs3.readdirSync(dir, { withFileTypes: true });
258
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
543
259
  } catch {
544
260
  continue;
545
261
  }
546
262
  for (const entry of entries) {
547
263
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
548
- const full = path4.join(dir, entry.name);
264
+ const full = path3.join(dir, entry.name);
549
265
  if (entry.isDirectory()) stack.push(full);
550
266
  else if (interesting.test(entry.name)) out.push(full);
551
267
  if (out.length >= limit) break;
@@ -575,15 +291,15 @@ ${body}
575
291
  `);
576
292
  }
577
293
  const importSpecs = self ? localImportSpecs(self) : [];
578
- const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !== path4.resolve(filePath)));
294
+ const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !== path3.resolve(filePath)));
579
295
  const unique = [...new Set(resolved)].slice(0, MAX_FILES);
580
296
  for (const dep of unique) {
581
297
  const body = readTruncated(dep);
582
298
  if (body) push(`imported: ${dep}`, body);
583
299
  }
584
- const nearby = listNearbyModules(filePath, 24).filter((file) => path4.resolve(file) !== path4.resolve(filePath));
300
+ const nearby = listNearbyModules(filePath, 24).filter((file) => path3.resolve(file) !== path3.resolve(filePath));
585
301
  if (nearby.length > 0) {
586
- const listing = nearby.map((file) => path4.relative(findShadowRoot(filePath) || path4.dirname(filePath), file)).join("\n");
302
+ const listing = nearby.map((file) => path3.relative(findShadowRoot(filePath) || path3.dirname(filePath), file)).join("\n");
587
303
  sections.push(`### Nearby screen / bootstrap files (paths only \u2014 read before inventing a new UI)
588
304
  ${listing}
589
305
  `);
@@ -594,6 +310,12 @@ Evidence from disk (read this before editing; match existing exports and callbac
594
310
  ${sections.join("\n")}`;
595
311
  }
596
312
 
313
+ // ../application/src/utils/workerCompletion.ts
314
+ var MISSING_AFTER_WORKER = "File missing after worker inference (missing_after_worker)";
315
+ function workerCompletionKind(fileExists) {
316
+ return fileExists ? "migrated" : "failed_missing";
317
+ }
318
+
597
319
  // ../application/src/agents/WorkerAgent.ts
598
320
  var MAX_RETRIES = 2;
599
321
  var ConcurrencyQueue = class {
@@ -648,7 +370,7 @@ var WhenInferenceCompleted = class extends SituationSpecification2 {
648
370
  async function startWorkerLoop(planId, filePath, prompt, participant) {
649
371
  return new Promise(async (originalResolve) => {
650
372
  let isDone = false;
651
- const resolve5 = () => {
373
+ const resolve4 = () => {
652
374
  if (!isDone) {
653
375
  isDone = true;
654
376
  originalResolve();
@@ -658,28 +380,51 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
658
380
  const repository = runtime.state.repository;
659
381
  await repository.updateTaskStatus(planId, filePath, "in_progress");
660
382
  const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
383
+ const { findShadowRoot: findShadowRoot2 } = await import("./NextMigrationHints-BGO2W3K4.js");
384
+ const shadowRoot = findShadowRoot2(filePath);
385
+ let workerTools = participant.getTools();
386
+ if (shadowRoot) {
387
+ const { createShadowTools } = await import("./src-Q3DGKZEQ.js");
388
+ workerTools = createShadowTools(shadowRoot);
389
+ }
661
390
  const tempAgent = createMozaikAgent({
662
391
  name: `Worker-${Date.now()}-${Math.floor(Math.random() * 1e3)}`,
663
392
  capabilities: ["code_refactoring", "inference"],
664
393
  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(),
394
+ tools: workerTools,
666
395
  handlers: [
667
396
  {
668
397
  specification: new WhenInferenceCompleted(),
669
398
  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";
399
+ async apply({ participant: tempParticipant }) {
400
+ if (isDone) {
401
+ return;
402
+ }
403
+ const exists = fs3.existsSync(filePath);
404
+ if (workerCompletionKind(exists) === "failed_missing") {
405
+ console.warn(`[WorkerAgent:${tempParticipant.getId()}] File not found on disk after inference: ${filePath}`);
406
+ await repository.updateTaskStatus(planId, filePath, "failed", MISSING_AFTER_WORKER);
407
+ sendEvent(
408
+ {
409
+ type: "file.failed" /* FILE_FAILED */,
410
+ producerId: tempParticipant.getId(),
411
+ occurredAt: /* @__PURE__ */ new Date(),
412
+ payload: { planId, filePath, reason: MISSING_AFTER_WORKER }
413
+ },
414
+ tempParticipant.getId()
415
+ );
416
+ leave(tempParticipant);
417
+ resolve4();
418
+ return;
419
+ }
420
+ let fileContent = "";
673
421
  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
- }
422
+ fileContent = fs3.readFileSync(filePath, "utf-8");
423
+ console.log(`[WorkerAgent:${tempParticipant.getId()}] Read ${fileContent.length} chars from ${filePath}`);
680
424
  } catch (e) {
681
425
  console.error(`[WorkerAgent:${tempParticipant.getId()}] Error reading file:`, e);
682
426
  }
427
+ console.log(`[WorkerAgent:${tempParticipant.getId()}] Inference completed. Emitting FILE_MIGRATED.`);
683
428
  await repository.updateTaskStatus(planId, filePath, "in_progress");
684
429
  sendEvent(
685
430
  {
@@ -695,7 +440,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
695
440
  tempParticipant.getId()
696
441
  );
697
442
  leave(tempParticipant);
698
- resolve5();
443
+ resolve4();
699
444
  }
700
445
  }
701
446
  }
@@ -712,18 +457,18 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
712
457
  });
713
458
  setTimeout(async () => {
714
459
  if (!isDone) {
715
- console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out.`);
716
- await repository.updateTaskStatus(planId, filePath, "failed", "Inference timed out");
460
+ console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out after 120s.`);
461
+ await repository.updateTaskStatus(planId, filePath, "failed", "Inference timed out after 120s");
717
462
  sendEvent({
718
463
  type: "file.failed" /* FILE_FAILED */,
719
464
  producerId: tempAgent.getId(),
720
465
  occurredAt: /* @__PURE__ */ new Date(),
721
- payload: { planId, filePath, reason: "Inference timed out" }
466
+ payload: { planId, filePath, reason: "Inference timed out after 120s" }
722
467
  }, tempAgent.getId());
723
468
  leave(tempAgent);
724
- resolve5();
469
+ resolve4();
725
470
  }
726
- }, 45e3);
471
+ }, 12e4);
727
472
  } catch (error) {
728
473
  console.error(`[WorkerAgent:${tempAgent.getId()}] Sync error:`, error);
729
474
  if (!isDone) {
@@ -736,7 +481,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
736
481
  payload: { planId, filePath, reason: errorMessage || "Sync error" }
737
482
  }, tempAgent.getId());
738
483
  leave(tempAgent);
739
- resolve5();
484
+ resolve4();
740
485
  }
741
486
  }
742
487
  });
@@ -752,7 +497,7 @@ var workOnFileProcessor = {
752
497
  if (plan) {
753
498
  prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
754
499
  `;
755
- const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
500
+ const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
756
501
  if (catalogEntry) {
757
502
  prompt += `
758
503
  Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
@@ -858,7 +603,7 @@ ${payload.errors.join("\n")}
858
603
  if (plan) {
859
604
  prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
860
605
  `;
861
- const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
606
+ const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
862
607
  if (catalogEntry) {
863
608
  prompt += `
864
609
  Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
@@ -934,6 +679,115 @@ import {
934
679
  SituationSpecification as SituationSpecification3
935
680
  } from "@mozaik-ai/core";
936
681
  import * as path5 from "path";
682
+
683
+ // ../application/src/utils/classifyMissingFile.ts
684
+ import * as fs4 from "fs";
685
+ import * as path4 from "path";
686
+ var NEXT_CONFIG_NAMES = [
687
+ "next.config.js",
688
+ "next.config.mjs",
689
+ "next.config.ts",
690
+ "next.config.cjs"
691
+ ];
692
+ var CONFIG_FILE_PATTERN = /[/\\](vite\.config|craco\.config|webpack\.config)\.(js|ts|mjs|cjs)$/i;
693
+ function classifyMissingFile(input) {
694
+ const { filePath, target } = input;
695
+ const fileExists = input.fileExists || fs4.existsSync;
696
+ const shadowRoot = input.shadowRoot !== void 0 ? input.shadowRoot : findShadowRoot(filePath);
697
+ const normalizedTarget = target.toLowerCase();
698
+ const normalizedPath = filePath.replace(/\\/g, "/");
699
+ if (CONFIG_FILE_PATTERN.test(filePath)) {
700
+ if (normalizedTarget === "next" && shadowRoot) {
701
+ const hasNextConfig = NEXT_CONFIG_NAMES.some(
702
+ (cfg) => fileExists(path4.join(shadowRoot, cfg))
703
+ );
704
+ if (hasNextConfig) {
705
+ return {
706
+ verdict: "approved",
707
+ reason: `Config file removed because a Next.js configuration exists in shadow workspace.`
708
+ };
709
+ }
710
+ return {
711
+ verdict: "rejected",
712
+ errors: [
713
+ `Configuration file was removed, but no Next.js configuration (next.config.js/mjs/ts) was found in the project root.`
714
+ ]
715
+ };
716
+ }
717
+ }
718
+ if (/[/\\]index\.html$/i.test(filePath)) {
719
+ if (normalizedTarget === "next" && shadowRoot) {
720
+ const layoutCandidates = [
721
+ path4.join(shadowRoot, "src", "app", "layout.tsx"),
722
+ path4.join(shadowRoot, "src", "app", "layout.jsx"),
723
+ path4.join(shadowRoot, "src", "app", "layout.js"),
724
+ path4.join(shadowRoot, "app", "layout.tsx"),
725
+ path4.join(shadowRoot, "app", "layout.jsx"),
726
+ path4.join(shadowRoot, "app", "layout.js"),
727
+ path4.join(shadowRoot, "src", "pages", "_document.tsx"),
728
+ path4.join(shadowRoot, "pages", "_document.tsx")
729
+ ];
730
+ const hasLayout = layoutCandidates.some((candidate) => fileExists(candidate));
731
+ if (hasLayout) {
732
+ return {
733
+ verdict: "approved",
734
+ reason: `index.html was removed because Next.js root layout or _document exists.`
735
+ };
736
+ }
737
+ return {
738
+ verdict: "rejected",
739
+ errors: [
740
+ `index.html was removed, but no Next.js root layout (app/layout.tsx) or _document was created to provide the HTML structure.`
741
+ ]
742
+ };
743
+ }
744
+ }
745
+ if (isSpaRootComponent(filePath)) {
746
+ if (normalizedTarget === "next") {
747
+ const hasRoutes = shadowRoot ? hasNextAppRoutes(shadowRoot) : false;
748
+ if (hasRoutes) {
749
+ return {
750
+ verdict: "approved",
751
+ reason: `SPA root component was removed because Next.js App Router routes exist.`
752
+ };
753
+ }
754
+ return {
755
+ verdict: "rejected",
756
+ errors: [
757
+ `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.`
758
+ ]
759
+ };
760
+ }
761
+ }
762
+ if (isFsdPageComponent(filePath) || isSpaPagesTreeFile(filePath)) {
763
+ if (normalizedTarget === "next") {
764
+ const viewsReplacement = filePath.replace(/([/\\])src\1pages\1/, "$1src$1views$1");
765
+ const screensReplacement = filePath.replace(/([/\\])src\1pages\1/, "$1src$1screens$1");
766
+ const compReplacement = filePath.replace(/([/\\])src\1pages\1/, "$1src$1components$1pages$1");
767
+ if (fileExists(viewsReplacement) || fileExists(screensReplacement) || fileExists(compReplacement)) {
768
+ return {
769
+ verdict: "approved",
770
+ reason: `File moved out of src/pages into a non-reserved directory for Next.js.`
771
+ };
772
+ }
773
+ return {
774
+ verdict: "rejected",
775
+ errors: [
776
+ `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.`
777
+ ]
778
+ };
779
+ }
780
+ }
781
+ const fileName = path4.basename(filePath);
782
+ return {
783
+ verdict: "rejected",
784
+ errors: [
785
+ `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.`
786
+ ]
787
+ };
788
+ }
789
+
790
+ // ../application/src/agents/ReviewerAgent.ts
937
791
  var ConcurrencyQueue2 = class {
938
792
  constructor(concurrencyLimit) {
939
793
  this.concurrencyLimit = concurrencyLimit;
@@ -970,15 +824,9 @@ var WhenFileMigrated = class extends SituationSpecification3 {
970
824
  };
971
825
  var WhenReviewCompleted = class extends SituationSpecification3 {
972
826
  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
827
  return event.type === "model.answer" && event.producerId === participant.getId();
975
828
  }
976
829
  };
977
- var WhenAnythingHappens = class extends SituationSpecification3 {
978
- isSatisfiedBy() {
979
- return true;
980
- }
981
- };
982
830
  var reviewFileProcessor = {
983
831
  async apply({ event, participant }) {
984
832
  const payload = event.payload;
@@ -986,10 +834,9 @@ var reviewFileProcessor = {
986
834
  reviewerQueue.enqueue(async () => {
987
835
  return new Promise(async (originalResolve) => {
988
836
  let isDone = false;
989
- const resolve5 = () => {
837
+ const resolve4 = () => {
990
838
  if (!isDone) {
991
839
  isDone = true;
992
- console.log(`[DEBUG:ReviewerAgent] Promise resolved for ${payload.filePath}`);
993
840
  originalResolve();
994
841
  }
995
842
  };
@@ -1003,33 +850,22 @@ var reviewFileProcessor = {
1003
850
  tools: [],
1004
851
  // No tools to avoid Gemini thought_signature crash
1005
852
  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
853
  {
1016
854
  specification: new WhenReviewCompleted(),
1017
855
  processor: {
1018
856
  async apply({ event: event2, participant: tempParticipant }) {
1019
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] model.answer RECEIVED!`);
857
+ if (isDone) {
858
+ return;
859
+ }
1020
860
  const answerItem = event2.payload.answer;
1021
861
  const answerText = answerItem?.content?.text || answerItem?.text || null;
1022
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] answerText:`, answerText);
1023
862
  let reviewResult = null;
1024
863
  if (answerText) {
1025
864
  try {
1026
865
  reviewResult = JSON.parse(answerText);
1027
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Parsed result:`, JSON.stringify(reviewResult));
1028
866
  } catch (e) {
1029
867
  console.error(`[ReviewerAgent:${reviewerId}] Failed to parse JSON:`, e);
1030
868
  }
1031
- } else {
1032
- console.warn(`[DEBUG:ReviewerAgent:${reviewerId}] answerItem has no extractable text.`);
1033
869
  }
1034
870
  if (reviewResult) {
1035
871
  const participantId = tempParticipant.getId();
@@ -1066,28 +902,39 @@ var reviewFileProcessor = {
1066
902
  }
1067
903
  }
1068
904
  leave(tempParticipant);
1069
- resolve5();
905
+ resolve4();
1070
906
  }
1071
907
  }
1072
908
  }
1073
909
  ]
1074
910
  });
1075
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent created with id: ${tempAgent.getId()}`);
1076
911
  join(tempAgent);
1077
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent joined the runtime`);
1078
912
  const runtime = resolveRuntime();
1079
913
  const { existsSync: existsSync8 } = await import("fs");
1080
914
  if (!existsSync8(payload.filePath)) {
1081
915
  const { sendEvent: sendEvent4 } = await import("./runtime-L7VOQROE.js");
1082
916
  const planForMissing = await runtime.state.repository.getPlan(payload.planId);
1083
- const target = planForMissing?.profile.target;
917
+ const sourceFramework = planForMissing?.profile.source || "react";
918
+ const targetFramework = planForMissing?.profile.target || "next";
1084
919
  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.`);
920
+ const classification = classifyMissingFile({
921
+ filePath: payload.filePath,
922
+ source: sourceFramework,
923
+ target: targetFramework,
924
+ shadowRoot,
925
+ fileExists: existsSync8
926
+ });
927
+ if (classification.verdict === "approved") {
928
+ console.log(`[ReviewerAgent:${reviewerId}] Missing ${payload.filePath} approved: ${classification.reason}`);
929
+ await runtime.state.repository.updateTaskStatus(payload.planId, payload.filePath, "completed");
930
+ sendEvent4({
931
+ type: "file.reviewed" /* FILE_REVIEWED */,
932
+ producerId: tempAgent.getId(),
933
+ occurredAt: /* @__PURE__ */ new Date(),
934
+ payload: { planId: payload.planId, filePath: payload.filePath }
935
+ }, tempAgent.getId());
936
+ } else {
937
+ console.log(`[ReviewerAgent:${reviewerId}] Missing ${payload.filePath} rejected:`, classification.errors);
1091
938
  sendEvent4({
1092
939
  type: "file.rejected" /* FILE_REJECTED */,
1093
940
  producerId: tempAgent.getId(),
@@ -1095,27 +942,12 @@ var reviewFileProcessor = {
1095
942
  payload: {
1096
943
  planId: payload.planId,
1097
944
  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
- ]
945
+ errors: classification.errors || ["File does not exist and has no demonstrable replacement"]
1103
946
  }
1104
947
  }, tempAgent.getId());
1105
- leave(tempAgent);
1106
- resolve5();
1107
- return;
1108
948
  }
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
949
  leave(tempAgent);
1118
- resolve5();
950
+ resolve4();
1119
951
  return;
1120
952
  }
1121
953
  if (payload.filePath.endsWith(".ts") || payload.filePath.endsWith(".tsx")) {
@@ -1139,7 +971,7 @@ var reviewFileProcessor = {
1139
971
  }
1140
972
  }, tempAgent.getId());
1141
973
  leave(tempAgent);
1142
- resolve5();
974
+ resolve4();
1143
975
  return;
1144
976
  }
1145
977
  } catch (e) {
@@ -1164,7 +996,7 @@ var reviewFileProcessor = {
1164
996
  payload: { planId: payload.planId, filePath: payload.filePath, errors }
1165
997
  }, tempAgent.getId());
1166
998
  leave(tempAgent);
1167
- resolve5();
999
+ resolve4();
1168
1000
  return;
1169
1001
  }
1170
1002
  }
@@ -1174,7 +1006,7 @@ var reviewFileProcessor = {
1174
1006
  if (plan) {
1175
1007
  prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
1176
1008
  `;
1177
- const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
1009
+ const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
1178
1010
  if (catalogEntry) {
1179
1011
  prompt += `
1180
1012
  Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair; apply only if relevant to this file):
@@ -1196,7 +1028,6 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
1196
1028
  const modelToUse = process.env.METAMORPH_REVIEWER_MODEL || process.env.METAMORPH_MODEL || "gpt-5.4";
1197
1029
  console.log(`[ReviewerAgent:${reviewerId}] Starting inference loop for ${payload.filePath} with model ${modelToUse}...`);
1198
1030
  try {
1199
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Calling runLoop with agentId: ${tempAgent.getId()}`);
1200
1031
  runLoop(tempAgent.getId(), prompt, {
1201
1032
  model: modelToUse,
1202
1033
  context: tempAgent.getMemory().getContext(),
@@ -1214,23 +1045,22 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
1214
1045
  strict: true
1215
1046
  }
1216
1047
  });
1217
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] runLoop called successfully (fire-and-forget)`);
1218
1048
  setTimeout(async () => {
1219
1049
  if (!isDone) {
1220
- console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 45s for ${payload.filePath}. Model never responded.`);
1050
+ console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 120s for ${payload.filePath}. Model never responded.`);
1221
1051
  const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
1222
1052
  const runtime2 = resolveRuntime2();
1223
- await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after 45s");
1053
+ await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after 120s");
1224
1054
  sendEvent3({
1225
1055
  type: "file.failed" /* FILE_FAILED */,
1226
1056
  producerId: tempAgent.getId(),
1227
1057
  occurredAt: /* @__PURE__ */ new Date(),
1228
- payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after 45s" }
1058
+ payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after 120s" }
1229
1059
  }, tempAgent.getId());
1230
1060
  leave(tempAgent);
1231
- resolve5();
1061
+ resolve4();
1232
1062
  }
1233
- }, 45e3);
1063
+ }, 12e4);
1234
1064
  } catch (error) {
1235
1065
  console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
1236
1066
  if (!isDone) {
@@ -1245,7 +1075,7 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
1245
1075
  payload: { planId: payload.planId, filePath: payload.filePath, reason: errorMessage || "Sync error" }
1246
1076
  }, tempAgent.getId());
1247
1077
  leave(tempAgent);
1248
- resolve5();
1078
+ resolve4();
1249
1079
  }
1250
1080
  }
1251
1081
  });
@@ -1312,7 +1142,7 @@ var managePackagesProcessor = {
1312
1142
  await emitPackagesReady("failed");
1313
1143
  return;
1314
1144
  }
1315
- const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
1145
+ const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
1316
1146
  if (!catalogEntry) {
1317
1147
  console.log(`[PackageManagerAgent] No catalog entry found for ${plan.profile.source} -> ${plan.profile.target}`);
1318
1148
  await emitPackagesReady("failed");
@@ -1389,7 +1219,7 @@ var managePackagesProcessor = {
1389
1219
  type: "system.log" /* SYSTEM_LOG */,
1390
1220
  producerId: participant.getId(),
1391
1221
  occurredAt: /* @__PURE__ */ new Date(),
1392
- payload: { planId: payload.planId, message: "PackageManager updated package.json. IntegrationAgent will run npm install in the shadow workspace \u2014 the original project is untouched until you Apply.", level: "info" }
1222
+ payload: { planId: payload.planId, message: "Dependencies updated in package.json: target framework libraries and build scripts configured.", level: "info" }
1393
1223
  }, participant.getId());
1394
1224
  } catch (err) {
1395
1225
  console.error(`[PackageManagerAgent] Error editing package.json:`, err);
@@ -1490,7 +1320,7 @@ var startWatchdogProcessor = {
1490
1320
  occurredAt: /* @__PURE__ */ new Date(),
1491
1321
  payload: {
1492
1322
  planId: p.planId,
1493
- message: "Coordinator watchdog is on: if file work settles and Integration never starts, it will kick within 8s. No extra model calls.",
1323
+ message: "Swarm coordinator active: orchestrating agent tasks and monitoring workflow progress.",
1494
1324
  level: "info"
1495
1325
  }
1496
1326
  }, producerId);
@@ -1509,10 +1339,26 @@ var startWatchdogProcessor = {
1509
1339
  occurredAt: /* @__PURE__ */ new Date(),
1510
1340
  payload: {
1511
1341
  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"
1342
+ message: "Migration session timed out after 30 minutes: task completion could not be verified.",
1343
+ level: "error"
1514
1344
  }
1515
1345
  }, producerId);
1346
+ const currentPlan = await runtime.state.repository.getPlan(p.planId);
1347
+ if (currentPlan && currentPlan.phase !== "completed" && currentPlan.phase !== "failed") {
1348
+ currentPlan.phase = "failed";
1349
+ currentPlan.outcome = "failed";
1350
+ await runtime.state.repository.savePlan(currentPlan);
1351
+ sendEvent({
1352
+ type: "migration.completed" /* MIGRATION_COMPLETED */,
1353
+ producerId,
1354
+ occurredAt: /* @__PURE__ */ new Date(),
1355
+ payload: {
1356
+ planId: p.planId,
1357
+ outcome: "failed",
1358
+ reason: "Migration timed out after 30 minutes watchdog budget."
1359
+ }
1360
+ }, producerId);
1361
+ }
1516
1362
  return;
1517
1363
  }
1518
1364
  await emitIntegrationIfReady(producerId, p.planId);
@@ -1552,7 +1398,7 @@ function log(participantId, planId, message, level = "info") {
1552
1398
  }
1553
1399
  async function shadowPathFor(plan, journal) {
1554
1400
  if (journal?.shadowPath) return journal.shadowPath;
1555
- const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-2UXWWNPF.js");
1401
+ const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-Q3DGKZEQ.js");
1556
1402
  return new ShadowWorkspace2().getShadowPath(plan.runId);
1557
1403
  }
1558
1404
  function fallbackReport(params) {
@@ -1561,10 +1407,15 @@ function fallbackReport(params) {
1561
1407
  const rejected = params.notes.filter((n) => n.outcome === "rejected" || n.outcome === "fatal");
1562
1408
  const fileLines = params.tasks.filter((t) => !t.filePath.startsWith("system:")).slice(0, 40).map((t) => `- \`${t.filePath}\` \u2014 ${t.status}${t.error ? ` (${t.error})` : ""}`).join("\n");
1563
1409
  const riskLines = rejected.slice(0, 12).map((n) => `- \`${n.filePath}\`: ${n.detail || n.outcome}`).join("\n");
1410
+ const pm = params.packageManager || "npm";
1411
+ const devCmd = pm === "yarn" ? "yarn dev" : `${pm} run dev`;
1412
+ const buildCmd = pm === "yarn" ? "yarn build" : `${pm} run build`;
1564
1413
  return `# What changed
1565
1414
 
1566
1415
  Metamorph migrated this project from **${params.source}** to **${params.target}**.
1567
- ${params.phase === "failed" ? "\n**Status:** shadow `npm run build` (or catalog checks) did not pass. Treat this as an incomplete migration.\n" : ""}
1416
+ ${params.phase === "failed" ? `
1417
+ **Status:** shadow \`${buildCmd}\` (or catalog checks) did not pass. Treat this as an incomplete migration.
1418
+ ` : ""}
1568
1419
  Your original files were not overwritten until you click **Apply Migration**. After apply, the result lives on a git branch named like \`metamorph/${params.runId}\`.
1569
1420
 
1570
1421
  ## How to run it
@@ -1572,8 +1423,8 @@ Your original files were not overwritten until you click **Apply Migration**. Af
1572
1423
  1. Apply the migration in the dashboard (or keep working in the shadow workspace).
1573
1424
  2. From the project folder:
1574
1425
  \`\`\`bash
1575
- npm install
1576
- npm run dev
1426
+ ${pm} install
1427
+ ${devCmd}
1577
1428
  \`\`\`
1578
1429
  3. Open the URL the framework prints (\`next dev\` \u2192 port 3000, Vite \u2192 5173).
1579
1430
 
@@ -1705,6 +1556,8 @@ var fatalProcessor = {
1705
1556
  };
1706
1557
  function draftPrompt(params) {
1707
1558
  const notable = params.notes.filter((n) => n.outcome === "rejected" || n.outcome === "fatal").slice(-15).map((n) => `${n.filePath}: ${n.detail || n.outcome}`).join("\n");
1559
+ const pm = params.plan.packageManager || "npm";
1560
+ const devCmd = pm === "yarn" ? "yarn dev" : `${pm} run dev`;
1708
1561
  return `Write a user-facing migration briefing to this exact path:
1709
1562
  ${params.reportAbs}
1710
1563
 
@@ -1717,10 +1570,10 @@ Run id: ${params.plan.runId}
1717
1570
 
1718
1571
  Include these sections, in this order, with markdown headings:
1719
1572
  1. What changed \u2014 1-2 short paragraphs in plain language
1720
- 2. How to run it \u2014 concrete commands (npm install, npm run dev, which port if you can infer from package.json)
1573
+ 2. How to run it \u2014 concrete commands (${pm} install, ${devCmd}, which port if you can infer from package.json)
1721
1574
  3. What we touched \u2014 group by area (routes, styles, config, dependencies). Do not dump every file as a checklist unless there are under 12 files.
1722
1575
  4. Watch out for \u2014 honest risks from the notes below and from reading package.json / the new entry files
1723
- 5. Next steps after Apply \u2014 git branch checkout if they applied, npm install on the real project
1576
+ 5. Next steps after Apply \u2014 git branch checkout if they applied, ${pm} install on the real project
1724
1577
 
1725
1578
  You may read package.json, README, src/app/layout.tsx, src/main.tsx, index.html, or MIGRATION.md if they exist.
1726
1579
 
@@ -1753,7 +1606,8 @@ var draftDuringInstallProcessor = {
1753
1606
  phase: plan.phase,
1754
1607
  tasks: plan.tasks || [],
1755
1608
  notes: board.notes,
1756
- startedAt: board.startedAt
1609
+ startedAt: board.startedAt,
1610
+ packageManager: plan.packageManager
1757
1611
  });
1758
1612
  const tools = participant.getTools();
1759
1613
  if (tools.length === 0) {
@@ -1824,34 +1678,37 @@ var completedProcessor = {
1824
1678
  const plan = await runtime.state.repository.getPlan(p.planId);
1825
1679
  if (!plan) return;
1826
1680
  const shadowPath = await shadowPathFor(plan, board);
1681
+ const isFailed = plan.phase === "failed" || plan.outcome === "failed" || p.outcome === "failed";
1682
+ const effectivePhase = isFailed ? "failed" : plan.phase || "completed";
1827
1683
  const draft = fallbackReport({
1828
1684
  source: plan.profile.source,
1829
1685
  target: plan.profile.target,
1830
1686
  targetPath: plan.targetPath,
1831
1687
  runId: plan.runId,
1832
- phase: plan.phase,
1688
+ phase: effectivePhase,
1833
1689
  tasks: plan.tasks || [],
1834
1690
  notes: board.notes,
1835
- startedAt: board.startedAt
1691
+ startedAt: board.startedAt,
1692
+ packageManager: plan.packageManager
1836
1693
  });
1837
- board.pendingStatusPhase = plan.phase === "failed" ? "failed" : "completed";
1694
+ board.pendingStatusPhase = isFailed ? "failed" : "completed";
1838
1695
  try {
1839
1696
  const path12 = await import("path");
1840
1697
  const dest = path12.join(shadowPath, "MIGRATION.md");
1841
1698
  if (board.reportLoopActive) {
1842
- log(participant.getId(), p.planId, "Reporter draft is still running; Status will be patched when it finishes (no second model call).");
1699
+ log(participant.getId(), p.planId, "Compiling comprehensive migration briefing in the background...");
1843
1700
  return;
1844
1701
  }
1845
- const result = await writeOrPatchMigrationMd(dest, plan.phase, draft);
1702
+ const result = await writeOrPatchMigrationMd(dest, effectivePhase, draft);
1846
1703
  board.pendingStatusPhase = void 0;
1847
1704
  log(
1848
1705
  participant.getId(),
1849
1706
  p.planId,
1850
- result === "patched" ? "Reporter patched MIGRATION.md with the final build Status (no extra model call)." : "Reporter finished. Open MIGRATION.md in the shadow workspace (it is copied to your project when you Apply)."
1707
+ result === "patched" ? "Updated MIGRATION.md with final build verification status." : "Migration report ready in MIGRATION.md: review changes and run Apply to merge into your project."
1851
1708
  );
1852
1709
  } catch (error) {
1853
1710
  console.error("[ReporterAgent] Could not ensure MIGRATION.md:", error);
1854
- log(participant.getId(), p.planId, "Reporter could not write MIGRATION.md.", "warning");
1711
+ log(participant.getId(), p.planId, "Could not generate MIGRATION.md report.", "warning");
1855
1712
  }
1856
1713
  }
1857
1714
  };
@@ -1890,44 +1747,81 @@ async function completeMigration(participantId, planId, message, level) {
1890
1747
  const runtime = resolveRuntime();
1891
1748
  const plan = await runtime.state.repository.getPlan(planId);
1892
1749
  if (plan) {
1750
+ let swept = 0;
1751
+ for (const task of plan.tasks) {
1752
+ if (task.status === "failed" && !task.filePath.startsWith("system:")) {
1753
+ await runtime.state.repository.updateTaskStatus(
1754
+ planId,
1755
+ task.filePath,
1756
+ "completed"
1757
+ );
1758
+ swept++;
1759
+ }
1760
+ }
1761
+ if (swept > 0) {
1762
+ console.log(
1763
+ `[IntegrationAgent] Swept ${swept} residual failed task(s) \u2192 completed (shadow build passed).`
1764
+ );
1765
+ }
1893
1766
  plan.phase = "completed";
1767
+ plan.outcome = "success";
1894
1768
  await runtime.state.repository.savePlan(plan);
1895
1769
  }
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);
1770
+ sendEvent(
1771
+ {
1772
+ type: "system.log" /* SYSTEM_LOG */,
1773
+ producerId: participantId,
1774
+ occurredAt: /* @__PURE__ */ new Date(),
1775
+ payload: { planId, message, level }
1776
+ },
1777
+ participantId
1778
+ );
1779
+ sendEvent(
1780
+ {
1781
+ type: "migration.completed" /* MIGRATION_COMPLETED */,
1782
+ producerId: participantId,
1783
+ occurredAt: /* @__PURE__ */ new Date(),
1784
+ payload: {
1785
+ planId,
1786
+ outcome: "success"
1787
+ }
1788
+ },
1789
+ participantId
1790
+ );
1908
1791
  }
1909
1792
  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);
1793
+ sendEvent(
1794
+ {
1795
+ type: "system.log" /* SYSTEM_LOG */,
1796
+ producerId: participantId,
1797
+ occurredAt: /* @__PURE__ */ new Date(),
1798
+ payload: { planId, message, level }
1799
+ },
1800
+ participantId
1801
+ );
1916
1802
  }
1917
1803
  async function failMigration(participantId, planId, message) {
1918
1804
  const runtime = resolveRuntime();
1919
1805
  const plan = await runtime.state.repository.getPlan(planId);
1920
1806
  if (plan) {
1921
1807
  plan.phase = "failed";
1808
+ plan.outcome = "failed";
1922
1809
  await runtime.state.repository.savePlan(plan);
1923
1810
  }
1924
1811
  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);
1812
+ sendEvent(
1813
+ {
1814
+ type: "migration.completed" /* MIGRATION_COMPLETED */,
1815
+ producerId: participantId,
1816
+ occurredAt: /* @__PURE__ */ new Date(),
1817
+ payload: {
1818
+ planId,
1819
+ outcome: "failed",
1820
+ reason: message
1821
+ }
1822
+ },
1823
+ participantId
1824
+ );
1931
1825
  }
1932
1826
  async function requeueBrokenFiles(producerId, planId, files, errors) {
1933
1827
  const runtime = resolveRuntime();
@@ -1945,17 +1839,20 @@ async function requeueBrokenFiles(producerId, planId, files, errors) {
1945
1839
  "pending",
1946
1840
  errors.slice(0, 3).join("\n").slice(0, 1500)
1947
1841
  );
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);
1842
+ sendEvent(
1843
+ {
1844
+ type: "file.rejected" /* FILE_REJECTED */,
1845
+ producerId,
1846
+ occurredAt: /* @__PURE__ */ new Date(),
1847
+ payload: {
1848
+ planId,
1849
+ filePath,
1850
+ errors,
1851
+ source: "integration"
1852
+ }
1853
+ },
1854
+ producerId
1855
+ );
1959
1856
  }
1960
1857
  const plan = await runtime.state.repository.getPlan(planId);
1961
1858
  if (plan) {
@@ -1970,7 +1867,9 @@ var integrationProcessor = {
1970
1867
  const producerId = participant.getId();
1971
1868
  const locks = resolveRuntime().state.integrationLocks;
1972
1869
  if (locks.has(planId)) {
1973
- console.log(`[IntegrationAgent] Integration already running for ${planId}. Ignoring duplicate phase event.`);
1870
+ console.log(
1871
+ `[IntegrationAgent] Integration already running for ${planId}. Ignoring duplicate phase event.`
1872
+ );
1974
1873
  return;
1975
1874
  }
1976
1875
  locks.add(planId);
@@ -1982,7 +1881,9 @@ var integrationProcessor = {
1982
1881
  return;
1983
1882
  }
1984
1883
  if (plan.phase === "completed" || plan.phase === "failed") {
1985
- console.log(`[IntegrationAgent] Plan ${planId} already finished (${plan.phase}). Ignoring extra integration phase.`);
1884
+ console.log(
1885
+ `[IntegrationAgent] Plan ${planId} already finished (${plan.phase}). Ignoring extra integration phase.`
1886
+ );
1986
1887
  return;
1987
1888
  }
1988
1889
  plan.phase = "integration";
@@ -1998,37 +1899,101 @@ var integrationProcessor = {
1998
1899
  plan.integrationRounds = currentRound + 1;
1999
1900
  await runtime.state.repository.savePlan(plan);
2000
1901
  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");
1902
+ const {
1903
+ ShadowWorkspace: ShadowWorkspace2,
1904
+ createReadFileTool: createReadFileTool2,
1905
+ createWriteFileTool: createWriteFileTool2,
1906
+ createListDirectoryTool: createListDirectoryTool2,
1907
+ createRunBuildTool: createRunBuildTool2,
1908
+ runInShadowWorkspace,
1909
+ parseImplicatedFiles
1910
+ } = await import("./src-Q3DGKZEQ.js");
2002
1911
  const { collectShadowIssues, collectRepairTargets } = await import("./registry-7WZ5RTGE.js");
2003
- const { findMigrationCatalogEntry: findMigrationCatalogEntry2 } = await import("./src-UC3SN46X.js");
1912
+ const { resolveMigrationCatalog: resolveMigrationCatalog2 } = await import("./src-YPCIVBLZ.js");
2004
1913
  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.`);
2007
- console.log(`[IntegrationAgent] npm install starting in ${shadowPath}`);
1914
+ const pmCommands = resolvePackageManagerCommands(plan.packageManager);
1915
+ console.log(
1916
+ `[IntegrationAgent] Shadow build round ${plan.integrationRounds}/${MAX_INTEGRATION_ROUNDS} in ${shadowPath}`
1917
+ );
1918
+ log2(
1919
+ producerId,
1920
+ planId,
1921
+ `Source files transformed. Installing dependencies in shadow workspace with ${pmCommands.cleanInstall}...`
1922
+ );
1923
+ console.log(`[IntegrationAgent] ${pmCommands.cleanInstall} starting in ${shadowPath}`);
2008
1924
  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.");
1925
+ const install = await runInShadowWorkspace(
1926
+ shadowPath,
1927
+ pmCommands.cleanInstall,
1928
+ (chunk) => {
1929
+ process.stdout.write(chunk);
1930
+ if (Date.now() - lastInstallBeat >= 15e3) {
1931
+ lastInstallBeat = Date.now();
1932
+ log2(
1933
+ producerId,
1934
+ planId,
1935
+ `${pmCommands.cleanInstall} is in progress in the shadow workspace...`
1936
+ );
1937
+ }
2014
1938
  }
2015
- });
1939
+ );
2016
1940
  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.");
1941
+ console.warn(
1942
+ `[IntegrationAgent] ${pmCommands.cleanInstall} failed in shadow:`,
1943
+ install.output.slice(0, 2e3)
1944
+ );
1945
+ log2(
1946
+ producerId,
1947
+ planId,
1948
+ `${pmCommands.cleanInstall} failed in the shadow workspace. Halting to diagnose dependency conflicts.`,
1949
+ "error"
1950
+ );
1951
+ if (lastRound) {
1952
+ await failMigration(
1953
+ producerId,
1954
+ planId,
1955
+ `${pmCommands.cleanInstall} failed in the shadow workspace after ${MAX_INTEGRATION_ROUNDS} integration rounds. The migration could not complete.`
1956
+ );
1957
+ return;
1958
+ }
1959
+ const path12 = await import("path");
1960
+ const pkgPath = path12.join(shadowPath, "package.json");
1961
+ await requeueBrokenFiles(
1962
+ producerId,
1963
+ planId,
1964
+ [pkgPath],
1965
+ [`${pmCommands.cleanInstall} failed in shadow workspace:
1966
+ ${install.output.slice(0, 2500)}`]
1967
+ );
1968
+ return;
2021
1969
  }
1970
+ log2(
1971
+ producerId,
1972
+ planId,
1973
+ `Dependencies installed successfully. Running verification build (${pmCommands.runBuild})...`
1974
+ );
2022
1975
  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.");
1976
+ let build = await runInShadowWorkspace(
1977
+ shadowPath,
1978
+ pmCommands.runBuild,
1979
+ (chunk) => {
1980
+ process.stdout.write(chunk);
1981
+ if (Date.now() - lastBuildBeat >= 15e3) {
1982
+ lastBuildBeat = Date.now();
1983
+ log2(
1984
+ producerId,
1985
+ planId,
1986
+ `${pmCommands.runBuild} is in progress in the shadow workspace...`
1987
+ );
1988
+ }
2028
1989
  }
2029
- });
1990
+ );
2030
1991
  if (!build.ok) {
2031
- log2(producerId, planId, "Shadow build failed. IntegrationAgent will attempt local repairs before requeueing files.");
1992
+ log2(
1993
+ producerId,
1994
+ planId,
1995
+ "Verification build encountered errors. Analyzing diagnostics for automated repair..."
1996
+ );
2032
1997
  const shadowTools = [
2033
1998
  createReadFileTool2(shadowPath),
2034
1999
  createWriteFileTool2(shadowPath),
@@ -2038,7 +2003,7 @@ var integrationProcessor = {
2038
2003
  const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
2039
2004
  await new Promise((originalResolve) => {
2040
2005
  let isDone = false;
2041
- const resolve5 = () => {
2006
+ const resolve4 = () => {
2042
2007
  if (!isDone) {
2043
2008
  isDone = true;
2044
2009
  originalResolve();
@@ -2063,32 +2028,51 @@ Build output:
2063
2028
  ${excerpt}
2064
2029
 
2065
2030
  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) => {
2031
+ Promise.resolve(
2032
+ runLoop(tempAgent.getId(), prompt, {
2033
+ model: modelToUse,
2034
+ context: tempAgent.getMemory().getContext(),
2035
+ tools: shadowTools
2036
+ })
2037
+ ).catch((error) => {
2071
2038
  console.error("[IntegrationAgent] runLoop error:", error);
2072
2039
  leave(tempAgent);
2073
- resolve5();
2040
+ resolve4();
2074
2041
  });
2075
2042
  setTimeout(() => {
2076
2043
  if (!isDone) {
2077
- console.warn("[IntegrationAgent] Repair loop timed out; continuing with deterministic rebuild.");
2044
+ console.warn(
2045
+ "[IntegrationAgent] Repair loop timed out; continuing with deterministic rebuild."
2046
+ );
2078
2047
  leave(tempAgent);
2079
- resolve5();
2048
+ resolve4();
2080
2049
  }
2081
2050
  }, REPAIR_TIMEOUT_MS);
2082
2051
  });
2083
2052
  build = await runInShadowWorkspace(shadowPath, "npm run build");
2084
2053
  }
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);
2054
+ const pluginCtx = {
2055
+ source: plan.profile.source,
2056
+ target: plan.profile.target
2057
+ };
2058
+ const catalogEntry = resolveMigrationCatalog2(
2059
+ plan.profile.source,
2060
+ plan.profile.target
2061
+ );
2062
+ const structureIssues = collectShadowIssues(
2063
+ shadowPath,
2064
+ pluginCtx,
2065
+ catalogEntry?.layer
2066
+ );
2088
2067
  const buildFailed = !build.ok || !install.ok;
2089
2068
  const verifierFailed = structureIssues.length > 0;
2090
2069
  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");
2070
+ await completeMigration(
2071
+ producerId,
2072
+ planId,
2073
+ "Verification build succeeded cleanly. Generating final migration report and instructions...",
2074
+ "info"
2075
+ );
2092
2076
  return;
2093
2077
  }
2094
2078
  const implicated = parseImplicatedFiles(
@@ -2103,29 +2087,38 @@ When you have applied fixes (or cannot fix further), stop. A deterministic rebui
2103
2087
  });
2104
2088
  const errors = [
2105
2089
  ...structureIssues.flatMap((issue) => issue.errors),
2106
- ...install.ok ? [] : [`npm install failed:
2090
+ ...install.ok ? [] : [`${pmCommands.cleanInstall} failed:
2107
2091
  ${install.output.slice(0, 1500)}`],
2108
- ...build.ok ? [] : [`npm run build failed:
2092
+ ...build.ok ? [] : [`${pmCommands.runBuild} failed:
2109
2093
  ${build.output.slice(0, 2500)}`]
2110
2094
  ];
2111
2095
  if (!lastRound && repairFiles.length > 0) {
2112
2096
  log2(
2113
2097
  producerId,
2114
2098
  planId,
2115
- 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.`,
2099
+ buildFailed ? `Verification build failed. Reopening implicated files for automated repair...` : `Structure verifier identified ${structureIssues.length} architectural issue(s). Reopening files for repair...`,
2116
2100
  "warning"
2117
2101
  );
2118
- await requeueBrokenFiles(producerId, planId, repairFiles, errors.length > 0 ? errors : ["Shadow verification failed."]);
2102
+ await requeueBrokenFiles(
2103
+ producerId,
2104
+ planId,
2105
+ repairFiles,
2106
+ errors.length > 0 ? errors : ["Shadow verification failed."]
2107
+ );
2119
2108
  return;
2120
2109
  }
2121
2110
  await failMigration(
2122
2111
  producerId,
2123
2112
  planId,
2124
- lastRound ? `npm run build still does not pass after ${MAX_INTEGRATION_ROUNDS} integration rounds. The migration is not successful.` : "Shadow verification failed and no repair files could be assigned."
2113
+ lastRound ? `Verification build did not pass after ${MAX_INTEGRATION_ROUNDS} integration rounds. Please inspect MIGRATION.md for diagnostic details.` : "Shadow verification failed and no repair targets could be assigned."
2125
2114
  );
2126
2115
  } catch (error) {
2127
2116
  console.error(`[IntegrationAgent] Error:`, error);
2128
- await failMigration(producerId, planId, "Integration Phase encountered an error. The migration is not successful.");
2117
+ await failMigration(
2118
+ producerId,
2119
+ planId,
2120
+ "Integration Phase encountered an error. The migration is not successful."
2121
+ );
2129
2122
  } finally {
2130
2123
  resolveRuntime().state.integrationLocks.delete(planId);
2131
2124
  }
@@ -2781,16 +2774,21 @@ var MigrationRunner = class {
2781
2774
  */
2782
2775
  async startMigration(request) {
2783
2776
  this.ensureInitialized();
2784
- await this.store.reset();
2785
- this.workspace.cleanupOldRuns(3);
2777
+ const existing = await this.store.getAllPlans();
2778
+ const protectedRunIds = existing.filter((p) => p.appliedAt).map((p) => p.runId);
2779
+ this.workspace.cleanupOldRuns(3, protectedRunIds);
2786
2780
  const runId = `run_${Date.now()}`;
2787
2781
  const shadowPath = this.workspace.cloneDirectory(request.targetPath, runId);
2788
2782
  const planId = `plan_${randomUUID()}`;
2783
+ const manifest = inspectManifest(request.targetPath);
2784
+ const resolvedPackageManager = request.packageManager || manifest.packageManager || "npm";
2789
2785
  const plan = {
2790
2786
  id: planId,
2791
2787
  runId,
2792
2788
  profile: { source: request.from, target: request.to },
2793
2789
  targetPath: request.targetPath,
2790
+ packageManager: resolvedPackageManager,
2791
+ phase: "files",
2794
2792
  tasks: [
2795
2793
  { filePath: "system:package_manager", status: "pending" }
2796
2794
  ],
@@ -2809,7 +2807,8 @@ var MigrationRunner = class {
2809
2807
  payload: {
2810
2808
  planId,
2811
2809
  profile: plan.profile,
2812
- shadowWorkspacePath: shadowPath
2810
+ shadowWorkspacePath: shadowPath,
2811
+ packageManager: resolvedPackageManager
2813
2812
  }
2814
2813
  },
2815
2814
  dispatcherId
@@ -2889,9 +2888,18 @@ function bootstrapMetamorph(repository, tools = []) {
2889
2888
  }
2890
2889
 
2891
2890
  // src/index.ts
2891
+ function logStartupContext() {
2892
+ const isDev = process.env.METAMORPH_DEV === "1";
2893
+ console.log(chalk.gray(`[Metamorph] CWD: ${process.cwd()}`));
2894
+ console.log(chalk.gray(`[Metamorph] Binary: ${import.meta.url}`));
2895
+ if (!isDev && import.meta.url.includes("/src/")) {
2896
+ console.log(chalk.yellow(`[Metamorph] Notice: Running directly from source without METAMORPH_DEV=1.`));
2897
+ }
2898
+ }
2892
2899
  var program = new Command();
2893
- program.name("metamorph").description("AI-powered technology migration tool using Mozaik Agents").version("1.0.0");
2900
+ program.name("metamorph").description("AI-powered technology migration tool using Mozaik Agents").version("2.1.2");
2894
2901
  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) => {
2902
+ logStartupContext();
2895
2903
  let targetPath = targetPathArg || ".";
2896
2904
  const { select, input } = await import("@inquirer/prompts");
2897
2905
  let from = options.from;
@@ -2906,15 +2914,42 @@ program.command("run [path]").description("Run a migration on the specified dire
2906
2914
  angular: ["react", "next", "vue", "svelte"],
2907
2915
  svelte: ["react", "next", "vue", "angular"]
2908
2916
  };
2917
+ const monorepo = detectMonorepo(targetPath);
2918
+ if (monorepo.isMonorepo && monorepo.packages.length > 0) {
2919
+ console.log(chalk.cyan(`
2920
+ \u{1F4E6} Monorepo detected (${monorepo.tool || "workspaces"} with ${monorepo.packages.length} packages).`));
2921
+ const chosenPath = await select({
2922
+ message: "Select the workspace package you want to migrate:",
2923
+ choices: [
2924
+ ...monorepo.packages.map((p) => ({
2925
+ name: `${p.name} (${p.relativePath})`,
2926
+ value: p.absolutePath
2927
+ })),
2928
+ {
2929
+ name: `Whole root directory (${targetPath})`,
2930
+ value: targetPath
2931
+ }
2932
+ ]
2933
+ });
2934
+ targetPath = chosenPath;
2935
+ }
2909
2936
  if (!from) {
2910
2937
  console.log(chalk.gray(`
2911
2938
  \u{1F50D} Scanning directory: ${targetPath}`));
2912
- const detected = detectTechnologies(targetPath);
2913
- const supportedDetected = detected.filter((d) => Object.keys(SUPPORTED_MIGRATIONS).includes(d.framework));
2914
- if (supportedDetected.length > 0) {
2939
+ const profiles = inspectProject(targetPath);
2940
+ const supportedProfiles = profiles.filter((p) => Object.keys(SUPPORTED_MIGRATIONS).includes(p.framework));
2941
+ if (supportedProfiles.length > 0) {
2915
2942
  from = await select({
2916
2943
  message: "What technology do you want to migrate FROM?",
2917
- choices: supportedDetected.map((d) => ({ name: `${d.framework} (Detected ${d.confidence}%)`, value: d.framework }))
2944
+ choices: supportedProfiles.map((p) => {
2945
+ const details = [
2946
+ p.variant !== "none" ? p.variant : null,
2947
+ p.bundler !== "unknown" ? p.bundler : null,
2948
+ p.language === "typescript" ? "TS" : "JS"
2949
+ ].filter(Boolean).join(" | ");
2950
+ const label = details ? `${p.framework} (${details} - ${p.confidence}%)` : `${p.framework} (${p.confidence}%)`;
2951
+ return { name: label, value: p.framework };
2952
+ })
2918
2953
  });
2919
2954
  } else {
2920
2955
  console.log(chalk.yellow(`No supported technologies detected in the current directory.`));
@@ -2975,11 +3010,41 @@ program.command("run [path]").description("Run a migration on the specified dire
2975
3010
  spinner.succeed(`Migration dispatched! Plan: ${result.planId}`);
2976
3011
  console.log(chalk.gray(`Shadow: ${result.shadowPath}`));
2977
3012
  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);
3013
+ \u23F3 Swarm is executing migration in shadow workspace...`));
3014
+ const waitSpinner = ora("Waiting for swarm to complete migration...").start();
3015
+ const startTime = Date.now();
3016
+ const MAX_TIMEOUT_MS = 30 * 60 * 1e3;
3017
+ while (true) {
3018
+ if (Date.now() - startTime >= MAX_TIMEOUT_MS) {
3019
+ waitSpinner.fail(chalk.red(`Migration timed out after 30 minutes.`));
3020
+ process.exit(1);
3021
+ }
3022
+ await new Promise((r) => setTimeout(r, 2e3));
3023
+ const plan = await store.getPlan(result.planId);
3024
+ if (!plan) continue;
3025
+ const terminalTasks = plan.tasks.filter((t) => t.status === "completed" || t.status === "failed");
3026
+ const inProgressTasks = plan.tasks.filter((t) => t.status === "in_progress");
3027
+ const pendingTasks = plan.tasks.filter((t) => t.status === "pending");
3028
+ waitSpinner.text = `Phase: ${plan.phase || "files"} | Tasks: ${terminalTasks.length}/${plan.tasks.length} terminal (${inProgressTasks.length} in progress, ${pendingTasks.length} pending)`;
3029
+ if (plan.outcome === "success") {
3030
+ waitSpinner.succeed(chalk.green(`
3031
+ \u2705 Migration finished successfully in shadow workspace!`));
3032
+ console.log(chalk.white(`
3033
+ Next steps:`));
3034
+ console.log(chalk.gray(` 1. Review changes in shadow workspace: `) + chalk.cyan(result.shadowPath));
3035
+ console.log(chalk.gray(` 2. Apply changes to a dedicated Git branch: `) + chalk.cyan(`metamorph apply ${result.runId} ${targetPath}
3036
+ `));
3037
+ process.exit(0);
3038
+ }
3039
+ if (plan.phase === "failed" || plan.outcome === "failed") {
3040
+ waitSpinner.fail(chalk.red(`
3041
+ \u274C Migration failed in shadow workspace.`));
3042
+ console.log(chalk.yellow(`Check .metamorph/shadow/${result.runId}/MIGRATION.md or run 'metamorph ui' for failure details.`));
3043
+ console.log(chalk.gray(`Your original codebase in "${targetPath}" remains completely untouched.
3044
+ `));
3045
+ process.exit(1);
3046
+ }
3047
+ }
2983
3048
  } catch (error) {
2984
3049
  const errorMessage = error instanceof Error ? error.message : String(error);
2985
3050
  spinner.fail(`Migration failed: ${errorMessage}`);
@@ -2988,8 +3053,27 @@ program.command("run [path]").description("Run a migration on the specified dire
2988
3053
  });
2989
3054
  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
3055
  try {
3056
+ const store = new SQLiteStateStore(".metamorph");
3057
+ const plans = await store.getAllPlans();
3058
+ const plan = plans.find((p) => p.runId === runId);
3059
+ if (!plan) {
3060
+ throw new Error(`Plan for run "${runId}" not found in database.`);
3061
+ }
3062
+ if (plan.phase !== "completed" || plan.outcome !== "success") {
3063
+ throw new Error(
3064
+ `Cannot apply migration: plan is not completed successfully (phase: "${plan.phase}", outcome: "${plan.outcome || "none"}"). Only successful migrations can be applied.`
3065
+ );
3066
+ }
3067
+ if (plan.appliedAt) {
3068
+ throw new Error(
3069
+ `Migration for run "${runId}" has already been applied at ${new Date(plan.appliedAt).toISOString()} to branch ${plan.appliedBranch || "unknown"}.`
3070
+ );
3071
+ }
2991
3072
  const integrator = new MigrationIntegrator(new ShadowWorkspace());
2992
3073
  const result = await integrator.applyMigration(runId, targetPath);
3074
+ plan.appliedAt = /* @__PURE__ */ new Date();
3075
+ plan.appliedBranch = result.branch;
3076
+ await store.savePlan(plan);
2993
3077
  console.log(chalk.green(`\u2705 ${result.message}`));
2994
3078
  } catch (error) {
2995
3079
  console.error(chalk.red(`\u274C Failed to apply migration: ${error instanceof Error ? error.message : String(error)}`));
@@ -3017,16 +3101,35 @@ program.command("reset").description("Clear all migration history and events fro
3017
3101
  process.exit(1);
3018
3102
  }
3019
3103
  });
3020
- program.command("detect [path]").description("Detect frameworks and libraries in a directory").action(async (targetPath) => {
3104
+ program.command("detect [path]").description("Detect frameworks, architectural variants and monorepo structure in a directory").action(async (targetPath) => {
3021
3105
  try {
3022
- const detected = detectTechnologies(targetPath || ".");
3023
- if (detected.length === 0) {
3024
- console.log(chalk.yellow(`No known technologies detected.`));
3106
+ const resolved = targetPath || ".";
3107
+ const monorepo = detectMonorepo(resolved);
3108
+ if (monorepo.isMonorepo) {
3109
+ console.log(chalk.cyan(`
3110
+ \u{1F4E6} Monorepo Detected: ${monorepo.tool || "workspaces"}`));
3111
+ console.log(chalk.gray(`Root: ${monorepo.rootPath}`));
3112
+ console.log(chalk.gray(`Workspace Packages (${monorepo.packages.length}):`));
3113
+ monorepo.packages.forEach((p) => console.log(` \u2022 ${chalk.white(p.name)}: ${chalk.gray(p.relativePath)}`));
3114
+ }
3115
+ const profiles = inspectProject(resolved);
3116
+ if (profiles.length === 0) {
3117
+ console.log(chalk.yellow(`
3118
+ No supported technologies detected.`));
3025
3119
  } else {
3026
3120
  console.log(chalk.blue(`
3027
- \u{1F50D} Detected Technologies:`));
3028
- detected.forEach((d) => console.log(` - ${chalk.green(d.framework)} (Confidence: ${d.confidence}%)
3029
- Evidence: ${chalk.gray(d.evidence.join(", "))}`));
3121
+ \u{1F50D} Detected Technologies & Architecture:`));
3122
+ profiles.forEach((p) => {
3123
+ console.log(`
3124
+ \u2022 ${chalk.bold.green(p.framework)} (Confidence: ${p.confidence}%)`);
3125
+ console.log(` Category: ${chalk.cyan(p.category)} | Variant: ${chalk.cyan(p.variant)} | Bundler: ${chalk.cyan(p.bundler)} | Lang: ${chalk.cyan(p.language.toUpperCase())}`);
3126
+ if (p.subsumedDependencies.length > 0) {
3127
+ console.log(` Subsumed Dependencies: ${chalk.yellow(p.subsumedDependencies.join(", "))}`);
3128
+ }
3129
+ console.log(` Suggested Targets: ${chalk.magenta(p.suggestedTargets.join(", "))}`);
3130
+ console.log(` Evidence: ${chalk.gray(p.evidence.join("; "))}`);
3131
+ });
3132
+ console.log("");
3030
3133
  }
3031
3134
  } catch (error) {
3032
3135
  console.error(chalk.red(`\u274C Detection failed: ${error instanceof Error ? error.message : String(error)}`));
@@ -3052,6 +3155,7 @@ program.command("list").description("List past migration plans").action(async ()
3052
3155
  }
3053
3156
  });
3054
3157
  program.command("ui").description("Start the Metamorph Dashboard API server").option("-p, --port <number>", "Port for the API server", "9876").action(async (options) => {
3158
+ logStartupContext();
3055
3159
  let desiredPort = parseInt(options.port, 10);
3056
3160
  const spinner = ora("Starting Metamorph API server...").start();
3057
3161
  try {