@nikelyh/metamorph 2.0.10 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ MigrationIntegrator,
4
+ SQLiteStateStore,
5
+ ShadowWorkspace,
6
+ createApiServer,
7
+ createCheckProjectDiagnosticsTool,
8
+ createCreateFileTool,
9
+ createDeleteFileTool,
10
+ createListDirectoryTool,
11
+ createReadFileTool,
12
+ createRenameFileTool,
13
+ createRunBuildTool,
14
+ createWriteFileTool
15
+ } from "./chunk-OXXEISZT.js";
16
+ import {
17
+ findMigrationCatalogEntry,
18
+ formatCatalogRules
19
+ } from "./chunk-VEAYUIOZ.js";
2
20
  import {
3
21
  detectTechnologies
4
22
  } from "./chunk-3QN2Q75J.js";
@@ -10,1233 +28,573 @@ import {
10
28
  resolveRuntime,
11
29
  runLoop,
12
30
  sendEvent
13
- } from "./chunk-GNWRSZXT.js";
31
+ } from "./chunk-GKRBTA6E.js";
32
+ import {
33
+ collectFileHints,
34
+ registerMigrationPlugin
35
+ } from "./chunk-O2QLEES3.js";
14
36
 
15
37
  // src/index.ts
16
38
  import { Command } from "commander";
17
39
  import chalk from "chalk";
18
40
  import ora from "ora";
19
41
  import "dotenv/config";
42
+ import * as path11 from "path";
20
43
 
21
- // ../infrastructure/src/db/SQLiteStateStore.ts
22
- import { DatabaseSync } from "node:sqlite";
23
- import { join as join2 } from "path";
24
- import { mkdirSync } from "fs";
25
- var SQLiteStateStore = class {
26
- db;
27
- constructor(storageDir = ".metamorph") {
28
- try {
29
- mkdirSync(storageDir, { recursive: true });
30
- } catch (e) {
31
- }
32
- const dbPath = join2(storageDir, "history.db");
33
- this.db = new DatabaseSync(dbPath);
34
- this.initTables();
35
- }
36
- /**
37
- * Creates the necessary tables if they do not exist.
38
- */
39
- initTables() {
40
- this.db.exec(`
41
- CREATE TABLE IF NOT EXISTS plans (
42
- id TEXT PRIMARY KEY,
43
- run_id TEXT,
44
- target_path TEXT,
45
- source_framework TEXT,
46
- target_framework TEXT,
47
- rules_json TEXT,
48
- created_at TEXT
49
- );
50
- `);
51
- try {
52
- this.db.exec("ALTER TABLE plans ADD COLUMN run_id TEXT");
53
- } catch (e) {
54
- }
55
- try {
56
- this.db.exec("ALTER TABLE plans ADD COLUMN target_path TEXT");
57
- } catch (e) {
58
- }
59
- this.db.exec(`
60
- CREATE TABLE IF NOT EXISTS tasks (
61
- plan_id TEXT,
62
- file_path TEXT,
63
- status TEXT,
64
- dependencies_json TEXT,
65
- error TEXT,
66
- PRIMARY KEY (plan_id, file_path),
67
- FOREIGN KEY (plan_id) REFERENCES plans(id)
68
- );
69
- `);
70
- this.db.exec(`
71
- CREATE TABLE IF NOT EXISTS events (
72
- id INTEGER PRIMARY KEY AUTOINCREMENT,
73
- event_name TEXT,
74
- payload_json TEXT,
75
- timestamp TEXT
76
- );
77
- `);
78
- }
79
- async savePlan(plan) {
80
- const stmtPlan = this.db.prepare(`
81
- INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at)
82
- VALUES (?, ?, ?, ?, ?, ?, ?)
83
- `);
84
- stmtPlan.run(
85
- plan.id,
86
- plan.runId,
87
- plan.targetPath || "",
88
- plan.profile.source,
89
- plan.profile.target,
90
- JSON.stringify(plan.profile.rules || []),
91
- plan.createdAt.toISOString()
92
- );
93
- const stmtTask = this.db.prepare(`
94
- INSERT OR REPLACE INTO tasks (plan_id, file_path, status, dependencies_json, error)
95
- VALUES (?, ?, ?, ?, ?)
96
- `);
97
- for (const task of plan.tasks) {
98
- stmtTask.run(
99
- plan.id,
100
- task.filePath,
101
- task.status,
102
- JSON.stringify(task.dependencies || []),
103
- task.error || null
104
- );
105
- }
44
+ // ../application/src/agents/MapperAgent.ts
45
+ import * as fs from "fs";
46
+ import * as path from "path";
47
+ import {
48
+ createAgent,
49
+ SituationSpecification
50
+ } from "@mozaik-ai/core";
51
+ var WhenMigrationStarts = class extends SituationSpecification {
52
+ isSatisfiedBy({ event }) {
53
+ return event.type === "migration.started" /* MIGRATION_STARTED */;
106
54
  }
107
- async getPlan(id) {
108
- const stmtPlan = this.db.prepare(`SELECT * FROM plans WHERE id = ?`);
109
- const planRow = stmtPlan.get(id);
110
- if (!planRow) {
111
- return null;
55
+ };
56
+ var mapFilesProcessor = {
57
+ async apply({ event, participant }) {
58
+ const payload = event.payload;
59
+ console.log(`[MapperAgent] Migration started for Plan: ${payload.planId}`);
60
+ const shadowDir = payload.shadowWorkspacePath || "";
61
+ if (!shadowDir) {
62
+ console.warn("[MapperAgent] No shadow workspace provided. Skipping.");
63
+ return;
112
64
  }
113
- const stmtTasks = this.db.prepare(`SELECT * FROM tasks WHERE plan_id = ?`);
114
- const taskRows = stmtTasks.all(id);
115
- return {
116
- id: planRow.id,
117
- runId: planRow.run_id,
118
- targetPath: planRow.target_path,
119
- profile: {
120
- source: planRow.source_framework,
121
- target: planRow.target_framework,
122
- rules: JSON.parse(planRow.rules_json)
123
- },
124
- createdAt: new Date(planRow.created_at),
125
- tasks: taskRows.map((row) => ({
126
- filePath: row.file_path,
127
- status: row.status,
128
- dependencies: JSON.parse(row.dependencies_json),
129
- error: row.error || void 0
130
- }))
65
+ const filesToMigrate = [];
66
+ const scanDir = (dir) => {
67
+ if (!fs.existsSync(dir)) return;
68
+ const files = fs.readdirSync(dir);
69
+ const ignoredDirs = ["node_modules", "dist", "build", "out", "coverage", ".next"];
70
+ for (const file of files) {
71
+ if (ignoredDirs.includes(file) || file.startsWith(".")) continue;
72
+ const fullPath = path.join(dir, file);
73
+ if (fs.statSync(fullPath).isDirectory()) {
74
+ scanDir(fullPath);
75
+ } else if (/\.(ts|js|tsx|jsx|vue|svelte)$/.test(file) || file.endsWith(".html") && file !== "index.html") {
76
+ filesToMigrate.push(fullPath);
77
+ }
78
+ }
131
79
  };
132
- }
133
- async updateTaskStatus(planId, filePath, status, error) {
134
- const stmt = this.db.prepare(`
135
- INSERT INTO tasks (plan_id, file_path, status, error, dependencies_json)
136
- VALUES (?, ?, ?, ?, '[]')
137
- ON CONFLICT(plan_id, file_path) DO UPDATE
138
- SET status = excluded.status, error = excluded.error
139
- `);
140
- stmt.run(planId, filePath, status, error || null);
141
- }
142
- async logEvent(eventName, payload) {
143
- const stmt = this.db.prepare(`
144
- INSERT INTO events (event_name, payload_json, timestamp)
145
- VALUES (?, ?, ?)
146
- `);
147
- stmt.run(eventName, JSON.stringify(payload), (/* @__PURE__ */ new Date()).toISOString());
148
- }
149
- async getAllPlans() {
150
- const stmtPlans = this.db.prepare(`SELECT * FROM plans ORDER BY created_at DESC`);
151
- const planRows = stmtPlans.all();
152
- const stmtTasks = this.db.prepare(`SELECT * FROM tasks`);
153
- const taskRows = stmtTasks.all();
154
- return planRows.map((planRow) => {
155
- const planTasks = taskRows.filter((t) => t.plan_id === planRow.id);
156
- return {
157
- id: planRow.id,
158
- runId: planRow.run_id,
159
- targetPath: planRow.target_path,
160
- profile: {
161
- source: planRow.source_framework,
162
- target: planRow.target_framework,
163
- rules: JSON.parse(planRow.rules_json)
80
+ scanDir(shadowDir);
81
+ const runtime = resolveRuntime();
82
+ const repository = runtime.state.repository;
83
+ for (const discoveredFile of filesToMigrate) {
84
+ console.log(`[MapperAgent] Discovered file: ${discoveredFile}`);
85
+ await repository.updateTaskStatus(payload.planId, discoveredFile, "pending");
86
+ sendEvent(
87
+ {
88
+ type: "file.discovered" /* FILE_DISCOVERED */,
89
+ producerId: participant.getId(),
90
+ occurredAt: /* @__PURE__ */ new Date(),
91
+ payload: {
92
+ planId: payload.planId,
93
+ filePath: discoveredFile
94
+ }
164
95
  },
165
- createdAt: new Date(planRow.created_at),
166
- tasks: planTasks.map((row) => ({
167
- filePath: row.file_path,
168
- status: row.status,
169
- dependencies: JSON.parse(row.dependencies_json),
170
- error: row.error || void 0
171
- }))
172
- };
173
- });
174
- }
175
- async getEvents() {
176
- const stmt = this.db.prepare(`SELECT * FROM events ORDER BY id DESC LIMIT 100`);
177
- const rows = stmt.all();
178
- return rows.map((row) => ({
179
- id: row.id,
180
- eventName: row.event_name,
181
- payload: JSON.parse(row.payload_json),
182
- timestamp: new Date(row.timestamp)
183
- }));
184
- }
185
- async reset() {
186
- this.db.exec(`DELETE FROM tasks`);
187
- this.db.exec(`DELETE FROM plans`);
188
- this.db.exec(`DELETE FROM events`);
96
+ participant.getId()
97
+ );
98
+ }
189
99
  }
190
100
  };
101
+ var mapFilesHandler = {
102
+ specification: new WhenMigrationStarts(),
103
+ processor: mapFilesProcessor
104
+ };
105
+ function createMapperAgent() {
106
+ return createAgent({
107
+ name: "Mapper",
108
+ capabilities: ["code_analysis"],
109
+ instruction: "You are the Architect Mapper. Your job is to analyze the source code and discover files that need migration.",
110
+ tools: [],
111
+ // AST scanning tools will go here later
112
+ handlers: [mapFilesHandler]
113
+ });
114
+ }
191
115
 
192
- // ../infrastructure/src/tools/AstTools.ts
193
- import { Project } from "ts-morph";
194
- import { existsSync } from "fs";
195
- var project = new Project();
196
- function createReadFileTool() {
197
- return {
198
- type: "function",
199
- name: "read_file",
200
- description: "Reads the content of a source file using ts-morph.",
201
- parameters: {
202
- type: "object",
203
- properties: {
204
- filePath: { type: "string", description: "The relative path to the file to read." }
205
- },
206
- required: ["filePath"],
207
- additionalProperties: false
208
- },
209
- strict: true,
210
- invoke: async ({ filePath }) => {
211
- if (!existsSync(filePath)) {
212
- return { error: `File not found: ${filePath}` };
213
- }
214
- let sourceFile = project.getSourceFile(filePath);
215
- if (!sourceFile) {
216
- project.addSourceFileAtPath(filePath);
217
- sourceFile = project.getSourceFileOrThrow(filePath);
116
+ // ../application/src/agents/WorkerAgent.ts
117
+ import {
118
+ createAgent as createAgent2,
119
+ SituationSpecification as SituationSpecification2
120
+ } from "@mozaik-ai/core";
121
+ import * as fs4 from "fs";
122
+
123
+ // ../application/src/utils/FileTreeBuilder.ts
124
+ import * as path2 from "path";
125
+ function getCommonBaseDir(filePaths) {
126
+ if (!filePaths || filePaths.length === 0) return "";
127
+ if (filePaths.length === 1) return path2.dirname(filePaths[0]);
128
+ const splitPaths = filePaths.map((p) => path2.normalize(p).split(path2.sep));
129
+ let commonPath = "";
130
+ for (let i = 0; i < splitPaths[0].length; i++) {
131
+ const part = splitPaths[0][i];
132
+ const isCommon = splitPaths.every((parts) => parts[i] === part);
133
+ if (isCommon) {
134
+ commonPath = commonPath === "" ? part : commonPath + path2.sep + part;
135
+ } else {
136
+ break;
137
+ }
138
+ }
139
+ return commonPath;
140
+ }
141
+ function buildFileTree(filePaths) {
142
+ if (!filePaths || filePaths.length === 0) return "No files found.";
143
+ const baseDir = getCommonBaseDir(filePaths);
144
+ const relativePaths = filePaths.map((p) => path2.relative(baseDir, p)).filter((p) => p.length > 0).sort();
145
+ if (relativePaths.length === 0) return "No files found.";
146
+ const tree = {};
147
+ for (const relPath of relativePaths) {
148
+ const parts = relPath.split(path2.sep);
149
+ let current = tree;
150
+ for (let i = 0; i < parts.length; i++) {
151
+ const part = parts[i];
152
+ if (i === parts.length - 1) {
153
+ current[part] = null;
218
154
  } else {
219
- await sourceFile.refreshFromFileSystem();
155
+ current[part] = current[part] || {};
156
+ current = current[part];
220
157
  }
221
- return {
222
- filePath,
223
- content: sourceFile.getFullText()
224
- };
225
158
  }
226
- };
227
- }
228
- function createWriteFileTool() {
229
- return {
230
- type: "function",
231
- name: "write_file",
232
- description: "Overwrites the content of a source file with the provided refactored code using ts-morph.",
233
- parameters: {
234
- type: "object",
235
- properties: {
236
- filePath: { type: "string", description: "The relative path to the file to modify." },
237
- newContent: { type: "string", description: "The entire new source code to write to the file." }
238
- },
239
- required: ["filePath", "newContent"],
240
- additionalProperties: false
241
- },
242
- strict: true,
243
- invoke: async ({ filePath, newContent }) => {
244
- let sourceFile = project.getSourceFile(filePath);
245
- if (!sourceFile) {
246
- if (existsSync(filePath)) {
247
- project.addSourceFileAtPath(filePath);
248
- sourceFile = project.getSourceFileOrThrow(filePath);
249
- } else {
250
- sourceFile = project.createSourceFile(filePath, "");
251
- }
159
+ }
160
+ let output = `/
161
+ `;
162
+ function renderTree(node, prefix = "") {
163
+ if (node === null) return;
164
+ const keys = Object.keys(node);
165
+ for (let i = 0; i < keys.length; i++) {
166
+ const key = keys[i];
167
+ const isLast = i === keys.length - 1;
168
+ const isFile = node[key] === null;
169
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
170
+ output += `${prefix}${connector}${key}
171
+ `;
172
+ if (!isFile) {
173
+ const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
174
+ renderTree(node[key], nextPrefix);
252
175
  }
253
- sourceFile.replaceWithText(newContent);
254
- await sourceFile.save();
255
- return {
256
- success: true,
257
- message: `File ${filePath} successfully updated.`
258
- };
259
176
  }
260
- };
177
+ }
178
+ renderTree(tree);
179
+ return output.trim();
261
180
  }
262
- function createRenameFileTool() {
263
- return {
264
- type: "function",
265
- name: "rename_file",
266
- description: "Renames a file in the project.",
267
- parameters: {
268
- type: "object",
269
- properties: {
270
- oldPath: { type: "string", description: "Current relative path of the file." },
271
- newPath: { type: "string", description: "New relative path for the file." }
272
- },
273
- required: ["oldPath", "newPath"],
274
- additionalProperties: false
275
- },
276
- strict: true,
277
- invoke: async ({ oldPath, newPath }) => {
278
- const { renameSync, existsSync: existsSync5 } = await import("fs");
279
- if (!existsSync5(oldPath)) {
280
- return { error: `File not found: ${oldPath}` };
281
- }
282
- renameSync(oldPath, newPath);
283
- const sourceFile = project.getSourceFile(oldPath);
284
- if (sourceFile) {
285
- project.removeSourceFile(sourceFile);
286
- if (existsSync5(newPath)) {
287
- project.addSourceFileAtPath(newPath);
288
- }
181
+
182
+ // ../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
+ import * as fs2 from "fs";
188
+ 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;
289
216
  }
290
- return { success: true, message: `File renamed from ${oldPath} to ${newPath}.` };
291
217
  }
292
- };
218
+ }
219
+ return false;
293
220
  }
294
- function createCreateFileTool() {
295
- return {
296
- type: "function",
297
- name: "create_file",
298
- description: "Creates a new file with the specified content.",
299
- parameters: {
300
- type: "object",
301
- properties: {
302
- filePath: { type: "string", description: "Relative path of the new file to create." },
303
- content: { type: "string", description: "The content of the new file." }
304
- },
305
- required: ["filePath", "content"],
306
- additionalProperties: false
307
- },
308
- strict: true,
309
- invoke: async ({ filePath, content }) => {
310
- const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync2 } = await import("fs");
311
- const { dirname: dirname2 } = await import("path");
312
- mkdirSync2(dirname2(filePath), { recursive: true });
313
- writeFileSync2(filePath, content, "utf-8");
314
- project.addSourceFileAtPath(filePath);
315
- return { success: true, message: `File created at ${filePath}.` };
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);
316
267
  }
317
- };
268
+ }
269
+ return out;
318
270
  }
319
- function createDeleteFileTool() {
320
- return {
321
- type: "function",
322
- name: "delete_file",
323
- description: "Deletes a file from the project.",
324
- parameters: {
325
- type: "object",
326
- properties: {
327
- filePath: { type: "string", description: "Relative path of the file to delete." }
328
- },
329
- required: ["filePath"],
330
- additionalProperties: false
331
- },
332
- strict: true,
333
- invoke: async ({ filePath }) => {
334
- const { renameSync, existsSync: existsSync5 } = await import("fs");
335
- if (!existsSync5(filePath)) {
336
- return { error: `File not found: ${filePath}` };
337
- }
338
- const obsoletePath = `${filePath}.obsolete`;
339
- renameSync(filePath, obsoletePath);
340
- const sourceFile = project.getSourceFile(filePath);
341
- if (sourceFile) {
342
- project.removeSourceFile(sourceFile);
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 });
343
318
  }
344
- return { success: true, message: `File marked as obsolete at ${obsoletePath} instead of deleting.` };
345
319
  }
346
- };
347
- }
348
-
349
- // ../infrastructure/src/workspace/ShadowWorkspace.ts
350
- import fs from "fs-extra";
351
- import { join as join3, resolve } from "path";
352
- var ShadowWorkspace = class {
353
- baseDir;
354
- constructor(basePath = ".metamorph/shadow") {
355
- this.baseDir = resolve(process.cwd(), basePath);
356
320
  }
357
- /**
358
- * Clones a target directory into the shadow workspace.
359
- * @param targetPath The absolute or relative path to the directory to clone.
360
- * @param runId A unique identifier for this run (e.g. timestamp).
361
- * @returns The absolute path to the cloned directory inside the shadow workspace.
362
- */
363
- cloneDirectory(targetPath, runId) {
364
- const sourceDir = resolve(process.cwd(), targetPath);
365
- if (!fs.existsSync(sourceDir)) {
366
- throw new Error(`Target directory does not exist: ${sourceDir}`);
367
- }
368
- const shadowDir = join3(this.baseDir, runId);
369
- if (fs.existsSync(shadowDir)) {
370
- fs.emptyDirSync(shadowDir);
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);
371
344
  }
372
- const ignoredDirs = ["node_modules", ".git", ".metamorph", "dist", "build", "out", "coverage", ".next"];
373
- const items = fs.readdirSync(sourceDir);
374
- for (const item of items) {
375
- if (ignoredDirs.includes(item)) {
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 {
376
373
  continue;
377
374
  }
378
- const itemSrc = join3(sourceDir, item);
379
- const itemDest = join3(shadowDir, item);
380
- fs.copySync(itemSrc, itemDest, {
381
- filter: (src) => !ignoredDirs.some((dir) => src.includes(`/${dir}/`) || src.endsWith(`/${dir}`) || src.includes(`\\${dir}\\`) || src.endsWith(`\\${dir}`))
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
+ ]
382
386
  });
383
387
  }
384
- return shadowDir;
385
388
  }
386
- /**
387
- * Removes the shadow workspace for a given run, effectively
388
- * discarding any in-progress migration work.
389
- */
390
- rollback(runId) {
391
- const shadowDir = join3(this.baseDir, runId);
392
- if (fs.existsSync(shadowDir)) {
393
- fs.removeSync(shadowDir);
394
- }
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
+ });
395
410
  }
396
- /**
397
- * Returns the absolute path of the shadow dir for a given runId.
398
- */
399
- getShadowPath(runId) {
400
- return join3(this.baseDir, runId);
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
+ });
401
418
  }
402
- /**
403
- * Cleans up old shadow runs to save disk space, keeping only the specified number of most recent runs.
404
- * @param keepCount Number of recent runs to keep (default: 3)
405
- */
406
- cleanupOldRuns(keepCount = 3) {
407
- if (!fs.existsSync(this.baseDir)) return;
408
- const runs = fs.readdirSync(this.baseDir).filter((f) => f.startsWith("run_") && fs.statSync(join3(this.baseDir, f)).isDirectory());
409
- runs.sort();
410
- if (runs.length > keepCount) {
411
- const toDelete = runs.slice(0, runs.length - keepCount);
412
- for (const run of toDelete) {
413
- this.rollback(run);
414
- }
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
+ });
415
439
  }
416
440
  }
417
- };
418
-
419
- // ../infrastructure/src/workspace/MigrationIntegrator.ts
420
- import fs2 from "fs-extra";
421
- import * as path from "path";
422
- import { execSync } from "child_process";
423
- var MigrationIntegrator = class {
424
- shadowWorkspace;
425
- constructor(shadowWorkspace) {
426
- this.shadowWorkspace = shadowWorkspace;
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
+ });
427
459
  }
