agent-mp 0.5.12 → 0.5.15

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.
@@ -50,9 +50,16 @@ export declare class AgentEngine {
50
50
  * Allowed subdirectory files: <service>/architecture.md (not touched here)
51
51
  */
52
52
  private _cleanContextDir;
53
+ /**
54
+ * Pre-read the actual file contents (manifests, env, entry points, controllers, schemas)
55
+ * for every component in the project, so the LLM does not need tool-use to inspect them.
56
+ * This is critical when the explorer runs against the Qwen API (no tools available)
57
+ * to avoid the LLM hallucinating tool calls instead of producing the real document.
58
+ */
59
+ private _prefetchProjectFiles;
53
60
  runExplorer(task?: string): Promise<string>;
54
- /** Called when the current binary IS the configured explorer CLI (prevents recursion).
55
- * Builds the full exploration prompt and calls Qwen API using own credentials. */
61
+ /** Legacy entry point delegates to runExplorer.
62
+ * Kept for backward compatibility with anything that may still call it. */
56
63
  runExplorerDirect(task?: string): Promise<string>;
57
64
  runFullCycle(task: string): Promise<void>;
58
65
  }
@@ -202,6 +202,107 @@ function extractJson(text) {
202
202
  throw new Error('No JSON found');
203
203
  return JSON.parse(c.slice(first, last + 1));
204
204
  }
