agent-mp 0.5.13 → 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.
- package/dist/core/engine.d.ts +9 -2
- package/dist/core/engine.js +294 -146
- package/package.json +2 -2
package/dist/core/engine.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
55
|
-
*
|
|
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
|
}
|
package/dist/core/engine.js
CHANGED
|
@@ -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
|
-
//
|
|
987
|
-
|
|
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
|
|
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
|
|
1041
|
-
|
|
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
|
-
|
|
1267
|
+
COMO TRABAJAR (lectura ya hecha por el engine)
|
|
1045
1268
|
================================================================
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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)
|
|
@@ -1297,9 +1515,23 @@ REGLAS DE PATHS:
|
|
|
1297
1515
|
- Si existe documentacion previa, ACTUALIZALA preservando lo que sigue siendo valido y agregando lo nuevo`;
|
|
1298
1516
|
const res = await this.runWithFallback('explorer', prompt, 'Exploracion');
|
|
1299
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
|
+
}
|
|
1300
1531
|
// Parse sections separated by === path/file.md === markers
|
|
1301
1532
|
const sections = text.split(/===\s+(.+?\.md)\s*===/).slice(1);
|
|
1302
|
-
|
|
1533
|
+
// First pass: collect all valid file writes WITHOUT touching disk yet (transactional)
|
|
1534
|
+
const pendingWrites = [];
|
|
1303
1535
|
for (let i = 0; i < sections.length; i += 2) {
|
|
1304
1536
|
const fileName = sections[i].trim();
|
|
1305
1537
|
let content = sections[i + 1] ? sections[i + 1].trim() : '';
|
|
@@ -1307,141 +1539,57 @@ REGLAS DE PATHS:
|
|
|
1307
1539
|
continue;
|
|
1308
1540
|
// Clean up code fences if present
|
|
1309
1541
|
content = content.replace(/^```markdown\s*/i, '').replace(/^```\s*$/gm, '').trim();
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
}
|
|
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]);
|
|
1323
1554
|
}
|
|
1324
|
-
else {
|
|
1325
|
-
|
|
1326
|
-
const pathParts = relPath.split(/[\/\\]/).filter(Boolean);
|
|
1327
|
-
if (pathParts.length >= 3 && pathParts[pathParts.length - 2] === 'modules') {
|
|
1328
|
-
// component/modules/file.md
|
|
1329
|
-
targetPath = path.join(contextDir, pathParts[pathParts.length - 3], 'modules', pathParts[pathParts.length - 1]);
|
|
1330
|
-
}
|
|
1331
|
-
else if (pathParts.length >= 2) {
|
|
1332
|
-
// component/file.md
|
|
1333
|
-
targetPath = path.join(contextDir, pathParts[pathParts.length - 2], pathParts[pathParts.length - 1]);
|
|
1334
|
-
}
|
|
1335
|
-
else {
|
|
1336
|
-
// Fallback
|
|
1337
|
-
continue;
|
|
1338
|
-
}
|
|
1555
|
+
else if (pathParts.length >= 2) {
|
|
1556
|
+
targetPath = path.join(contextDir, pathParts[pathParts.length - 2], pathParts[pathParts.length - 1]);
|
|
1339
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 {
|
|
1340
1573
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
1341
1574
|
await writeFile(targetPath, content);
|
|
1342
1575
|
filesWritten++;
|
|
1343
1576
|
}
|
|
1344
1577
|
catch (err) {
|
|
1345
|
-
log.warn(`Failed to write ${
|
|
1578
|
+
log.warn(`Failed to write ${label}: ${err.message}`);
|
|
1346
1579
|
}
|
|
1347
1580
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
}
|
|
1351
|
-
// 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).
|
|
1352
1583
|
try {
|
|
1353
|
-
await
|
|
1584
|
+
await this._cleanContextDir(contextDir);
|
|
1354
1585
|
}
|
|
1355
|
-
catch { /* don't fail if
|
|
1586
|
+
catch { /* don't fail run if cleanup fails */ }
|
|
1356
1587
|
return text;
|
|
1357
1588
|
}
|
|
1358
|
-
/**
|
|
1359
|
-
*
|
|
1589
|
+
/** Legacy entry point — delegates to runExplorer.
|
|
1590
|
+
* Kept for backward compatibility with anything that may still call it. */
|
|
1360
1591
|
async runExplorerDirect(task) {
|
|
1361
|
-
|
|
1362
|
-
if (!role)
|
|
1363
|
-
throw new Error('Explorer role not configured.');
|
|
1364
|
-
const agentDir = path.join(this.projectDir, '.agent');
|
|
1365
|
-
const contextDir = path.join(agentDir, 'context');
|
|
1366
|
-
await fs.mkdir(contextDir, { recursive: true });
|
|
1367
|
-
// Clean up stray files before running
|
|
1368
|
-
await this._cleanContextDir(contextDir);
|
|
1369
|
-
const archPath = path.join(contextDir, 'architecture.md');
|
|
1370
|
-
let existingArch = '';
|
|
1371
|
-
try {
|
|
1372
|
-
existingArch = await readFile(archPath);
|
|
1373
|
-
}
|
|
1374
|
-
catch { /* new */ }
|
|
1375
|
-
const effectiveTask = task || 'Explorar y documentar todas las aplicaciones y servicios del proyecto';
|
|
1376
|
-
const context = await this.buildOrchestratorContext();
|
|
1377
|
-
const prompt = this.buildRolePrompt('explorer', `TAREA DE EXPLORACION: ${effectiveTask}
|
|
1378
|
-
DIRECTORIO_TRABAJO: ${this.projectDir}
|
|
1379
|
-
PROYECTO: ${this.config.project}
|
|
1380
|
-
|
|
1381
|
-
${existingArch ? `DOCUMENTACION EXISTENTE:\n${existingArch.slice(0, 3000)}\n` : 'Sin documentacion previa.\n'}
|
|
1382
|
-
CONTEXTO: ${context.slice(0, 2000)}
|
|
1383
|
-
|
|
1384
|
-
OBJETIVO
|
|
1385
|
-
Generar documentacion ESCALONADA en 3 niveles (global / componente / modulo), util para perfiles funcionales y tecnicos. Detallada pero concreta. Cero suposiciones.
|
|
1386
|
-
|
|
1387
|
-
REGLA DE ORO — PROHIBIDO ESPECULAR
|
|
1388
|
-
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.
|
|
1389
|
-
|
|
1390
|
-
WORKFLOW (usa tus tools)
|
|
1391
|
-
1. Lista DIRECTORIO_TRABAJO y detecta los componentes no triviales.
|
|
1392
|
-
2. Para cada componente, LEE como minimo:
|
|
1393
|
-
- Manifest (package.json | requirements.txt | pom.xml | build.gradle | go.mod | Cargo.toml)
|
|
1394
|
-
- Entry point (main.ts | index.js | app.py | Application.java | cmd/main.go)
|
|
1395
|
-
- .env / .env.example / application.yml / settings.py
|
|
1396
|
-
- 2-3 controladores/routers principales (para endpoints reales)
|
|
1397
|
-
- 2-3 schemas/models principales (para shapes reales)
|
|
1398
|
-
3. Mapea relaciones reales (URLs en .env, imports cross-component, conexiones a DB/colas).
|
|
1399
|
-
|
|
1400
|
-
LENGUAJE DUAL
|
|
1401
|
-
Cada seccion abre con UNA linea simple (que entienda un PM). Despues, el detalle tecnico.
|
|
1402
|
-
- MAL: "El microservicio orquesta la persistencia mediante un repositorio."
|
|
1403
|
-
- BIEN: "Guarda la informacion del usuario. Internamente usa un repositorio que abstrae MongoDB."
|
|
1404
|
-
|
|
1405
|
-
NIVEL 0 — ${archPath} (50-150 lineas)
|
|
1406
|
-
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.
|
|
1407
|
-
|
|
1408
|
-
NIVEL 1 — ${contextDir}/<componente>/architecture.md (80-200 lineas, uno por componente)
|
|
1409
|
-
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.
|
|
1410
|
-
|
|
1411
|
-
NIVEL 2 — ${contextDir}/<componente>/modules/<modulo>.md (40-120 lineas, uno por modulo significativo)
|
|
1412
|
-
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.
|
|
1413
|
-
|
|
1414
|
-
CALIBRACION
|
|
1415
|
-
- NIVEL 0: 50-150 lineas. NIVEL 1: 80-200 por componente. NIVEL 2: 40-120 por modulo.
|
|
1416
|
-
- Si te queda corto, leiste pocos archivos. Si te queda largo, recorta el relleno.
|
|
1417
|
-
- Componentes triviales (solo scripts/docs): mencionalos en NIVEL 0 y NO les crees subcarpeta.
|
|
1418
|
-
|
|
1419
|
-
REGLAS DE ESCRITURA
|
|
1420
|
-
- Solo podes escribir archivos dentro de ${contextDir}/
|
|
1421
|
-
- NO crees archivos sueltos en la raiz de ${contextDir}/ (excepto architecture.md)
|
|
1422
|
-
- Por componente usa: ${contextDir}/<componente>/architecture.md
|
|
1423
|
-
- Por modulo usa: ${contextDir}/<componente>/modules/<modulo>.md
|
|
1424
|
-
- Si existe documentacion previa, ACTUALIZALA preservando lo valido`);
|
|
1425
|
-
let result;
|
|
1426
|
-
const sp = this._startSpinner(`agent-explorer ${role.model}`);
|
|
1427
|
-
try {
|
|
1428
|
-
result = await callQwenAPI(prompt, role.model, (c) => this._parseChunk(c).forEach(l => sp.push(l)));
|
|
1429
|
-
sp.stop();
|
|
1430
|
-
}
|
|
1431
|
-
catch (err) {
|
|
1432
|
-
sp.stop();
|
|
1433
|
-
if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
|
|
1434
|
-
console.log(chalk.red('\n ✗ Sesión Qwen expirada.'));
|
|
1435
|
-
console.log(chalk.yellow(' Ejecutá: agent-mp --login (o agent-explorer --login)\n'));
|
|
1436
|
-
return '';
|
|
1437
|
-
}
|
|
1438
|
-
throw err;
|
|
1439
|
-
}
|
|
1440
|
-
try {
|
|
1441
|
-
await writeFile(path.join(contextDir, 'explorer-last.md'), `# Explorer Report\n\nTask: ${effectiveTask}\nDate: ${new Date().toISOString()}\n\n${result}\n`);
|
|
1442
|
-
}
|
|
1443
|
-
catch { /* ignore */ }
|
|
1444
|
-
return result;
|
|
1592
|
+
return this.runExplorer(task);
|
|
1445
1593
|
}
|
|
1446
1594
|
async runFullCycle(task) {
|
|
1447
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.
|
|
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-
|
|
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",
|