428
- /**
429
- * Applies the migrated code from the shadow workspace back to the target directory.
430
- * Prioritizes Git if available, otherwise falls back to a Zip backup.
431
- *
432
- * @param runId The ID of the migration run to apply.
433
- * @param targetPath The original target path of the project.
434
- */
435
- async applyMigration(runId, targetPath) {
436
- const shadowDir = this.shadowWorkspace.getShadowPath(runId);
437
- if (!fs2.existsSync(shadowDir)) {
438
- throw new Error(`Shadow run not found: ${shadowDir}`);
439
- }
440
- const resolvedTarget = path.resolve(process.cwd(), targetPath);
441
- if (this.isGitRepo(resolvedTarget)) {
442
- return this.applyWithGit(runId, shadowDir, resolvedTarget);
443
- } else {
444
- return this.applyWithZipFallback(runId, shadowDir, resolvedTarget);
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 {
445
476
  }
446
477
  }
447
- isGitRepo(targetPath) {
448
- return fs2.existsSync(path.join(targetPath, ".git"));
478
+ return issues;
479
+ }
480
+
481
+ // ../application/src/utils/NeighborContext.ts
482
+ var MAX_FILES = 8;
483
+ var MAX_FILE_CHARS = 4e3;
484
+ var MAX_TOTAL_CHARS = 24e3;
485
+ function readTruncated(filePath) {
486
+ try {
487
+ if (!fs3.existsSync(filePath) || !fs3.statSync(filePath).isFile()) return null;
488
+ const raw = fs3.readFileSync(filePath, "utf-8");
489
+ if (raw.length <= MAX_FILE_CHARS) return raw;
490
+ return `${raw.slice(0, MAX_FILE_CHARS)}
491
+
492
+ /* \u2026 truncated (${raw.length} chars) */`;
493
+ } catch {
494
+ return null;
449
495
  }
450
- async applyWithGit(runId, shadowDir, targetPath) {
451
- const branchName = `metamorph/${runId}`;
496
+ }
497
+ function findSrcRoot2(filePath) {
498
+ const normalized = filePath.replace(/\\/g, "/");
499
+ const idx = normalized.lastIndexOf("/src/");
500
+ if (idx === -1) return null;
501
+ return filePath.slice(0, idx + 4);
502
+ }
503
+ function resolveImport(fromFile, spec) {
504
+ const dir = path4.dirname(fromFile);
505
+ 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);
509
+ const candidates = [
510
+ base,
511
+ `${base}.tsx`,
512
+ `${base}.ts`,
513
+ `${base}.jsx`,
514
+ `${base}.js`,
515
+ `${base}.vue`,
516
+ `${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")
521
+ ];
522
+ return candidates.find((file) => fs3.existsSync(file) && fs3.statSync(file).isFile()) || null;
523
+ }
524
+ function localImportSpecs(content) {
525
+ const specs = [];
526
+ const re = /from\s+['"](\.\.?\/[^'"]+|@\/[^'"]+)['"]/g;
527
+ let match;
528
+ while ((match = re.exec(content)) !== null) specs.push(match[1]);
529
+ return specs;
530
+ }
531
+ function listNearbyModules(fromFile, limit) {
532
+ const shadow = findShadowRoot(fromFile);
533
+ const src = findSrcRoot2(fromFile) || (shadow ? path4.join(shadow, "src") : path4.dirname(fromFile));
534
+ if (!fs3.existsSync(src)) return [];
535
+ const interesting = /(?:Page|page|View|Screen|layout|Layout|main|App|routes|Routes|app\.routes)\.(t|j)sx?$|\.(vue|svelte)$/;
536
+ const out = [];
537
+ const stack = [src];
538
+ while (stack.length > 0 && out.length < limit) {
539
+ const dir = stack.pop();
540
+ let entries;
452
541
  try {
453
- execSync(`git checkout -B ${branchName}`, { cwd: targetPath, stdio: "ignore" });
454
- } catch (e) {
455
- if (e instanceof Error) {
456
- throw new Error(`Failed to create/reset git branch ${branchName}: ${e.message}`);
457
- }
458
- throw new Error(`Failed to create/reset git branch ${branchName}: ${String(e)}`);
542
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
543
+ } catch {
544
+ continue;
459
545
  }
460
- this.copyShadowToTarget(shadowDir, targetPath);
461
- try {
462
- execSync(`git add .`, { cwd: targetPath, stdio: "ignore" });
463
- execSync(`git commit --allow-empty -m "chore: apply metamorph ai migration (${runId})"`, { cwd: targetPath, stdio: "ignore" });
464
- } catch (e) {
465
- if (e instanceof Error) {
466
- throw new Error(`Failed to commit changes to branch ${branchName}: ${e.message}`);
467
- }
468
- throw new Error(`Failed to commit changes to branch ${branchName}: ${String(e)}`);
546
+ for (const entry of entries) {
547
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
548
+ const full = path4.join(dir, entry.name);
549
+ if (entry.isDirectory()) stack.push(full);
550
+ else if (interesting.test(entry.name)) out.push(full);
551
+ if (out.length >= limit) break;
469
552
  }
470
- return `Successfully applied migration via Git to branch: ${branchName}`;
471
553
  }
472
- async applyWithZipFallback(runId, shadowDir, targetPath) {
473
- const backupDir = path.resolve(process.cwd(), `.metamorph/backups/${runId}_backup`);
474
- fs2.ensureDirSync(backupDir);
475
- fs2.copySync(targetPath, backupDir, {
476
- filter: (src) => !src.includes("node_modules") && !src.includes(".git") && !src.includes(".metamorph")
477
- });
478
- this.copyShadowToTarget(shadowDir, targetPath);
479
- return `Successfully applied migration. Git was not detected, so a backup was saved at: ${backupDir}`;
480
- }
481
- copyShadowToTarget(shadowDir, targetPath) {
482
- const ignoredDirs = ["node_modules", ".git", ".metamorph"];
483
- if (fs2.existsSync(targetPath)) {
484
- const targetItems = fs2.readdirSync(targetPath);
485
- for (const item of targetItems) {
486
- if (ignoredDirs.includes(item)) continue;
487
- fs2.removeSync(path.join(targetPath, item));
488
- }
489
- }
490
- const shadowItems = fs2.readdirSync(shadowDir);
491
- for (const item of shadowItems) {
492
- if (ignoredDirs.includes(item)) continue;
493
- const itemSrc = path.join(shadowDir, item);
494
- const itemDest = path.join(targetPath, item);
495
- fs2.copySync(itemSrc, itemDest, { overwrite: true });
496
- }
554
+ return out;
555
+ }
556
+ function buildNeighborContext(filePath) {
557
+ const sections = [];
558
+ let budget = MAX_TOTAL_CHARS;
559
+ const push = (title, body) => {
560
+ const block = `### ${title}
561
+ \`\`\`
562
+ ${body}
563
+ \`\`\`
564
+ `;
565
+ if (block.length > budget) return;
566
+ budget -= block.length;
567
+ sections.push(block);
568
+ };
569
+ const self = readTruncated(filePath);
570
+ if (self) {
571
+ push(filePath, self);
572
+ } else {
573
+ sections.push(`### ${filePath}
574
+ (file does not exist yet \u2014 you may need to create it)
575
+ `);
497
576
  }
498
- };
499
-
500
- // ../domain/src/entities/catalogs/backendCatalog.ts
501
- var backendCatalog = [
502
- {
503
- source: "nestjs",
504
- target: "express",
505
- description: "Migration from NestJS framework to a plain Express application structure.",
506
- filesToDelete: ["nest-cli.json", "tsconfig.build.json"],
507
- dependenciesToRemove: ["@nestjs/common", "@nestjs/core", "@nestjs/platform-express", "reflect-metadata", "rxjs"],
508
- dependenciesToAdd: { "express": "^4.18.2" },
509
- devDependenciesToRemove: ["@nestjs/cli", "@nestjs/schematics", "@nestjs/testing", "@types/express", "@types/supertest", "source-map-support", "supertest"],
510
- devDependenciesToAdd: { "@types/express": "^4.17.17" },
511
- architecturalRules: [
512
- "Remove all NestJS decorators (@Module, @Controller, @Injectable, @Get, @Post, etc.).",
513
- 'Remove all NestJS core imports (e.g., from "@nestjs/common" or "@nestjs/core").',
514
- "Controllers must be converted into Express Router factory functions or classes that return an Express Router.",
515
- "Modules must be converted to plain dependency composition functions or startup scripts that wire Routers together.",
516
- "Services/Providers must be converted to plain TypeScript classes or functions, with dependencies passed explicitly via constructors (manual dependency injection) since NestJS DI is gone.",
517
- "Make sure to export the router or the configured Express application, not a Nest module."
518
- ],
519
- examples: [
520
- {
521
- description: "Converting a NestJS Controller to an Express Router",
522
- before: `import { Controller, Get } from '@nestjs/common';
523
- import { AppService } from './app.service';
524
-
525
- @Controller()
526
- export class AppController {
527
- constructor(private readonly appService: AppService) {}
528
-
529
- @Get()
530
- getHello(): string {
531
- return this.appService.getHello();
577
+ const importSpecs = self ? localImportSpecs(self) : [];
578
+ const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !== path4.resolve(filePath)));
579
+ const unique = [...new Set(resolved)].slice(0, MAX_FILES);
580
+ for (const dep of unique) {
581
+ const body = readTruncated(dep);
582
+ if (body) push(`imported: ${dep}`, body);
583
+ }
584
+ const nearby = listNearbyModules(filePath, 24).filter((file) => path4.resolve(file) !== path4.resolve(filePath));
585
+ if (nearby.length > 0) {
586
+ const listing = nearby.map((file) => path4.relative(findShadowRoot(filePath) || path4.dirname(filePath), file)).join("\n");
587
+ sections.push(`### Nearby screen / bootstrap files (paths only \u2014 read before inventing a new UI)
588
+ ${listing}
589
+ `);
532
590
  }
533
- }`,
534
- after: `import { Router } from 'express';
535
- import { AppService } from './app.service';
536
-
537
- export function createAppRouter(appService: AppService): Router {
538
- const router = Router();
539
-
540
- router.get('/', (req, res) => {
541
- const result = appService.getHello();
542
- res.send(result);
543
- });
544
-
545
- return router;
546
- }`
547
- }
548
- ]
549
- },
550
- {
551
- source: "express",
552
- target: "nestjs",
553
- description: "Migration from a plain Express application to the NestJS framework.",
554
- dependenciesToRemove: ["express"],
555
- dependenciesToAdd: {
556
- "@nestjs/common": "latest",
557
- "@nestjs/core": "latest",
558
- "@nestjs/platform-express": "latest",
559
- "reflect-metadata": "^0.1.13",
560
- "rxjs": "^7.8.1"
561
- },
562
- devDependenciesToRemove: ["@types/express"],
563
- devDependenciesToAdd: {
564
- "@nestjs/cli": "latest",
565
- "@nestjs/schematics": "latest",
566
- "@nestjs/testing": "latest",
567
- "@types/supertest": "^2.0.12",
568
- "source-map-support": "^0.5.21",
569
- "supertest": "^6.3.3"
570
- },
571
- architecturalRules: [
572
- "Express routing logic must be encapsulated in classes decorated with @Controller().",
573
- "Business logic must be encapsulated in classes decorated with @Injectable() (Providers/Services).",
574
- "All Controllers and Providers must be registered in a class decorated with @Module().",
575
- "Use NestJS decorators for routing (@Get(), @Post(), @Param(), @Body(), etc.) instead of Express route definitions.",
576
- "Rely on NestJS dependency injection instead of manual class instantiation or function passing.",
577
- "The entry point must use NestFactory.create() to bootstrap the application, replacing app.listen().",
578
- "You are authorized to rename, create, or delete files to adhere strictly to the NestJS directory and file structure conventions (e.g., app.module.ts, app.controller.ts, app.service.ts, main.ts)."
579
- ],
580
- examples: [
581
- {
582
- description: "Converting an Express Router to a NestJS Controller and Service",
583
- before: `import { Router } from 'express';
584
- import { getHelloLogic } from './logic';
585
-
586
- export function createAppRouter(): Router {
587
- const router = Router();
588
- router.get('/', (req, res) => {
589
- res.send(getHelloLogic());
590
- });
591
- return router;
592
- }`,
593
- after: `import { Controller, Get, Injectable } from '@nestjs/common';
594
-
595
- @Injectable()
596
- export class AppService {
597
- getHello(): string {
598
- return 'Hello from logic';
599
- }
600
- }
601
-
602
- @Controller()
603
- export class AppController {
604
- constructor(private readonly appService: AppService) {}
605
-
606
- @Get()
607
- getHello(): string {
608
- return this.appService.getHello();
609
- }
610
- }`
611
- }
612
- ]
613
- },
614
- {
615
- source: "nestjs",
616
- target: "fastify",
617
- description: "Migration from NestJS framework to a Fastify application structure.",
618
- filesToDelete: ["nest-cli.json", "tsconfig.build.json"],
619
- dependenciesToRemove: ["@nestjs/common", "@nestjs/core", "@nestjs/platform-express", "reflect-metadata", "rxjs"],
620
- dependenciesToAdd: { "fastify": "latest" },
621
- devDependenciesToRemove: ["@nestjs/cli", "@nestjs/schematics", "@nestjs/testing", "@types/express", "@types/supertest", "source-map-support", "supertest"],
622
- devDependenciesToAdd: { "fastify-plugin": "latest" },
623
- architecturalRules: [
624
- "Remove all NestJS decorators (@Module, @Controller, @Injectable, @Get, @Post, etc.).",
625
- "Remove all NestJS core imports.",
626
- "The entry point (main.ts) must initialize a FastifyInstance (fastify()) instead of NestFactory.",
627
- "Controllers and routing logic must be converted to Fastify route declarations (fastify.get, fastify.post).",
628
- "Ideally, encapsulate routes and modules as Fastify plugins.",
629
- "Remove NestJS dependency injection; use standard JavaScript/TypeScript classes or functions passing dependencies directly.",
630
- "Ensure standard Fastify schema validation (JSON Schema) is added if types or DTOs are available.",
631
- "You are authorized to rename, create, or delete files to adhere to a standard fastify plugin-based directory structure."
632
- ],
633
- examples: [
634
- {
635
- description: "Converting a NestJS Controller to a Fastify Plugin",
636
- before: `import { Controller, Get } from '@nestjs/common';
637
- import { AppService } from './app.service';
638
-
639
- @Controller('users')
640
- export class UsersController {
641
- constructor(private readonly appService: AppService) {}
642
-
643
- @Get()
644
- getUsers() {
645
- return this.appService.getUsers();
646
- }
647
- }`,
648
- after: `import { FastifyInstance } from 'fastify';
649
- import { AppService } from './app.service';
650
-
651
- export default async function usersPlugin(fastify: FastifyInstance, options: any) {
652
- const appService = new AppService(); // Or inject via options
653
-
654
- fastify.get('/users', async (request, reply) => {
655
- return appService.getUsers();
656
- });
657
- }`
658
- }
659
- ]
660
- },
661
- {
662
- source: "express",
663
- target: "fastify",
664
- description: "Migration from a plain Express application to Fastify.",
665
- dependenciesToRemove: ["express"],
666
- dependenciesToAdd: { "fastify": "latest", "fastify-plugin": "latest" },
667
- devDependenciesToRemove: ["@types/express"],
668
- devDependenciesToAdd: {},
669
- architecturalRules: [
670
- "Replace all Express app instantiations with fastify().",
671
- "Replace Express middleware signatures (req, res, next) with Fastify hooks (onRequest, preHandler, etc.) or Fastify plugins.",
672
- "Replace Express routing (app.use(), app.get()) with Fastify routing (fastify.get(), fastify.register()).",
673
- "Replace Express response methods (res.send(), res.json()) with Fastify reply methods (reply.send()) or simply return the payload from the async handler.",
674
- "Migrate standard Express middlewares (like cors, helmet) to their Fastify native equivalents (@fastify/cors, @fastify/helmet) if they are present.",
675
- "You are authorized to rename, create, or delete files to restructure the Express app into modular Fastify plugins."
676
- ],
677
- examples: [
678
- {
679
- description: "Converting an Express Route to a Fastify Route",
680
- before: `const express = require('express');
681
- const app = express();
682
-
683
- app.get('/api/data', (req, res) => {
684
- res.json({ message: 'Hello Express' });
685
- });
686
-
687
- app.listen(3000);`,
688
- after: `import Fastify from 'fastify';
689
-
690
- const fastify = Fastify({ logger: true });
691
-
692
- fastify.get('/api/data', async (request, reply) => {
693
- return { message: 'Hello Fastify' };
694
- });
695
-
696
- fastify.listen({ port: 3000 }, (err) => {
697
- if (err) {
698
- fastify.log.error(err);
699
- process.exit(1);
700
- }
701
- });`
702
- }
703
- ]
704
- },
705
- {
706
- source: "fastify",
707
- target: "express",
708
- description: "Migration from a Fastify application to Express.",
709
- dependenciesToRemove: ["fastify", "fastify-plugin"],
710
- dependenciesToAdd: { "express": "latest" },
711
- devDependenciesToRemove: [],
712
- devDependenciesToAdd: { "@types/express": "latest" },
713
- architecturalRules: [
714
- "Replace all fastify() app instantiations with express().",
715
- "Replace Fastify hooks (onRequest, preHandler, etc.) with standard Express middleware signatures (req, res, next).",
716
- "Replace Fastify routing (fastify.get, fastify.register) with Express routing (app.get, express.Router().use).",
717
- "Replace Fastify reply methods (reply.send) or implicit returns with Express response methods (res.send, res.json).",
718
- "CRITICAL: Fastify catches async errors automatically. When migrating to Express, ensure all asynchronous route handlers use try/catch blocks passing the error to next(err), or wrap them in an async wrapper.",
719
- "You are authorized to rename, create, or delete files to restructure the Fastify plugins into standard Express routers."
720
- ],
721
- examples: [
722
- {
723
- description: "Converting a Fastify Route to an Express Route",
724
- before: `import Fastify from 'fastify';
725
-
726
- const fastify = Fastify();
727
-
728
- fastify.get('/api/data', async (request, reply) => {
729
- return { message: 'Hello Fastify' };
730
- });
731
-
732
- fastify.listen({ port: 3000 });`,
733
- after: `const express = require('express');
734
- const app = express();
735
-
736
- app.get('/api/data', async (req, res, next) => {
737
- try {
738
- res.json({ message: 'Hello Fastify' });
739
- } catch (err) {
740
- next(err);
741
- }
742
- });
743
-
744
- app.listen(3000);`
745
- }
746
- ]
747
- },
748
- {
749
- source: "fastify",
750
- target: "nestjs",
751
- description: "Migration from Fastify framework to NestJS (using platform-express by default).",
752
- dependenciesToRemove: ["fastify", "fastify-plugin"],
753
- dependenciesToAdd: {
754
- "@nestjs/common": "latest",
755
- "@nestjs/core": "latest",
756
- "@nestjs/platform-express": "latest",
757
- "reflect-metadata": "^0.1.13",
758
- "rxjs": "^7.8.1"
759
- },
760
- devDependenciesToRemove: [],
761
- devDependenciesToAdd: {
762
- "@nestjs/cli": "latest",
763
- "@nestjs/schematics": "latest",
764
- "@nestjs/testing": "latest",
765
- "@types/supertest": "^2.0.12",
766
- "source-map-support": "^0.5.21",
767
- "supertest": "^6.3.3"
768
- },
769
- architecturalRules: [
770
- "Fastify routing logic must be encapsulated in classes decorated with @Controller().",
771
- "Business logic must be encapsulated in classes decorated with @Injectable() (Providers/Services).",
772
- "All Controllers and Providers must be registered in a class decorated with @Module().",
773
- "Use NestJS decorators for routing (@Get(), @Post(), @Param(), @Body(), etc.) instead of fastify route definitions.",
774
- "Rely on NestJS dependency injection instead of Fastify decorators or options passing.",
775
- "The entry point must use NestFactory.create() to bootstrap the application, replacing fastify().",
776
- "You are authorized to rename, create, or delete files to adhere strictly to the NestJS directory and file structure conventions (e.g., app.module.ts, app.controller.ts, app.service.ts, main.ts)."
777
- ],
778
- examples: [
779
- {
780
- description: "Converting a Fastify Plugin to a NestJS Controller and Service",
781
- before: `export default async function usersPlugin(fastify, options) {
782
- fastify.get('/users', async (request, reply) => {
783
- return { users: [] };
784
- });
785
- }`,
786
- after: `import { Controller, Get, Injectable } from '@nestjs/common';
787
-
788
- @Injectable()
789
- export class UsersService {
790
- getUsers() {
791
- return { users: [] };
792
- }
793
- }
794
-
795
- @Controller('users')
796
- export class UsersController {
797
- constructor(private readonly usersService: UsersService) {}
798
-
799
- @Get()
800
- getUsers() {
801
- return this.usersService.getUsers();
802
- }
803
- }`
804
- }
805
- ]
806
- }
807
- ];
808
-
809
- // ../domain/src/entities/catalogs/frontendCatalog.ts
810
- var frontendCatalog = [
811
- {
812
- source: "react",
813
- target: "next",
814
- description: "Migration from a plain React SPA (like Vite or Create React App) to Next.js using the App Router.",
815
- filesToDelete: ["vite.config.ts", "vite.config.js", "index.html"],
816
- dependenciesToRemove: ["react-router-dom", "vite", "@vitejs/plugin-react"],
817
- dependenciesToAdd: { "next": "latest", "react": "latest", "react-dom": "latest" },
818
- devDependenciesToRemove: [],
819
- devDependenciesToAdd: {},
820
- architecturalRules: [
821
- 'Migrate standard React routing (e.g. react-router-dom) to Next.js file-system routing in the "app" directory.',
822
- "Convert the main entry point (usually main.tsx or index.tsx) into Next.js app/layout.tsx and app/page.tsx.",
823
- 'For components that use browser-only APIs (like window, useEffect, useState), add the "use client" directive at the very top of the file.',
824
- "Remove any react-router-dom <BrowserRouter>, <Routes>, and <Route> components.",
825
- "Replace react-router-dom <Link> components with Next.js next/link <Link> components.",
826
- "You are authorized to rename, create, or delete files to restructure the source into the Next.js App Router structure (e.g. app/page.tsx, app/layout.tsx)."
827
- ],
828
- examples: [
829
- {
830
- description: "Converting a React Router setup to Next.js App Router",
831
- before: `import { BrowserRouter, Routes, Route } from 'react-router-dom';
832
- import Home from './Home';
833
- import About from './About';
834
-
835
- export default function App() {
836
- return (
837
- <BrowserRouter>
838
- <Routes>
839
- <Route path="/" element={<Home />} />
840
- <Route path="/about" element={<About />} />
841
- </Routes>
842
- </BrowserRouter>
843
- );
844
- }`,
845
- after: `// This routing logic is deleted.
846
- // Instead, create app/page.tsx for Home and app/about/page.tsx for About.`
847
- }
848
- ]
849
- },
850
- {
851
- source: "next",
852
- target: "react",
853
- description: "Migration from Next.js to a plain React SPA using Vite.",
854
- filesToDelete: ["next.config.js", "next.config.mjs", "next.config.ts"],
855
- dependenciesToRemove: ["next"],
856
- dependenciesToAdd: { "react-router-dom": "latest" },
857
- devDependenciesToRemove: [],
858
- devDependenciesToAdd: { "vite": "latest", "@vitejs/plugin-react": "latest" },
859
- architecturalRules: [
860
- "Replace Next.js file-system routing (app/ or pages/) with declarative React Router (react-router-dom) components.",
861
- 'Remove all "use client" and "use server" directives, as they are Next.js specific.',
862
- "Convert Next.js Server Components into standard React Client Components (they will need to fetch data via useEffect or a data-fetching library).",
863
- 'Replace Next.js <Link href="..."> with React Router <Link to="...">.',
864
- "Replace Next.js next/image <Image> with standard HTML <img>.",
865
- "Create a standard Vite entry point (index.html at the root, and src/main.tsx) to bootstrap the React application."
866
- ]
867
- },
868
- {
869
- source: "vue",
870
- target: "react",
871
- description: "Migration from Vue.js (Vue 3 Composition API) to React.",
872
- filesToDelete: ["vite.config.ts", "vue.config.js"],
873
- dependenciesToRemove: ["vue", "vue-router", "pinia"],
874
- dependenciesToAdd: { "react": "latest", "react-dom": "latest", "react-router-dom": "latest" },
875
- devDependenciesToRemove: ["@vitejs/plugin-vue"],
876
- devDependenciesToAdd: { "@vitejs/plugin-react": "latest" },
877
- architecturalRules: [
878
- "Convert all .vue Single File Components (SFC) into .tsx or .jsx React function components.",
879
- "Replace Vue <template> with React JSX/TSX return statements.",
880
- "Replace Vue ref() and reactive() with React useState().",
881
- "Replace Vue computed() with React useMemo().",
882
- "Replace Vue watch() and lifecycle hooks (onMounted, etc.) with React useEffect().",
883
- "Replace vue-router with react-router-dom.",
884
- "Convert Vue props definition (defineProps) into React function component parameters.",
885
- "Convert Vue emit (defineEmits) into React callback props (e.g. onAction={...})."
886
- ]
887
- },
888
- {
889
- source: "react",
890
- target: "vue",
891
- description: "Migration from React to Vue.js (Vue 3 Composition API).",
892
- filesToDelete: ["vite.config.ts"],
893
- dependenciesToRemove: ["react", "react-dom", "react-router-dom"],
894
- dependenciesToAdd: { "vue": "latest", "vue-router": "latest" },
895
- devDependenciesToRemove: ["@vitejs/plugin-react"],
896
- devDependenciesToAdd: { "@vitejs/plugin-vue": "latest" },
897
- architecturalRules: [
898
- "Convert React .tsx/.jsx function components into Vue 3 .vue Single File Components (using <script setup>).",
899
- "Move the JSX return statement into the Vue <template> block.",
900
- "Replace React useState() with Vue ref() or reactive().",
901
- "Replace React useMemo() with Vue computed().",
902
- "Replace React useEffect() with Vue watch() or lifecycle hooks (onMounted).",
903
- "Replace react-router-dom with vue-router.",
904
- "Extract component props into defineProps().",
905
- "Extract callback props (e.g. onClick) into defineEmits() and emit events."
906
- ]
907
- }
908
- ];
909
-
910
- // ../domain/src/entities/catalogs/index.ts
911
- var MigrationCatalog = [
912
- ...backendCatalog,
913
- ...frontendCatalog
914
- ];
915
- function findMigrationCatalogEntry(source, target) {
916
- return MigrationCatalog.find(
917
- (entry) => entry.source.toLowerCase() === source.toLowerCase() && entry.target.toLowerCase() === target.toLowerCase()
918
- );
919
- }
920
-
921
- // ../infrastructure/src/server/api.ts
922
- async function createApiServer(store, migrationRunner) {
923
- const express = (await import("express")).default;
924
- const cors = (await import("cors")).default;
925
- const app = express();
926
- app.use(cors());
927
- app.use(express.json());
928
- app.get("/api/plans", async (_req, res) => {
929
- try {
930
- const plans = await store.getAllPlans();
931
- res.json(plans);
932
- } catch (error) {
933
- if (error instanceof Error) {
934
- res.status(500).json({ error: error.message });
935
- } else {
936
- res.status(500).json({ error: String(error) });
937
- }
938
- }
939
- });
940
- app.get("/api/events", async (_req, res) => {
941
- try {
942
- const events = await store.getEvents();
943
- res.json(events);
944
- } catch (error) {
945
- if (error instanceof Error) {
946
- res.status(500).json({ error: error.message });
947
- } else {
948
- res.status(500).json({ error: String(error) });
949
- }
950
- }
951
- });
952
- app.get("/api/browse", async (req, res) => {
953
- try {
954
- const { readdirSync: readdirSync2, statSync: statSync2 } = await import("fs");
955
- const { resolve: resolve3, join: join8 } = await import("path");
956
- const targetPath = req.query.path ? resolve3(String(req.query.path)) : resolve3(".");
957
- const entries = readdirSync2(targetPath).filter(
958
- (name) => !name.startsWith(".") && name !== "node_modules" && name !== "dist" && name !== "build"
959
- ).map((name) => {
960
- const full = join8(targetPath, name);
961
- try {
962
- const stat = statSync2(full);
963
- return {
964
- name,
965
- path: full,
966
- isDirectory: stat.isDirectory()
967
- };
968
- } catch {
969
- return null;
970
- }
971
- }).filter(Boolean);
972
- res.json({
973
- current: targetPath,
974
- parent: resolve3(targetPath, ".."),
975
- entries
976
- });
977
- } catch (error) {
978
- if (error instanceof Error) {
979
- res.status(500).json({ error: error.message });
980
- } else {
981
- res.status(500).json({ error: String(error) });
982
- }
983
- }
984
- });
985
- app.get("/api/detect", async (req, res) => {
986
- try {
987
- const { detectTechnologies: detectTechnologies2 } = await import("./TechDetector-5YP44RVR.js");
988
- const targetPath = req.query.path ? String(req.query.path) : ".";
989
- const detected = detectTechnologies2(targetPath);
990
- res.json({
991
- detected,
992
- primary: detected.length > 0 ? detected[0].framework : null
993
- });
994
- } catch (error) {
995
- if (error instanceof Error) {
996
- res.status(500).json({ error: error.message });
997
- } else {
998
- res.status(500).json({ error: String(error) });
999
- }
1000
- }
1001
- });
1002
- if (migrationRunner) {
1003
- app.post("/api/migrations/start", async (req, res) => {
1004
- try {
1005
- const { targetPath, from, to } = req.body;
1006
- if (!targetPath || !from || !to) {
1007
- res.status(400).json({
1008
- error: "Missing required fields: targetPath, from, to"
1009
- });
1010
- return;
1011
- }
1012
- const catalogEntry = findMigrationCatalogEntry(from, to);
1013
- if (!catalogEntry) {
1014
- res.status(400).json({
1015
- error: `Unsupported migration profile: ${from} -> ${to}. Please check the supported catalog.`
1016
- });
1017
- return;
1018
- }
1019
- const result = await migrationRunner.startMigration({
1020
- targetPath,
1021
- from,
1022
- to
1023
- });
1024
- res.json({ ok: true, ...result });
1025
- } catch (error) {
1026
- if (error instanceof Error) {
1027
- res.status(500).json({ error: error.message });
1028
- } else {
1029
- res.status(500).json({ error: String(error) });
1030
- }
1031
- }
1032
- });
1033
- app.post("/api/migrations/apply", async (req, res) => {
1034
- try {
1035
- const { runId, targetPath } = req.body;
1036
- if (!runId || !targetPath) {
1037
- res.status(400).json({
1038
- error: "Missing required fields: runId, targetPath"
1039
- });
1040
- return;
1041
- }
1042
- const integrator = new MigrationIntegrator(migrationRunner.workspace);
1043
- const message = await integrator.applyMigration(runId, targetPath);
1044
- res.json({ ok: true, message });
1045
- } catch (error) {
1046
- if (error instanceof Error) {
1047
- res.status(500).json({ error: error.message });
1048
- } else {
1049
- res.status(500).json({ error: String(error) });
1050
- }
1051
- }
1052
- });
1053
- app.post("/api/migrations/rollback", async (req, res) => {
1054
- try {
1055
- const { runId } = req.body;
1056
- if (!runId) {
1057
- res.status(400).json({
1058
- error: "Missing required field: runId"
1059
- });
1060
- return;
1061
- }
1062
- await migrationRunner.rollbackMigration(runId);
1063
- res.json({ ok: true, message: `Rolled back run: ${runId}` });
1064
- } catch (error) {
1065
- if (error instanceof Error) {
1066
- res.status(500).json({ error: error.message });
1067
- } else {
1068
- res.status(500).json({ error: String(error) });
1069
- }
1070
- }
1071
- });
1072
- app.post("/api/migrations/reset", async (_req, res) => {
1073
- try {
1074
- await store.reset();
1075
- res.json({ ok: true, message: "Migration state reset successfully" });
1076
- } catch (error) {
1077
- if (error instanceof Error) {
1078
- res.status(500).json({ error: error.message });
1079
- } else {
1080
- res.status(500).json({ error: String(error) });
1081
- }
1082
- }
1083
- });
1084
- }
1085
- const path6 = await import("path");
1086
- const fs7 = await import("fs");
1087
- let publicDir = path6.join(import.meta.dirname, "public");
1088
- if (!fs7.existsSync(publicDir)) {
1089
- publicDir = path6.join(import.meta.dirname, "../public");
1090
- }
1091
- if (fs7.existsSync(publicDir)) {
1092
- app.use(express.static(publicDir));
1093
- app.get(/^(.*)$/, (_req, res) => {
1094
- res.sendFile(path6.join(publicDir, "index.html"));
1095
- });
1096
- }
1097
- return app;
1098
- }
1099
-
1100
- // ../application/src/agents/MapperAgent.ts
1101
- import * as fs3 from "fs";
1102
- import * as path2 from "path";
1103
- import {
1104
- createAgent,
1105
- SituationSpecification
1106
- } from "@mozaik-ai/core";
1107
- var WhenMigrationStarts = class extends SituationSpecification {
1108
- isSatisfiedBy({ event }) {
1109
- return event.type === "migration.started" /* MIGRATION_STARTED */;
1110
- }
1111
- };
1112
- var mapFilesProcessor = {
1113
- async apply({ event, participant }) {
1114
- const payload = event.payload;
1115
- console.log(`[MapperAgent] Migration started for Plan: ${payload.planId}`);
1116
- const shadowDir = payload.shadowWorkspacePath || "";
1117
- if (!shadowDir) {
1118
- console.warn("[MapperAgent] No shadow workspace provided. Skipping.");
1119
- return;
1120
- }
1121
- const filesToMigrate = [];
1122
- const scanDir = (dir) => {
1123
- if (!fs3.existsSync(dir)) return;
1124
- const files = fs3.readdirSync(dir);
1125
- const ignoredDirs = ["node_modules", "dist", "build", "out", "coverage", ".next"];
1126
- for (const file of files) {
1127
- if (ignoredDirs.includes(file) || file.startsWith(".")) continue;
1128
- const fullPath = path2.join(dir, file);
1129
- if (fs3.statSync(fullPath).isDirectory()) {
1130
- scanDir(fullPath);
1131
- } else if (file.endsWith(".ts") || file.endsWith(".js")) {
1132
- filesToMigrate.push(fullPath);
1133
- }
1134
- }
1135
- };
1136
- scanDir(shadowDir);
1137
- const runtime = resolveRuntime();
1138
- const repository = runtime.state.repository;
1139
- for (const discoveredFile of filesToMigrate) {
1140
- console.log(`[MapperAgent] Discovered file: ${discoveredFile}`);
1141
- await repository.updateTaskStatus(payload.planId, discoveredFile, "pending");
1142
- sendEvent(
1143
- {
1144
- type: "file.discovered" /* FILE_DISCOVERED */,
1145
- producerId: participant.getId(),
1146
- occurredAt: /* @__PURE__ */ new Date(),
1147
- payload: {
1148
- planId: payload.planId,
1149
- filePath: discoveredFile
1150
- }
1151
- },
1152
- participant.getId()
1153
- );
1154
- }
1155
- }
1156
- };
1157
- var mapFilesHandler = {
1158
- specification: new WhenMigrationStarts(),
1159
- processor: mapFilesProcessor
1160
- };
1161
- function createMapperAgent() {
1162
- return createAgent({
1163
- name: "Mapper",
1164
- capabilities: ["code_analysis"],
1165
- instruction: "You are the Architect Mapper. Your job is to analyze the source code and discover files that need migration.",
1166
- tools: [],
1167
- // AST scanning tools will go here later
1168
- handlers: [mapFilesHandler]
1169
- });
1170
- }
1171
-
1172
- // ../application/src/agents/WorkerAgent.ts
1173
- import * as fs4 from "fs";
1174
- import {
1175
- createAgent as createAgent2,
1176
- SituationSpecification as SituationSpecification2
1177
- } from "@mozaik-ai/core";
1178
-
1179
- // ../application/src/utils/FileTreeBuilder.ts
1180
- import * as path3 from "path";
1181
- function getCommonBaseDir(filePaths) {
1182
- if (!filePaths || filePaths.length === 0) return "";
1183
- if (filePaths.length === 1) return path3.dirname(filePaths[0]);
1184
- const splitPaths = filePaths.map((p) => path3.normalize(p).split(path3.sep));
1185
- let commonPath = "";
1186
- for (let i = 0; i < splitPaths[0].length; i++) {
1187
- const part = splitPaths[0][i];
1188
- const isCommon = splitPaths.every((parts) => parts[i] === part);
1189
- if (isCommon) {
1190
- commonPath = commonPath === "" ? part : commonPath + path3.sep + part;
1191
- } else {
1192
- break;
1193
- }
1194
- }
1195
- return commonPath;
1196
- }
1197
- function buildFileTree(filePaths) {
1198
- if (!filePaths || filePaths.length === 0) return "No files found.";
1199
- const baseDir = getCommonBaseDir(filePaths);
1200
- const relativePaths = filePaths.map((p) => path3.relative(baseDir, p)).filter((p) => p.length > 0).sort();
1201
- if (relativePaths.length === 0) return "No files found.";
1202
- const tree = {};
1203
- for (const relPath of relativePaths) {
1204
- const parts = relPath.split(path3.sep);
1205
- let current = tree;
1206
- for (let i = 0; i < parts.length; i++) {
1207
- const part = parts[i];
1208
- if (i === parts.length - 1) {
1209
- current[part] = null;
1210
- } else {
1211
- current[part] = current[part] || {};
1212
- current = current[part];
1213
- }
1214
- }
1215
- }
1216
- let output = `/
1217
- `;
1218
- function renderTree(node, prefix = "") {
1219
- if (node === null) return;
1220
- const keys = Object.keys(node);
1221
- for (let i = 0; i < keys.length; i++) {
1222
- const key = keys[i];
1223
- const isLast = i === keys.length - 1;
1224
- const isFile = node[key] === null;
1225
- const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1226
- output += `${prefix}${connector}${key}
1227
- `;
1228
- if (!isFile) {
1229
- const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
1230
- renderTree(node[key], nextPrefix);
1231
- }
1232
- }
1233
- }
1234
- renderTree(tree);
1235
- return output.trim();
1236
- }
591
+ if (sections.length === 0) return "";
592
+ return `
593
+ Evidence from disk (read this before editing; match existing exports and callback prop names):
594
+ ${sections.join("\n")}`;
595
+ }
1237
596
 