205
+ /** Read a file safely, truncating to maxChars. Returns null if missing or empty. */
206
+ async function readFileSafe(p, maxChars) {
207
+ try {
208
+ const content = await fs.readFile(p, 'utf-8');
209
+ if (!content || !content.trim())
210
+ return null;
211
+ if (content.length > maxChars) {
212
+ return content.slice(0, maxChars) + `\n... [truncado, ${content.length - maxChars} caracteres mas]`;
213
+ }
214
+ return content;
215
+ }
216
+ catch {
217
+ return null;
218
+ }
219
+ }
220
+ const PREFETCH_SKIP_DIRS = new Set([
221
+ '.agent', 'node_modules', '.git', 'dist', 'build', 'target',
222
+ '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.next',
223
+ 'coverage', '.cache', 'out', 'bin', 'obj',
224
+ ]);
225
+ /** Walk a component dir looking for entry point, controllers, routers, schemas, models. */
226
+ async function walkForKeyFiles(root) {
227
+ const result = {
228
+ entry: null,
229
+ controllers: [],
230
+ schemas: [],
231
+ };
232
+ const SOURCE_EXT = /\.(ts|tsx|js|jsx|py|go|java|kt|rs|cs)$/;
233
+ const SKIP_FILE = /\.(test|spec|d)\.[a-z]+$/i;
234
+ const ENTRY_NAMES = new Set([
235
+ 'main.ts', 'main.js', 'main.py', 'main.go', 'Main.java',
236
+ 'index.ts', 'index.js', 'index.py',
237
+ 'app.ts', 'app.js', 'app.py',
238
+ 'server.ts', 'server.js', 'server.py',
239
+ 'application.ts', 'application.js', 'application.py',
240
+ ]);
241
+ const ENTRY_REGEX = /Application\.(java|kt)$/;
242
+ const CTRL_REGEX = /(controller|router|routes|handler|endpoint|resource)/i;
243
+ const SCHEMA_REGEX = /(schema|\.entity|\.model|\.dto|models?\.|entities?\.)/i;
244
+ async function walk(dir, depth) {
245
+ if (depth > 6)
246
+ return;
247
+ if (result.controllers.length >= 5 && result.schemas.length >= 5 && result.entry)
248
+ return;
249
+ let entries;
250
+ try {
251
+ entries = await fs.readdir(dir, { withFileTypes: true });
252
+ }
253
+ catch {
254
+ return;
255
+ }
256
+ for (const e of entries) {
257
+ if (e.name.startsWith('.'))
258
+ continue;
259
+ if (PREFETCH_SKIP_DIRS.has(e.name))
260
+ continue;
261
+ const p = path.join(dir, e.name);
262
+ if (e.isDirectory()) {
263
+ await walk(p, depth + 1);
264
+ continue;
265
+ }
266
+ if (!e.isFile())
267
+ continue;
268
+ if (!SOURCE_EXT.test(e.name))
269
+ continue;
270
+ if (SKIP_FILE.test(e.name))
271
+ continue;
272
+ // entry point detection
273
+ if (!result.entry && (ENTRY_NAMES.has(e.name) || ENTRY_REGEX.test(e.name))) {
274
+ result.entry = p;
275
+ }
276
+ if (CTRL_REGEX.test(e.name) && result.controllers.length < 5) {
277
+ result.controllers.push(p);
278
+ }
279
+ if (SCHEMA_REGEX.test(e.name) && result.schemas.length < 5) {
280
+ result.schemas.push(p);
281
+ }
282
+ }
283
+ }
284
+ await walk(root, 0);
285
+ return result;
286
+ }
287
+ /** Detect if the LLM hallucinated tool calls instead of producing the document content. */
288
+ function looksLikeHallucinatedToolCalls(text) {
289
+ if (!text)
290
+ return true;
291
+ // Common hallucination patterns when the LLM thinks it's calling tools
292
+ const TOOL_PATTERNS = [
293
+ /===\s*TOOL_CALL\s*:/i,
294
+ /^\s*<tool_call>/m,
295
+ /^\s*<function_call>/m,
296
+ /^\s*```tool_use/im,
297
+ ];
298
+ if (TOOL_PATTERNS.some(re => re.test(text)))
299
+ return true;
300
+ // If the text has NO valid file markers at all, it's also a failure case
301
+ const fileMarkerMatches = text.match(/===\s+[^\n=]+?\.md\s*===/g);
302
+ if (!fileMarkerMatches || fileMarkerMatches.length === 0)
303
+ return true;
304
+ return false;
305
+ }
205
306
  export class AgentEngine {
206
307
  config;
207
308
  projectDir;
@@ -969,6 +1070,114 @@ INSTRUCCIONES:
969
1070
  }
970
1071
  }
971
1072
  }
1073
+ /**
1074
+ * Pre-read the actual file contents (manifests, env, entry points, controllers, schemas)
1075
+ * for every component in the project, so the LLM does not need tool-use to inspect them.
1076
+ * This is critical when the explorer runs against the Qwen API (no tools available)
1077
+ * to avoid the LLM hallucinating tool calls instead of producing the real document.
1078
+ */
1079
+ async _prefetchProjectFiles() {
1080
+ const MAX_MANIFEST = 5000;
1081
+ const MAX_ENV = 2000;
1082
+ const MAX_README = 3000;
1083
+ const MAX_ENTRY = 3000;
1084
+ const MAX_CTRL = 2500;
1085
+ const MAX_SCHEMA = 1800;
1086
+ let topEntries;
1087
+ try {
1088
+ topEntries = await fs.readdir(this.projectDir, { withFileTypes: true });
1089
+ }
1090
+ catch {
1091
+ return '';
1092
+ }
1093
+ const sections = [];
1094
+ // Top-level manifest / readme / docker (gives global context)
1095
+ const TOP_GLOBAL_FILES = ['package.json', 'pom.xml', 'build.gradle', 'requirements.txt', 'go.mod', 'Cargo.toml', 'README.md', 'docker-compose.yml', 'docker-compose.yaml', 'Makefile'];
1096
+ const topGlobal = [];
1097
+ for (const fname of TOP_GLOBAL_FILES) {
1098
+ const content = await readFileSafe(path.join(this.projectDir, fname), MAX_MANIFEST);
1099
+ if (content)
1100
+ topGlobal.push(`### ROOT: ${fname}\n\`\`\`\n${content}\n\`\`\``);
1101
+ }
1102
+ if (topGlobal.length > 0) {
1103
+ sections.push(`## CONTEXTO GLOBAL DEL PROYECTO\n\n${topGlobal.join('\n\n')}`);
1104
+ }
1105
+ // Each top-level directory = candidate component
1106
+ const components = topEntries
1107
+ .filter(e => e.isDirectory() && !PREFETCH_SKIP_DIRS.has(e.name) && !e.name.startsWith('.'))
1108
+ .sort((a, b) => a.name.localeCompare(b.name));
1109
+ for (const comp of components) {
1110
+ const compDir = path.join(this.projectDir, comp.name);
1111
+ const compSection = [`## COMPONENTE: ${comp.name}`];
1112
+ // Manifest
1113
+ const MANIFESTS = ['package.json', 'requirements.txt', 'pyproject.toml', 'pom.xml', 'build.gradle', 'build.gradle.kts', 'Cargo.toml', 'go.mod', 'composer.json', 'Gemfile'];
1114
+ let manifestFound = false;
1115
+ for (const m of MANIFESTS) {
1116
+ const content = await readFileSafe(path.join(compDir, m), MAX_MANIFEST);
1117
+ if (content) {
1118
+ compSection.push(`### MANIFEST: ${m}\n\`\`\`\n${content}\n\`\`\``);
1119
+ manifestFound = true;
1120
+ break;
1121
+ }
1122
+ }
1123
+ // README
1124
+ const readme = await readFileSafe(path.join(compDir, 'README.md'), MAX_README)
1125
+ || await readFileSafe(path.join(compDir, 'readme.md'), MAX_README);
1126
+ if (readme)
1127
+ compSection.push(`### README\n${readme}`);
1128
+ // Env / config files (try several conventional locations)
1129
+ const ENV_FILES = ['.env', '.env.example', '.env.sample', '.env.local', '.env.dev', 'application.yml', 'application.yaml', 'application.properties', 'config.yaml', 'config.yml'];
1130
+ const ENV_DIRS = [compDir, path.join(compDir, 'src', 'main', 'resources'), path.join(compDir, 'config'), path.join(compDir, 'src', 'config')];
1131
+ const seenEnv = new Set();
1132
+ let envCount = 0;
1133
+ for (const dir of ENV_DIRS) {
1134
+ for (const f of ENV_FILES) {
1135
+ if (envCount >= 3)
1136
+ break;
1137
+ const p = path.join(dir, f);
1138
+ if (seenEnv.has(p))
1139
+ continue;
1140
+ seenEnv.add(p);
1141
+ const content = await readFileSafe(p, MAX_ENV);
1142
+ if (content) {
1143
+ const rel = path.relative(compDir, p);
1144
+ compSection.push(`### CONFIG/ENV: ${rel}\n\`\`\`\n${content}\n\`\`\``);
1145
+ envCount++;
1146
+ }
1147
+ }
1148
+ }
1149
+ // Walk source for entry, controllers, schemas
1150
+ const found = await walkForKeyFiles(compDir);
1151
+ if (found.entry) {
1152
+ const content = await readFileSafe(found.entry, MAX_ENTRY);
1153
+ if (content) {
1154
+ const rel = path.relative(compDir, found.entry);
1155
+ compSection.push(`### ENTRY POINT: ${rel}\n\`\`\`\n${content}\n\`\`\``);
1156
+ }
1157
+ }
1158
+ const ctrls = found.controllers.slice(0, 3);
1159
+ for (const ctrl of ctrls) {
1160
+ const content = await readFileSafe(ctrl, MAX_CTRL);
1161
+ if (content) {
1162
+ const rel = path.relative(compDir, ctrl);
1163
+ compSection.push(`### CONTROLLER/ROUTER: ${rel}\n\`\`\`\n${content}\n\`\`\``);
1164
+ }
1165
+ }
1166
+ const schemas = found.schemas.slice(0, 3);
1167
+ for (const sch of schemas) {
1168
+ const content = await readFileSafe(sch, MAX_SCHEMA);
1169
+ if (content) {
1170
+ const rel = path.relative(compDir, sch);
1171
+ compSection.push(`### SCHEMA/MODEL: ${rel}\n\`\`\`\n${content}\n\`\`\``);
1172
+ }
1173
+ }
1174
+ // Only add the component if we read at least the manifest or one source file
1175
+ if (manifestFound || found.entry || found.controllers.length > 0 || found.schemas.length > 0) {
1176
+ sections.push(compSection.join('\n\n'));
1177
+ }
1178
+ }
1179
+ return sections.join('\n\n---\n\n');
1180
+ }
972
1181
  async runExplorer(task) {
973
1182
  if (!this.config.roles.explorer) {
974
1183
  if (!this.config.fallback_global) {
@@ -983,8 +1192,8 @@ INSTRUCCIONES:
983
1192
  const contextDir = path.join(agentDir, 'context');
984
1193
  await fs.mkdir(contextDir, { recursive: true });
985
1194
  await fs.mkdir(path.join(agentDir, 'rules'), { recursive: true });
986
- // Clean up stray files before running
987
- await this._cleanContextDir(contextDir);
1195
+ // NOTE: cleanup of stray files moved to AFTER successful parse, to avoid
1196
+ // wiping previous docs when the explorer fails (e.g. tool-call hallucination).
988
1197
  // Build a quick filesystem snapshot to give the explorer context
989
1198
  let fsSnapshot = '';
990
1199
  try {
@@ -992,6 +1201,17 @@ INSTRUCCIONES:
992
1201
  fsSnapshot = execSync(`find "${this.projectDir}" -maxdepth 3 -not -path '*/.agent/*' -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/__pycache__/*' -not -path '*/.venv/*' -not -path '*/target/*' -not -path '*/build/*' -not -name '.git' | sort | head -300`, { encoding: 'utf-8', timeout: 10000 });
993
1202
  }
994
1203
  catch { /* ignore */ }
1204
+ // Pre-read real file contents so the LLM has actual data instead of needing tools
1205
+ const sp0 = this._startSpinner('explorer pre-fetch reading project files');
1206
+ let prefetched = '';
1207
+ try {
1208
+ prefetched = await this._prefetchProjectFiles();
1209
+ sp0.push(`${prefetched.split('## COMPONENTE:').length - 1} componente(s) leidos`);
1210
+ }
1211
+ catch (err) {
1212
+ sp0.push(`prefetch error: ${err.message}`);
1213
+ }
1214
+ sp0.stop();
995
1215
  // Read existing main architecture doc
996
1216
  const mainArchPath = path.join(contextDir, 'architecture.md');
997
1217
  let existingMainArch = '';
@@ -1024,6 +1244,8 @@ STACK: ${this.config.stack}
1024
1244
  ESTRUCTURA DE ARCHIVOS (excluye .agent, node_modules, .git, dist, __pycache__, .venv, target, build):
1025
1245
  ${fsSnapshot}
1026
1246
 
1247
+ ${prefetched ? `================================================================\nARCHIVOS REALES LEIDOS DEL PROYECTO (esta es tu fuente de verdad)\n================================================================\n${prefetched}\n` : ''}
1248
+
1027
1249
  ${existingMainArch ? `=== DOC EXISTENTE: architecture.md (principal) ===\n${existingMainArch.slice(0, 2000)}\n` : ''}
1028
1250
  ${existingComponentDocs ? `=== DOC EXISTENTE por componente ===\n${existingComponentDocs.slice(0, 3000)}\n` : ''}
1029
1251
 
@@ -1031,36 +1253,32 @@ ${existingComponentDocs ? `=== DOC EXISTENTE por componente ===\n${existingCompo
1031
1253
  OBJETIVO
1032
1254
  ================================================================
1033
1255
  Generar documentacion ESCALONADA en 3 niveles, util TANTO para personas funcionales (PM, negocio) como tecnicas (devs).
1034
- Detallada pero NO excesiva. Concreta, con datos REALES leidos de los archivos. Cero especulaciones.
1256
+ Detallada pero NO excesiva. Concreta, con datos REALES extraidos de los archivos que estan mas arriba. Cero especulaciones.
1035
1257
 
1036
1258
  ================================================================
1037
1259
  REGLA DE ORO — PROHIBIDO INVENTAR
1038
1260
  ================================================================
1039
1261
  NO uses jamas: "Inferido", "Probablemente", "Posiblemente", "Asumido", "(supuesto)", "(quizas)", "parece ser".
1040
- Si no podes verificar un dato leyendo un archivo concreto OMITELO. Es mejor una doc corta y veraz que una larga llena de suposiciones.
1041
- Cada puerto, version, endpoint, env var, schema o ruta que aparezca en la doc DEBE haber sido leido de un archivo real.
1262
+ Si un dato (puerto, version, endpoint, env var, schema, ruta) no aparece en los ARCHIVOS REALES de arriba OMITILO.
1263
+ Es mejor una doc corta y veraz que una larga llena de suposiciones.
1264
+ NO devuelvas marcadores tipo "TOOL_CALL", "tool_use", "function_call" — vos NO tenes que llamar herramientas, tenes que devolver MARKDOWN.
1042
1265
 
1043
1266
  ================================================================
1044
- WORKFLOW QUE TENES QUE EJECUTAR (usa tus tools de read_file/grep_search/list_directory)
1267
+ COMO TRABAJAR (lectura ya hecha por el engine)
1045
1268
  ================================================================
1046
- 1. Lista DIRECTORIO_TRABAJO. Identifica los componentes (apps/services/libs). Descarta lo trivial (solo configs/docs/scripts).
1047
- 2. Para CADA componente no trivial, LEE COMO MINIMO:
1048
- a. Manifest del proyecto: package.json | requirements.txt | pyproject.toml | pom.xml | build.gradle | Cargo.toml | go.mod
1049
- de ahi sacas: nombre, version, framework, dependencias clave con sus versiones, scripts/tasks, entry point
1050
- b. Entry point real: src/main.ts | index.js | app.py | Application.java | cmd/main.go (segun stack)
1051
- de ahi sacas: puerto, middlewares, modulos cargados, configuracion inicial
1052
- c. Archivo de variables de entorno: .env | .env.example | application.yml | config.* | settings.py
1053
- de ahi sacas: variables reales con su proposito
1054
- d. Al menos 2-3 controladores/routers/handlers principales
1055
- de ahi extraes endpoints REALES (metodo, ruta, parametros)
1056
- e. Al menos 2-3 modelos/schemas/entities principales
1057
- → de ahi extraes el shape de datos
1058
- f. README.md del componente si existe
1059
- 3. Mapea RELACIONES REALES entre componentes (no asumas):
1060
- - Buscando URLs/hosts de otros servicios en .env
1061
- - Buscando imports cross-component
1062
- - Buscando cadenas de conexion a BBDD/colas/cache
1063
- 4. Recien con esa informacion concreta, genera los archivos de documentacion.
1269
+ El engine YA leyo por vos los archivos clave de cada componente y te los pasa arriba en la seccion "ARCHIVOS REALES LEIDOS DEL PROYECTO". Tu tarea es:
1270
+
1271
+ 1. Identificar los componentes en la estructura de archivos.
1272
+ 2. Para cada componente, extraer de sus archivos reales:
1273
+ - Del MANIFEST: nombre, version, framework, dependencias clave con sus versiones, scripts, entry point
1274
+ - Del ENTRY POINT: puerto real, middlewares, modulos cargados
1275
+ - De los CONFIG/ENV: variables reales y su proposito
1276
+ - De los CONTROLLER/ROUTER: endpoints REALES (metodo, ruta, parametros)
1277
+ - De los SCHEMA/MODEL: shape real de los datos
1278
+ 3. Identificar relaciones REALES entre componentes (URLs/hosts de otros servicios en .env, imports cross-component, conexiones a BBDD).
1279
+ 4. Generar la documentacion en los 3 niveles, en formato markdown, dentro de los bloques === ... ===.
1280
+
1281
+ NO inventes datos. Si un componente no tiene una de esas piezas en los archivos leidos, simplemente omiti esa seccion en su documentacion.
1064
1282
 
1065
1283
  ================================================================
1066
1284
  LENGUAJE DUAL (regla critica)
@@ -1264,33 +1482,56 @@ FORMATO DE SALIDA — OBLIGATORIO
1264
1482
  ================================================================
1265
1483
  Devolve UNICAMENTE bloques de archivos separados por marcadores ===. Nada de explicaciones extra fuera de los bloques.
1266
1484
 
1267
- === architecture.md ===
1485
+ IMPORTANTE: TODAS las rutas son RELATIVAS al directorio del proyecto.
1486
+ Los archivos se escriben SIEMPRE dentro de .agent/context/
1487
+
1488
+ Ejemplos de marcadores:
1489
+ === .agent/context/architecture.md ===
1490
+ === .agent/context/datamart-data-access-api/architecture.md ===
1491
+ === .agent/context/datamart-data-access-api/modules/auth.md ===
1492
+ === .agent/context/nexus-core-api/architecture.md ===
1493
+ === .agent/context/nexus-core-api/modules/users.md ===
1494
+
1495
+ === .agent/context/architecture.md ===
1268
1496
  [contenido completo del NIVEL 0]
1269
1497
 
1270
- === nombre-componente-1/architecture.md ===
1498
+ === .agent/context/nombre-componente-1/architecture.md ===
1271
1499
  [contenido completo del NIVEL 1 del componente 1]
1272
1500
 
1273
- === nombre-componente-1/modules/auth.md ===
1501
+ === .agent/context/nombre-componente-1/modules/auth.md ===
1274
1502
  [contenido completo del NIVEL 2 del modulo auth]
1275
1503
 
1276
- === nombre-componente-1/modules/leads.md ===
1504
+ === .agent/context/nombre-componente-1/modules/leads.md ===
1277
1505
  [contenido completo del NIVEL 2 del modulo leads]
1278
1506
 
1279
- === nombre-componente-2/architecture.md ===
1507
+ === .agent/context/nombre-componente-2/architecture.md ===
1280
1508
  ...
1281
1509
 
1282
1510
  REGLAS DE PATHS:
1283
- - El archivo principal SIEMPRE es: === architecture.md ===
1284
- - Por componente: === [nombre-componente]/architecture.md ===
1285
- - Por modulo interno: === [nombre-componente]/modules/[nombre-modulo].md ===
1286
- - USA SIEMPRE rutas RELATIVAS, jamas absolutas
1511
+ - El archivo principal SIEMPRE es: === .agent/context/architecture.md ===
1512
+ - Por componente: === .agent/context/[nombre-componente]/architecture.md ===
1513
+ - Por modulo interno: === .agent/context/[nombre-componente]/modules/[nombre-modulo].md ===
1287
1514
  - Documenta TODOS los componentes no triviales del DIRECTORIO_TRABAJO
1288
1515
  - Si existe documentacion previa, ACTUALIZALA preservando lo que sigue siendo valido y agregando lo nuevo`;
1289
1516
  const res = await this.runWithFallback('explorer', prompt, 'Exploracion');
1290
1517
  const text = extractCliText(res);
1518
+ // Always overwrite the single last-run report so we have a debug trail
1519
+ try {
1520
+ await writeFile(path.join(contextDir, 'explorer-last.md'), `# Explorer Report\n\nTask: ${effectiveTask}\nDate: ${new Date().toISOString()}\n\n${text}\n`);
1521
+ }
1522
+ catch { /* don't fail if save fails */ }
1523
+ // Detect tool-call hallucination or empty responses BEFORE touching anything
1524
+ if (looksLikeHallucinatedToolCalls(text)) {
1525
+ log.error('Explorer no devolvio bloques === *.md ===');
1526
+ log.warn('Posible causa: el LLM intento llamar tools que no estan disponibles via API.');
1527
+ log.warn(`Output guardado en: ${path.join(contextDir, 'explorer-last.md')}`);
1528
+ log.warn('Documentacion previa preservada (no se modifico nada).');
1529
+ return text;
1530
+ }
1291
1531
  // Parse sections separated by === path/file.md === markers
1292
1532
  const sections = text.split(/===\s+(.+?\.md)\s*===/).slice(1);
1293
- let filesWritten = 0;
1533
+ // First pass: collect all valid file writes WITHOUT touching disk yet (transactional)
1534
+ const pendingWrites = [];
1294
1535
  for (let i = 0; i < sections.length; i += 2) {
1295
1536
  const fileName = sections[i].trim();
1296
1537
  let content = sections[i + 1] ? sections[i + 1].trim() : '';
@@ -1298,145 +1539,57 @@ REGLAS DE PATHS:
1298
1539
  continue;
1299
1540
  // Clean up code fences if present
1300
1541
  content = content.replace(/^```markdown\s*/i, '').replace(/^```\s*$/gm, '').trim();
1301
- try {
1302
- let targetPath;
1303
- if (fileName === 'architecture.md' || fileName === 'ARCHITECTURE.md') {
1304
- // Main architecture doc
1305
- targetPath = mainArchPath;
1542
+ if (!content)
1543
+ continue;
1544
+ // Normalize: strip any .agent/context/ prefix from the marker
1545
+ let relPath = fileName.replace(/^\.agent\/context\//i, '').replace(/^\/+/, '');
1546
+ let targetPath = null;
1547
+ if (relPath === 'architecture.md' || relPath === 'ARCHITECTURE.md') {
1548
+ targetPath = mainArchPath;
1549
+ }
1550
+ else {
1551
+ const pathParts = relPath.split(/[\/\\]/).filter(Boolean);
1552
+ if (pathParts.length >= 3 && pathParts[pathParts.length - 2] === 'modules') {
1553
+ targetPath = path.join(contextDir, pathParts[pathParts.length - 3], 'modules', pathParts[pathParts.length - 1]);
1306
1554
  }
1307
- else {
1308
- // CRITICAL: Extract only the relative path components, strip any absolute path prefix
1309
- // fileName could be: "datamart-data-access-api/architecture.md"
1310
- // or "/home/user/project/datamart-data-access-api/architecture.md" (BAD — must ignore prefix)
1311
- // We only want the components relative to contextDir
1312
- // Remove any leading absolute path by extracting the last meaningful path segments
1313
- const cleanName = fileName.replace(/^.*[\/\\]/, ''); // strip any prefix
1314
- // Check if it's a component architecture file: "component/architecture.md" or "component/modules/mod.md"
1315
- const pathParts = fileName.split(/[\/\\]/).filter(Boolean);
1316
- // Keep only the last 2-3 parts (component name + file)
1317
- // e.g. ["/home", "user", "proj", "datamart", "architecture.md"] → ["datamart", "architecture.md"]
1318
- // e.g. ["datamart-data-access-api", "architecture.md"] → same
1319
- // e.g. ["datamart-data-access-api", "modules", "config.md"] → same
1320
- let relPath;
1321
- if (pathParts.length >= 3 && pathParts[pathParts.length - 2] === 'modules') {
1322
- // component/modules/file.md
1323
- relPath = path.join(pathParts[pathParts.length - 3], 'modules', pathParts[pathParts.length - 1]);
1324
- }
1325
- else if (pathParts.length >= 2) {
1326
- // component/file.md
1327
- relPath = path.join(pathParts[pathParts.length - 2], pathParts[pathParts.length - 1]);
1328
- }
1329
- else {
1330
- // fallback
1331
- relPath = path.join('modules', cleanName);
1332
- }
1333
- targetPath = path.join(contextDir, relPath);
1555
+ else if (pathParts.length >= 2) {
1556
+ targetPath = path.join(contextDir, pathParts[pathParts.length - 2], pathParts[pathParts.length - 1]);
1334
1557
  }
1558
+ }
1559
+ if (!targetPath)
1560
+ continue;
1561
+ pendingWrites.push({ targetPath, content, label: relPath });
1562
+ }
1563
+ if (pendingWrites.length === 0) {
1564
+ log.error('Explorer devolvio texto pero ningun bloque === *.md === fue parseable.');
1565
+ log.warn(`Output guardado en: ${path.join(contextDir, 'explorer-last.md')}`);
1566
+ log.warn('Documentacion previa preservada (no se modifico nada).');
1567
+ return text;
1568
+ }
1569
+ // Second pass: write everything to disk now that we know we have valid output
1570
+ let filesWritten = 0;
1571
+ for (const { targetPath, content, label } of pendingWrites) {
1572
+ try {
1335
1573
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
1336
1574
  await writeFile(targetPath, content);
1337
1575
  filesWritten++;
1338
1576
  }
1339
1577
  catch (err) {
1340
- log.warn(`Failed to write ${fileName}: ${err.message}`);
1578
+ log.warn(`Failed to write ${label}: ${err.message}`);
1341
1579
  }
1342
1580
  }
1343
- if (filesWritten > 0) {
1344
- log.ok(`${filesWritten} documentation file(s) written`);
1345
- }
1346
- // Overwrite the single last-run report (no timestamp accumulation)
1581
+ log.ok(`${filesWritten} documentation file(s) written`);
1582
+ // NOW it's safe to clean stray root-level .md files (after successful write).
1347
1583
  try {
1348
- await writeFile(path.join(contextDir, 'explorer-last.md'), `# Explorer Report\n\nTask: ${effectiveTask}\nDate: ${new Date().toISOString()}\n\n${text}\n`);
1584
+ await this._cleanContextDir(contextDir);
1349
1585
  }
1350
- catch { /* don't fail if save fails */ }
1586
+ catch { /* don't fail run if cleanup fails */ }
1351
1587
  return text;
1352
1588
  }
1353
- /** Called when the current binary IS the configured explorer CLI (prevents recursion).
1354
- * Builds the full exploration prompt and calls Qwen API using own credentials. */
1589
+ /** Legacy entry point delegates to runExplorer.
1590
+ * Kept for backward compatibility with anything that may still call it. */
1355
1591
  async runExplorerDirect(task) {
1356
- const role = this.config.roles.explorer;
1357
- if (!role)
1358
- throw new Error('Explorer role not configured.');
1359
- const agentDir = path.join(this.projectDir, '.agent');
1360
- const contextDir = path.join(agentDir, 'context');
1361
- await fs.mkdir(contextDir, { recursive: true });
1362
- // Clean up stray files before running
1363
- await this._cleanContextDir(contextDir);
1364
- const archPath = path.join(contextDir, 'architecture.md');
1365
- let existingArch = '';
1366
- try {
1367
- existingArch = await readFile(archPath);
1368
- }
1369
- catch { /* new */ }
1370
- const effectiveTask = task || 'Explorar y documentar todas las aplicaciones y servicios del proyecto';
1371
- const context = await this.buildOrchestratorContext();
1372
- const prompt = this.buildRolePrompt('explorer', `TAREA DE EXPLORACION: ${effectiveTask}
1373
- DIRECTORIO_TRABAJO: ${this.projectDir}
1374
- PROYECTO: ${this.config.project}
1375
-
1376
- ${existingArch ? `DOCUMENTACION EXISTENTE:\n${existingArch.slice(0, 3000)}\n` : 'Sin documentacion previa.\n'}
1377
- CONTEXTO: ${context.slice(0, 2000)}
1378
-
1379
- OBJETIVO
1380
- Generar documentacion ESCALONADA en 3 niveles (global / componente / modulo), util para perfiles funcionales y tecnicos. Detallada pero concreta. Cero suposiciones.
1381
-
1382
- REGLA DE ORO — PROHIBIDO ESPECULAR
1383
- NO uses "Inferido", "Probablemente", "Asumido", "(quizas)", "parece". Si no podes verificar un dato leyendo un archivo, OMITELO. Cada puerto, version, endpoint, env var y schema debe haber sido leido de un archivo real.
1384
-
1385
- WORKFLOW (usa tus tools)
1386
- 1. Lista DIRECTORIO_TRABAJO y detecta los componentes no triviales.
1387
- 2. Para cada componente, LEE como minimo:
1388
- - Manifest (package.json | requirements.txt | pom.xml | build.gradle | go.mod | Cargo.toml)
1389
- - Entry point (main.ts | index.js | app.py | Application.java | cmd/main.go)
1390
- - .env / .env.example / application.yml / settings.py
1391
- - 2-3 controladores/routers principales (para endpoints reales)
1392
- - 2-3 schemas/models principales (para shapes reales)
1393
- 3. Mapea relaciones reales (URLs en .env, imports cross-component, conexiones a DB/colas).
1394
-
1395
- LENGUAJE DUAL
1396
- Cada seccion abre con UNA linea simple (que entienda un PM). Despues, el detalle tecnico.
1397
- - MAL: "El microservicio orquesta la persistencia mediante un repositorio."
1398
- - BIEN: "Guarda la informacion del usuario. Internamente usa un repositorio que abstrae MongoDB."
1399
-
1400
- NIVEL 0 — ${archPath} (50-150 lineas)
1401
- Secciones: Overview funcional · Tabla de componentes (con stack+version, puerto, proposito, entry point) · Tabla de relaciones (origen, destino, tipo, proposito) · Diagrama ASCII · 3-5 flujos end-to-end (mapping funcional → tecnico) · Prerequisitos · Comandos globales.
1402
-
1403
- NIVEL 1 — ${contextDir}/<componente>/architecture.md (80-200 lineas, uno por componente)
1404
- Secciones: Que hace (simple) · Casos de uso (3-6 bullets en formato negocio) · Stack tecnico (tabla con versiones reales) · Puerto y URLs · Estructura interna (arbol real) · Tabla de modulos · Tabla de endpoints reales · Tabla de variables de entorno · Integraciones · Como levantar · Comandos.
1405
-
1406
- NIVEL 2 — ${contextDir}/<componente>/modules/<modulo>.md (40-120 lineas, uno por modulo significativo)
1407
- Secciones: Funcion (simple) · Funcion (tecnica) · Flujos / casos de uso paso a paso · Endpoints / interfaces · Schemas reales en codigo · Reglas de negocio · Archivos clave · Dependencias internas/externas/cross-component.
1408
-
1409
- CALIBRACION
1410
- - NIVEL 0: 50-150 lineas. NIVEL 1: 80-200 por componente. NIVEL 2: 40-120 por modulo.
1411
- - Si te queda corto, leiste pocos archivos. Si te queda largo, recorta el relleno.
1412
- - Componentes triviales (solo scripts/docs): mencionalos en NIVEL 0 y NO les crees subcarpeta.
1413
-
1414
- REGLAS DE ESCRITURA
1415
- - Solo podes escribir archivos dentro de ${contextDir}/
1416
- - NO crees archivos sueltos en la raiz de ${contextDir}/ (excepto architecture.md)
1417
- - Por componente usa: ${contextDir}/<componente>/architecture.md
1418
- - Por modulo usa: ${contextDir}/<componente>/modules/<modulo>.md
1419
- - Si existe documentacion previa, ACTUALIZALA preservando lo valido`);
1420
- let result;
1421
- const sp = this._startSpinner(`agent-explorer ${role.model}`);
1422
- try {
1423
- result = await callQwenAPI(prompt, role.model, (c) => this._parseChunk(c).forEach(l => sp.push(l)));
1424
- sp.stop();
1425
- }
1426
- catch (err) {
1427
- sp.stop();
1428
- if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
1429
- console.log(chalk.red('\n ✗ Sesión Qwen expirada.'));
1430
- console.log(chalk.yellow(' Ejecutá: agent-mp --login (o agent-explorer --login)\n'));
1431
- return '';
1432
- }
1433
- throw err;
1434
- }
1435
- try {
1436
- await writeFile(path.join(contextDir, 'explorer-last.md'), `# Explorer Report\n\nTask: ${effectiveTask}\nDate: ${new Date().toISOString()}\n\n${result}\n`);
1437
- }
1438
- catch { /* ignore */ }
1439
- return result;
1592
+ return this.runExplorer(task);
1440
1593
  }
1441
1594
  async runFullCycle(task) {
1442
1595
  // Header is now shown by the REPL before the first user message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-mp",
3
- "version": "0.5.12",
3
+ "version": "0.5.15",
4
4
  "description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,7 +8,7 @@
8
8
  "dist/"
9
9
  ],
10
10
  "bin": {
11
- "agent-mp": "dist/index.js"
11
+ "agent-explorer": "dist/index.js"
12
12
  },
13
13
  "scripts": {
14
14
  "build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.tmp && mv dist/index.tmp dist/index.js && chmod +x dist/index.js",