1238
597
  // ../application/src/agents/WorkerAgent.ts
1239
- var retryCountMap = /* @__PURE__ */ new Map();
1240
598
  var MAX_RETRIES = 2;
1241
599
  var ConcurrencyQueue = class {
1242
600
  constructor(concurrencyLimit) {
@@ -1266,7 +624,7 @@ var ConcurrencyQueue = class {
1266
624
  }
1267
625
  }
1268
626
  };
1269
- var workerQueue = new ConcurrencyQueue(2);
627
+ var workerQueue = new ConcurrencyQueue(3);
1270
628
  var WhenFileDiscovered = class extends SituationSpecification2 {
1271
629
  isSatisfiedBy({ event }) {
1272
630
  return event.type === "file.discovered" /* FILE_DISCOVERED */;
@@ -1290,7 +648,7 @@ var WhenInferenceCompleted = class extends SituationSpecification2 {
1290
648
  async function startWorkerLoop(planId, filePath, prompt, participant) {
1291
649
  return new Promise(async (originalResolve) => {
1292
650
  let isDone = false;
1293
- const resolve3 = () => {
651
+ const resolve5 = () => {
1294
652
  if (!isDone) {
1295
653
  isDone = true;
1296
654
  originalResolve();
@@ -1298,13 +656,12 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
1298
656
  };
1299
657
  const runtime = resolveRuntime();
1300
658
  const repository = runtime.state.repository;
1301
- console.log(`[WorkerAgent] Marking file as 'in_progress' in database...`);
1302
659
  await repository.updateTaskStatus(planId, filePath, "in_progress");
1303
660
  const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
1304
661
  const tempAgent = createMozaikAgent({
1305
662
  name: `Worker-${Date.now()}-${Math.floor(Math.random() * 1e3)}`,
1306
663
  capabilities: ["code_refactoring", "inference"],
1307
- instruction: "You are the Programmer Worker. Your job is to refactor specific files based on the migration profile. If the target framework requires a different file structure, you are authorized to use your tools to rename, create, or delete files to adhere to architectural rules. When you are done modifying the file(s), simply finish your response.",
664
+ 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.",
1308
665
  tools: participant.getTools(),
1309
666
  handlers: [
1310
667
  {
@@ -1323,7 +680,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
1323
680
  } catch (e) {
1324
681
  console.error(`[WorkerAgent:${tempParticipant.getId()}] Error reading file:`, e);
1325
682
  }
1326
- await repository.updateTaskStatus(planId, filePath, "completed");
683
+ await repository.updateTaskStatus(planId, filePath, "in_progress");
1327
684
  sendEvent(
1328
685
  {
1329
686
  type: "file.migrated" /* FILE_MIGRATED */,
@@ -1338,7 +695,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
1338
695
  tempParticipant.getId()
1339
696
  );
1340
697
  leave(tempParticipant);
1341
- resolve3();
698
+ resolve5();
1342
699
  }
1343
700
  }
1344
701
  }
@@ -1357,8 +714,14 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
1357
714
  if (!isDone) {
1358
715
  console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out.`);
1359
716
  await repository.updateTaskStatus(planId, filePath, "failed", "Inference timed out");
717
+ sendEvent({
718
+ type: "file.failed" /* FILE_FAILED */,
719
+ producerId: tempAgent.getId(),
720
+ occurredAt: /* @__PURE__ */ new Date(),
721
+ payload: { planId, filePath, reason: "Inference timed out" }
722
+ }, tempAgent.getId());
1360
723
  leave(tempAgent);
1361
- resolve3();
724
+ resolve5();
1362
725
  }
1363
726
  }, 45e3);
1364
727
  } catch (error) {
@@ -1366,8 +729,14 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
1366
729
  if (!isDone) {
1367
730
  const errorMessage = error instanceof Error ? error.message : String(error);
1368
731
  await repository.updateTaskStatus(planId, filePath, "failed", errorMessage || "Sync error");
732
+ sendEvent({
733
+ type: "file.failed" /* FILE_FAILED */,
734
+ producerId: tempAgent.getId(),
735
+ occurredAt: /* @__PURE__ */ new Date(),
736
+ payload: { planId, filePath, reason: errorMessage || "Sync error" }
737
+ }, tempAgent.getId());
1369
738
  leave(tempAgent);
1370
- resolve3();
739
+ resolve5();
1371
740
  }
1372
741
  }
1373
742
  });
@@ -1386,10 +755,10 @@ var workOnFileProcessor = {
1386
755
  const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
1387
756
  if (catalogEntry) {
1388
757
  prompt += `
1389
- Strict Architectural Rules for this Migration:
758
+ Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
759
+ `;
760
+ prompt += `${formatCatalogRules(catalogEntry.ruleSections)}
1390
761
  `;
1391
- catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
1392
- `);
1393
762
  if (catalogEntry.examples && catalogEntry.examples.length > 0) {
1394
763
  prompt += `
1395
764
  Reference Examples:
@@ -1426,8 +795,20 @@ ${projectTree}
1426
795
  prompt += `Use this context to ensure your changes align with the overall project structure and other files.
1427
796
  `;
1428
797
  }
798
+ prompt += collectFileHints(
799
+ payload.filePath,
800
+ { source: plan.profile.source, target: plan.profile.target },
801
+ catalogEntry?.layer
802
+ );
1429
803
  }
1430
- prompt += `Please read the file using your tools, rewrite it according to the rules, and write it back. If the rules require breaking this file into multiple files, use your create, rename, or delete file tools accordingly.`;
804
+ prompt += buildNeighborContext(payload.filePath);
805
+ prompt += `
806
+ You are not a mechanical find-replace bot. Deduce the correct change from the evidence above and from tools (read_file, list_directory).
807
+ Before writing:
808
+ 1. Read every local import you will call.
809
+ 2. Keep that module's public prop/export names exactly.
810
+ 3. If this is a router/bootstrap file, import an existing screen from the nearby paths \u2014 do not paste a new copy of its JSX.
811
+ Then write the file(s). If structure must change, create/rename/delete accordingly.`;
1431
812
  workerQueue.enqueue(async () => {
1432
813
  await startWorkerLoop(payload.planId, payload.filePath, prompt, participant);
1433
814
  });
@@ -1437,22 +818,39 @@ var fixRejectedFileProcessor = {
1437
818
  async apply({ event, participant }) {
1438
819
  const payload = event.payload;
1439
820
  const retryKey = `${payload.planId}:${payload.filePath}`;
1440
- const currentRetries = retryCountMap.get(retryKey) || 0;
821
+ const retries = resolveRuntime().state.retryCounts;
822
+ if (payload.source === "integration") {
823
+ retries.delete(retryKey);
824
+ }
825
+ const currentRetries = retries.get(retryKey) || 0;
1441
826
  if (currentRetries >= MAX_RETRIES) {
1442
827
  console.warn(`[WorkerAgent] File ${payload.filePath} exceeded max retries (${MAX_RETRIES}). Marking as failed.`);
1443
828
  const runtime2 = resolveRuntime();
1444
829
  await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", `Exceeded max retries (${MAX_RETRIES})`);
1445
- retryCountMap.delete(retryKey);
830
+ retries.delete(retryKey);
831
+ sendEvent({
832
+ type: "file.failed" /* FILE_FAILED */,
833
+ producerId: participant.getId(),
834
+ occurredAt: /* @__PURE__ */ new Date(),
835
+ payload: { planId: payload.planId, filePath: payload.filePath, reason: `Exceeded max retries (${MAX_RETRIES})` }
836
+ }, participant.getId());
1446
837
  return;
1447
838
  }
1448
- retryCountMap.set(retryKey, currentRetries + 1);
839
+ retries.set(retryKey, currentRetries + 1);
1449
840
  console.log(`[WorkerAgent] File REJECTED (attempt ${currentRetries + 1}/${MAX_RETRIES}), initiating repair loop: ${payload.filePath}`);
1450
841
  const runtime = resolveRuntime();
1451
842
  const plan = await runtime.state.repository.getPlan(payload.planId);
1452
843
  let prompt = `You need to FIX the file at path: ${payload.filePath}
1453
844
  `;
1454
- prompt += `Your previous migration was REJECTED by the Quality Assurance Reviewer.
845
+ if (payload.source === "integration") {
846
+ prompt += `The Integration Agent rejected this file because the shadow workspace failed npm install / npm run build, or a catalog verifier flagged it.
847
+ `;
848
+ prompt += `If the file does not exist yet, create it. If it is a router/bootstrap file, import existing screens \u2014 do not re-implement them or rename their callback props.
849
+ `;
850
+ } else {
851
+ prompt += `Your previous migration was REJECTED by the Quality Assurance Reviewer.
1455
852
  `;
853
+ }
1456
854
  prompt += `Feedback/Errors:
1457
855
  ${payload.errors.join("\n")}
1458
856
 
@@ -1463,10 +861,10 @@ ${payload.errors.join("\n")}
1463
861
  const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
1464
862
  if (catalogEntry) {
1465
863
  prompt += `
1466
- Strict Architectural Rules for this Migration:
864
+ Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
865
+ `;
866
+ prompt += `${formatCatalogRules(catalogEntry.ruleSections)}
1467
867
  `;
1468
- catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
1469
- `);
1470
868
  }
1471
869
  if (plan.tasks && plan.tasks.length > 0) {
1472
870
  const allFiles = plan.tasks.map((t) => t.filePath);
@@ -1481,19 +879,26 @@ ${projectTree}
1481
879
  prompt += `Ensure you respect this overall project structure while fixing the issues.
1482
880
  `;
1483
881
  }
882
+ prompt += collectFileHints(
883
+ payload.filePath,
884
+ { source: plan.profile.source, target: plan.profile.target },
885
+ catalogEntry?.layer
886
+ );
1484
887
  }
1485
- prompt += `Please read the file, fix the issues mentioned, and write it back. You may restructure the files if the architectural rules demand it.`;
888
+ prompt += buildNeighborContext(payload.filePath);
889
+ prompt += `
890
+ Deduce the fix from the errors and the disk evidence. Read imported modules (and list_directory if you need another screen). Do not guess APIs. Then write the corrected file(s). If the path does not exist, create it. Do not delete existing screens just to make a router file compile.`;
1486
891
  workerQueue.enqueue(async () => {
1487
892
  await startWorkerLoop(payload.planId, payload.filePath, prompt, participant);
1488
893
  });
1489
894
  }
1490
895
  };
1491
896
  var fatalMismatchProcessor = {
1492
- async apply({ event }) {
897
+ async apply({ event, participant }) {
1493
898
  const payload = event.payload;
1494
899
  console.error(`[WorkerAgent] \u{1F6D1} FATAL MISMATCH received for ${payload.filePath}. Reason: ${payload.reason}`);
1495
900
  const retryKey = `${payload.planId}:${payload.filePath}`;
1496
- retryCountMap.delete(retryKey);
901
+ resolveRuntime().state.retryCounts.delete(retryKey);
1497
902
  const runtime = resolveRuntime();
1498
903
  await runtime.state.repository.updateTaskStatus(
1499
904
  payload.planId,
@@ -1501,6 +906,12 @@ var fatalMismatchProcessor = {
1501
906
  "failed",
1502
907
  `Fatal Architectural Mismatch: ${payload.reason}`
1503
908
  );
909
+ sendEvent({
910
+ type: "file.failed" /* FILE_FAILED */,
911
+ producerId: participant.getId(),
912
+ occurredAt: /* @__PURE__ */ new Date(),
913
+ payload: { planId: payload.planId, filePath: payload.filePath, reason: payload.reason }
914
+ }, participant.getId());
1504
915
  }
1505
916
  };
1506
917
  function createWorkerAgent(tools) {
@@ -1522,6 +933,7 @@ import {
1522
933
  createAgent as createAgent3,
1523
934
  SituationSpecification as SituationSpecification3
1524
935
  } from "@mozaik-ai/core";
936
+ import * as path5 from "path";
1525
937
  var ConcurrencyQueue2 = class {
1526
938
  constructor(concurrencyLimit) {
1527
939
  this.concurrencyLimit = concurrencyLimit;
@@ -1550,7 +962,7 @@ var ConcurrencyQueue2 = class {
1550
962
  }
1551
963
  }
1552
964
  };
1553
- var reviewerQueue = new ConcurrencyQueue2(2);
965
+ var reviewerQueue = new ConcurrencyQueue2(3);
1554
966
  var WhenFileMigrated = class extends SituationSpecification3 {
1555
967
  isSatisfiedBy({ event }) {
1556
968
  return event.type === "file.migrated" /* FILE_MIGRATED */;
@@ -1574,7 +986,7 @@ var reviewFileProcessor = {
1574
986
  reviewerQueue.enqueue(async () => {
1575
987
  return new Promise(async (originalResolve) => {
1576
988
  let isDone = false;
1577
- const resolve3 = () => {
989
+ const resolve5 = () => {
1578
990
  if (!isDone) {
1579
991
  isDone = true;
1580
992
  console.log(`[DEBUG:ReviewerAgent] Promise resolved for ${payload.filePath}`);
@@ -1582,12 +994,12 @@ var reviewFileProcessor = {
1582
994
  }
1583
995
  };
1584
996
  const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
1585
- const { sendEvent: sendEvent3 } = await import("./runtime-5OVGH2Y7.js");
997
+ const { sendEvent: sendEvent3 } = await import("./runtime-L7VOQROE.js");
1586
998
  const reviewerId = `Reviewer-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
1587
999
  const tempAgent = createMozaikAgent({
1588
1000
  name: reviewerId,
1589
1001
  capabilities: ["code_review", "inference"],
1590
- instruction: "You are the Quality Assurance Reviewer. You must review the migrated code. Note that you are reviewing a SINGLE FILE within a larger migration. Evaluate if the file violates architectural rules, but DO NOT reject it simply because it does not demonstrate all architectural features (e.g. do not reject a test file or a bootstrap file just because it lacks a Controller). If the worker completely violated the architectural paradigm in a way that cannot be fixed by a simple code edit, use status FATAL_MISMATCH to abort.",
1002
+ instruction: "You are a staff engineer reviewing a migrated file against its neighbors. Reject if public props/emits do not match imported modules, if a router file re-implements a screen instead of importing it, or if a screen was hollowed out. UI that lived under src/pages in a SPA is not a Next Pages Router file. Do not reject a file just because it lacks every catalog feature. FATAL_MISMATCH only for an unfixable paradigm break.",
1591
1003
  tools: [],
1592
1004
  // No tools to avoid Gemini thought_signature crash
1593
1005
  handlers: [
@@ -1624,6 +1036,8 @@ var reviewFileProcessor = {
1624
1036
  try {
1625
1037
  if (reviewResult.status === "APPROVED") {
1626
1038
  console.log(`[ReviewerAgent:${reviewerId}] File ${payload.filePath} APPROVED!`);
1039
+ const runtime2 = await resolveRuntime();
1040
+ await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "completed");
1627
1041
  sendEvent3({
1628
1042
  type: "file.reviewed" /* FILE_REVIEWED */,
1629
1043
  producerId: participantId,
@@ -1650,27 +1064,9 @@ var reviewFileProcessor = {
1650
1064
  } catch (e) {
1651
1065
  console.error(`[ReviewerAgent:${reviewerId}] Failed to send review event:`, e);
1652
1066
  }
1653
- try {
1654
- const runtime2 = await resolveRuntime();
1655
- const plan2 = await runtime2.state.repository.getPlan(payload.planId);
1656
- if (plan2) {
1657
- const allTerminal = plan2.tasks.every((t) => t.status === "completed" || t.status === "failed");
1658
- if (allTerminal) {
1659
- console.log(`[App] All tasks reached terminal state. Emitting MIGRATION_COMPLETED.`);
1660
- sendEvent3({
1661
- type: "migration.completed" /* MIGRATION_COMPLETED */,
1662
- producerId: participantId,
1663
- occurredAt: /* @__PURE__ */ new Date(),
1664
- payload: { planId: payload.planId }
1665
- }, participantId);
1666
- }
1667
- }
1668
- } catch (e) {
1669
- console.error(`[ReviewerAgent:${reviewerId}] Failed to check migration completion:`, e);
1670
- }
1671
1067
  }
1672
1068
  leave(tempParticipant);
1673
- resolve3();
1069
+ resolve5();
1674
1070
  }
1675
1071
  }
1676
1072
  }
@@ -1680,16 +1076,58 @@ var reviewFileProcessor = {
1680
1076
  join(tempAgent);
1681
1077
  console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent joined the runtime`);
1682
1078
  const runtime = resolveRuntime();
1079
+ const { existsSync: existsSync8 } = await import("fs");
1080
+ if (!existsSync8(payload.filePath)) {
1081
+ const { sendEvent: sendEvent4 } = await import("./runtime-L7VOQROE.js");
1082
+ const planForMissing = await runtime.state.repository.getPlan(payload.planId);
1083
+ const target = planForMissing?.profile.target;
1084
+ 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.`);
1091
+ sendEvent4({
1092
+ type: "file.rejected" /* FILE_REJECTED */,
1093
+ producerId: tempAgent.getId(),
1094
+ occurredAt: /* @__PURE__ */ new Date(),
1095
+ payload: {
1096
+ planId: payload.planId,
1097
+ 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
+ ]
1103
+ }
1104
+ }, tempAgent.getId());
1105
+ leave(tempAgent);
1106
+ resolve5();
1107
+ return;
1108
+ }
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
+ leave(tempAgent);
1118
+ resolve5();
1119
+ return;
1120
+ }
1683
1121
  if (payload.filePath.endsWith(".ts") || payload.filePath.endsWith(".tsx")) {
1684
1122
  try {
1685
- const { Project: Project2 } = await import("ts-morph");
1686
- const tsProject = new Project2();
1123
+ const { Project } = await import("ts-morph");
1124
+ const tsProject = new Project();
1687
1125
  const sf = tsProject.addSourceFileAtPath(payload.filePath);
1688
1126
  const diagnostics = sf.getPreEmitDiagnostics();
1689
1127
  const syntaxErrors = diagnostics.filter((d) => d.getCode() >= 1e3 && d.getCode() < 2e3);
1690
1128
  if (syntaxErrors.length > 0) {
1691
1129
  console.error(`[ReviewerAgent:${reviewerId}] Syntax Error in ${payload.filePath}. Rejecting immediately.`);
1692
- const { sendEvent: sendEvent4 } = await import("./runtime-5OVGH2Y7.js");
1130
+ const { sendEvent: sendEvent4 } = await import("./runtime-L7VOQROE.js");
1693
1131
  sendEvent4({
1694
1132
  type: "file.rejected" /* FILE_REJECTED */,
1695
1133
  producerId: tempAgent.getId(),
@@ -1701,7 +1139,7 @@ var reviewFileProcessor = {
1701
1139
  }
1702
1140
  }, tempAgent.getId());
1703
1141
  leave(tempAgent);
1704
- resolve3();
1142
+ resolve5();
1705
1143
  return;
1706
1144
  }
1707
1145
  } catch (e) {
@@ -1709,6 +1147,28 @@ var reviewFileProcessor = {
1709
1147
  }
1710
1148
  }
1711
1149
  const plan = await runtime.state.repository.getPlan(payload.planId);
1150
+ if (plan && plan.profile.source.toLowerCase() === "react" && plan.profile.target.toLowerCase() === "next") {
1151
+ const shadowRoot = findShadowRoot(payload.filePath);
1152
+ if (shadowRoot) {
1153
+ const current = path5.resolve(payload.filePath);
1154
+ const structureIssues = analyzeReactToNextStructure(shadowRoot).filter(
1155
+ (issue) => path5.resolve(issue.filePath) === current
1156
+ );
1157
+ if (structureIssues.length > 0) {
1158
+ const errors = structureIssues.flatMap((issue) => issue.errors);
1159
+ console.error(`[ReviewerAgent:${reviewerId}] Structure verifier rejected ${payload.filePath}.`, errors);
1160
+ sendEvent3({
1161
+ type: "file.rejected" /* FILE_REJECTED */,
1162
+ producerId: tempAgent.getId(),
1163
+ occurredAt: /* @__PURE__ */ new Date(),
1164
+ payload: { planId: payload.planId, filePath: payload.filePath, errors }
1165
+ }, tempAgent.getId());
1166
+ leave(tempAgent);
1167
+ resolve5();
1168
+ return;
1169
+ }
1170
+ }
1171
+ }
1712
1172
  let prompt = `Review the following file migration: ${payload.filePath}
1713
1173
  `;
1714
1174
  if (plan) {
@@ -1717,360 +1177,1579 @@ var reviewFileProcessor = {
1717
1177
  const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
1718
1178
  if (catalogEntry) {
1719
1179
  prompt += `
1720
- Strict Architectural Rules for this Migration (apply only if relevant to this file's purpose):
1180
+ Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair; apply only if relevant to this file):
1721
1181
  `;
1722
- catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
1723
- `);
1182
+ prompt += `${formatCatalogRules(catalogEntry.ruleSections)}
1183
+ `;
1184
+ }
1185
+ prompt += collectFileHints(
1186
+ payload.filePath,
1187
+ { source: plan.profile.source, target: plan.profile.target },
1188
+ catalogEntry?.layer
1189
+ );
1190
+ }
1191
+ prompt += `
1192
+ ${buildNeighborContext(payload.filePath)}
1193
+ Diff or new content of the file under review:
1194
+ ${payload.diff}
1195
+ Compare the diff to imported modules in the evidence. If props/exports do not match, REJECT with a specific error. You MUST respond in JSON.`;
1196
+ const modelToUse = process.env.METAMORPH_REVIEWER_MODEL || process.env.METAMORPH_MODEL || "gpt-5.4";
1197
+ console.log(`[ReviewerAgent:${reviewerId}] Starting inference loop for ${payload.filePath} with model ${modelToUse}...`);
1198
+ try {
1199
+ console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Calling runLoop with agentId: ${tempAgent.getId()}`);
1200
+ runLoop(tempAgent.getId(), prompt, {
1201
+ model: modelToUse,
1202
+ context: tempAgent.getMemory().getContext(),
1203
+ structuredOutput: {
1204
+ name: "review_result",
1205
+ schema: {
1206
+ type: "object",
1207
+ properties: {
1208
+ status: { type: "string", enum: ["APPROVED", "REJECTED", "FATAL_MISMATCH"] },
1209
+ errors: { type: "array", items: { type: "string" } }
1210
+ },
1211
+ required: ["status", "errors"],
1212
+ additionalProperties: false
1213
+ },
1214
+ strict: true
1215
+ }
1216
+ });
1217
+ console.log(`[DEBUG:ReviewerAgent:${reviewerId}] runLoop called successfully (fire-and-forget)`);
1218
+ setTimeout(async () => {
1219
+ if (!isDone) {
1220
+ console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 45s for ${payload.filePath}. Model never responded.`);
1221
+ const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
1222
+ const runtime2 = resolveRuntime2();
1223
+ await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after 45s");
1224
+ sendEvent3({
1225
+ type: "file.failed" /* FILE_FAILED */,
1226
+ producerId: tempAgent.getId(),
1227
+ occurredAt: /* @__PURE__ */ new Date(),
1228
+ payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after 45s" }
1229
+ }, tempAgent.getId());
1230
+ leave(tempAgent);
1231
+ resolve5();
1232
+ }
1233
+ }, 45e3);
1234
+ } catch (error) {
1235
+ console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
1236
+ if (!isDone) {
1237
+ const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
1238
+ const runtime2 = resolveRuntime2();
1239
+ const errorMessage = error instanceof Error ? error.message : String(error);
1240
+ await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", errorMessage || "Sync error");
1241
+ sendEvent3({
1242
+ type: "file.failed" /* FILE_FAILED */,
1243
+ producerId: tempAgent.getId(),
1244
+ occurredAt: /* @__PURE__ */ new Date(),
1245
+ payload: { planId: payload.planId, filePath: payload.filePath, reason: errorMessage || "Sync error" }
1246
+ }, tempAgent.getId());
1247
+ leave(tempAgent);
1248
+ resolve5();
1249
+ }
1250
+ }
1251
+ });
1252
+ });
1253
+ }
1254
+ };
1255
+ var reviewFileHandler = {
1256
+ specification: new WhenFileMigrated(),
1257
+ processor: reviewFileProcessor
1258
+ };
1259
+ function createReviewerAgent(tools) {
1260
+ return createAgent3({
1261
+ name: "Reviewer",
1262
+ capabilities: ["code_review", "inference"],
1263
+ instruction: "You are the Reviewer Worker.",
1264
+ tools: [],
1265
+ // No tools
1266
+ handlers: [reviewFileHandler]
1267
+ });
1268
+ }
1269
+
1270
+ // ../application/src/agents/PackageManagerAgent.ts
1271
+ import * as fs5 from "fs";
1272
+ import * as path6 from "path";
1273
+ import {
1274
+ createAgent as createAgent4,
1275
+ SituationSpecification as SituationSpecification4
1276
+ } from "@mozaik-ai/core";
1277
+ var WhenMigrationStarts2 = class extends SituationSpecification4 {
1278
+ isSatisfiedBy({ event }) {
1279
+ return event.type === "migration.started" /* MIGRATION_STARTED */;
1280
+ }
1281
+ };
1282
+ var managePackagesProcessor = {
1283
+ async apply({ event, participant }) {
1284
+ const payload = event.payload;
1285
+ console.log(`[PackageManagerAgent] Managing dependencies and cleanup for Plan: ${payload.planId}`);
1286
+ const runtime = resolveRuntime();
1287
+ const emitPackagesReady = async (status = "completed") => {
1288
+ const currentPlan = await runtime.state.repository.getPlan(payload.planId);
1289
+ if (currentPlan) {
1290
+ const pmTaskToComplete = currentPlan.tasks.find((t) => t.filePath === "system:package_manager");
1291
+ if (pmTaskToComplete) {
1292
+ pmTaskToComplete.status = status;
1293
+ await runtime.state.repository.savePlan(currentPlan);
1294
+ }
1295
+ }
1296
+ sendEvent({
1297
+ type: "phase.packages_ready" /* PHASE_PACKAGES_READY */,
1298
+ producerId: participant.getId(),
1299
+ occurredAt: /* @__PURE__ */ new Date(),
1300
+ payload: { planId: payload.planId }
1301
+ }, participant.getId());
1302
+ };
1303
+ const shadowDir = payload.shadowWorkspacePath || "";
1304
+ if (!shadowDir || !fs5.existsSync(shadowDir)) {
1305
+ console.warn("[PackageManagerAgent] No valid shadow workspace provided. Skipping.");
1306
+ await emitPackagesReady("failed");
1307
+ return;
1308
+ }
1309
+ const plan = await runtime.state.repository.getPlan(payload.planId);
1310
+ if (!plan) {
1311
+ console.warn("[PackageManagerAgent] Migration plan not found.");
1312
+ await emitPackagesReady("failed");
1313
+ return;
1314
+ }
1315
+ const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
1316
+ if (!catalogEntry) {
1317
+ console.log(`[PackageManagerAgent] No catalog entry found for ${plan.profile.source} -> ${plan.profile.target}`);
1318
+ await emitPackagesReady("failed");
1319
+ return;
1320
+ }
1321
+ const pmTask = plan.tasks.find((t) => t.filePath === "system:package_manager");
1322
+ if (pmTask) {
1323
+ pmTask.status = "in_progress";
1324
+ await runtime.state.repository.savePlan(plan);
1325
+ }
1326
+ if (catalogEntry.filesToDelete && catalogEntry.filesToDelete.length > 0) {
1327
+ for (const fileToDelete of catalogEntry.filesToDelete) {
1328
+ const fullPath = path6.join(shadowDir, fileToDelete);
1329
+ if (fs5.existsSync(fullPath)) {
1330
+ fs5.unlinkSync(fullPath);
1331
+ console.log(`[PackageManagerAgent] Deleted deprecated file: ${fileToDelete}`);
1332
+ }
1333
+ }
1334
+ }
1335
+ if (catalogEntry.filesToScaffold) {
1336
+ for (const [filename, content] of Object.entries(catalogEntry.filesToScaffold)) {
1337
+ const fullPath = path6.join(shadowDir, filename);
1338
+ if (!fs5.existsSync(fullPath)) {
1339
+ fs5.mkdirSync(path6.dirname(fullPath), { recursive: true });
1340
+ fs5.writeFileSync(fullPath, content, "utf-8");
1341
+ console.log(`[PackageManagerAgent] Scaffolded missing structural file: ${filename}`);
1342
+ }
1343
+ }
1344
+ }
1345
+ const packageJsonPath = path6.join(shadowDir, "package.json");
1346
+ if (fs5.existsSync(packageJsonPath)) {
1347
+ try {
1348
+ const pkgRaw = fs5.readFileSync(packageJsonPath, "utf-8");
1349
+ const pkg = JSON.parse(pkgRaw);
1350
+ if (!pkg.dependencies) pkg.dependencies = {};
1351
+ if (!pkg.devDependencies) pkg.devDependencies = {};
1352
+ if (catalogEntry.dependenciesToRemove) {
1353
+ for (const dep of catalogEntry.dependenciesToRemove) {
1354
+ delete pkg.dependencies[dep];
1355
+ delete pkg.devDependencies[dep];
1356
+ }
1357
+ }
1358
+ if (catalogEntry.devDependenciesToRemove) {
1359
+ for (const dep of catalogEntry.devDependenciesToRemove) {
1360
+ delete pkg.dependencies[dep];
1361
+ delete pkg.devDependencies[dep];
1362
+ }
1363
+ }
1364
+ if (catalogEntry.dependenciesToAdd) {
1365
+ for (const [dep, ver] of Object.entries(catalogEntry.dependenciesToAdd)) {
1366
+ pkg.dependencies[dep] = ver;
1367
+ }
1368
+ }
1369
+ if (catalogEntry.devDependenciesToAdd) {
1370
+ for (const [dep, ver] of Object.entries(catalogEntry.devDependenciesToAdd)) {
1371
+ pkg.devDependencies[dep] = ver;
1372
+ }
1373
+ }
1374
+ if (Object.keys(pkg.devDependencies).length === 0) delete pkg.devDependencies;
1375
+ if (catalogEntry.scriptsToUpdate) {
1376
+ if (!pkg.scripts) pkg.scripts = {};
1377
+ for (const [scriptName, scriptCmd] of Object.entries(catalogEntry.scriptsToUpdate)) {
1378
+ pkg.scripts[scriptName] = scriptCmd;
1724
1379
  }
1725
1380
  }
1726
- prompt += `
1727
- Diff or new content:
1728
- ${payload.diff}
1729
- Does it meet the criteria? You MUST respond in JSON.`;
1730
- const modelToUse = process.env.METAMORPH_REVIEWER_MODEL || process.env.METAMORPH_MODEL || "gpt-5.4-mini";
1731
- console.log(`[ReviewerAgent:${reviewerId}] Starting inference loop for ${payload.filePath} with model ${modelToUse}...`);
1732
- try {
1733
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Calling runLoop with agentId: ${tempAgent.getId()}`);
1734
- runLoop(tempAgent.getId(), prompt, {
1735
- model: modelToUse,
1736
- context: tempAgent.getMemory().getContext(),
1737
- structuredOutput: {
1738
- name: "review_result",
1739
- schema: {
1740
- type: "object",
1741
- properties: {
1742
- status: { type: "string", enum: ["APPROVED", "REJECTED", "FATAL_MISMATCH"] },
1743
- errors: { type: "array", items: { type: "string" } }
1744
- },
1745
- required: ["status", "errors"],
1746
- additionalProperties: false
1747
- },
1748
- strict: true
1749
- }
1750
- });
1751
- console.log(`[DEBUG:ReviewerAgent:${reviewerId}] runLoop called successfully (fire-and-forget)`);
1752
- setTimeout(async () => {
1753
- if (!isDone) {
1754
- console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 45s for ${payload.filePath}. Model never responded.`);
1755
- const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
1756
- const runtime2 = resolveRuntime2();
1757
- await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after 45s");
1758
- leave(tempAgent);
1759
- resolve3();
1381
+ if (catalogEntry.scriptsToRemove && pkg.scripts) {
1382
+ for (const scriptName of catalogEntry.scriptsToRemove) {
1383
+ delete pkg.scripts[scriptName];
1384
+ }
1385
+ }
1386
+ fs5.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1387
+ console.log(`[PackageManagerAgent] Updated package.json dependencies directly (no npm subprocess).`);
1388
+ sendEvent({
1389
+ type: "system.log" /* SYSTEM_LOG */,
1390
+ producerId: participant.getId(),
1391
+ 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" }
1393
+ }, participant.getId());
1394
+ } catch (err) {
1395
+ console.error(`[PackageManagerAgent] Error editing package.json:`, err);
1396
+ }
1397
+ } else {
1398
+ console.log(`[PackageManagerAgent] No package.json found at ${packageJsonPath}`);
1399
+ }
1400
+ await emitPackagesReady("completed");
1401
+ }
1402
+ };
1403
+ var managePackagesHandler = {
1404
+ specification: new WhenMigrationStarts2(),
1405
+ processor: managePackagesProcessor
1406
+ };
1407
+ function createPackageManagerAgent() {
1408
+ return createAgent4({
1409
+ name: "PackageManager",
1410
+ capabilities: ["package_management"],
1411
+ instruction: "You are the Package Manager Agent. Your job is to resolve dependencies and clean up unused framework files.",
1412
+ tools: [],
1413
+ handlers: [managePackagesHandler]
1414
+ });
1415
+ }
1416
+
1417
+ // ../application/src/agents/CoordinatorAgent.ts
1418
+ import {
1419
+ createAgent as createAgent5,
1420
+ SituationSpecification as SituationSpecification5
1421
+ } from "@mozaik-ai/core";
1422
+
1423
+ // ../application/src/agents/planReady.ts
1424
+ function planReadyForIntegration(plan) {
1425
+ if (plan.phase === "integration" || plan.phase === "completed" || plan.phase === "failed") {
1426
+ return false;
1427
+ }
1428
+ const files = plan.tasks.filter((task) => !task.filePath.startsWith("system:"));
1429
+ if (files.length === 0) return false;
1430
+ const packages = plan.tasks.find((task) => task.filePath === "system:package_manager");
1431
+ if (packages && packages.status !== "completed" && packages.status !== "failed") {
1432
+ return false;
1433
+ }
1434
+ const live = plan.tasks.filter((task) => !task.filePath.startsWith("system:"));
1435
+ if (live.some((task) => task.status === "pending" || task.status === "in_progress")) {
1436
+ return false;
1437
+ }
1438
+ return live.every((task) => task.status === "completed" || task.status === "failed");
1439
+ }
1440
+
1441
+ // ../application/src/agents/CoordinatorAgent.ts
1442
+ var WATCHDOG_MS = 8e3;
1443
+ var WATCHDOG_MAX_MS = 30 * 60 * 1e3;
1444
+ var WhenSwarmMayBeIdle = class extends SituationSpecification5 {
1445
+ isSatisfiedBy({ event }) {
1446
+ return event.type === "file.reviewed" /* FILE_REVIEWED */ || event.type === "file.failed" /* FILE_FAILED */ || event.type === "file.fatal_mismatch" /* FILE_FATAL_MISMATCH */ || event.type === "phase.packages_ready" /* PHASE_PACKAGES_READY */;
1447
+ }
1448
+ };
1449
+ var WhenMigrationStarted = class extends SituationSpecification5 {
1450
+ isSatisfiedBy({ event }) {
1451
+ return event.type === "migration.started" /* MIGRATION_STARTED */;
1452
+ }
1453
+ };
1454
+ async function emitIntegrationIfReady(producerId, planId) {
1455
+ const runtime = resolveRuntime();
1456
+ const plan = await runtime.state.repository.getPlan(planId);
1457
+ if (!plan || !planReadyForIntegration(plan)) return;
1458
+ console.log(`[Coordinator] File work and packages are settled. Emitting phase.integration_started.`);
1459
+ sendEvent({
1460
+ type: "phase.integration_started" /* PHASE_INTEGRATION_STARTED */,
1461
+ producerId,
1462
+ occurredAt: /* @__PURE__ */ new Date(),
1463
+ payload: { planId }
1464
+ }, producerId);
1465
+ }
1466
+ function stopWatchdog(planId) {
1467
+ const runtime = resolveRuntime();
1468
+ const timer = runtime.state.integrationWatchdogs.get(planId);
1469
+ if (!timer) return;
1470
+ clearInterval(timer);
1471
+ runtime.state.integrationWatchdogs.delete(planId);
1472
+ }
1473
+ var startIntegrationIfIdle = {
1474
+ async apply({ event, participant }) {
1475
+ const planId = event.payload.planId;
1476
+ if (!planId) return;
1477
+ await emitIntegrationIfReady(participant.getId(), planId);
1478
+ }
1479
+ };
1480
+ var startWatchdogProcessor = {
1481
+ async apply({ event, participant }) {
1482
+ const p = event.payload;
1483
+ const runtime = resolveRuntime();
1484
+ stopWatchdog(p.planId);
1485
+ const startedAt = Date.now();
1486
+ const producerId = participant.getId();
1487
+ sendEvent({
1488
+ type: "system.log" /* SYSTEM_LOG */,
1489
+ producerId,
1490
+ occurredAt: /* @__PURE__ */ new Date(),
1491
+ payload: {
1492
+ 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.",
1494
+ level: "info"
1495
+ }
1496
+ }, producerId);
1497
+ const timer = setInterval(async () => {
1498
+ try {
1499
+ const plan = await runtime.state.repository.getPlan(p.planId);
1500
+ if (!plan || plan.phase === "completed" || plan.phase === "failed") {
1501
+ stopWatchdog(p.planId);
1502
+ return;
1503
+ }
1504
+ if (Date.now() - startedAt >= WATCHDOG_MAX_MS) {
1505
+ stopWatchdog(p.planId);
1506
+ sendEvent({
1507
+ type: "system.log" /* SYSTEM_LOG */,
1508
+ producerId,
1509
+ occurredAt: /* @__PURE__ */ new Date(),
1510
+ payload: {
1511
+ 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"
1760
1514
  }
1761
- }, 45e3);
1762
- } catch (error) {
1763
- console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
1764
- if (!isDone) {
1765
- const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
1766
- const runtime2 = resolveRuntime2();
1767
- const errorMessage = error instanceof Error ? error.message : String(error);
1768
- await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", errorMessage || "Sync error");
1769
- leave(tempAgent);
1770
- resolve3();
1771
- }
1515
+ }, producerId);
1516
+ return;
1772
1517
  }
1773
- });
1774
- });
1518
+ await emitIntegrationIfReady(producerId, p.planId);
1519
+ } catch (error) {
1520
+ console.error("[Coordinator] Watchdog tick failed:", error);
1521
+ }
1522
+ }, WATCHDOG_MS);
1523
+ runtime.state.integrationWatchdogs.set(p.planId, timer);
1775
1524
  }
1776
1525
  };
1777
- var reviewFileHandler = {
1778
- specification: new WhenFileMigrated(),
1779
- processor: reviewFileProcessor
1780
- };
1781
- function createReviewerAgent(tools) {
1782
- return createAgent3({
1783
- name: "Reviewer",
1784
- capabilities: ["code_review", "inference"],
1785
- instruction: "You are the Reviewer Worker.",
1526
+ function createCoordinatorAgent() {
1527
+ return createAgent5({
1528
+ name: "Coordinator",
1529
+ capabilities: [],
1530
+ instruction: "You watch the bus and start Integration when file tasks and packages are settled. You do not call the model.",
1786
1531
  tools: [],
1787
- // No tools
1788
- handlers: [reviewFileHandler]
1532
+ handlers: [
1533
+ { specification: new WhenMigrationStarted(), processor: startWatchdogProcessor },
1534
+ { specification: new WhenSwarmMayBeIdle(), processor: startIntegrationIfIdle }
1535
+ ]
1789
1536
  });
1790
1537
  }
1791
1538
 
1792
- // ../application/src/agents/PackageManagerAgent.ts
1793
- import * as fs5 from "fs";
1794
- import * as path4 from "path";
1539
+ // ../application/src/agents/ReporterAgent.ts
1795
1540
  import {
1796
- createAgent as createAgent4,
1797
- SituationSpecification as SituationSpecification4
1541
+ Agent as Agent6,
1542
+ createAgent as createAgent6,
1543
+ SituationSpecification as SituationSpecification6
1798
1544
  } from "@mozaik-ai/core";
1799
- var WhenMigrationStarts2 = class extends SituationSpecification4 {
1545
+ function log(participantId, planId, message, level = "info") {
1546
+ sendEvent({
1547
+ type: "system.log" /* SYSTEM_LOG */,
1548
+ producerId: participantId,
1549
+ occurredAt: /* @__PURE__ */ new Date(),
1550
+ payload: { planId, message, level }
1551
+ }, participantId);
1552
+ }
1553
+ async function shadowPathFor(plan, journal) {
1554
+ if (journal?.shadowPath) return journal.shadowPath;
1555
+ const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-2UXWWNPF.js");
1556
+ return new ShadowWorkspace2().getShadowPath(plan.runId);
1557
+ }
1558
+ function fallbackReport(params) {
1559
+ const completed = params.tasks.filter((t) => t.status === "completed");
1560
+ const failed = params.tasks.filter((t) => t.status === "failed");
1561
+ const rejected = params.notes.filter((n) => n.outcome === "rejected" || n.outcome === "fatal");
1562
+ 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
+ const riskLines = rejected.slice(0, 12).map((n) => `- \`${n.filePath}\`: ${n.detail || n.outcome}`).join("\n");
1564
+ return `# What changed
1565
+
1566
+ 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" : ""}
1568
+ 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
+
1570
+ ## How to run it
1571
+
1572
+ 1. Apply the migration in the dashboard (or keep working in the shadow workspace).
1573
+ 2. From the project folder:
1574
+ \`\`\`bash
1575
+ npm install
1576
+ npm run dev
1577
+ \`\`\`
1578
+ 3. Open the URL the framework prints (\`next dev\` \u2192 port 3000, Vite \u2192 5173).
1579
+
1580
+ ## What we touched
1581
+
1582
+ - Files completed: ${completed.length}
1583
+ - Files failed: ${failed.length}
1584
+ - Started: ${params.startedAt}
1585
+ - Finished: ${(/* @__PURE__ */ new Date()).toLocaleString()}
1586
+
1587
+ ${fileLines || "_No file tasks recorded._"}
1588
+
1589
+ ## Watch out for
1590
+
1591
+ ${riskLines || "- Review `package.json` scripts and the new entry files (layout/page or `index.html` + `src/main.tsx`).\n- Confirm global CSS still loads in the browser."}
1592
+
1593
+ ## Need a retry?
1594
+
1595
+ Discard in the dashboard and start a new run. Do not mix two shadow workspaces in the same tree.
1596
+ `;
1597
+ }
1598
+ var STATUS_PASSED = "**Status:** Shadow `npm run build` passed.";
1599
+ var STATUS_FAILED = "**Status:** Shadow `npm run build` (or catalog checks) did not pass. Treat this as an incomplete migration.";
1600
+ function statusLine(phase) {
1601
+ return phase === "failed" ? STATUS_FAILED : STATUS_PASSED;
1602
+ }
1603
+ function patchReportStatus(markdown, phase) {
1604
+ const line = statusLine(phase);
1605
+ const stripped = markdown.replace(/\n?\*\*Status:\*\*[^\n]*\n?/g, "\n");
1606
+ if (stripped.startsWith("# ")) {
1607
+ const nl = stripped.indexOf("\n");
1608
+ if (nl === -1) return `${stripped}
1609
+
1610
+ ${line}
1611
+ `;
1612
+ return `${stripped.slice(0, nl)}
1613
+
1614
+ ${line}${stripped.slice(nl)}`;
1615
+ }
1616
+ return `${line}
1617
+
1618
+ ${stripped.trimStart()}`;
1619
+ }
1620
+ async function writeOrPatchMigrationMd(dest, phase, fallback) {
1621
+ const fs8 = await import("fs");
1622
+ if (!fs8.existsSync(dest) || fs8.statSync(dest).size < 80) {
1623
+ fs8.writeFileSync(dest, patchReportStatus(fallback, phase), "utf-8");
1624
+ return "wrote";
1625
+ }
1626
+ const current = fs8.readFileSync(dest, "utf-8");
1627
+ fs8.writeFileSync(dest, patchReportStatus(current, phase), "utf-8");
1628
+ return "patched";
1629
+ }
1630
+ var WhenMigrationStarted2 = class extends SituationSpecification6 {
1800
1631
  isSatisfiedBy({ event }) {
1801
1632
  return event.type === "migration.started" /* MIGRATION_STARTED */;
1802
1633
  }
1803
1634
  };
1804
- var managePackagesProcessor = {
1635
+ var WhenFileMigrated2 = class extends SituationSpecification6 {
1636
+ isSatisfiedBy({ event }) {
1637
+ return event.type === "file.migrated" /* FILE_MIGRATED */;
1638
+ }
1639
+ };
1640
+ var WhenFileReviewed = class extends SituationSpecification6 {
1641
+ isSatisfiedBy({ event }) {
1642
+ return event.type === "file.reviewed" /* FILE_REVIEWED */;
1643
+ }
1644
+ };
1645
+ var WhenFileRejected2 = class extends SituationSpecification6 {
1646
+ isSatisfiedBy({ event }) {
1647
+ return event.type === "file.rejected" /* FILE_REJECTED */;
1648
+ }
1649
+ };
1650
+ var WhenFileFatalMismatch2 = class extends SituationSpecification6 {
1651
+ isSatisfiedBy({ event }) {
1652
+ return event.type === "file.fatal_mismatch" /* FILE_FATAL_MISMATCH */;
1653
+ }
1654
+ };
1655
+ var WhenIntegrationStarts = class extends SituationSpecification6 {
1656
+ isSatisfiedBy({ event }) {
1657
+ return event.type === "phase.integration_started" /* PHASE_INTEGRATION_STARTED */;
1658
+ }
1659
+ };
1660
+ var WhenMigrationCompleted = class extends SituationSpecification6 {
1661
+ isSatisfiedBy({ event }) {
1662
+ return event.type === "migration.completed" /* MIGRATION_COMPLETED */;
1663
+ }
1664
+ };
1665
+ var WhenOwnAnswer = class extends SituationSpecification6 {
1666
+ isSatisfiedBy({ event, participant }) {
1667
+ return event.type === "model.answer" && event.producerId === participant.getId();
1668
+ }
1669
+ };
1670
+ var startedProcessor = {
1805
1671
  async apply({ event, participant }) {
1806
- const payload = event.payload;
1807
- console.log(`[PackageManagerAgent] Managing dependencies and cleanup for Plan: ${payload.planId}`);
1808
- const shadowDir = payload.shadowWorkspacePath || "";
1809
- if (!shadowDir || !fs5.existsSync(shadowDir)) {
1810
- console.warn("[PackageManagerAgent] No valid shadow workspace provided. Skipping.");
1672
+ const p = event.payload;
1673
+ const entry = resolveRuntime().state.journal(p.planId);
1674
+ if (p.shadowWorkspacePath) entry.shadowPath = p.shadowWorkspacePath;
1675
+ log(participant.getId(), p.planId, "Reporter is on the bus. It will draft MIGRATION.md during install/build (one model call, in parallel with Integration).");
1676
+ }
1677
+ };
1678
+ var migratedProcessor = {
1679
+ async apply({ event }) {
1680
+ const p = event.payload;
1681
+ resolveRuntime().state.journal(p.planId).notes.push({ filePath: p.filePath, outcome: "migrated" });
1682
+ }
1683
+ };
1684
+ var reviewedProcessor = {
1685
+ async apply({ event }) {
1686
+ const p = event.payload;
1687
+ resolveRuntime().state.journal(p.planId).notes.push({ filePath: p.filePath, outcome: "approved" });
1688
+ }
1689
+ };
1690
+ var rejectedProcessor = {
1691
+ async apply({ event }) {
1692
+ const p = event.payload;
1693
+ resolveRuntime().state.journal(p.planId).notes.push({
1694
+ filePath: p.filePath,
1695
+ outcome: "rejected",
1696
+ detail: (p.errors || []).slice(0, 3).join("; ")
1697
+ });
1698
+ }
1699
+ };
1700
+ var fatalProcessor = {
1701
+ async apply({ event }) {
1702
+ const p = event.payload;
1703
+ resolveRuntime().state.journal(p.planId).notes.push({ filePath: p.filePath, outcome: "fatal", detail: p.reason });
1704
+ }
1705
+ };
1706
+ function draftPrompt(params) {
1707
+ const notable = params.notes.filter((n) => n.outcome === "rejected" || n.outcome === "fatal").slice(-15).map((n) => `${n.filePath}: ${n.detail || n.outcome}`).join("\n");
1708
+ return `Write a user-facing migration briefing to this exact path:
1709
+ ${params.reportAbs}
1710
+
1711
+ Audience: the engineer who clicked Start Migration. They are not reading agent telemetry.
1712
+ File migration is done; Integration is installing/building in parallel \u2014 you may say the shadow build is still running.
1713
+
1714
+ Migration: ${params.plan.profile.source} \u2192 ${params.plan.profile.target}
1715
+ Project path: ${params.plan.targetPath || "(unknown)"}
1716
+ Run id: ${params.plan.runId}
1717
+
1718
+ Include these sections, in this order, with markdown headings:
1719
+ 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)
1721
+ 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
+ 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
1724
+
1725
+ You may read package.json, README, src/app/layout.tsx, src/main.tsx, index.html, or MIGRATION.md if they exist.
1726
+
1727
+ Rejected / structure notes:
1728
+ ${notable || "(none recorded)"}
1729
+
1730
+ If the model cannot infer something, say so briefly. Do not invent URLs, secrets, or features.
1731
+
1732
+ A factual draft you may rewrite (keep facts, improve voice):
1733
+ ---
1734
+ ${params.draft}
1735
+ ---
1736
+ When the file is written, stop.`;
1737
+ }
1738
+ var draftDuringInstallProcessor = {
1739
+ async apply({ event, participant }) {
1740
+ if (!(participant instanceof Agent6)) return;
1741
+ const p = event.payload;
1742
+ const runtime = resolveRuntime();
1743
+ const board = runtime.state.journal(p.planId);
1744
+ if (board.reportLoopActive || board.reportStarted) return;
1745
+ const plan = await runtime.state.repository.getPlan(p.planId);
1746
+ if (!plan) return;
1747
+ const shadowPath = await shadowPathFor(plan, board);
1748
+ const draft = fallbackReport({
1749
+ source: plan.profile.source,
1750
+ target: plan.profile.target,
1751
+ targetPath: plan.targetPath,
1752
+ runId: plan.runId,
1753
+ phase: plan.phase,
1754
+ tasks: plan.tasks || [],
1755
+ notes: board.notes,
1756
+ startedAt: board.startedAt
1757
+ });
1758
+ const tools = participant.getTools();
1759
+ if (tools.length === 0) {
1760
+ board.reportStarted = true;
1761
+ const fs8 = await import("fs");
1762
+ const path12 = await import("path");
1763
+ fs8.writeFileSync(path12.join(shadowPath, "MIGRATION.md"), draft, "utf-8");
1764
+ log(participant.getId(), p.planId, "Reporter wrote a fallback MIGRATION.md (no file tools on the agent).");
1811
1765
  return;
1812
1766
  }
1767
+ board.reportStarted = true;
1768
+ board.reportLoopActive = true;
1769
+ log(participant.getId(), p.planId, "Reporter is drafting MIGRATION.md while Integration runs npm install / build (one model call).");
1770
+ const reportAbs = `${shadowPath.replace(/\\/g, "/")}/MIGRATION.md`;
1771
+ const modelToUse = process.env.METAMORPH_MODEL || "gpt-5.4";
1772
+ runLoop(participant.getId(), draftPrompt({ reportAbs, plan, notes: board.notes, draft }), {
1773
+ model: modelToUse,
1774
+ context: participant.getMemory().getContext(),
1775
+ tools
1776
+ });
1777
+ setTimeout(async () => {
1778
+ const still = runtime.state.journals.get(p.planId);
1779
+ if (!still?.reportLoopActive) return;
1780
+ still.reportLoopActive = false;
1781
+ try {
1782
+ const path12 = await import("path");
1783
+ const dest = path12.join(shadowPath, "MIGRATION.md");
1784
+ const phase = still.pendingStatusPhase;
1785
+ await writeOrPatchMigrationMd(dest, phase, draft);
1786
+ still.pendingStatusPhase = void 0;
1787
+ log(participant.getId(), p.planId, "Reporter draft timed out; kept fallback MIGRATION.md.", "warning");
1788
+ } catch {
1789
+ }
1790
+ }, 9e4);
1791
+ }
1792
+ };
1793
+ var ownAnswerProcessor = {
1794
+ async apply() {
1813
1795
  const runtime = resolveRuntime();
1814
- const plan = await runtime.state.repository.getPlan(payload.planId);
1815
- if (!plan) {
1816
- console.warn("[PackageManagerAgent] Migration plan not found.");
1817
- return;
1796
+ for (const [planId, journal] of runtime.state.journals) {
1797
+ journal.reportLoopActive = false;
1798
+ if (!journal.pendingStatusPhase) continue;
1799
+ const plan = await runtime.state.repository.getPlan(planId);
1800
+ if (!plan) continue;
1801
+ const shadowPath = await shadowPathFor(plan, journal);
1802
+ const path12 = await import("path");
1803
+ const dest = path12.join(shadowPath, "MIGRATION.md");
1804
+ const draft = fallbackReport({
1805
+ source: plan.profile.source,
1806
+ target: plan.profile.target,
1807
+ targetPath: plan.targetPath,
1808
+ runId: plan.runId,
1809
+ phase: journal.pendingStatusPhase,
1810
+ tasks: plan.tasks || [],
1811
+ notes: journal.notes,
1812
+ startedAt: journal.startedAt
1813
+ });
1814
+ await writeOrPatchMigrationMd(dest, journal.pendingStatusPhase, draft);
1815
+ journal.pendingStatusPhase = void 0;
1818
1816
  }
1819
- const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
1820
- if (!catalogEntry) {
1821
- console.log(`[PackageManagerAgent] No catalog entry found for ${plan.profile.source} -> ${plan.profile.target}`);
1817
+ }
1818
+ };
1819
+ var completedProcessor = {
1820
+ async apply({ event, participant }) {
1821
+ const p = event.payload;
1822
+ const runtime = resolveRuntime();
1823
+ const board = runtime.state.journal(p.planId);
1824
+ const plan = await runtime.state.repository.getPlan(p.planId);
1825
+ if (!plan) return;
1826
+ const shadowPath = await shadowPathFor(plan, board);
1827
+ const draft = fallbackReport({
1828
+ source: plan.profile.source,
1829
+ target: plan.profile.target,
1830
+ targetPath: plan.targetPath,
1831
+ runId: plan.runId,
1832
+ phase: plan.phase,
1833
+ tasks: plan.tasks || [],
1834
+ notes: board.notes,
1835
+ startedAt: board.startedAt
1836
+ });
1837
+ board.pendingStatusPhase = plan.phase === "failed" ? "failed" : "completed";
1838
+ try {
1839
+ const path12 = await import("path");
1840
+ const dest = path12.join(shadowPath, "MIGRATION.md");
1841
+ 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).");
1843
+ return;
1844
+ }
1845
+ const result = await writeOrPatchMigrationMd(dest, plan.phase, draft);
1846
+ board.pendingStatusPhase = void 0;
1847
+ log(
1848
+ participant.getId(),
1849
+ 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)."
1851
+ );
1852
+ } catch (error) {
1853
+ console.error("[ReporterAgent] Could not ensure MIGRATION.md:", error);
1854
+ log(participant.getId(), p.planId, "Reporter could not write MIGRATION.md.", "warning");
1855
+ }
1856
+ }
1857
+ };
1858
+ function createReporterAgent(tools = []) {
1859
+ return createAgent6({
1860
+ name: "Reporter",
1861
+ capabilities: ["reporting", "inference"],
1862
+ instruction: "You are the Reporter on the Metamorph swarm. Watch the bus. When Integration starts, write MIGRATION.md for the human \u2014 not an internal log. Use write_file. Do not modify application source. Do not start extra model calls; one draft per run is enough.",
1863
+ tools,
1864
+ handlers: [
1865
+ { specification: new WhenMigrationStarted2(), processor: startedProcessor },
1866
+ { specification: new WhenFileMigrated2(), processor: migratedProcessor },
1867
+ { specification: new WhenFileReviewed(), processor: reviewedProcessor },
1868
+ { specification: new WhenFileRejected2(), processor: rejectedProcessor },
1869
+ { specification: new WhenFileFatalMismatch2(), processor: fatalProcessor },
1870
+ { specification: new WhenIntegrationStarts(), processor: draftDuringInstallProcessor },
1871
+ { specification: new WhenOwnAnswer(), processor: ownAnswerProcessor },
1872
+ { specification: new WhenMigrationCompleted(), processor: completedProcessor }
1873
+ ]
1874
+ });
1875
+ }
1876
+
1877
+ // ../application/src/agents/IntegrationAgent.ts
1878
+ import {
1879
+ createAgent as createAgent7,
1880
+ SituationSpecification as SituationSpecification7
1881
+ } from "@mozaik-ai/core";
1882
+ var MAX_INTEGRATION_ROUNDS = 4;
1883
+ var REPAIR_TIMEOUT_MS = 12e4;
1884
+ var WhenIntegrationStarts2 = class extends SituationSpecification7 {
1885
+ isSatisfiedBy({ event }) {
1886
+ return event.type === "phase.integration_started" /* PHASE_INTEGRATION_STARTED */;
1887
+ }
1888
+ };
1889
+ async function completeMigration(participantId, planId, message, level) {
1890
+ const runtime = resolveRuntime();
1891
+ const plan = await runtime.state.repository.getPlan(planId);
1892
+ if (plan) {
1893
+ plan.phase = "completed";
1894
+ await runtime.state.repository.savePlan(plan);
1895
+ }
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);
1908
+ }
1909
+ 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);
1916
+ }
1917
+ async function failMigration(participantId, planId, message) {
1918
+ const runtime = resolveRuntime();
1919
+ const plan = await runtime.state.repository.getPlan(planId);
1920
+ if (plan) {
1921
+ plan.phase = "failed";
1922
+ await runtime.state.repository.savePlan(plan);
1923
+ }
1924
+ 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);
1931
+ }
1932
+ async function requeueBrokenFiles(producerId, planId, files, errors) {
1933
+ const runtime = resolveRuntime();
1934
+ const unique = [...new Set(files.filter(Boolean))];
1935
+ log2(
1936
+ producerId,
1937
+ planId,
1938
+ `Shadow install/build did not pass. Reopening ${unique.length} file task(s) for Worker repair.`,
1939
+ "warning"
1940
+ );
1941
+ for (const filePath of unique) {
1942
+ await runtime.state.repository.updateTaskStatus(
1943
+ planId,
1944
+ filePath,
1945
+ "pending",
1946
+ errors.slice(0, 3).join("\n").slice(0, 1500)
1947
+ );
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);
1959
+ }
1960
+ const plan = await runtime.state.repository.getPlan(planId);
1961
+ if (plan) {
1962
+ plan.phase = "files";
1963
+ await runtime.state.repository.savePlan(plan);
1964
+ }
1965
+ }
1966
+ var integrationProcessor = {
1967
+ async apply({ event, participant }) {
1968
+ const payload = event.payload;
1969
+ const planId = payload.planId;
1970
+ const producerId = participant.getId();
1971
+ const locks = resolveRuntime().state.integrationLocks;
1972
+ if (locks.has(planId)) {
1973
+ console.log(`[IntegrationAgent] Integration already running for ${planId}. Ignoring duplicate phase event.`);
1822
1974
  return;
1823
1975
  }
1824
- const pmTask = plan.tasks.find((t) => t.filePath === "system:package_manager");
1825
- if (pmTask) {
1826
- pmTask.status = "in_progress";
1976
+ locks.add(planId);
1977
+ try {
1978
+ const runtime = resolveRuntime();
1979
+ const plan = await runtime.state.repository.getPlan(planId);
1980
+ if (!plan) {
1981
+ console.warn(`[IntegrationAgent] Plan ${planId} not found.`);
1982
+ return;
1983
+ }
1984
+ if (plan.phase === "completed" || plan.phase === "failed") {
1985
+ console.log(`[IntegrationAgent] Plan ${planId} already finished (${plan.phase}). Ignoring extra integration phase.`);
1986
+ return;
1987
+ }
1988
+ plan.phase = "integration";
1989
+ const currentRound = plan.integrationRounds ?? 0;
1990
+ if (currentRound >= MAX_INTEGRATION_ROUNDS) {
1991
+ await failMigration(
1992
+ producerId,
1993
+ planId,
1994
+ `Integration budget exhausted after ${MAX_INTEGRATION_ROUNDS} shadow build rounds. npm run build still does not pass \u2014 the run is not successful.`
1995
+ );
1996
+ return;
1997
+ }
1998
+ plan.integrationRounds = currentRound + 1;
1827
1999
  await runtime.state.repository.savePlan(plan);
1828
- }
1829
- if (catalogEntry.filesToDelete && catalogEntry.filesToDelete.length > 0) {
1830
- for (const fileToDelete of catalogEntry.filesToDelete) {
1831
- const fullPath = path4.join(shadowDir, fileToDelete);
1832
- if (fs5.existsSync(fullPath)) {
1833
- fs5.unlinkSync(fullPath);
1834
- console.log(`[PackageManagerAgent] Deleted deprecated file: ${fileToDelete}`);
2000
+ 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");
2002
+ const { collectShadowIssues, collectRepairTargets } = await import("./registry-7WZ5RTGE.js");
2003
+ const { findMigrationCatalogEntry: findMigrationCatalogEntry2 } = await import("./src-UC3SN46X.js");
2004
+ 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}`);
2008
+ 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.");
1835
2014
  }
2015
+ });
2016
+ 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.");
1836
2021
  }
1837
- }
1838
- const packageJsonPath = path4.join(shadowDir, "package.json");
1839
- if (fs5.existsSync(packageJsonPath)) {
1840
- const { spawn } = await import("child_process");
1841
- const runCommandAsync = (cmd, args) => {
1842
- return new Promise((resolve3, reject) => {
1843
- const proc = spawn(cmd, args, { cwd: shadowDir, stdio: "ignore", shell: true });
1844
- proc.on("close", (code) => {
1845
- if (code === 0) resolve3();
1846
- else reject(new Error(`Command ${cmd} ${args.join(" ")} failed with code ${code}`));
2022
+ 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.");
2028
+ }
2029
+ });
2030
+ if (!build.ok) {
2031
+ log2(producerId, planId, "Shadow build failed. IntegrationAgent will attempt local repairs before requeueing files.");
2032
+ const shadowTools = [
2033
+ createReadFileTool2(shadowPath),
2034
+ createWriteFileTool2(shadowPath),
2035
+ createListDirectoryTool2(shadowPath),
2036
+ createRunBuildTool2(shadowPath)
2037
+ ];
2038
+ const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
2039
+ await new Promise((originalResolve) => {
2040
+ let isDone = false;
2041
+ const resolve5 = () => {
2042
+ if (!isDone) {
2043
+ isDone = true;
2044
+ originalResolve();
2045
+ }
2046
+ };
2047
+ const tempAgent = createMozaikAgent({
2048
+ name: `Integration-${Date.now()}`,
2049
+ capabilities: ["integration_testing", "code_repair"],
2050
+ instruction: "You are a staff engineer repairing a migrated project. Read failing files and their imports. Deduce real APIs from disk. Do not invent prop names. Do not wait for Git.",
2051
+ tools: shadowTools,
2052
+ handlers: []
2053
+ });
2054
+ join(tempAgent);
2055
+ const modelToUse = process.env.METAMORPH_MODEL || "gpt-5.4";
2056
+ const excerpt = build.output.slice(0, 4e3);
2057
+ const prompt = `A real \`npm run build\` already failed in the shadow workspace at ${shadowPath}.
2058
+ You must deduce the fix from source, not guess. Use list_directory / read_file on the implicated files AND the modules they import. Keep existing callback/export names. If a router file was reassembled, import the existing screen instead.
2059
+
2060
+ Do NOT require Git. You may run run_project_build again after edits.
2061
+
2062
+ Build output:
2063
+ ${excerpt}
2064
+
2065
+ 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) => {
2071
+ console.error("[IntegrationAgent] runLoop error:", error);
2072
+ leave(tempAgent);
2073
+ resolve5();
1847
2074
  });
1848
- proc.on("error", reject);
2075
+ setTimeout(() => {
2076
+ if (!isDone) {
2077
+ console.warn("[IntegrationAgent] Repair loop timed out; continuing with deterministic rebuild.");
2078
+ leave(tempAgent);
2079
+ resolve5();
2080
+ }
2081
+ }, REPAIR_TIMEOUT_MS);
1849
2082
  });
1850
- };
1851
- try {
1852
- let toRemove = [];
1853
- if (catalogEntry.dependenciesToRemove) toRemove.push(...catalogEntry.dependenciesToRemove);
1854
- if (catalogEntry.devDependenciesToRemove) toRemove.push(...catalogEntry.devDependenciesToRemove);
1855
- if (toRemove.length > 0) {
1856
- console.log(`[PackageManagerAgent] Uninstalling: ${toRemove.join(" ")}`);
1857
- await runCommandAsync("npm", ["uninstall", ...toRemove]);
1858
- }
1859
- if (catalogEntry.dependenciesToAdd && Object.keys(catalogEntry.dependenciesToAdd).length > 0) {
1860
- const deps = Object.entries(catalogEntry.dependenciesToAdd).map(([pkg, ver]) => `${pkg}@${ver}`);
1861
- console.log(`[PackageManagerAgent] Installing dependencies (this may take a few minutes)...`);
1862
- sendEvent({
1863
- type: "system.log" /* SYSTEM_LOG */,
1864
- producerId: participant.getId(),
1865
- occurredAt: /* @__PURE__ */ new Date(),
1866
- payload: { planId: payload.planId, message: "Downloading and installing dependencies (this may take a few minutes)...", level: "info" }
1867
- }, participant.getId());
1868
- await runCommandAsync("npm", ["install", ...deps]);
1869
- }
1870
- if (catalogEntry.devDependenciesToAdd && Object.keys(catalogEntry.devDependenciesToAdd).length > 0) {
1871
- const devDeps = Object.entries(catalogEntry.devDependenciesToAdd).map(([pkg, ver]) => `${pkg}@${ver}`);
1872
- console.log(`[PackageManagerAgent] Installing devDependencies...`);
1873
- await runCommandAsync("npm", ["install", "-D", ...devDeps]);
1874
- }
1875
- console.log(`[PackageManagerAgent] Successfully updated dependencies via npm.`);
1876
- sendEvent({
1877
- type: "system.log" /* SYSTEM_LOG */,
1878
- producerId: participant.getId(),
1879
- occurredAt: /* @__PURE__ */ new Date(),
1880
- payload: { planId: payload.planId, message: "Dependencies installed successfully!", level: "info" }
1881
- }, participant.getId());
1882
- } catch (err) {
1883
- console.error(`[PackageManagerAgent] Error running npm commands:`, err);
2083
+ build = await runInShadowWorkspace(shadowPath, "npm run build");
1884
2084
  }
1885
- } else {
1886
- console.log(`[PackageManagerAgent] No package.json found at ${packageJsonPath}`);
1887
- }
1888
- const currentPlan = await runtime.state.repository.getPlan(payload.planId);
1889
- if (currentPlan) {
1890
- const pmTaskToComplete = currentPlan.tasks.find((t) => t.filePath === "system:package_manager");
1891
- if (pmTaskToComplete) {
1892
- pmTaskToComplete.status = "completed";
1893
- await runtime.state.repository.savePlan(currentPlan);
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);
2088
+ const buildFailed = !build.ok || !install.ok;
2089
+ const verifierFailed = structureIssues.length > 0;
2090
+ 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");
2092
+ return;
1894
2093
  }
1895
- const allTerminal = currentPlan.tasks.every((t) => t.status === "completed" || t.status === "failed");
1896
- if (allTerminal) {
1897
- console.log(`[App] All tasks reached terminal state. Emitting MIGRATION_COMPLETED.`);
1898
- sendEvent({
1899
- type: "system.log" /* SYSTEM_LOG */,
1900
- producerId: participant.getId(),
1901
- occurredAt: /* @__PURE__ */ new Date(),
1902
- payload: { planId: payload.planId, message: "All agents have finished their work. Please review the results and click APPLY MIGRATION or DISCARD.", level: "info" }
1903
- }, participant.getId());
1904
- sendEvent({
1905
- type: "migration.completed" /* MIGRATION_COMPLETED */,
1906
- producerId: participant.getId(),
1907
- occurredAt: /* @__PURE__ */ new Date(),
1908
- payload: { planId: payload.planId }
1909
- }, participant.getId());
2094
+ const implicated = parseImplicatedFiles(
2095
+ `${install.ok ? "" : install.output + "\n"}${build.output}`,
2096
+ shadowPath
2097
+ );
2098
+ const repairFiles = collectRepairTargets(shadowPath, pluginCtx, {
2099
+ implicated,
2100
+ issues: structureIssues,
2101
+ catalog: catalogEntry,
2102
+ layer: catalogEntry?.layer
2103
+ });
2104
+ const errors = [
2105
+ ...structureIssues.flatMap((issue) => issue.errors),
2106
+ ...install.ok ? [] : [`npm install failed:
2107
+ ${install.output.slice(0, 1500)}`],
2108
+ ...build.ok ? [] : [`npm run build failed:
2109
+ ${build.output.slice(0, 2500)}`]
2110
+ ];
2111
+ if (!lastRound && repairFiles.length > 0) {
2112
+ log2(
2113
+ producerId,
2114
+ 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.`,
2116
+ "warning"
2117
+ );
2118
+ await requeueBrokenFiles(producerId, planId, repairFiles, errors.length > 0 ? errors : ["Shadow verification failed."]);
2119
+ return;
1910
2120
  }
2121
+ await failMigration(
2122
+ producerId,
2123
+ 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."
2125
+ );
2126
+ } catch (error) {
2127
+ console.error(`[IntegrationAgent] Error:`, error);
2128
+ await failMigration(producerId, planId, "Integration Phase encountered an error. The migration is not successful.");
2129
+ } finally {
2130
+ resolveRuntime().state.integrationLocks.delete(planId);
1911
2131
  }
1912
2132
  }
1913
2133
  };
1914
- var managePackagesHandler = {
1915
- specification: new WhenMigrationStarts2(),
1916
- processor: managePackagesProcessor
2134
+ var manageIntegrationHandler = {
2135
+ specification: new WhenIntegrationStarts2(),
2136
+ processor: integrationProcessor
1917
2137
  };
1918
- function createPackageManagerAgent() {
1919
- return createAgent4({
1920
- name: "PackageManager",
1921
- capabilities: ["package_management"],
1922
- instruction: "You are the Package Manager Agent. Your job is to resolve dependencies and clean up unused framework files.",
1923
- tools: [],
1924
- handlers: [managePackagesHandler]
2138
+ function createIntegrationAgent(tools = []) {
2139
+ return createAgent7({
2140
+ name: "IntegrationAgent",
2141
+ capabilities: ["integration_testing", "code_repair"],
2142
+ instruction: "You are the Integration Agent. You run npm install and npm run build in the shadow workspace. A passing compile is not enough if catalog verifiers fail. Never mark the migration complete unless the shadow build passes and verifiers are clean. On failure, reopen file tasks for Worker.",
2143
+ tools,
2144
+ handlers: [manageIntegrationHandler]
1925
2145
  });
1926
2146
  }
1927
2147
 
1928
- // ../application/src/agents/ReporterAgent.ts
2148
+ // ../application/src/migration/plugins/frontendTargets.ts
2149
+ import * as path8 from "path";
2150
+
2151
+ // ../application/src/utils/FrontendRuntimeHints.ts
1929
2152
  import * as fs6 from "fs";
1930
- import * as path5 from "path";
1931
- import {
1932
- createAgent as createAgent5,
1933
- SituationSpecification as SituationSpecification5
1934
- } from "@mozaik-ai/core";
1935
- var WhenMigrationStarted = class extends SituationSpecification5 {
1936
- isSatisfiedBy({ event }) {
1937
- return event.type === "migration.started" /* MIGRATION_STARTED */;
2153
+ import * as path7 from "path";
2154
+ function readIfExists(...candidates) {
2155
+ for (const filePath of candidates) {
2156
+ if (fs6.existsSync(filePath) && fs6.statSync(filePath).isFile()) {
2157
+ return { filePath, content: fs6.readFileSync(filePath, "utf-8") };
2158
+ }
1938
2159
  }
1939
- };
1940
- var WhenFileMigrated2 = class extends SituationSpecification5 {
1941
- isSatisfiedBy({ event }) {
1942
- return event.type === "file.migrated" /* FILE_MIGRATED */;
2160
+ return null;
2161
+ }
2162
+ function leftoverScripts(shadowRoot, forbidden, message) {
2163
+ const pkgPath = path7.join(shadowRoot, "package.json");
2164
+ if (!fs6.existsSync(pkgPath)) return [];
2165
+ try {
2166
+ const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
2167
+ const hits = Object.entries(pkg.scripts || {}).filter(([, cmd]) => forbidden.test(cmd));
2168
+ if (hits.length === 0) return [];
2169
+ return [{
2170
+ filePath: pkgPath,
2171
+ errors: [`${message} (${hits.map(([name]) => name).join(", ")}).`]
2172
+ }];
2173
+ } catch {
2174
+ return [];
1943
2175
  }
1944
- };
1945
- var WhenFileReviewed = class extends SituationSpecification5 {
1946
- isSatisfiedBy({ event }) {
1947
- return event.type === "file.reviewed" /* FILE_REVIEWED */;
2176
+ }
2177
+ function viteConfigUsesPlugin(shadowRoot, pluginNeedle) {
2178
+ const file = readIfExists(
2179
+ path7.join(shadowRoot, "vite.config.ts"),
2180
+ path7.join(shadowRoot, "vite.config.js")
2181
+ );
2182
+ if (!file) {
2183
+ return [{
2184
+ filePath: path7.join(shadowRoot, "vite.config.ts"),
2185
+ errors: ["Missing vite.config.ts for this Vite SPA target."]
2186
+ }];
2187
+ }
2188
+ const issues = [];
2189
+ if (!pluginNeedle.test(file.content)) {
2190
+ issues.push({
2191
+ filePath: file.filePath,
2192
+ errors: ["vite.config.ts does not register the plugin for the target framework."]
2193
+ });
1948
2194
  }
1949
- };
1950
- var WhenFileRejected2 = class extends SituationSpecification5 {
1951
- isSatisfiedBy({ event }) {
1952
- return event.type === "file.rejected" /* FILE_REJECTED */;
2195
+ if (/__dirname/.test(file.content) && !/import\.meta\.dirname/.test(file.content)) {
2196
+ issues.push({
2197
+ filePath: file.filePath,
2198
+ errors: ["vite.config.ts uses __dirname in ESM. Replace it with import.meta.dirname."]
2199
+ });
1953
2200
  }
1954
- };
1955
- var WhenFileFatalMismatch2 = class extends SituationSpecification5 {
1956
- isSatisfiedBy({ event }) {
1957
- return event.type === "file.fatal_mismatch" /* FILE_FATAL_MISMATCH */;
2201
+ return issues;
2202
+ }
2203
+ function indexHtmlIssues(shadowRoot, mountId, scriptNeedle) {
2204
+ const html = readIfExists(path7.join(shadowRoot, "index.html"));
2205
+ if (!html) {
2206
+ return [{
2207
+ filePath: path7.join(shadowRoot, "index.html"),
2208
+ errors: [`Missing Vite index.html. It must contain <div id="${mountId}"> and a module script to the SPA entry.`]
2209
+ }];
2210
+ }
2211
+ const issues = [];
2212
+ if (!new RegExp(`id=["']${mountId}["']`).test(html.content)) {
2213
+ issues.push({
2214
+ filePath: html.filePath,
2215
+ errors: [`index.html must mount the app on #${mountId} (the entry's mount() / createApp / createRoot target).`]
2216
+ });
1958
2217
  }
1959
- };
1960
- var WhenMigrationCompleted = class extends SituationSpecification5 {
1961
- isSatisfiedBy({ event }) {
1962
- return event.type === "migration.completed" /* MIGRATION_COMPLETED */;
2218
+ if (!scriptNeedle.test(html.content)) {
2219
+ issues.push({
2220
+ filePath: html.filePath,
2221
+ errors: ['index.html must load the SPA entry as <script type="module" src="/src/main.ts"> (or main.tsx).']
2222
+ });
1963
2223
  }
1964
- };
1965
- function appendToReport(shadowDir, content) {
1966
- if (!shadowDir) return;
1967
- const reportPath = path5.join(shadowDir, "migration-report.md");
1968
- try {
1969
- fs6.appendFileSync(reportPath, content + "\n", "utf-8");
1970
- } catch (e) {
1971
- console.error("[ReporterAgent] Error writing to report:", e);
2224
+ return issues;
2225
+ }
2226
+ function leftoverNextApp(shadowRoot) {
2227
+ const files = [
2228
+ path7.join(shadowRoot, "src", "app", "page.tsx"),
2229
+ path7.join(shadowRoot, "src", "app", "layout.tsx"),
2230
+ path7.join(shadowRoot, "app", "page.tsx"),
2231
+ path7.join(shadowRoot, "app", "layout.tsx")
2232
+ ].filter((file) => fs6.existsSync(file));
2233
+ return files.map((filePath) => ({
2234
+ filePath,
2235
+ errors: ["Leftover Next App Router file. Delete it after the target SPA/CLI boots so it cannot be mistaken for the new tree."]
2236
+ }));
2237
+ }
2238
+ function leftoverViteWhenAngular(shadowRoot) {
2239
+ const issues = [];
2240
+ for (const rel of ["vite.config.ts", "vite.config.js", "index.html"]) {
2241
+ const filePath = path7.join(shadowRoot, rel);
2242
+ if (fs6.existsSync(filePath)) {
2243
+ issues.push({
2244
+ filePath,
2245
+ errors: [`Leftover Vite file ${rel}. Angular boots from src/main.ts + src/index.html + angular.json \u2014 delete the Vite entry.`]
2246
+ });
2247
+ }
1972
2248
  }
2249
+ return issues;
1973
2250
  }
1974
- var startedProcessor = {
1975
- async apply({ event }) {
1976
- const p = event.payload;
1977
- if (!("shadowWorkspacePath" in p)) return;
1978
- const shadowPath = p.shadowWorkspacePath;
1979
- if (!shadowPath) return;
1980
- const reportPath = path5.join(shadowPath, "migration-report.md");
1981
- const header = `# Metamorph Migration Report
1982
- **Plan ID**: ${p.planId}
1983
- **Date Started**: ${(/* @__PURE__ */ new Date()).toLocaleString()}
1984
- ---
1985
-
1986
- ## Migration Log
1987
-
2251
+ function isStubEntry(content) {
2252
+ const hasLocalImport = /from\s+['"](\.\.?\/|@\/)/.test(content);
2253
+ const emptyHeading = /<h1[^>]*>[^<]{0,40}<\/h1>|TODO:|coming soon/i.test(content);
2254
+ return emptyHeading && !hasLocalImport;
2255
+ }
2256
+ function analyzeVueTarget(shadowRoot) {
2257
+ const issues = [
2258
+ ...indexHtmlIssues(shadowRoot, "app", /src\/main\.(t|j)sx?/),
2259
+ ...viteConfigUsesPlugin(shadowRoot, /plugin-vue|@vitejs\/plugin-vue/),
2260
+ ...leftoverScripts(shadowRoot, /\b(next|ng)\b/, "package.json still runs Next/Angular scripts; Vue target must use Vite"),
2261
+ ...leftoverNextApp(shadowRoot)
2262
+ ];
2263
+ const main = readIfExists(path7.join(shadowRoot, "src", "main.ts"), path7.join(shadowRoot, "src", "main.js"));
2264
+ if (!main) {
2265
+ issues.push({
2266
+ filePath: path7.join(shadowRoot, "src", "main.ts"),
2267
+ errors: ['Missing src/main.ts. createApp(App).use(router).mount("#app") and import global CSS.']
2268
+ });
2269
+ } else {
2270
+ if (!/createApp\(/.test(main.content) || !/mount\(/.test(main.content)) {
2271
+ issues.push({
2272
+ filePath: main.filePath,
2273
+ errors: ['Vue entry must createApp(...).mount("#app"). Do not leave ReactDOM.createRoot here.']
2274
+ });
2275
+ }
2276
+ if (/createRoot|react-dom/.test(main.content)) {
2277
+ issues.push({
2278
+ filePath: main.filePath,
2279
+ errors: ["Vue entry still mounts React. Replace createRoot with createApp and delete ReactDOM."]
2280
+ });
2281
+ }
2282
+ if (!/import\s+['"][^'"]+\.(?:css|scss|sass)['"]/.test(main.content)) {
2283
+ const css = path7.join(shadowRoot, "src", "index.css");
2284
+ if (fs6.existsSync(css)) {
2285
+ issues.push({
2286
+ filePath: main.filePath,
2287
+ errors: ["Global CSS exists but is not imported from src/main.ts. Vite will ship an unstyled app."]
2288
+ });
2289
+ }
2290
+ }
2291
+ if (isStubEntry(main.content)) {
2292
+ issues.push({
2293
+ filePath: main.filePath,
2294
+ errors: ["Vue entry does not import the real App/router. Import existing screens; do not mount an empty shell."]
2295
+ });
2296
+ }
2297
+ }
2298
+ return issues;
2299
+ }
2300
+ function analyzeSvelteTarget(shadowRoot) {
2301
+ const issues = [
2302
+ ...indexHtmlIssues(shadowRoot, "app", /src\/main\.(t|j)sx?/),
2303
+ ...viteConfigUsesPlugin(shadowRoot, /vite-plugin-svelte|svelte\(/),
2304
+ ...leftoverScripts(shadowRoot, /\b(next|ng)\b/, "package.json still runs Next/Angular scripts; Svelte target must use Vite"),
2305
+ ...leftoverNextApp(shadowRoot)
2306
+ ];
2307
+ const kit = path7.join(shadowRoot, "src", "routes", "+page.svelte");
2308
+ if (fs6.existsSync(kit)) {
2309
+ issues.push({
2310
+ filePath: kit,
2311
+ errors: ["SvelteKit +page.svelte appeared but this catalog targets a Vite SPA. Use App.svelte + mount() instead of Kit file routes."]
2312
+ });
2313
+ }
2314
+ const main = readIfExists(path7.join(shadowRoot, "src", "main.ts"), path7.join(shadowRoot, "src", "main.js"));
2315
+ if (!main) {
2316
+ issues.push({
2317
+ filePath: path7.join(shadowRoot, "src", "main.ts"),
2318
+ errors: ['Missing src/main.ts. Use mount(App, { target: document.getElementById("app") }) and import global CSS.']
2319
+ });
2320
+ } else if (!/from\s+['"]svelte['"]/.test(main.content) && !/new\s+\w+\(\s*\{\s*target/.test(main.content)) {
2321
+ issues.push({
2322
+ filePath: main.filePath,
2323
+ errors: ["Svelte entry must mount the root .svelte component onto #app."]
2324
+ });
2325
+ }
2326
+ return issues;
2327
+ }
2328
+ function analyzeAngularTarget(shadowRoot) {
2329
+ const issues = [
2330
+ ...leftoverViteWhenAngular(shadowRoot),
2331
+ ...leftoverScripts(shadowRoot, /\b(vite|next)\b/, "package.json still runs Vite/Next; Angular target must use ng serve / ng build")
2332
+ ];
2333
+ const angularJson = path7.join(shadowRoot, "angular.json");
2334
+ if (!fs6.existsSync(angularJson)) {
2335
+ issues.push({
2336
+ filePath: angularJson,
2337
+ errors: ["Missing angular.json. ng build will not run. Scaffold it and point browser to src/main.ts and styles to src/styles.css."]
2338
+ });
2339
+ }
2340
+ const main = readIfExists(path7.join(shadowRoot, "src", "main.ts"));
2341
+ if (!main || !/bootstrapApplication\(/.test(main.content || "")) {
2342
+ issues.push({
2343
+ filePath: path7.join(shadowRoot, "src", "main.ts"),
2344
+ errors: ["Missing bootstrapApplication in src/main.ts. Angular standalone boots here, not from a Vite main.tsx."]
2345
+ });
2346
+ }
2347
+ const routes = readIfExists(path7.join(shadowRoot, "src", "app", "app.routes.ts"));
2348
+ if (routes && /export const routes: Routes = \[\s*\]/.test(routes.content)) {
2349
+ issues.push({
2350
+ filePath: routes.filePath,
2351
+ errors: ["app.routes.ts has no routes. Map each source URL to the existing feature component. App stays a <router-outlet /> shell."]
2352
+ });
2353
+ }
2354
+ const html = readIfExists(path7.join(shadowRoot, "src", "index.html"));
2355
+ if (!html) {
2356
+ issues.push({
2357
+ filePath: path7.join(shadowRoot, "src", "index.html"),
2358
+ errors: ["Missing src/index.html with <app-root></app-root>."]
2359
+ });
2360
+ }
2361
+ return issues;
2362
+ }
2363
+ function vueFileHint(filePath, target) {
2364
+ if (target !== "vue") return "";
2365
+ const normalized = filePath.replace(/\\/g, "/");
2366
+ if (/\/main\.(t|j)sx?$/.test(normalized)) {
2367
+ return `
2368
+ This is the Vue entry. It must createApp(App).use(router).mount("#app") and import global CSS.
2369
+ index.html uses #app, not #root. Do not leave createRoot/ReactDOM.
1988
2370
  `;
1989
- fs6.writeFileSync(reportPath, header, "utf-8");
1990
- console.log(`[ReporterAgent] Initialized report at ${reportPath}`);
1991
2371
  }
1992
- };
1993
- var migratedProcessor = {
1994
- async apply({ event }) {
1995
- const p = event.payload;
1996
- const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
1997
- const runtime = resolveRuntime2();
1998
- const plan = await runtime.state.repository.getPlan(p.planId);
1999
- if (!plan) return;
2000
- const shadowDir = plan.targetPath;
2001
- appendToReport(shadowDir, `- \u23F3 **Refactored**: \`${p.filePath}\` (Pending Review)`);
2372
+ if (/\.(t|j)sx$/.test(normalized) && !/vite\.config/.test(normalized)) {
2373
+ return `
2374
+ Convert this UI file into a .vue SFC (<script setup> + template + style). Keep prop and emit names. Then delete this leftover TSX once the SFC exists.
2375
+ `;
2002
2376
  }
2003
- };
2004
- var reviewedProcessor = {
2005
- async apply({ event }) {
2006
- const p = event.payload;
2007
- const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
2008
- const runtime = resolveRuntime2();
2009
- const plan = await runtime.state.repository.getPlan(p.planId);
2010
- if (!plan) return;
2011
- appendToReport(plan.targetPath, `- \u2705 **Approved**: \`${p.filePath}\``);
2377
+ return "";
2378
+ }
2379
+ function svelteFileHint(filePath, target) {
2380
+ if (target !== "svelte") return "";
2381
+ const normalized = filePath.replace(/\\/g, "/");
2382
+ if (/\/main\.(t|j)sx?$/.test(normalized)) {
2383
+ return `
2384
+ This is the Svelte entry. mount(App, { target: document.getElementById("app") }). Do not create SvelteKit +page.svelte routes.
2385
+ `;
2386
+ }
2387
+ return "";
2388
+ }
2389
+ function angularFileHint(filePath, target) {
2390
+ if (target !== "angular") return "";
2391
+ const normalized = filePath.replace(/\\/g, "/");
2392
+ if (/app\.routes\.ts$/.test(normalized)) {
2393
+ return `
2394
+ Map each source route to an existing feature component. Do not inline that component's template. App is <router-outlet />.
2395
+ Keep @Input / @Output names when the host binds them.
2396
+ `;
2397
+ }
2398
+ if (/\/main\.(t|j)sx$/.test(normalized)) {
2399
+ return `
2400
+ Vite/React main.tsx is not the Angular entry. Angular boots from src/main.ts via bootstrapApplication. Delete this file after src/main.ts exists.
2401
+ `;
2402
+ }
2403
+ return "";
2404
+ }
2405
+
2406
+ // ../application/src/migration/plugins/frontendTargets.ts
2407
+ var vueTargetPlugin = {
2408
+ id: "target-vue",
2409
+ target: "vue",
2410
+ layer: "frontend",
2411
+ fileHint(filePath, ctx) {
2412
+ return vueFileHint(filePath, ctx.target);
2413
+ },
2414
+ verifyShadow(shadowRoot) {
2415
+ return analyzeVueTarget(shadowRoot);
2416
+ },
2417
+ repairTargets(shadowRoot) {
2418
+ return [
2419
+ path8.join(shadowRoot, "package.json"),
2420
+ path8.join(shadowRoot, "vite.config.ts"),
2421
+ path8.join(shadowRoot, "index.html"),
2422
+ path8.join(shadowRoot, "src", "main.ts"),
2423
+ path8.join(shadowRoot, "src", "App.vue")
2424
+ ];
2012
2425
  }
2013
2426
  };
2014
- var rejectedProcessor = {
2015
- async apply({ event }) {
2016
- const p = event.payload;
2017
- const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
2018
- const runtime = resolveRuntime2();
2019
- const plan = await runtime.state.repository.getPlan(p.planId);
2020
- if (!plan) return;
2021
- appendToReport(plan.targetPath, `- \u26A0\uFE0F **Rejected**: \`${p.filePath}\`
2022
- > Errors: ${p.errors?.join(", ")}`);
2427
+ var svelteTargetPlugin = {
2428
+ id: "target-svelte",
2429
+ target: "svelte",
2430
+ layer: "frontend",
2431
+ fileHint(filePath, ctx) {
2432
+ return svelteFileHint(filePath, ctx.target);
2433
+ },
2434
+ verifyShadow(shadowRoot) {
2435
+ return analyzeSvelteTarget(shadowRoot);
2436
+ },
2437
+ repairTargets(shadowRoot) {
2438
+ return [
2439
+ path8.join(shadowRoot, "package.json"),
2440
+ path8.join(shadowRoot, "vite.config.ts"),
2441
+ path8.join(shadowRoot, "svelte.config.js"),
2442
+ path8.join(shadowRoot, "index.html"),
2443
+ path8.join(shadowRoot, "src", "main.ts"),
2444
+ path8.join(shadowRoot, "src", "App.svelte")
2445
+ ];
2023
2446
  }
2024
2447
  };
2025
- var fatalProcessor = {
2026
- async apply({ event }) {
2027
- const p = event.payload;
2028
- const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
2029
- const runtime = resolveRuntime2();
2030
- const plan = await runtime.state.repository.getPlan(p.planId);
2031
- if (!plan) return;
2032
- appendToReport(plan.targetPath, `- \u274C **FATAL MISMATCH**: \`${p.filePath}\`
2033
- > Reason: ${p.reason}`);
2448
+ var angularTargetPlugin = {
2449
+ id: "target-angular",
2450
+ target: "angular",
2451
+ layer: "frontend",
2452
+ fileHint(filePath, ctx) {
2453
+ return angularFileHint(filePath, ctx.target);
2454
+ },
2455
+ verifyShadow(shadowRoot) {
2456
+ return analyzeAngularTarget(shadowRoot);
2457
+ },
2458
+ repairTargets(shadowRoot) {
2459
+ return [
2460
+ path8.join(shadowRoot, "package.json"),
2461
+ path8.join(shadowRoot, "angular.json"),
2462
+ path8.join(shadowRoot, "src", "main.ts"),
2463
+ path8.join(shadowRoot, "src", "index.html"),
2464
+ path8.join(shadowRoot, "src", "styles.css"),
2465
+ path8.join(shadowRoot, "src", "app", "app.ts"),
2466
+ path8.join(shadowRoot, "src", "app", "app.routes.ts"),
2467
+ path8.join(shadowRoot, "src", "app", "app.config.ts")
2468
+ ];
2034
2469
  }
2035
2470
  };
2036
- var completedProcessor = {
2037
- async apply({ event }) {
2038
- const p = event.payload;
2039
- const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
2040
- const runtime = resolveRuntime2();
2041
- const plan = await runtime.state.repository.getPlan(p.planId);
2042
- if (!plan) return;
2043
- const totalFiles = plan.tasks ? plan.tasks.length : 0;
2044
- const migratedFiles = plan.tasks ? plan.tasks.filter((t) => t.status === "completed").length : 0;
2045
- const footer = `
2046
- ---
2047
- ## Migration Completed
2048
- **Date Finished**: ${(/* @__PURE__ */ new Date()).toLocaleString()}
2049
- **Total Files**: ${totalFiles}
2050
- **Successfully Migrated**: ${migratedFiles}
2471
+
2472
+ // ../application/src/migration/plugins/nextToReact.ts
2473
+ import * as fs7 from "fs";
2474
+ import * as path9 from "path";
2475
+ function readIfExists2(...candidates) {
2476
+ for (const filePath of candidates) {
2477
+ if (fs7.existsSync(filePath) && fs7.statSync(filePath).isFile()) {
2478
+ return { filePath, content: fs7.readFileSync(filePath, "utf-8") };
2479
+ }
2480
+ }
2481
+ return null;
2482
+ }
2483
+ function listCssFiles(dir) {
2484
+ if (!fs7.existsSync(dir)) return [];
2485
+ return fs7.readdirSync(dir).filter((name) => /\.(css|scss|sass)$/i.test(name)).map((name) => path9.join(dir, name));
2486
+ }
2487
+ function hasLocalComponentImport(content) {
2488
+ return /import\s+(?:type\s+)?(?:[\w*{}\s,]+)\s+from\s+['"](\.\.?\/|@\/)(?!.*\.(?:css|scss|sass)['"])/i.test(content) || /import\s+['"](\.\.?\/|@\/)[^'"]+\.(t|j)sx['"]/.test(content);
2489
+ }
2490
+ function isStubSpaEntry(content) {
2491
+ const emptyMount = /<div\s+id=["']app["']\s*\/>|<div\s*\/>\s*[;)]|return\s+null\s*;?|return\s*\(\s*<>\s*<\/>\s*\)/i.test(content);
2492
+ const hasRouter = /BrowserRouter|RouterProvider|createBrowserRouter|HashRouter/i.test(content);
2493
+ if (hasRouter && hasLocalComponentImport(content)) return false;
2494
+ if (emptyMount) return true;
2495
+ if (!hasLocalComponentImport(content) && /createRoot\(/.test(content)) return true;
2496
+ return false;
2497
+ }
2498
+ function listTsxFiles(dir) {
2499
+ if (!fs7.existsSync(dir)) return [];
2500
+ const out = [];
2501
+ const stack = [dir];
2502
+ while (stack.length > 0) {
2503
+ const current = stack.pop();
2504
+ for (const entry of fs7.readdirSync(current, { withFileTypes: true })) {
2505
+ const full = path9.join(current, entry.name);
2506
+ if (entry.isDirectory()) stack.push(full);
2507
+ else if (/\.(t|j)sx$/.test(entry.name) && !entry.name.endsWith(".obsolete")) out.push(full);
2508
+ }
2509
+ }
2510
+ return out;
2511
+ }
2512
+ function leftoverNextAppFiles(shadowRoot) {
2513
+ const dirs = [path9.join(shadowRoot, "src", "app"), path9.join(shadowRoot, "app")];
2514
+ const found = [];
2515
+ for (const dir of dirs) {
2516
+ for (const file of listTsxFiles(dir)) {
2517
+ const base = path9.basename(file);
2518
+ if (/^(page|layout|template|loading|error|not-found)\.(t|j)sx$/.test(base)) {
2519
+ found.push(file);
2520
+ }
2521
+ }
2522
+ }
2523
+ return found;
2524
+ }
2525
+ function findUnmountedScreens(shadowRoot, entryContent) {
2526
+ const candidates = [
2527
+ path9.join(shadowRoot, "src", "App.tsx"),
2528
+ path9.join(shadowRoot, "src", "App.jsx"),
2529
+ path9.join(shadowRoot, "src", "routes", "AppRoutes.tsx"),
2530
+ path9.join(shadowRoot, "src", "routes", "AppRoutes.jsx")
2531
+ ];
2532
+ const unused = [];
2533
+ for (const file of candidates) {
2534
+ if (!fs7.existsSync(file)) continue;
2535
+ const stem = path9.basename(file).replace(/\.(t|j)sx$/, "");
2536
+ if (!entryContent.includes(stem)) unused.push(file);
2537
+ }
2538
+ return unused;
2539
+ }
2540
+ function analyzeNextToReactBootstrap(shadowRoot) {
2541
+ const issues = [];
2542
+ const main = readIfExists2(
2543
+ path9.join(shadowRoot, "src", "main.tsx"),
2544
+ path9.join(shadowRoot, "src", "main.jsx"),
2545
+ path9.join(shadowRoot, "src", "index.tsx")
2546
+ );
2547
+ if (!main) {
2548
+ issues.push({
2549
+ filePath: path9.join(shadowRoot, "src", "main.tsx"),
2550
+ errors: [
2551
+ "Missing SPA entry. Create src/main.tsx that mounts BrowserRouter and the former home screen into #root. Vite can start with a blank page if this file only renders an empty div."
2552
+ ]
2553
+ });
2554
+ return issues;
2555
+ }
2556
+ if (isStubSpaEntry(main.content)) {
2557
+ issues.push({
2558
+ filePath: main.filePath,
2559
+ errors: [
2560
+ "src/main.tsx does not mount the application UI. Import the router and the former home screen. Do not render only an empty element, null, or a placeholder \u2014 that is a blank page even when npm run dev succeeds."
2561
+ ]
2562
+ });
2563
+ } else if (!/BrowserRouter|RouterProvider|createBrowserRouter|HashRouter/i.test(main.content)) {
2564
+ issues.push({
2565
+ filePath: main.filePath,
2566
+ errors: [
2567
+ "SPA entry has no React Router. Wrap the tree in <BrowserRouter> (or RouterProvider) so former Next routes still have URLs."
2568
+ ]
2569
+ });
2570
+ }
2571
+ const unused = findUnmountedScreens(shadowRoot, main.content);
2572
+ for (const file of unused) {
2573
+ issues.push({
2574
+ filePath: file,
2575
+ errors: [
2576
+ `${path9.relative(shadowRoot, file)} exists but is not imported from the SPA entry. Wire it from src/main.tsx or it will never render.`
2577
+ ]
2578
+ });
2579
+ }
2580
+ for (const file of leftoverNextAppFiles(shadowRoot)) {
2581
+ issues.push({
2582
+ filePath: file,
2583
+ errors: [
2584
+ "Leftover Next App Router file. After the Vite SPA boots, delete src/app/page.tsx and layout.tsx (or they confuse the next run)."
2585
+ ]
2586
+ });
2587
+ }
2588
+ const pkgPath = path9.join(shadowRoot, "package.json");
2589
+ if (fs7.existsSync(pkgPath)) {
2590
+ try {
2591
+ const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
2592
+ const nextScripts = Object.entries(pkg.scripts || {}).filter(([, cmd]) => /\bnext\b/.test(cmd));
2593
+ if (nextScripts.length > 0) {
2594
+ issues.push({
2595
+ filePath: pkgPath,
2596
+ errors: [
2597
+ `package.json still runs Next in scripts (${nextScripts.map(([name]) => name).join(", ")}). Replace them with Vite/eslint equivalents (dev/build/preview/start/lint).`
2598
+ ]
2599
+ });
2600
+ }
2601
+ } catch {
2602
+ }
2603
+ }
2604
+ return issues;
2605
+ }
2606
+ function analyzeNextToReactStyles(shadowRoot) {
2607
+ const issues = [];
2608
+ const main = readIfExists2(
2609
+ path9.join(shadowRoot, "src", "main.tsx"),
2610
+ path9.join(shadowRoot, "src", "main.jsx"),
2611
+ path9.join(shadowRoot, "src", "main.ts"),
2612
+ path9.join(shadowRoot, "src", "index.tsx")
2613
+ );
2614
+ const layout = readIfExists2(
2615
+ path9.join(shadowRoot, "src", "app", "layout.tsx"),
2616
+ path9.join(shadowRoot, "src", "app", "layout.jsx"),
2617
+ path9.join(shadowRoot, "app", "layout.tsx"),
2618
+ path9.join(shadowRoot, "app", "layout.jsx")
2619
+ );
2620
+ const cssFromLayout = [];
2621
+ if (layout) {
2622
+ const importRe = /import\s+['"]([^'"]+\.(?:css|scss|sass))['"]/g;
2623
+ let match;
2624
+ while ((match = importRe.exec(layout.content)) !== null) {
2625
+ cssFromLayout.push(match[1]);
2626
+ }
2627
+ }
2628
+ const srcCss = listCssFiles(path9.join(shadowRoot, "src"));
2629
+ const hasGlobalCss = srcCss.length > 0 || cssFromLayout.length > 0;
2630
+ if (!main) {
2631
+ if (hasGlobalCss) {
2632
+ issues.push({
2633
+ filePath: path9.join(shadowRoot, "src", "main.tsx"),
2634
+ errors: [
2635
+ "Missing Vite/React entry (src/main.tsx). Create it so it mounts the app into #root and imports the global CSS that Next layout used to load."
2636
+ ]
2637
+ });
2638
+ }
2639
+ return issues;
2640
+ }
2641
+ const entryImportsCss = /import\s+['"][^'"]+\.(?:css|scss|sass)['"]/.test(main.content);
2642
+ if (hasGlobalCss && !entryImportsCss) {
2643
+ const hint = cssFromLayout[0] || (srcCss[0] ? `./${path9.basename(srcCss[0])}` : "./index.css");
2644
+ issues.push({
2645
+ filePath: main.filePath,
2646
+ errors: [
2647
+ `Global CSS is not imported from the SPA entry. Next.js applied styles via layout; Vite only bundles CSS imported from JS. Add \`import '${hint}'\` (or the path layout used) to ${path9.basename(main.filePath)}.`
2648
+ ]
2649
+ });
2650
+ }
2651
+ const html = readIfExists2(path9.join(shadowRoot, "index.html"));
2652
+ if (html && !/src\/main\.(t|j)sx?/.test(html.content) && !/\/src\/index\.(t|j)sx?/.test(html.content)) {
2653
+ issues.push({
2654
+ filePath: html.filePath,
2655
+ errors: [
2656
+ 'index.html must load the SPA entry as a module script (typically <script type="module" src="/src/main.tsx">).'
2657
+ ]
2658
+ });
2659
+ }
2660
+ const viteConfig = readIfExists2(
2661
+ path9.join(shadowRoot, "vite.config.ts"),
2662
+ path9.join(shadowRoot, "vite.config.js")
2663
+ );
2664
+ if (viteConfig && /__dirname/.test(viteConfig.content) && !/import\.meta\.dirname/.test(viteConfig.content)) {
2665
+ issues.push({
2666
+ filePath: viteConfig.filePath,
2667
+ errors: [
2668
+ "vite.config.ts uses __dirname in ESM. Replace it with import.meta.dirname so aliases and CSS resolution work under Vite."
2669
+ ]
2670
+ });
2671
+ }
2672
+ return issues;
2673
+ }
2674
+ var nextToReactPlugin = {
2675
+ id: "next-to-react",
2676
+ source: "next",
2677
+ target: "react",
2678
+ layer: "frontend",
2679
+ fileHint(filePath) {
2680
+ const normalized = filePath.replace(/\\/g, "/");
2681
+ if (/\/(main|index)\.(t|j)sx?$/i.test(normalized) && !/\/src\/app\//i.test(normalized)) {
2682
+ return `
2683
+ This is the Vite/React entry. It must:
2684
+ 1. import the global CSS Next loaded from layout (often ./index.css)
2685
+ 2. wrap the tree in BrowserRouter
2686
+ 3. render the former home screen (whatever the source used as "/")
2687
+ Never render only an empty mount node \u2014 Vite will show a blank page and still report ready.
2688
+ `;
2689
+ }
2690
+ if (/layout\.(t|j)sx?$/i.test(normalized) && /\/(src\/)?app\//i.test(normalized)) {
2691
+ return `
2692
+ This Next layout will not run in Vite. Move its global CSS import to src/main.tsx and its HTML shell into index.html. Then delete this layout after the SPA boots.
2051
2693
  `;
2052
- appendToReport(plan.targetPath, footer);
2694
+ }
2695
+ return "";
2696
+ },
2697
+ verifyShadow(shadowRoot) {
2698
+ return [...analyzeNextToReactStyles(shadowRoot), ...analyzeNextToReactBootstrap(shadowRoot)];
2699
+ },
2700
+ repairTargets(shadowRoot) {
2701
+ return [
2702
+ path9.join(shadowRoot, "package.json"),
2703
+ path9.join(shadowRoot, "vite.config.ts"),
2704
+ path9.join(shadowRoot, "vite.config.js"),
2705
+ path9.join(shadowRoot, "index.html"),
2706
+ path9.join(shadowRoot, "src", "main.tsx"),
2707
+ path9.join(shadowRoot, "src", "main.jsx"),
2708
+ path9.join(shadowRoot, "src", "App.tsx"),
2709
+ path9.join(shadowRoot, "src", "App.jsx")
2710
+ ];
2053
2711
  }
2054
2712
  };
2055
- function createReporterAgent() {
2056
- return createAgent5({
2057
- name: "Reporter",
2058
- capabilities: ["reporting"],
2059
- instruction: "You are the Reporter Agent. Your job is to compile a human-readable markdown report of the migration process.",
2060
- tools: [],
2061
- handlers: [
2062
- { specification: new WhenMigrationStarted(), processor: startedProcessor },
2063
- { specification: new WhenFileMigrated2(), processor: migratedProcessor },
2064
- { specification: new WhenFileReviewed(), processor: reviewedProcessor },
2065
- { specification: new WhenFileRejected2(), processor: rejectedProcessor },
2066
- { specification: new WhenFileFatalMismatch2(), processor: fatalProcessor },
2067
- { specification: new WhenMigrationCompleted(), processor: completedProcessor }
2068
- ]
2069
- });
2713
+
2714
+ // ../application/src/migration/plugins/reactToNext.ts
2715
+ import * as path10 from "path";
2716
+ var reactToNextPlugin = {
2717
+ id: "react-to-next",
2718
+ source: "react",
2719
+ target: "next",
2720
+ layer: "frontend",
2721
+ fileHint(filePath, ctx) {
2722
+ return nextMigrationFileHint(filePath, ctx.target);
2723
+ },
2724
+ verifyShadow(shadowRoot) {
2725
+ return analyzeReactToNextStructure(shadowRoot);
2726
+ },
2727
+ repairTargets(shadowRoot) {
2728
+ return [
2729
+ path10.join(shadowRoot, "package.json"),
2730
+ path10.join(shadowRoot, "next.config.ts"),
2731
+ path10.join(shadowRoot, "next.config.js"),
2732
+ path10.join(shadowRoot, "next.config.mjs"),
2733
+ path10.join(shadowRoot, "src", "app", "page.tsx"),
2734
+ path10.join(shadowRoot, "src", "app", "layout.tsx"),
2735
+ path10.join(shadowRoot, "app", "page.tsx"),
2736
+ path10.join(shadowRoot, "app", "layout.tsx"),
2737
+ ...listAppRouterFiles(shadowRoot)
2738
+ ];
2739
+ }
2740
+ };
2741
+
2742
+ // ../application/src/migration/plugins/index.ts
2743
+ function registerBuiltinMigrationPlugins() {
2744
+ registerMigrationPlugin(reactToNextPlugin);
2745
+ registerMigrationPlugin(nextToReactPlugin);
2746
+ registerMigrationPlugin(vueTargetPlugin);
2747
+ registerMigrationPlugin(svelteTargetPlugin);
2748
+ registerMigrationPlugin(angularTargetPlugin);
2070
2749
  }
2071
2750
 
2072
2751
  // ../application/src/index.ts
2073
- import { createAgent as createAgent6, SituationSpecification as SituationSpecification6 } from "@mozaik-ai/core";
2752
+ import { createAgent as createAgent8, createHuman, SituationSpecification as SituationSpecification8 } from "@mozaik-ai/core";
2074
2753
 
2075
2754
  // ../application/src/MigrationRunner.ts
2076
2755
  import { randomUUID } from "crypto";
@@ -2118,13 +2797,14 @@ var MigrationRunner = class {
2118
2797
  createdAt: /* @__PURE__ */ new Date()
2119
2798
  };
2120
2799
  await this.store.savePlan(plan);
2121
- const { createHuman } = await import("@mozaik-ai/core");
2122
- const human = createHuman({ name: "System", capabilities: [], handlers: [] });
2123
- join(human);
2800
+ const dispatcherId = resolveRuntime().state.dispatcherId;
2801
+ if (!dispatcherId) {
2802
+ throw new Error("Mozaik System participant is not joined.");
2803
+ }
2124
2804
  sendEvent(
2125
2805
  {
2126
2806
  type: "migration.started" /* MIGRATION_STARTED */,
2127
- producerId: human.getId(),
2807
+ producerId: dispatcherId,
2128
2808
  occurredAt: /* @__PURE__ */ new Date(),
2129
2809
  payload: {
2130
2810
  planId,
@@ -2132,7 +2812,7 @@ var MigrationRunner = class {
2132
2812
  shadowWorkspacePath: shadowPath
2133
2813
  }
2134
2814
  },
2135
- human.getId()
2815
+ dispatcherId
2136
2816
  );
2137
2817
  return { planId, shadowPath, runId };
2138
2818
  }
@@ -2148,15 +2828,19 @@ var MigrationRunner = class {
2148
2828
 
2149
2829
  // ../application/src/index.ts
2150
2830
  function bootstrapMetamorph(repository, tools = []) {
2831
+ registerBuiltinMigrationPlugins();
2151
2832
  initializeRuntime({
2152
2833
  state: new MetamorphState(repository)
2153
2834
  });
2154
- class AllEventsSpecification extends SituationSpecification6 {
2835
+ const dispatcher = createHuman({ name: "System", capabilities: [], handlers: [] });
2836
+ join(dispatcher);
2837
+ resolveRuntime().state.dispatcherId = dispatcher.getId();
2838
+ class AllEventsSpecification extends SituationSpecification8 {
2155
2839
  isSatisfiedBy() {
2156
2840
  return true;
2157
2841
  }
2158
2842
  }
2159
- const loggerAgent = createAgent6({
2843
+ const loggerAgent = createAgent8({
2160
2844
  name: "TelemetryLogger",
2161
2845
  capabilities: [],
2162
2846
  instruction: "You silently log everything.",
@@ -2166,10 +2850,10 @@ function bootstrapMetamorph(repository, tools = []) {
2166
2850
  specification: new AllEventsSpecification(),
2167
2851
  processor: {
2168
2852
  async apply({ event }) {
2169
- const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
2853
+ const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
2170
2854
  const runtime = resolveRuntime2();
2171
2855
  const repository2 = runtime.state.repository;
2172
- if (event.type.includes("migration") || event.type.includes("file")) {
2856
+ if (event.type.includes("migration") || event.type.includes("file") || event.type.includes("phase") || event.type.includes("system")) {
2173
2857
  await repository2.logEvent(event.type, {
2174
2858
  ...event.payload,
2175
2859
  producerId: event.producerId
@@ -2185,13 +2869,18 @@ function bootstrapMetamorph(repository, tools = []) {
2185
2869
  const worker = createWorkerAgent(tools);
2186
2870
  const reviewer = createReviewerAgent(tools);
2187
2871
  const packageManager = createPackageManagerAgent();
2188
- const reporter = createReporterAgent();
2872
+ const coordinator = createCoordinatorAgent();
2873
+ const reporterTools = tools.filter((tool) => tool.name === "read_file" || tool.name === "write_file" || tool.name === "list_directory");
2874
+ const reporter = createReporterAgent(reporterTools);
2875
+ const integrationAgent = createIntegrationAgent(tools);
2189
2876
  join(mapper);
2190
2877
  join(worker);
2191
2878
  join(reviewer);
2192
2879
  join(packageManager);
2880
+ join(coordinator);
2193
2881
  join(reporter);
2194
- console.log(`[App] Mozaik initialized. Agents joined: Mapper, Worker, Reviewer, PackageManager, Reporter`);
2882
+ join(integrationAgent);
2883
+ console.log(`[App] Mozaik initialized. Agents joined: Mapper, Worker, Reviewer, PackageManager, Coordinator, Reporter, IntegrationAgent`);
2195
2884
  return {
2196
2885
  mapperId: mapper.getId(),
2197
2886
  workerId: worker.getId(),
@@ -2211,9 +2900,11 @@ program.command("run [path]").description("Run a migration on the specified dire
2211
2900
  express: ["fastify", "nestjs"],
2212
2901
  fastify: ["express", "nestjs"],
2213
2902
  nestjs: ["express", "fastify"],
2214
- react: ["next", "vue"],
2215
- next: ["react"],
2216
- vue: ["react"]
2903
+ react: ["next", "vue", "angular", "svelte"],
2904
+ next: ["react", "vue", "angular", "svelte"],
2905
+ vue: ["react", "next", "angular", "svelte"],
2906
+ angular: ["react", "next", "vue", "svelte"],
2907
+ svelte: ["react", "next", "vue", "angular"]
2217
2908
  };
2218
2909
  if (!from) {
2219
2910
  console.log(chalk.gray(`
@@ -2264,12 +2955,16 @@ program.command("run [path]").description("Run a migration on the specified dire
2264
2955
  const spinner = ora("Initializing...").start();
2265
2956
  try {
2266
2957
  const store = new SQLiteStateStore(".metamorph");
2958
+ const shadowBase = path11.resolve(".metamorph/shadow");
2267
2959
  const tools = [
2268
- createReadFileTool(),
2269
- createWriteFileTool(),
2270
- createRenameFileTool(),
2271
- createCreateFileTool(),
2272
- createDeleteFileTool()
2960
+ createReadFileTool(shadowBase),
2961
+ createWriteFileTool(shadowBase),
2962
+ createRenameFileTool(shadowBase),
2963
+ createCreateFileTool(shadowBase),
2964
+ createDeleteFileTool(shadowBase),
2965
+ createListDirectoryTool(shadowBase),
2966
+ createCheckProjectDiagnosticsTool(shadowBase),
2967
+ createRunBuildTool(shadowBase)
2273
2968
  ];
2274
2969
  const runner = new MigrationRunner(store, tools);
2275
2970
  const result = await runner.startMigration({
@@ -2281,7 +2976,7 @@ program.command("run [path]").description("Run a migration on the specified dire
2281
2976
  console.log(chalk.gray(`Shadow: ${result.shadowPath}`));
2282
2977
  console.log(chalk.blue(`
2283
2978
  \u23F3 Agents are now working...`));
2284
- await new Promise((resolve3) => setTimeout(resolve3, 2e4));
2979
+ await new Promise((resolve5) => setTimeout(resolve5, 2e4));
2285
2980
  console.log(chalk.green(`
2286
2981
  \u2705 Migration simulation finished.`));
2287
2982
  process.exit(0);
@@ -2291,11 +2986,11 @@ program.command("run [path]").description("Run a migration on the specified dire
2291
2986
  process.exit(1);
2292
2987
  }
2293
2988
  });
2294
- program.command("apply <runId> <targetPath>").description("Apply a completed migration to the target project").action(async (runId, targetPath) => {
2989
+ 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) => {
2295
2990
  try {
2296
2991
  const integrator = new MigrationIntegrator(new ShadowWorkspace());
2297
- const message = await integrator.applyMigration(runId, targetPath);
2298
- console.log(chalk.green(`\u2705 ${message}`));
2992
+ const result = await integrator.applyMigration(runId, targetPath);
2993
+ console.log(chalk.green(`\u2705 ${result.message}`));
2299
2994
  } catch (error) {
2300
2995
  console.error(chalk.red(`\u274C Failed to apply migration: ${error instanceof Error ? error.message : String(error)}`));
2301
2996
  process.exit(1);
@@ -2379,12 +3074,16 @@ program.command("ui").description("Start the Metamorph Dashboard API server").op
2379
3074
  process.exit(1);
2380
3075
  }
2381
3076
  const store = new SQLiteStateStore(".metamorph");
3077
+ const shadowBase = path11.resolve(".metamorph/shadow");
2382
3078
  const tools = [
2383
- createReadFileTool(),
2384
- createWriteFileTool(),
2385
- createRenameFileTool(),
2386
- createCreateFileTool(),
2387
- createDeleteFileTool()
3079
+ createReadFileTool(shadowBase),
3080
+ createWriteFileTool(shadowBase),
3081
+ createRenameFileTool(shadowBase),
3082
+ createCreateFileTool(shadowBase),
3083
+ createDeleteFileTool(shadowBase),
3084
+ createListDirectoryTool(shadowBase),
3085
+ createCheckProjectDiagnosticsTool(shadowBase),
3086
+ createRunBuildTool(shadowBase)
2388
3087
  ];
2389
3088
  const runner = new MigrationRunner(store, tools);
2390
3089
  const app = await createApiServer(store, runner);