elsabro 2.1.0 → 2.3.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/agents/elsabro-orchestrator.md +113 -0
- package/commands/elsabro/add-phase.md +17 -0
- package/commands/elsabro/add-todo.md +111 -53
- package/commands/elsabro/audit-milestone.md +19 -0
- package/commands/elsabro/check-todos.md +210 -31
- package/commands/elsabro/complete-milestone.md +20 -1
- package/commands/elsabro/debug.md +19 -0
- package/commands/elsabro/discuss-phase.md +18 -1
- package/commands/elsabro/execute.md +511 -58
- package/commands/elsabro/insert-phase.md +18 -1
- package/commands/elsabro/list-phase-assumptions.md +17 -0
- package/commands/elsabro/new-milestone.md +19 -0
- package/commands/elsabro/new.md +19 -0
- package/commands/elsabro/pause-work.md +19 -0
- package/commands/elsabro/plan-milestone-gaps.md +20 -1
- package/commands/elsabro/plan.md +264 -36
- package/commands/elsabro/progress.md +203 -79
- package/commands/elsabro/quick.md +19 -0
- package/commands/elsabro/remove-phase.md +17 -0
- package/commands/elsabro/research-phase.md +18 -1
- package/commands/elsabro/resume-work.md +19 -0
- package/commands/elsabro/start.md +399 -98
- package/commands/elsabro/verify-work.md +138 -5
- package/hooks/confirm-destructive.sh +145 -0
- package/hooks/hooks-config.json +81 -0
- package/hooks/lint-check.sh +238 -0
- package/hooks/post-edit-test.sh +189 -0
- package/package.json +3 -2
- package/references/SYSTEM_INDEX.md +241 -0
- package/references/command-flow.md +352 -0
- package/references/enforcement-rules.md +331 -0
- package/references/error-contracts-tests.md +1171 -0
- package/references/error-contracts.md +3102 -0
- package/references/error-handling-instructions.md +26 -12
- package/references/parallel-worktrees.md +293 -0
- package/references/state-sync.md +381 -0
- package/references/task-dispatcher.md +388 -0
- package/references/tasks-integration.md +380 -0
- package/scripts/setup-parallel-worktrees.sh +319 -0
- package/skills/api-microservice.md +765 -0
- package/skills/api-setup.md +76 -3
- package/skills/auth-setup.md +46 -6
- package/skills/chrome-extension.md +584 -0
- package/skills/cicd-setup.md +1206 -0
- package/skills/cli-tool.md +884 -0
- package/skills/database-setup.md +41 -5
- package/skills/desktop-app.md +1351 -0
- package/skills/expo-app.md +35 -2
- package/skills/full-stack-app.md +543 -0
- package/skills/memory-update.md +207 -0
- package/skills/mobile-app.md +813 -0
- package/skills/nextjs-app.md +33 -2
- package/skills/payments-setup.md +76 -1
- package/skills/review.md +331 -0
- package/skills/saas-starter.md +639 -0
- package/skills/sentry-setup.md +41 -7
- package/skills/techdebt.md +289 -0
- package/skills/testing-setup.md +1218 -0
- package/skills/tutor.md +219 -0
- package/templates/.planning/notes/.gitkeep +0 -0
- package/templates/CLAUDE.md.template +48 -0
- package/templates/error-handling-config.json +79 -2
- package/templates/mistakes.md.template +52 -0
- package/templates/patterns.md.template +114 -0
|
@@ -0,0 +1,3102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: error-contracts
|
|
3
|
+
description: Contratos de error para ejecucion paralela confiable
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ELSABRO Error Contracts
|
|
8
|
+
|
|
9
|
+
## Vision General
|
|
10
|
+
|
|
11
|
+
Este documento define los 7 contratos de error que garantizan ejecucion paralela confiable en ELSABRO. Cada contrato actua como una capa defensiva en cascada.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
+============================================================================+
|
|
15
|
+
| CASCADA DEFENSIVA DE CONTRATOS |
|
|
16
|
+
+============================================================================+
|
|
17
|
+
| |
|
|
18
|
+
| [1] REGISTRY VALIDATOR |
|
|
19
|
+
| | |
|
|
20
|
+
| v Agentes/comandos existen? |
|
|
21
|
+
| [2] TASK LIFECYCLE -----> NO --> ERROR: AGENT_NOT_FOUND |
|
|
22
|
+
| | |
|
|
23
|
+
| v Estado valido? |
|
|
24
|
+
| [3] TIMEOUT HANDLER -----> NO --> ERROR: INVALID_STATE_TRANSITION |
|
|
25
|
+
| | |
|
|
26
|
+
| v Dentro de tiempo? |
|
|
27
|
+
| [4] RETRY POLICY --------> NO --> ERROR: TASK_TIMEOUT |
|
|
28
|
+
| | |
|
|
29
|
+
| v Reintentos agotados? |
|
|
30
|
+
| [5] ERROR AGGREGATOR ----> SI --> ERROR: RETRY_EXHAUSTED |
|
|
31
|
+
| | |
|
|
32
|
+
| v Errores recolectados |
|
|
33
|
+
| [6] SEVERITY CLASSIFIER -> Clasificar cada error |
|
|
34
|
+
| | |
|
|
35
|
+
| v Decidir accion |
|
|
36
|
+
| [7] SESSION VALIDATOR ---> Validar integridad de estado |
|
|
37
|
+
| | |
|
|
38
|
+
| v |
|
|
39
|
+
| [OK] CONTINUAR o [STOP] ABORTAR |
|
|
40
|
+
| |
|
|
41
|
+
+============================================================================+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Contrato 1: ContractRegistryValidator
|
|
47
|
+
|
|
48
|
+
### Proposito
|
|
49
|
+
|
|
50
|
+
Validar que todos los agentes, comandos y recursos referenciados existan antes de ejecutar cualquier operacion paralela.
|
|
51
|
+
|
|
52
|
+
### Cuando se Activa
|
|
53
|
+
|
|
54
|
+
- Antes de lanzar cualquier Task() con subagente
|
|
55
|
+
- Antes de invocar comandos /elsabro:*
|
|
56
|
+
- Antes de referenciar recursos externos (skills, profiles)
|
|
57
|
+
|
|
58
|
+
### Comportamiento en Fallo
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
SI recurso no existe:
|
|
62
|
+
1. NO lanzar la tarea
|
|
63
|
+
2. Generar ERROR con codigo especifico
|
|
64
|
+
3. Sugerir alternativas si existen
|
|
65
|
+
4. Abortar operacion paralela si es critico
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Notificacion al Usuario
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
+======================================================+
|
|
72
|
+
| ERROR: AGENT_NOT_FOUND |
|
|
73
|
+
| Severity: CRITICAL |
|
|
74
|
+
+======================================================+
|
|
75
|
+
| |
|
|
76
|
+
| El agente "elsabro-nonexistent" no existe |
|
|
77
|
+
| |
|
|
78
|
+
| Agentes similares disponibles: |
|
|
79
|
+
| - elsabro-executor |
|
|
80
|
+
| - elsabro-verifier |
|
|
81
|
+
| - elsabro-analyst |
|
|
82
|
+
| |
|
|
83
|
+
+------------------------------------------------------+
|
|
84
|
+
| ACCION REQUERIDA: |
|
|
85
|
+
| -> Corregir nombre del agente en la configuracion |
|
|
86
|
+
+======================================================+
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Codigo Ejecutable
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
/**
|
|
93
|
+
* ContractRegistryValidator
|
|
94
|
+
* Valida existencia de agentes y recursos antes de ejecucion
|
|
95
|
+
*/
|
|
96
|
+
class ContractRegistryValidator {
|
|
97
|
+
constructor() {
|
|
98
|
+
// Registro de agentes validos por categoria
|
|
99
|
+
this.validAgents = {
|
|
100
|
+
exploration: [
|
|
101
|
+
'Explore',
|
|
102
|
+
'Plan',
|
|
103
|
+
'feature-dev:code-explorer'
|
|
104
|
+
],
|
|
105
|
+
implementation: [
|
|
106
|
+
'elsabro-executor',
|
|
107
|
+
'elsabro-analyst',
|
|
108
|
+
'elsabro-planner',
|
|
109
|
+
'elsabro-debugger',
|
|
110
|
+
'feature-dev:code-architect'
|
|
111
|
+
],
|
|
112
|
+
verification: [
|
|
113
|
+
'elsabro-verifier',
|
|
114
|
+
'pr-review-toolkit:code-reviewer',
|
|
115
|
+
'pr-review-toolkit:silent-failure-hunter',
|
|
116
|
+
'pr-review-toolkit:pr-test-analyzer'
|
|
117
|
+
],
|
|
118
|
+
utility: [
|
|
119
|
+
'elsabro-qa',
|
|
120
|
+
'elsabro-tech-writer',
|
|
121
|
+
'elsabro-scrum-master',
|
|
122
|
+
'elsabro-ux-designer',
|
|
123
|
+
'elsabro-quick-dev',
|
|
124
|
+
'elsabro-yolo-dev'
|
|
125
|
+
]
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Comandos validos
|
|
129
|
+
this.validCommands = [
|
|
130
|
+
'start', 'plan', 'execute', 'verify-work', 'debug', 'quick', 'new',
|
|
131
|
+
'pause-work', 'resume-work', 'progress',
|
|
132
|
+
'new-milestone', 'complete-milestone', 'audit-milestone', 'plan-milestone-gaps',
|
|
133
|
+
'add-phase', 'insert-phase', 'remove-phase', 'discuss-phase', 'research-phase',
|
|
134
|
+
'add-todo', 'check-todos',
|
|
135
|
+
'help', 'settings', 'set-profile', 'update', 'map-codebase', 'verify'
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Valida un agente individual
|
|
141
|
+
* @param {string} agentName - Nombre del agente
|
|
142
|
+
* @returns {Object} Resultado de validacion
|
|
143
|
+
*/
|
|
144
|
+
validateAgent(agentName) {
|
|
145
|
+
const allAgents = Object.values(this.validAgents).flat();
|
|
146
|
+
|
|
147
|
+
if (allAgents.includes(agentName)) {
|
|
148
|
+
return {
|
|
149
|
+
valid: true,
|
|
150
|
+
agent: agentName,
|
|
151
|
+
category: this.getAgentCategory(agentName)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Buscar sugerencias similares
|
|
156
|
+
const suggestions = this.findSimilarAgents(agentName, allAgents);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
valid: false,
|
|
160
|
+
agent: agentName,
|
|
161
|
+
error: {
|
|
162
|
+
code: 'AGENT_NOT_FOUND',
|
|
163
|
+
severity: 'CRITICAL',
|
|
164
|
+
message: `El agente "${agentName}" no existe en el registro`,
|
|
165
|
+
suggestions: suggestions,
|
|
166
|
+
canContinue: false
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Valida multiples agentes para ejecucion paralela
|
|
173
|
+
* @param {string[]} agentNames - Lista de agentes
|
|
174
|
+
* @returns {Object} Resultado agregado
|
|
175
|
+
*/
|
|
176
|
+
validateAgents(agentNames) {
|
|
177
|
+
const results = agentNames.map(name => this.validateAgent(name));
|
|
178
|
+
const invalid = results.filter(r => !r.valid);
|
|
179
|
+
|
|
180
|
+
if (invalid.length === 0) {
|
|
181
|
+
return {
|
|
182
|
+
valid: true,
|
|
183
|
+
agents: results,
|
|
184
|
+
readyForParallel: true
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
valid: false,
|
|
190
|
+
agents: results,
|
|
191
|
+
errors: invalid.map(r => r.error),
|
|
192
|
+
readyForParallel: false,
|
|
193
|
+
aggregatedError: {
|
|
194
|
+
code: 'AGENTS_VALIDATION_FAILED',
|
|
195
|
+
severity: 'CRITICAL',
|
|
196
|
+
message: `${invalid.length} de ${agentNames.length} agentes no son validos`,
|
|
197
|
+
details: invalid
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Valida un comando ELSABRO
|
|
204
|
+
* @param {string} commandName - Nombre del comando
|
|
205
|
+
* @returns {Object} Resultado de validacion
|
|
206
|
+
*/
|
|
207
|
+
validateCommand(commandName) {
|
|
208
|
+
const normalizedName = commandName.replace(/^\/elsabro:/, '');
|
|
209
|
+
|
|
210
|
+
if (this.validCommands.includes(normalizedName)) {
|
|
211
|
+
return {
|
|
212
|
+
valid: true,
|
|
213
|
+
command: normalizedName
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const suggestions = this.findSimilarCommands(normalizedName);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
valid: false,
|
|
221
|
+
command: commandName,
|
|
222
|
+
error: {
|
|
223
|
+
code: 'COMMAND_NOT_FOUND',
|
|
224
|
+
severity: 'CRITICAL',
|
|
225
|
+
message: `El comando "${commandName}" no existe`,
|
|
226
|
+
suggestions: suggestions.map(s => `/elsabro:${s}`),
|
|
227
|
+
canContinue: false
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Obtiene la categoria de un agente
|
|
234
|
+
*/
|
|
235
|
+
getAgentCategory(agentName) {
|
|
236
|
+
for (const [category, agents] of Object.entries(this.validAgents)) {
|
|
237
|
+
if (agents.includes(agentName)) {
|
|
238
|
+
return category;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Encuentra agentes similares usando distancia de Levenshtein
|
|
246
|
+
*/
|
|
247
|
+
findSimilarAgents(input, agents) {
|
|
248
|
+
return agents
|
|
249
|
+
.map(agent => ({
|
|
250
|
+
name: agent,
|
|
251
|
+
distance: this.levenshteinDistance(input.toLowerCase(), agent.toLowerCase())
|
|
252
|
+
}))
|
|
253
|
+
.filter(a => a.distance <= 5)
|
|
254
|
+
.sort((a, b) => a.distance - b.distance)
|
|
255
|
+
.slice(0, 3)
|
|
256
|
+
.map(a => a.name);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Encuentra comandos similares
|
|
261
|
+
*/
|
|
262
|
+
findSimilarCommands(input) {
|
|
263
|
+
return this.validCommands
|
|
264
|
+
.map(cmd => ({
|
|
265
|
+
name: cmd,
|
|
266
|
+
distance: this.levenshteinDistance(input.toLowerCase(), cmd.toLowerCase())
|
|
267
|
+
}))
|
|
268
|
+
.filter(c => c.distance <= 4)
|
|
269
|
+
.sort((a, b) => a.distance - b.distance)
|
|
270
|
+
.slice(0, 3)
|
|
271
|
+
.map(c => c.name);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Calcula distancia de Levenshtein entre dos strings
|
|
276
|
+
*/
|
|
277
|
+
levenshteinDistance(str1, str2) {
|
|
278
|
+
const m = str1.length;
|
|
279
|
+
const n = str2.length;
|
|
280
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
283
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
284
|
+
|
|
285
|
+
for (let i = 1; i <= m; i++) {
|
|
286
|
+
for (let j = 1; j <= n; j++) {
|
|
287
|
+
if (str1[i - 1] === str2[j - 1]) {
|
|
288
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
289
|
+
} else {
|
|
290
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return dp[m][n];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Valida un batch de agentes
|
|
300
|
+
* @param {Object[]} agents - Lista de objetos agente a validar
|
|
301
|
+
* @returns {Object} Resultado de validacion batch
|
|
302
|
+
*/
|
|
303
|
+
async validateBatch(agents) {
|
|
304
|
+
const results = await Promise.all(
|
|
305
|
+
agents.map(agent => this.validate("agent", agent))
|
|
306
|
+
);
|
|
307
|
+
const failures = results.filter(r => !r.valid);
|
|
308
|
+
return {
|
|
309
|
+
total: agents.length,
|
|
310
|
+
valid: results.length - failures.length,
|
|
311
|
+
invalid: failures.length,
|
|
312
|
+
details: failures,
|
|
313
|
+
canProceed: failures.length === 0
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Ejemplo de uso
|
|
319
|
+
const validator = new ContractRegistryValidator();
|
|
320
|
+
|
|
321
|
+
// Validar agentes para ejecucion paralela
|
|
322
|
+
const result = validator.validateAgents([
|
|
323
|
+
'elsabro-executor',
|
|
324
|
+
'feature-dev:code-explorer',
|
|
325
|
+
'elsabro-nonexistent' // Este fallara
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
if (!result.valid) {
|
|
329
|
+
console.log('Validacion fallida:', result.aggregatedError);
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Contrato 2: ContractTaskLifecycle
|
|
336
|
+
|
|
337
|
+
### Proposito
|
|
338
|
+
|
|
339
|
+
Garantizar que todas las tareas sigan el ciclo de vida correcto: INIT -> ACTIVE -> COMPLETE (o ERROR).
|
|
340
|
+
|
|
341
|
+
### Cuando se Activa
|
|
342
|
+
|
|
343
|
+
- Al crear una Task con TaskCreate
|
|
344
|
+
- Al cambiar estado con TaskUpdate
|
|
345
|
+
- Al completar o fallar una tarea
|
|
346
|
+
|
|
347
|
+
### Comportamiento en Fallo
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
SI transicion invalida:
|
|
351
|
+
1. Rechazar la operacion
|
|
352
|
+
2. Mantener estado anterior
|
|
353
|
+
3. Generar ERROR con transicion invalida
|
|
354
|
+
4. Sugerir transicion correcta
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Notificacion al Usuario
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
+======================================================+
|
|
361
|
+
| ERROR: INVALID_STATE_TRANSITION |
|
|
362
|
+
| Severity: HIGH |
|
|
363
|
+
+======================================================+
|
|
364
|
+
| |
|
|
365
|
+
| Transicion invalida detectada: |
|
|
366
|
+
| INIT -> COMPLETED (saltando ACTIVE) |
|
|
367
|
+
| |
|
|
368
|
+
| Flujo correcto: |
|
|
369
|
+
| INIT -> ACTIVE -> COMPLETED |
|
|
370
|
+
| INIT -> ACTIVE -> ERROR |
|
|
371
|
+
| |
|
|
372
|
+
+------------------------------------------------------+
|
|
373
|
+
| Opciones: |
|
|
374
|
+
| [f] Forzar transicion (no recomendado) |
|
|
375
|
+
| [c] Corregir flujo automaticamente |
|
|
376
|
+
| [a] Abortar operacion |
|
|
377
|
+
+======================================================+
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Codigo Ejecutable
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
/**
|
|
384
|
+
* ContractTaskLifecycle
|
|
385
|
+
* Garantiza transiciones de estado validas para tareas
|
|
386
|
+
*/
|
|
387
|
+
class ContractTaskLifecycle {
|
|
388
|
+
// Estados consistentes con config.json
|
|
389
|
+
static STATES = {
|
|
390
|
+
INIT: "INIT",
|
|
391
|
+
ACTIVE: "ACTIVE",
|
|
392
|
+
COMPLETE: "COMPLETE",
|
|
393
|
+
FAILED: "FAILED",
|
|
394
|
+
ROLLED_BACK: "ROLLED_BACK",
|
|
395
|
+
DELETED: "DELETED"
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
static VALID_TRANSITIONS = {
|
|
399
|
+
INIT: ["ACTIVE", "FAILED", "DELETED"],
|
|
400
|
+
ACTIVE: ["COMPLETE", "FAILED", "ROLLED_BACK"],
|
|
401
|
+
COMPLETE: ["ROLLED_BACK"],
|
|
402
|
+
FAILED: ["ACTIVE", "DELETED"],
|
|
403
|
+
ROLLED_BACK: ["INIT"],
|
|
404
|
+
DELETED: []
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
constructor() {
|
|
408
|
+
// Estados validos (usando static para consistencia)
|
|
409
|
+
this.states = ContractTaskLifecycle.STATES;
|
|
410
|
+
|
|
411
|
+
// Transiciones validas: desde -> [hacia]
|
|
412
|
+
this.validTransitions = ContractTaskLifecycle.VALID_TRANSITIONS;
|
|
413
|
+
|
|
414
|
+
// Historial de tareas para tracking
|
|
415
|
+
this.taskHistory = new Map();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Registra una nueva tarea
|
|
420
|
+
* @param {string} taskId - ID de la tarea
|
|
421
|
+
* @param {Object} metadata - Metadata de la tarea
|
|
422
|
+
*/
|
|
423
|
+
registerTask(taskId, metadata = {}) {
|
|
424
|
+
const task = {
|
|
425
|
+
id: taskId,
|
|
426
|
+
state: this.states.INIT,
|
|
427
|
+
createdAt: new Date().toISOString(),
|
|
428
|
+
updatedAt: new Date().toISOString(),
|
|
429
|
+
transitions: [{
|
|
430
|
+
from: null,
|
|
431
|
+
to: this.states.INIT,
|
|
432
|
+
at: new Date().toISOString()
|
|
433
|
+
}],
|
|
434
|
+
metadata: metadata
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
this.taskHistory.set(taskId, task);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
valid: true,
|
|
441
|
+
task: task
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Valida y ejecuta una transicion de estado
|
|
447
|
+
* @param {string} taskId - ID de la tarea
|
|
448
|
+
* @param {string} newState - Nuevo estado
|
|
449
|
+
* @returns {Object} Resultado de la transicion
|
|
450
|
+
*/
|
|
451
|
+
transition(taskId, newState) {
|
|
452
|
+
const task = this.taskHistory.get(taskId);
|
|
453
|
+
|
|
454
|
+
if (!task) {
|
|
455
|
+
return {
|
|
456
|
+
valid: false,
|
|
457
|
+
error: {
|
|
458
|
+
code: 'TASK_NOT_FOUND',
|
|
459
|
+
severity: 'CRITICAL',
|
|
460
|
+
message: `Tarea "${taskId}" no encontrada en el registro`,
|
|
461
|
+
canContinue: false
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const currentState = task.state;
|
|
467
|
+
const allowedTransitions = this.validTransitions[currentState] || [];
|
|
468
|
+
|
|
469
|
+
if (!allowedTransitions.includes(newState)) {
|
|
470
|
+
return {
|
|
471
|
+
valid: false,
|
|
472
|
+
error: {
|
|
473
|
+
code: 'INVALID_STATE_TRANSITION',
|
|
474
|
+
severity: 'HIGH',
|
|
475
|
+
message: `Transicion invalida: ${currentState} -> ${newState}`,
|
|
476
|
+
currentState: currentState,
|
|
477
|
+
attemptedState: newState,
|
|
478
|
+
allowedTransitions: allowedTransitions,
|
|
479
|
+
canContinue: true,
|
|
480
|
+
suggestedFix: this.suggestCorrectTransition(currentState, newState)
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Ejecutar transicion
|
|
486
|
+
task.transitions.push({
|
|
487
|
+
from: currentState,
|
|
488
|
+
to: newState,
|
|
489
|
+
at: new Date().toISOString()
|
|
490
|
+
});
|
|
491
|
+
task.state = newState;
|
|
492
|
+
task.updatedAt = new Date().toISOString();
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
valid: true,
|
|
496
|
+
task: task,
|
|
497
|
+
transition: {
|
|
498
|
+
from: currentState,
|
|
499
|
+
to: newState
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Valida el ciclo completo de una tarea
|
|
506
|
+
* @param {string} taskId - ID de la tarea
|
|
507
|
+
*/
|
|
508
|
+
validateLifecycle(taskId) {
|
|
509
|
+
const task = this.taskHistory.get(taskId);
|
|
510
|
+
|
|
511
|
+
if (!task) {
|
|
512
|
+
return { valid: false, error: 'Task not found' };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const requiredFlow = ['init', 'in_progress'];
|
|
516
|
+
const actualFlow = task.transitions.map(t => t.to);
|
|
517
|
+
|
|
518
|
+
// Verificar que paso por INIT y ACTIVE
|
|
519
|
+
const hasInit = actualFlow.includes('init');
|
|
520
|
+
const hasActive = actualFlow.includes('in_progress');
|
|
521
|
+
|
|
522
|
+
if (!hasInit || !hasActive) {
|
|
523
|
+
return {
|
|
524
|
+
valid: false,
|
|
525
|
+
error: {
|
|
526
|
+
code: 'INCOMPLETE_LIFECYCLE',
|
|
527
|
+
severity: 'MEDIUM',
|
|
528
|
+
message: 'La tarea no siguio el ciclo de vida completo',
|
|
529
|
+
expectedFlow: requiredFlow,
|
|
530
|
+
actualFlow: actualFlow
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
valid: true,
|
|
537
|
+
lifecycle: actualFlow,
|
|
538
|
+
duration: this.calculateDuration(task)
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Sugiere la transicion correcta
|
|
544
|
+
*/
|
|
545
|
+
suggestCorrectTransition(currentState, attemptedState) {
|
|
546
|
+
// Si intenta ir a COMPLETED desde INIT, sugerir pasar por ACTIVE
|
|
547
|
+
if (currentState === 'init' && attemptedState === 'completed') {
|
|
548
|
+
return {
|
|
549
|
+
suggestion: 'Primero transicionar a in_progress, luego a completed',
|
|
550
|
+
steps: ['in_progress', 'completed']
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
suggestion: `Transicionar a uno de: ${this.validTransitions[currentState].join(', ')}`,
|
|
556
|
+
steps: this.validTransitions[currentState]
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Calcula la duracion de una tarea
|
|
562
|
+
*/
|
|
563
|
+
calculateDuration(task) {
|
|
564
|
+
const start = new Date(task.createdAt);
|
|
565
|
+
const end = new Date(task.updatedAt);
|
|
566
|
+
return end - start;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Obtiene el estado actual de todas las tareas
|
|
571
|
+
*/
|
|
572
|
+
getTasksStatus() {
|
|
573
|
+
const status = {
|
|
574
|
+
total: this.taskHistory.size,
|
|
575
|
+
byState: {},
|
|
576
|
+
active: [],
|
|
577
|
+
failed: []
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
for (const [id, task] of this.taskHistory) {
|
|
581
|
+
status.byState[task.state] = (status.byState[task.state] || 0) + 1;
|
|
582
|
+
|
|
583
|
+
if (task.state === 'in_progress') {
|
|
584
|
+
status.active.push(id);
|
|
585
|
+
}
|
|
586
|
+
if (task.state === 'error') {
|
|
587
|
+
status.failed.push(id);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return status;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Ejemplo de uso
|
|
596
|
+
const lifecycle = new ContractTaskLifecycle();
|
|
597
|
+
|
|
598
|
+
// Flujo correcto
|
|
599
|
+
lifecycle.registerTask('task-001', { type: 'exploration' });
|
|
600
|
+
lifecycle.transition('task-001', 'in_progress');
|
|
601
|
+
lifecycle.transition('task-001', 'completed');
|
|
602
|
+
|
|
603
|
+
// Flujo incorrecto (detectado)
|
|
604
|
+
lifecycle.registerTask('task-002', { type: 'implementation' });
|
|
605
|
+
const badResult = lifecycle.transition('task-002', 'completed'); // Error!
|
|
606
|
+
if (!badResult.valid) {
|
|
607
|
+
console.log('Error detectado:', badResult.error);
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Contrato 3: ContractTimeoutHandler
|
|
614
|
+
|
|
615
|
+
### Proposito
|
|
616
|
+
|
|
617
|
+
Manejar timeouts de tareas paralelas con health checks periodicos y terminacion controlada.
|
|
618
|
+
|
|
619
|
+
### Cuando se Activa
|
|
620
|
+
|
|
621
|
+
- Al lanzar cualquier Task() paralela
|
|
622
|
+
- Durante health checks periodicos (cada 5 segundos)
|
|
623
|
+
- Cuando una tarea excede su tiempo maximo (30 minutos default)
|
|
624
|
+
|
|
625
|
+
### Comportamiento en Fallo
|
|
626
|
+
|
|
627
|
+
```
|
|
628
|
+
SI timeout excedido:
|
|
629
|
+
1. Intentar terminacion graceful (10s)
|
|
630
|
+
2. Si no responde, terminacion forzada
|
|
631
|
+
3. Marcar tarea como TIMEOUT
|
|
632
|
+
4. Notificar al agregador de errores
|
|
633
|
+
5. Decidir si continuar o abortar segun policy
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Notificacion al Usuario
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
+======================================================+
|
|
640
|
+
| WARNING: TASK_TIMEOUT |
|
|
641
|
+
| Severity: HIGH |
|
|
642
|
+
+======================================================+
|
|
643
|
+
| |
|
|
644
|
+
| La tarea "explore-codebase" excedio el timeout |
|
|
645
|
+
| |
|
|
646
|
+
| Tiempo configurado: 30 minutos |
|
|
647
|
+
| Tiempo transcurrido: 31:45 |
|
|
648
|
+
| Ultimo health check: hace 35 segundos |
|
|
649
|
+
| |
|
|
650
|
+
+------------------------------------------------------+
|
|
651
|
+
| Estado actual: |
|
|
652
|
+
| - Agente: elsabro-executor |
|
|
653
|
+
| - Fase: in_progress |
|
|
654
|
+
| - Output parcial: 1,234 tokens |
|
|
655
|
+
| |
|
|
656
|
+
+------------------------------------------------------+
|
|
657
|
+
| Opciones: |
|
|
658
|
+
| [e] Extender timeout (+15 min) |
|
|
659
|
+
| [t] Terminar y usar output parcial |
|
|
660
|
+
| [r] Reintentar desde inicio |
|
|
661
|
+
| [a] Abortar toda la operacion |
|
|
662
|
+
+======================================================+
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Codigo Ejecutable
|
|
666
|
+
|
|
667
|
+
```javascript
|
|
668
|
+
/**
|
|
669
|
+
* ContractTimeoutHandler
|
|
670
|
+
* Maneja timeouts con health checks y terminacion controlada
|
|
671
|
+
*/
|
|
672
|
+
class ContractTimeoutHandler {
|
|
673
|
+
constructor(options = {}) {
|
|
674
|
+
this.defaultTimeout = options.defaultTimeout || 30 * 60 * 1000; // 30 min
|
|
675
|
+
this.healthCheckInterval = options.healthCheckInterval || 5000; // 5 seg
|
|
676
|
+
this.gracefulShutdownTime = options.gracefulShutdownTime || 10000; // 10 seg
|
|
677
|
+
|
|
678
|
+
this.activeTasks = new Map();
|
|
679
|
+
this.healthCheckTimers = new Map();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Inicia el tracking de una tarea con timeout
|
|
684
|
+
* @param {string} taskId - ID de la tarea
|
|
685
|
+
* @param {Object} options - Opciones de timeout
|
|
686
|
+
*/
|
|
687
|
+
startTracking(taskId, options = {}) {
|
|
688
|
+
const timeout = options.timeout || this.defaultTimeout;
|
|
689
|
+
const startTime = Date.now();
|
|
690
|
+
|
|
691
|
+
const taskTracker = {
|
|
692
|
+
taskId: taskId,
|
|
693
|
+
startTime: startTime,
|
|
694
|
+
timeout: timeout,
|
|
695
|
+
lastHealthCheck: startTime,
|
|
696
|
+
healthCheckCount: 0,
|
|
697
|
+
status: 'active',
|
|
698
|
+
partialOutput: null
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
this.activeTasks.set(taskId, taskTracker);
|
|
702
|
+
|
|
703
|
+
// Iniciar health checks periodicos
|
|
704
|
+
const healthTimer = setInterval(() => {
|
|
705
|
+
this.performHealthCheck(taskId);
|
|
706
|
+
}, this.healthCheckInterval);
|
|
707
|
+
|
|
708
|
+
this.healthCheckTimers.set(taskId, healthTimer);
|
|
709
|
+
|
|
710
|
+
// Timer de timeout principal
|
|
711
|
+
setTimeout(() => {
|
|
712
|
+
this.handleTimeout(taskId);
|
|
713
|
+
}, timeout);
|
|
714
|
+
|
|
715
|
+
return {
|
|
716
|
+
taskId: taskId,
|
|
717
|
+
timeout: timeout,
|
|
718
|
+
startTime: new Date(startTime).toISOString()
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Realiza un health check de la tarea
|
|
724
|
+
* @param {string} taskId - ID de la tarea
|
|
725
|
+
*/
|
|
726
|
+
performHealthCheck(taskId) {
|
|
727
|
+
const tracker = this.activeTasks.get(taskId);
|
|
728
|
+
|
|
729
|
+
if (!tracker || tracker.status !== 'active') {
|
|
730
|
+
this.stopTracking(taskId);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const now = Date.now();
|
|
735
|
+
const elapsed = now - tracker.startTime;
|
|
736
|
+
const remaining = tracker.timeout - elapsed;
|
|
737
|
+
|
|
738
|
+
tracker.lastHealthCheck = now;
|
|
739
|
+
tracker.healthCheckCount++;
|
|
740
|
+
|
|
741
|
+
// Calcular progreso estimado
|
|
742
|
+
const progress = {
|
|
743
|
+
elapsed: elapsed,
|
|
744
|
+
remaining: remaining,
|
|
745
|
+
percentage: Math.min(100, (elapsed / tracker.timeout) * 100),
|
|
746
|
+
healthChecks: tracker.healthCheckCount
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// Emitir evento de health check
|
|
750
|
+
this.emitHealthStatus(taskId, progress);
|
|
751
|
+
|
|
752
|
+
// Warning si queda poco tiempo
|
|
753
|
+
if (remaining < 60000 && remaining > 0) { // Menos de 1 minuto
|
|
754
|
+
this.emitWarning(taskId, {
|
|
755
|
+
code: 'TIMEOUT_IMMINENT',
|
|
756
|
+
severity: 'MEDIUM',
|
|
757
|
+
message: `Tarea ${taskId} tiene menos de 1 minuto antes del timeout`,
|
|
758
|
+
remaining: remaining
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return progress;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Maneja el timeout de una tarea
|
|
767
|
+
* @param {string} taskId - ID de la tarea
|
|
768
|
+
*/
|
|
769
|
+
handleTimeout(taskId) {
|
|
770
|
+
const tracker = this.activeTasks.get(taskId);
|
|
771
|
+
|
|
772
|
+
if (!tracker || tracker.status !== 'active') {
|
|
773
|
+
return; // Ya fue manejado
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
tracker.status = 'timeout';
|
|
777
|
+
|
|
778
|
+
// Intentar terminacion graceful
|
|
779
|
+
const gracefulResult = this.attemptGracefulShutdown(taskId);
|
|
780
|
+
|
|
781
|
+
if (!gracefulResult.success) {
|
|
782
|
+
// Terminacion forzada
|
|
783
|
+
this.forceTerminate(taskId);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Generar error de timeout
|
|
787
|
+
const timeoutError = {
|
|
788
|
+
code: 'TASK_TIMEOUT',
|
|
789
|
+
severity: 'HIGH',
|
|
790
|
+
taskId: taskId,
|
|
791
|
+
configuredTimeout: tracker.timeout,
|
|
792
|
+
elapsedTime: Date.now() - tracker.startTime,
|
|
793
|
+
lastHealthCheck: new Date(tracker.lastHealthCheck).toISOString(),
|
|
794
|
+
partialOutput: tracker.partialOutput,
|
|
795
|
+
canContinue: true,
|
|
796
|
+
options: ['extend', 'terminate', 'retry', 'abort']
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
this.stopTracking(taskId);
|
|
800
|
+
|
|
801
|
+
return timeoutError;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Intenta terminacion graceful
|
|
806
|
+
*/
|
|
807
|
+
attemptGracefulShutdown(taskId) {
|
|
808
|
+
// Simular intento de terminacion graceful
|
|
809
|
+
// En implementacion real, enviaria senal a la tarea
|
|
810
|
+
return {
|
|
811
|
+
success: true,
|
|
812
|
+
taskId: taskId,
|
|
813
|
+
method: 'graceful'
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Fuerza terminacion de la tarea
|
|
819
|
+
*/
|
|
820
|
+
forceTerminate(taskId) {
|
|
821
|
+
const tracker = this.activeTasks.get(taskId);
|
|
822
|
+
if (tracker) {
|
|
823
|
+
tracker.status = 'terminated';
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
taskId: taskId,
|
|
828
|
+
method: 'forced',
|
|
829
|
+
timestamp: new Date().toISOString()
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Extiende el timeout de una tarea
|
|
835
|
+
* @param {string} taskId - ID de la tarea
|
|
836
|
+
* @param {number} additionalTime - Tiempo adicional en ms
|
|
837
|
+
*/
|
|
838
|
+
extendTimeout(taskId, additionalTime = 15 * 60 * 1000) {
|
|
839
|
+
const tracker = this.activeTasks.get(taskId);
|
|
840
|
+
|
|
841
|
+
if (!tracker) {
|
|
842
|
+
return {
|
|
843
|
+
success: false,
|
|
844
|
+
error: 'Task not found'
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
tracker.timeout += additionalTime;
|
|
849
|
+
tracker.status = 'active';
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
success: true,
|
|
853
|
+
taskId: taskId,
|
|
854
|
+
newTimeout: tracker.timeout,
|
|
855
|
+
additionalTime: additionalTime
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Detiene el tracking de una tarea
|
|
861
|
+
*/
|
|
862
|
+
stopTracking(taskId) {
|
|
863
|
+
const timer = this.healthCheckTimers.get(taskId);
|
|
864
|
+
if (timer) {
|
|
865
|
+
clearInterval(timer);
|
|
866
|
+
this.healthCheckTimers.delete(taskId);
|
|
867
|
+
}
|
|
868
|
+
this.activeTasks.delete(taskId);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Marca una tarea como completada
|
|
873
|
+
*/
|
|
874
|
+
markComplete(taskId, result = null) {
|
|
875
|
+
const tracker = this.activeTasks.get(taskId);
|
|
876
|
+
|
|
877
|
+
if (tracker) {
|
|
878
|
+
tracker.status = 'completed';
|
|
879
|
+
tracker.partialOutput = result;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
this.stopTracking(taskId);
|
|
883
|
+
|
|
884
|
+
return {
|
|
885
|
+
taskId: taskId,
|
|
886
|
+
status: 'completed',
|
|
887
|
+
duration: tracker ? Date.now() - tracker.startTime : null
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Emite estado de health check (para logging/UI)
|
|
893
|
+
*/
|
|
894
|
+
emitHealthStatus(taskId, progress) {
|
|
895
|
+
// En implementacion real, emitir a event bus o logger
|
|
896
|
+
console.log(`[HEALTH] Task ${taskId}: ${progress.percentage.toFixed(1)}% - ${progress.healthChecks} checks`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Emite warning
|
|
901
|
+
*/
|
|
902
|
+
emitWarning(taskId, warning) {
|
|
903
|
+
console.log(`[WARNING] ${warning.message}`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Obtiene estado de todas las tareas activas
|
|
908
|
+
*/
|
|
909
|
+
getActiveTasksStatus() {
|
|
910
|
+
const status = [];
|
|
911
|
+
|
|
912
|
+
for (const [taskId, tracker] of this.activeTasks) {
|
|
913
|
+
const elapsed = Date.now() - tracker.startTime;
|
|
914
|
+
status.push({
|
|
915
|
+
taskId: taskId,
|
|
916
|
+
status: tracker.status,
|
|
917
|
+
elapsed: elapsed,
|
|
918
|
+
remaining: tracker.timeout - elapsed,
|
|
919
|
+
healthChecks: tracker.healthCheckCount
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return status;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Alias para startTracking - para compatibilidad con contratos
|
|
928
|
+
* @param {string} operationId - ID de la operacion
|
|
929
|
+
* @param {number} limitMs - Limite de tiempo en ms
|
|
930
|
+
* @param {Object} metadata - Metadata adicional
|
|
931
|
+
*/
|
|
932
|
+
startTimeout(operationId, limitMs, metadata) {
|
|
933
|
+
return this.startTracking(operationId, { timeout: limitMs, ...metadata });
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Alias para markComplete - para compatibilidad con contratos
|
|
938
|
+
* @param {string} operationId - ID de la operacion
|
|
939
|
+
* @param {number} actualDuration - Duracion real de la operacion
|
|
940
|
+
*/
|
|
941
|
+
completeTimeout(operationId, actualDuration) {
|
|
942
|
+
return this.markComplete(operationId, { duration: actualDuration });
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Ejemplo de uso
|
|
947
|
+
const timeoutHandler = new ContractTimeoutHandler({
|
|
948
|
+
defaultTimeout: 5 * 60 * 1000, // 5 minutos para testing
|
|
949
|
+
healthCheckInterval: 1000 // 1 segundo
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// Iniciar tracking
|
|
953
|
+
timeoutHandler.startTracking('task-parallel-001');
|
|
954
|
+
timeoutHandler.startTracking('task-parallel-002');
|
|
955
|
+
|
|
956
|
+
// Obtener estado
|
|
957
|
+
const status = timeoutHandler.getActiveTasksStatus();
|
|
958
|
+
console.log('Active tasks:', status);
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
963
|
+
## Contrato 4: ContractRetryPolicy
|
|
964
|
+
|
|
965
|
+
### Proposito
|
|
966
|
+
|
|
967
|
+
Implementar politica de reintentos con backoff exponencial para operaciones fallidas.
|
|
968
|
+
|
|
969
|
+
### Cuando se Activa
|
|
970
|
+
|
|
971
|
+
- Cuando una tarea falla con error transitorio
|
|
972
|
+
- Despues de timeout
|
|
973
|
+
- En errores de red o rate limit
|
|
974
|
+
|
|
975
|
+
### Comportamiento en Fallo
|
|
976
|
+
|
|
977
|
+
```
|
|
978
|
+
Intento 1: Ejecutar inmediatamente
|
|
979
|
+
|
|
|
980
|
+
v FALLO
|
|
981
|
+
Intento 2: Esperar 1s, reintentar
|
|
982
|
+
|
|
|
983
|
+
v FALLO
|
|
984
|
+
Intento 3: Esperar 2s, reintentar
|
|
985
|
+
|
|
|
986
|
+
v FALLO
|
|
987
|
+
Intento 4: Esperar 4s, reintentar
|
|
988
|
+
|
|
|
989
|
+
v FALLO
|
|
990
|
+
-> RETRY_EXHAUSTED: Escalar a usuario
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
### Notificacion al Usuario
|
|
994
|
+
|
|
995
|
+
```
|
|
996
|
+
+------------------------------------------------------+
|
|
997
|
+
| RETRY: npm test |
|
|
998
|
+
+------------------------------------------------------+
|
|
999
|
+
| Attempt 1/3: FAILED (timeout) |
|
|
1000
|
+
| Waiting 1s before retry... |
|
|
1001
|
+
| |
|
|
1002
|
+
| Attempt 2/3: FAILED (exit code 1) |
|
|
1003
|
+
| Waiting 2s before retry... |
|
|
1004
|
+
| |
|
|
1005
|
+
| Attempt 3/3: FAILED (exit code 1) |
|
|
1006
|
+
| |
|
|
1007
|
+
| X All attempts exhausted |
|
|
1008
|
+
| -> Error escalated to user |
|
|
1009
|
+
+------------------------------------------------------+
|
|
1010
|
+
|
|
1011
|
+
+======================================================+
|
|
1012
|
+
| ERROR: RETRY_EXHAUSTED |
|
|
1013
|
+
| Severity: HIGH |
|
|
1014
|
+
+======================================================+
|
|
1015
|
+
| |
|
|
1016
|
+
| Intentos: 3/3 agotados |
|
|
1017
|
+
| Tiempo total: 8.5s |
|
|
1018
|
+
| |
|
|
1019
|
+
| Ultimo error: |
|
|
1020
|
+
| > Test suite failed to run |
|
|
1021
|
+
| > Cannot find module '@/utils' |
|
|
1022
|
+
| |
|
|
1023
|
+
+------------------------------------------------------+
|
|
1024
|
+
| El problema parece ser: |
|
|
1025
|
+
| -> Dependencia faltante o path alias incorrecto |
|
|
1026
|
+
| |
|
|
1027
|
+
| Opciones: |
|
|
1028
|
+
| [d] Investigar con /elsabro:debug |
|
|
1029
|
+
| [r] Reintentar despues de arreglar |
|
|
1030
|
+
| [s] Saltar esta tarea |
|
|
1031
|
+
| [a] Abortar |
|
|
1032
|
+
+======================================================+
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
### Codigo Ejecutable
|
|
1036
|
+
|
|
1037
|
+
```javascript
|
|
1038
|
+
/**
|
|
1039
|
+
* ContractRetryPolicy
|
|
1040
|
+
* Implementa reintentos con backoff exponencial
|
|
1041
|
+
*/
|
|
1042
|
+
class ContractRetryPolicy {
|
|
1043
|
+
constructor(options = {}) {
|
|
1044
|
+
this.maxAttempts = options.maxAttempts || 3;
|
|
1045
|
+
this.baseDelayMs = options.baseDelayMs || 1000;
|
|
1046
|
+
this.backoffMultiplier = options.backoffMultiplier || 2;
|
|
1047
|
+
this.maxDelayMs = options.maxDelayMs || 30000;
|
|
1048
|
+
this.timeoutPerAttemptMs = options.timeoutPerAttemptMs || 30000;
|
|
1049
|
+
this.totalTimeoutMs = options.totalTimeoutMs || 120000;
|
|
1050
|
+
|
|
1051
|
+
// Errores que SI deben reintentarse
|
|
1052
|
+
this.retryableErrors = [
|
|
1053
|
+
'TIMEOUT',
|
|
1054
|
+
'NETWORK_ERROR',
|
|
1055
|
+
'RATE_LIMIT',
|
|
1056
|
+
'SERVICE_UNAVAILABLE',
|
|
1057
|
+
'CONNECTION_RESET',
|
|
1058
|
+
'ECONNREFUSED',
|
|
1059
|
+
'ETIMEDOUT'
|
|
1060
|
+
];
|
|
1061
|
+
|
|
1062
|
+
// Errores que NO deben reintentarse
|
|
1063
|
+
this.nonRetryableErrors = [
|
|
1064
|
+
'PARSE_ERROR',
|
|
1065
|
+
'VALIDATION_ERROR',
|
|
1066
|
+
'LOGIC_ERROR',
|
|
1067
|
+
'FILE_NOT_FOUND',
|
|
1068
|
+
'PERMISSION_DENIED',
|
|
1069
|
+
'INVALID_ARGUMENT'
|
|
1070
|
+
];
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Determina si un error es reintentable
|
|
1075
|
+
* @param {Object} error - Error a evaluar
|
|
1076
|
+
*/
|
|
1077
|
+
isRetryable(error) {
|
|
1078
|
+
const errorCode = error.code || error.type || '';
|
|
1079
|
+
|
|
1080
|
+
// Verificar lista negra primero
|
|
1081
|
+
if (this.nonRetryableErrors.some(e => errorCode.includes(e))) {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Verificar lista blanca
|
|
1086
|
+
if (this.retryableErrors.some(e => errorCode.includes(e))) {
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Por defecto, no reintentar errores desconocidos
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Calcula el delay para el siguiente intento
|
|
1096
|
+
* @param {number} attempt - Numero de intento (1-based)
|
|
1097
|
+
*/
|
|
1098
|
+
calculateDelay(attempt) {
|
|
1099
|
+
const delay = this.baseDelayMs * Math.pow(this.backoffMultiplier, attempt - 1);
|
|
1100
|
+
return Math.min(delay, this.maxDelayMs);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Ejecuta una operacion con reintentos
|
|
1105
|
+
* @param {Function} operation - Operacion a ejecutar
|
|
1106
|
+
* @param {Object} context - Contexto de la operacion
|
|
1107
|
+
*/
|
|
1108
|
+
async executeWithRetry(operation, context = {}) {
|
|
1109
|
+
const startTime = Date.now();
|
|
1110
|
+
const attempts = [];
|
|
1111
|
+
let lastError = null;
|
|
1112
|
+
|
|
1113
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
|
|
1114
|
+
const attemptStart = Date.now();
|
|
1115
|
+
|
|
1116
|
+
try {
|
|
1117
|
+
// Verificar timeout total
|
|
1118
|
+
if (Date.now() - startTime > this.totalTimeoutMs) {
|
|
1119
|
+
throw {
|
|
1120
|
+
code: 'TOTAL_TIMEOUT_EXCEEDED',
|
|
1121
|
+
message: `Timeout total de ${this.totalTimeoutMs}ms excedido`
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Ejecutar operacion
|
|
1126
|
+
const result = await this.executeWithTimeout(operation, this.timeoutPerAttemptMs);
|
|
1127
|
+
|
|
1128
|
+
// Exito!
|
|
1129
|
+
attempts.push({
|
|
1130
|
+
attempt: attempt,
|
|
1131
|
+
status: 'SUCCESS',
|
|
1132
|
+
duration: Date.now() - attemptStart
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
return {
|
|
1136
|
+
success: true,
|
|
1137
|
+
result: result,
|
|
1138
|
+
attempts: attempts,
|
|
1139
|
+
totalDuration: Date.now() - startTime
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
lastError = error;
|
|
1144
|
+
|
|
1145
|
+
attempts.push({
|
|
1146
|
+
attempt: attempt,
|
|
1147
|
+
status: 'FAILED',
|
|
1148
|
+
error: error.code || error.message,
|
|
1149
|
+
duration: Date.now() - attemptStart
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// Verificar si debe reintentar
|
|
1153
|
+
if (!this.isRetryable(error)) {
|
|
1154
|
+
return {
|
|
1155
|
+
success: false,
|
|
1156
|
+
error: this.createNonRetryableError(error, attempts),
|
|
1157
|
+
attempts: attempts,
|
|
1158
|
+
totalDuration: Date.now() - startTime
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Si hay mas intentos, esperar y reintentar
|
|
1163
|
+
if (attempt < this.maxAttempts) {
|
|
1164
|
+
const delay = this.calculateDelay(attempt);
|
|
1165
|
+
await this.sleep(delay);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Todos los intentos fallaron
|
|
1171
|
+
return {
|
|
1172
|
+
success: false,
|
|
1173
|
+
error: this.createExhaustedError(lastError, attempts),
|
|
1174
|
+
attempts: attempts,
|
|
1175
|
+
totalDuration: Date.now() - startTime
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Ejecuta operacion con timeout
|
|
1181
|
+
*/
|
|
1182
|
+
async executeWithTimeout(operation, timeout) {
|
|
1183
|
+
return new Promise((resolve, reject) => {
|
|
1184
|
+
const timer = setTimeout(() => {
|
|
1185
|
+
reject({ code: 'TIMEOUT', message: `Operation timed out after ${timeout}ms` });
|
|
1186
|
+
}, timeout);
|
|
1187
|
+
|
|
1188
|
+
Promise.resolve(operation())
|
|
1189
|
+
.then(result => {
|
|
1190
|
+
clearTimeout(timer);
|
|
1191
|
+
resolve(result);
|
|
1192
|
+
})
|
|
1193
|
+
.catch(error => {
|
|
1194
|
+
clearTimeout(timer);
|
|
1195
|
+
reject(error);
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Crea error de reintentos agotados
|
|
1202
|
+
*/
|
|
1203
|
+
createExhaustedError(lastError, attempts) {
|
|
1204
|
+
return {
|
|
1205
|
+
code: 'RETRY_EXHAUSTED',
|
|
1206
|
+
severity: 'HIGH',
|
|
1207
|
+
message: `Todos los ${this.maxAttempts} intentos fallaron`,
|
|
1208
|
+
lastError: lastError,
|
|
1209
|
+
attempts: attempts,
|
|
1210
|
+
canContinue: true,
|
|
1211
|
+
options: ['debug', 'retry_manual', 'skip', 'abort'],
|
|
1212
|
+
suggestedAction: this.suggestAction(lastError)
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Crea error no reintentable
|
|
1218
|
+
*/
|
|
1219
|
+
createNonRetryableError(error, attempts) {
|
|
1220
|
+
return {
|
|
1221
|
+
code: 'NON_RETRYABLE_ERROR',
|
|
1222
|
+
severity: 'HIGH',
|
|
1223
|
+
message: `Error no reintentable: ${error.code || error.message}`,
|
|
1224
|
+
originalError: error,
|
|
1225
|
+
attempts: attempts,
|
|
1226
|
+
reason: 'El tipo de error indica un problema que no se resolvera con reintentos',
|
|
1227
|
+
canContinue: true,
|
|
1228
|
+
options: ['fix_manual', 'skip', 'abort']
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Sugiere accion basada en el error
|
|
1234
|
+
*/
|
|
1235
|
+
suggestAction(error) {
|
|
1236
|
+
const errorCode = error.code || '';
|
|
1237
|
+
|
|
1238
|
+
if (errorCode.includes('TIMEOUT')) {
|
|
1239
|
+
return 'Considerar extender el timeout o revisar la carga del sistema';
|
|
1240
|
+
}
|
|
1241
|
+
if (errorCode.includes('NETWORK')) {
|
|
1242
|
+
return 'Verificar conexion de red y disponibilidad del servicio';
|
|
1243
|
+
}
|
|
1244
|
+
if (errorCode.includes('RATE_LIMIT')) {
|
|
1245
|
+
return 'Esperar antes de reintentar o reducir frecuencia de requests';
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return 'Investigar la causa raiz con /elsabro:debug';
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Helper para sleep
|
|
1253
|
+
*/
|
|
1254
|
+
sleep(ms) {
|
|
1255
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Genera reporte visual de reintentos
|
|
1260
|
+
*/
|
|
1261
|
+
generateRetryReport(attempts, context = {}) {
|
|
1262
|
+
const lines = [
|
|
1263
|
+
'+------------------------------------------------------+',
|
|
1264
|
+
`| RETRY: ${context.operation || 'operation'}`.padEnd(55) + '|',
|
|
1265
|
+
'+------------------------------------------------------+'
|
|
1266
|
+
];
|
|
1267
|
+
|
|
1268
|
+
for (const attempt of attempts) {
|
|
1269
|
+
const status = attempt.status === 'SUCCESS' ? 'SUCCESS' : 'FAILED';
|
|
1270
|
+
const statusLine = `| Attempt ${attempt.attempt}/${this.maxAttempts}: ${status}`;
|
|
1271
|
+
|
|
1272
|
+
if (attempt.error) {
|
|
1273
|
+
lines.push(statusLine + ` (${attempt.error})`.padEnd(55 - statusLine.length) + '|');
|
|
1274
|
+
} else {
|
|
1275
|
+
lines.push(statusLine.padEnd(55) + '|');
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (attempt.attempt < attempts.length && attempt.status === 'FAILED') {
|
|
1279
|
+
const delay = this.calculateDelay(attempt.attempt) / 1000;
|
|
1280
|
+
lines.push(`| Waiting ${delay}s before retry...`.padEnd(55) + '|');
|
|
1281
|
+
lines.push('|'.padEnd(55) + '|');
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
1286
|
+
if (lastAttempt.status === 'FAILED' && attempts.length >= this.maxAttempts) {
|
|
1287
|
+
lines.push('|'.padEnd(55) + '|');
|
|
1288
|
+
lines.push('| X All attempts exhausted'.padEnd(55) + '|');
|
|
1289
|
+
lines.push('| -> Error escalated to user'.padEnd(55) + '|');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
lines.push('+------------------------------------------------------+');
|
|
1293
|
+
|
|
1294
|
+
return lines.join('\n');
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Ejemplo de uso
|
|
1299
|
+
const retryPolicy = new ContractRetryPolicy({
|
|
1300
|
+
maxAttempts: 3,
|
|
1301
|
+
baseDelayMs: 1000,
|
|
1302
|
+
backoffMultiplier: 2
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// Operacion que podria fallar
|
|
1306
|
+
async function unreliableOperation() {
|
|
1307
|
+
// Simular 60% de fallos
|
|
1308
|
+
if (Math.random() < 0.6) {
|
|
1309
|
+
throw { code: 'NETWORK_ERROR', message: 'Connection failed' };
|
|
1310
|
+
}
|
|
1311
|
+
return { success: true, data: 'result' };
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Ejecutar con reintentos
|
|
1315
|
+
retryPolicy.executeWithRetry(unreliableOperation)
|
|
1316
|
+
.then(result => {
|
|
1317
|
+
console.log(retryPolicy.generateRetryReport(result.attempts, { operation: 'api-call' }));
|
|
1318
|
+
});
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
---
|
|
1322
|
+
|
|
1323
|
+
## Contrato 5: ContractErrorAggregator
|
|
1324
|
+
|
|
1325
|
+
### Proposito
|
|
1326
|
+
|
|
1327
|
+
Colectar, agregar y aplicar politicas a errores de multiples tareas paralelas.
|
|
1328
|
+
|
|
1329
|
+
### Cuando se Activa
|
|
1330
|
+
|
|
1331
|
+
- Al finalizar una wave de tareas paralelas
|
|
1332
|
+
- Cuando cualquier tarea individual falla
|
|
1333
|
+
- Para decidir si continuar o abortar la operacion general
|
|
1334
|
+
|
|
1335
|
+
### Comportamiento en Fallo
|
|
1336
|
+
|
|
1337
|
+
```
|
|
1338
|
+
Politicas disponibles:
|
|
1339
|
+
- fail_fast: Un fallo = abortar todo
|
|
1340
|
+
- quorum: Continuar si >50% exito
|
|
1341
|
+
- continue_all: Continuar siempre, reportar fallos
|
|
1342
|
+
- critical_path: Abortar solo si falla tarea critica
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
### Notificacion al Usuario
|
|
1346
|
+
|
|
1347
|
+
```
|
|
1348
|
+
+======================================================+
|
|
1349
|
+
| PARALLEL EXECUTION COMPLETE |
|
|
1350
|
+
+======================================================+
|
|
1351
|
+
| Policy: quorum |
|
|
1352
|
+
| Total: 4 agents |
|
|
1353
|
+
| |
|
|
1354
|
+
| OK Agent 1 (code-reviewer): SUCCESS (45s) |
|
|
1355
|
+
| OK Agent 2 (typescript-pro): SUCCESS (32s) |
|
|
1356
|
+
| X Agent 3 (silent-failure): FAILED (timeout) |
|
|
1357
|
+
| OK Agent 4 (test-analyzer): SUCCESS (28s) |
|
|
1358
|
+
| |
|
|
1359
|
+
| Result: 3/4 (75%) - QUORUM MET |
|
|
1360
|
+
| Status: CONTINUING |
|
|
1361
|
+
+======================================================+
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
### Codigo Ejecutable
|
|
1365
|
+
|
|
1366
|
+
```javascript
|
|
1367
|
+
/**
|
|
1368
|
+
* ContractErrorAggregator
|
|
1369
|
+
* Colecta y aplica politicas a errores de ejecucion paralela
|
|
1370
|
+
*/
|
|
1371
|
+
class ContractErrorAggregator {
|
|
1372
|
+
constructor() {
|
|
1373
|
+
this.errors = [];
|
|
1374
|
+
this.results = [];
|
|
1375
|
+
this.policy = 'quorum'; // default
|
|
1376
|
+
this.criticalAgents = [];
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Configura la politica de agregacion
|
|
1381
|
+
* @param {string} policy - Politica a usar
|
|
1382
|
+
* @param {Object} options - Opciones adicionales
|
|
1383
|
+
*/
|
|
1384
|
+
setPolicy(policy, options = {}) {
|
|
1385
|
+
const validPolicies = ['fail_fast', 'quorum', 'continue_all', 'critical_path'];
|
|
1386
|
+
|
|
1387
|
+
if (!validPolicies.includes(policy)) {
|
|
1388
|
+
throw new Error(`Politica invalida: ${policy}. Validas: ${validPolicies.join(', ')}`);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
this.policy = policy;
|
|
1392
|
+
this.criticalAgents = options.criticalAgents || [];
|
|
1393
|
+
this.quorumThreshold = options.quorumThreshold || 0.5; // 50%
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Registra un resultado de tarea
|
|
1398
|
+
* @param {Object} result - Resultado de la tarea
|
|
1399
|
+
*/
|
|
1400
|
+
addResult(result) {
|
|
1401
|
+
this.results.push({
|
|
1402
|
+
taskId: result.taskId,
|
|
1403
|
+
agent: result.agent,
|
|
1404
|
+
status: result.status,
|
|
1405
|
+
duration: result.duration,
|
|
1406
|
+
output: result.output,
|
|
1407
|
+
error: result.error,
|
|
1408
|
+
timestamp: new Date().toISOString()
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
if (result.status === 'error' || result.status === 'timeout') {
|
|
1412
|
+
this.errors.push({
|
|
1413
|
+
taskId: result.taskId,
|
|
1414
|
+
agent: result.agent,
|
|
1415
|
+
error: result.error,
|
|
1416
|
+
severity: this.determineSeverity(result),
|
|
1417
|
+
isCritical: this.criticalAgents.includes(result.agent)
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
return this.evaluatePolicy();
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* Evalua si debe continuar segun la politica
|
|
1426
|
+
*/
|
|
1427
|
+
evaluatePolicy() {
|
|
1428
|
+
switch (this.policy) {
|
|
1429
|
+
case 'fail_fast':
|
|
1430
|
+
return this.evaluateFailFast();
|
|
1431
|
+
case 'quorum':
|
|
1432
|
+
return this.evaluateQuorum();
|
|
1433
|
+
case 'continue_all':
|
|
1434
|
+
return this.evaluateContinueAll();
|
|
1435
|
+
case 'critical_path':
|
|
1436
|
+
return this.evaluateCriticalPath();
|
|
1437
|
+
default:
|
|
1438
|
+
return this.evaluateQuorum();
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Politica: fail_fast
|
|
1444
|
+
* Un solo fallo = abortar todo
|
|
1445
|
+
*/
|
|
1446
|
+
evaluateFailFast() {
|
|
1447
|
+
if (this.errors.length > 0) {
|
|
1448
|
+
return {
|
|
1449
|
+
shouldContinue: false,
|
|
1450
|
+
policy: 'fail_fast',
|
|
1451
|
+
reason: 'Fallo detectado - abortando segun politica fail_fast',
|
|
1452
|
+
failedAgent: this.errors[0].agent,
|
|
1453
|
+
error: this.errors[0].error
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
return {
|
|
1458
|
+
shouldContinue: true,
|
|
1459
|
+
policy: 'fail_fast',
|
|
1460
|
+
reason: 'Sin fallos detectados'
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Politica: quorum
|
|
1466
|
+
* Continuar si >50% exito
|
|
1467
|
+
*/
|
|
1468
|
+
evaluateQuorum() {
|
|
1469
|
+
const total = this.results.length;
|
|
1470
|
+
const successes = this.results.filter(r => r.status === 'success').length;
|
|
1471
|
+
const successRate = total > 0 ? successes / total : 0;
|
|
1472
|
+
const quorumMet = successRate >= this.quorumThreshold;
|
|
1473
|
+
|
|
1474
|
+
return {
|
|
1475
|
+
shouldContinue: quorumMet,
|
|
1476
|
+
policy: 'quorum',
|
|
1477
|
+
successRate: successRate,
|
|
1478
|
+
threshold: this.quorumThreshold,
|
|
1479
|
+
quorumMet: quorumMet,
|
|
1480
|
+
reason: quorumMet
|
|
1481
|
+
? `Quorum alcanzado: ${successes}/${total} (${(successRate * 100).toFixed(0)}%)`
|
|
1482
|
+
: `Quorum NO alcanzado: ${successes}/${total} (${(successRate * 100).toFixed(0)}% < ${this.quorumThreshold * 100}%)`,
|
|
1483
|
+
failedAgents: this.errors.map(e => e.agent)
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Politica: continue_all
|
|
1489
|
+
* Continuar siempre, solo reportar fallos
|
|
1490
|
+
*/
|
|
1491
|
+
evaluateContinueAll() {
|
|
1492
|
+
return {
|
|
1493
|
+
shouldContinue: true,
|
|
1494
|
+
policy: 'continue_all',
|
|
1495
|
+
reason: 'Continuando segun politica continue_all',
|
|
1496
|
+
errors: this.errors,
|
|
1497
|
+
errorCount: this.errors.length
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Politica: critical_path
|
|
1503
|
+
* Abortar solo si falla un agente critico
|
|
1504
|
+
*/
|
|
1505
|
+
evaluateCriticalPath() {
|
|
1506
|
+
const criticalFailures = this.errors.filter(e => e.isCritical);
|
|
1507
|
+
|
|
1508
|
+
if (criticalFailures.length > 0) {
|
|
1509
|
+
return {
|
|
1510
|
+
shouldContinue: false,
|
|
1511
|
+
policy: 'critical_path',
|
|
1512
|
+
reason: 'Agente critico fallo',
|
|
1513
|
+
criticalFailures: criticalFailures,
|
|
1514
|
+
optionalFailures: this.errors.filter(e => !e.isCritical)
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
return {
|
|
1519
|
+
shouldContinue: true,
|
|
1520
|
+
policy: 'critical_path',
|
|
1521
|
+
reason: 'Agentes criticos OK (fallos en opcionales ignorados)',
|
|
1522
|
+
optionalFailures: this.errors
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* Determina la severidad de un error
|
|
1528
|
+
*/
|
|
1529
|
+
determineSeverity(result) {
|
|
1530
|
+
if (result.error?.severity) {
|
|
1531
|
+
return result.error.severity;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
if (result.status === 'timeout') {
|
|
1535
|
+
return 'HIGH';
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (this.criticalAgents.includes(result.agent)) {
|
|
1539
|
+
return 'CRITICAL';
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
return 'MEDIUM';
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* Genera reporte visual de ejecucion paralela
|
|
1547
|
+
*/
|
|
1548
|
+
generateReport() {
|
|
1549
|
+
const total = this.results.length;
|
|
1550
|
+
const successes = this.results.filter(r => r.status === 'success').length;
|
|
1551
|
+
const failures = this.errors.length;
|
|
1552
|
+
const successRate = total > 0 ? (successes / total * 100).toFixed(0) : 0;
|
|
1553
|
+
|
|
1554
|
+
const evaluation = this.evaluatePolicy();
|
|
1555
|
+
|
|
1556
|
+
const lines = [
|
|
1557
|
+
'+======================================================+',
|
|
1558
|
+
'| PARALLEL EXECUTION COMPLETE |',
|
|
1559
|
+
'+======================================================+',
|
|
1560
|
+
`| Policy: ${this.policy}`.padEnd(55) + '|',
|
|
1561
|
+
`| Total: ${total} agents`.padEnd(55) + '|',
|
|
1562
|
+
'|'.padEnd(55) + '|'
|
|
1563
|
+
];
|
|
1564
|
+
|
|
1565
|
+
// Resultados por agente
|
|
1566
|
+
for (const result of this.results) {
|
|
1567
|
+
const icon = result.status === 'success' ? 'OK' : 'X ';
|
|
1568
|
+
const duration = result.duration ? `(${(result.duration / 1000).toFixed(0)}s)` : '';
|
|
1569
|
+
const status = result.status.toUpperCase();
|
|
1570
|
+
const errorInfo = result.error ? ` (${result.error.code || 'error'})` : '';
|
|
1571
|
+
|
|
1572
|
+
const line = `| ${icon} Agent (${result.agent}): ${status}${errorInfo} ${duration}`;
|
|
1573
|
+
lines.push(line.padEnd(55) + '|');
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
lines.push('|'.padEnd(55) + '|');
|
|
1577
|
+
|
|
1578
|
+
// Resumen
|
|
1579
|
+
const quorumStatus = evaluation.shouldContinue ? 'MET' : 'NOT MET';
|
|
1580
|
+
lines.push(`| Result: ${successes}/${total} (${successRate}%) - ${this.policy.toUpperCase()} ${quorumStatus}`.padEnd(55) + '|');
|
|
1581
|
+
lines.push(`| Status: ${evaluation.shouldContinue ? 'CONTINUING' : 'STOPPING'}`.padEnd(55) + '|');
|
|
1582
|
+
lines.push('+======================================================+');
|
|
1583
|
+
|
|
1584
|
+
return lines.join('\n');
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Obtiene resumen de errores
|
|
1589
|
+
*/
|
|
1590
|
+
getErrorSummary() {
|
|
1591
|
+
const bySeverity = {};
|
|
1592
|
+
|
|
1593
|
+
for (const error of this.errors) {
|
|
1594
|
+
bySeverity[error.severity] = (bySeverity[error.severity] || 0) + 1;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
return {
|
|
1598
|
+
total: this.errors.length,
|
|
1599
|
+
bySeverity: bySeverity,
|
|
1600
|
+
criticalCount: this.errors.filter(e => e.isCritical).length,
|
|
1601
|
+
errors: this.errors
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* Limpia el estado para nueva ejecucion
|
|
1607
|
+
*/
|
|
1608
|
+
reset() {
|
|
1609
|
+
this.errors = [];
|
|
1610
|
+
this.results = [];
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// Ejemplo de uso
|
|
1615
|
+
const aggregator = new ContractErrorAggregator();
|
|
1616
|
+
|
|
1617
|
+
// Configurar politica
|
|
1618
|
+
aggregator.setPolicy('quorum', {
|
|
1619
|
+
quorumThreshold: 0.5,
|
|
1620
|
+
criticalAgents: ['elsabro-executor', 'elsabro-verifier']
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
// Simular resultados de ejecucion paralela
|
|
1624
|
+
aggregator.addResult({
|
|
1625
|
+
taskId: 'task-001',
|
|
1626
|
+
agent: 'code-reviewer',
|
|
1627
|
+
status: 'success',
|
|
1628
|
+
duration: 45000
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
aggregator.addResult({
|
|
1632
|
+
taskId: 'task-002',
|
|
1633
|
+
agent: 'typescript-pro',
|
|
1634
|
+
status: 'success',
|
|
1635
|
+
duration: 32000
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
aggregator.addResult({
|
|
1639
|
+
taskId: 'task-003',
|
|
1640
|
+
agent: 'silent-failure-hunter',
|
|
1641
|
+
status: 'timeout',
|
|
1642
|
+
error: { code: 'TIMEOUT' }
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
aggregator.addResult({
|
|
1646
|
+
taskId: 'task-004',
|
|
1647
|
+
agent: 'test-analyzer',
|
|
1648
|
+
status: 'success',
|
|
1649
|
+
duration: 28000
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
// Generar reporte
|
|
1653
|
+
console.log(aggregator.generateReport());
|
|
1654
|
+
```
|
|
1655
|
+
|
|
1656
|
+
---
|
|
1657
|
+
|
|
1658
|
+
## Contrato 6: ContractSeverityClassifier
|
|
1659
|
+
|
|
1660
|
+
### Proposito
|
|
1661
|
+
|
|
1662
|
+
Clasificar errores consistentemente en CRITICAL, HIGH, MEDIUM, LOW para tomar decisiones apropiadas.
|
|
1663
|
+
|
|
1664
|
+
### Cuando se Activa
|
|
1665
|
+
|
|
1666
|
+
- Al recibir cualquier error
|
|
1667
|
+
- Al agregar errores en el ContractErrorAggregator
|
|
1668
|
+
- Para decidir notificaciones y acciones
|
|
1669
|
+
|
|
1670
|
+
### Comportamiento en Fallo
|
|
1671
|
+
|
|
1672
|
+
```
|
|
1673
|
+
CRITICAL: Estado corrupto, no puede continuar
|
|
1674
|
+
-> Abortar inmediatamente
|
|
1675
|
+
-> Notificar con urgencia
|
|
1676
|
+
-> Sugerir recuperacion
|
|
1677
|
+
|
|
1678
|
+
HIGH: Problema serio que deberia parar
|
|
1679
|
+
-> Ofrecer opciones al usuario
|
|
1680
|
+
-> Permitir continuar con riesgo
|
|
1681
|
+
|
|
1682
|
+
MEDIUM: Warning, puede continuar
|
|
1683
|
+
-> Registrar
|
|
1684
|
+
-> Continuar automaticamente
|
|
1685
|
+
-> Notificar al final
|
|
1686
|
+
|
|
1687
|
+
LOW: Informativo
|
|
1688
|
+
-> Solo registrar
|
|
1689
|
+
-> Continuar sin interrupcion
|
|
1690
|
+
```
|
|
1691
|
+
|
|
1692
|
+
### Notificacion al Usuario
|
|
1693
|
+
|
|
1694
|
+
```
|
|
1695
|
+
CRITICAL:
|
|
1696
|
+
+======================================================+
|
|
1697
|
+
| ERROR: STATE_CORRUPTED |
|
|
1698
|
+
| Severity: CRITICAL |
|
|
1699
|
+
+======================================================+
|
|
1700
|
+
| |
|
|
1701
|
+
| El archivo de estado esta corrupto |
|
|
1702
|
+
| |
|
|
1703
|
+
| ACCION REQUERIDA: |
|
|
1704
|
+
| -> Ejecutar /elsabro:start para reinicializar |
|
|
1705
|
+
+======================================================+
|
|
1706
|
+
|
|
1707
|
+
HIGH:
|
|
1708
|
+
+======================================================+
|
|
1709
|
+
| ERROR: TESTS_FAILED |
|
|
1710
|
+
| Severity: HIGH |
|
|
1711
|
+
+======================================================+
|
|
1712
|
+
| |
|
|
1713
|
+
| 3 tests fallaron |
|
|
1714
|
+
| |
|
|
1715
|
+
| Opciones: |
|
|
1716
|
+
| [d] Debug con /elsabro:debug |
|
|
1717
|
+
| [s] Saltar (no recomendado) |
|
|
1718
|
+
| [a] Abortar |
|
|
1719
|
+
+======================================================+
|
|
1720
|
+
|
|
1721
|
+
MEDIUM:
|
|
1722
|
+
+------------------------------------------------------+
|
|
1723
|
+
| WARNING: LINT_WARNINGS |
|
|
1724
|
+
+------------------------------------------------------+
|
|
1725
|
+
| 5 lint warnings encontrados |
|
|
1726
|
+
| Continuando automaticamente... |
|
|
1727
|
+
+------------------------------------------------------+
|
|
1728
|
+
|
|
1729
|
+
LOW:
|
|
1730
|
+
[INFO] Retry exitoso despues de 1 intento fallido
|
|
1731
|
+
```
|
|
1732
|
+
|
|
1733
|
+
### Codigo Ejecutable
|
|
1734
|
+
|
|
1735
|
+
```javascript
|
|
1736
|
+
/**
|
|
1737
|
+
* ContractSeverityClassifier
|
|
1738
|
+
* Clasifica errores en niveles de severidad consistentes
|
|
1739
|
+
*/
|
|
1740
|
+
class ContractSeverityClassifier {
|
|
1741
|
+
constructor() {
|
|
1742
|
+
// Definicion de severidades
|
|
1743
|
+
this.severities = {
|
|
1744
|
+
CRITICAL: {
|
|
1745
|
+
level: 4,
|
|
1746
|
+
icon: 'ERROR',
|
|
1747
|
+
canContinue: false,
|
|
1748
|
+
notifyImmediately: true,
|
|
1749
|
+
requiresUserAction: true
|
|
1750
|
+
},
|
|
1751
|
+
HIGH: {
|
|
1752
|
+
level: 3,
|
|
1753
|
+
icon: 'ERROR',
|
|
1754
|
+
canContinue: true,
|
|
1755
|
+
notifyImmediately: true,
|
|
1756
|
+
requiresUserAction: true
|
|
1757
|
+
},
|
|
1758
|
+
MEDIUM: {
|
|
1759
|
+
level: 2,
|
|
1760
|
+
icon: 'WARNING',
|
|
1761
|
+
canContinue: true,
|
|
1762
|
+
notifyImmediately: false,
|
|
1763
|
+
requiresUserAction: false
|
|
1764
|
+
},
|
|
1765
|
+
LOW: {
|
|
1766
|
+
level: 1,
|
|
1767
|
+
icon: 'INFO',
|
|
1768
|
+
canContinue: true,
|
|
1769
|
+
notifyImmediately: false,
|
|
1770
|
+
requiresUserAction: false
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1774
|
+
// Mapeo de codigos de error a severidades
|
|
1775
|
+
this.errorSeverityMap = {
|
|
1776
|
+
// CRITICAL - No puede continuar
|
|
1777
|
+
'STATE_CORRUPTED': 'CRITICAL',
|
|
1778
|
+
'STATE_MISSING': 'CRITICAL',
|
|
1779
|
+
'CONFIG_INVALID': 'CRITICAL',
|
|
1780
|
+
'AGENT_NOT_FOUND': 'CRITICAL',
|
|
1781
|
+
'INCOMPATIBLE_VERSION': 'CRITICAL',
|
|
1782
|
+
'GIT_DETACHED_HEAD': 'CRITICAL',
|
|
1783
|
+
'GIT_CONFLICTS': 'CRITICAL',
|
|
1784
|
+
'SESSION_CORRUPTED': 'CRITICAL',
|
|
1785
|
+
|
|
1786
|
+
// HIGH - Deberia parar
|
|
1787
|
+
'TESTS_FAILED': 'HIGH',
|
|
1788
|
+
'BUILD_FAILED': 'HIGH',
|
|
1789
|
+
'VALIDATION_FAILED': 'HIGH',
|
|
1790
|
+
'TASK_TIMEOUT': 'HIGH',
|
|
1791
|
+
'RETRY_EXHAUSTED': 'HIGH',
|
|
1792
|
+
'API_ERROR': 'HIGH',
|
|
1793
|
+
'PERMISSION_DENIED': 'HIGH',
|
|
1794
|
+
'QUORUM_NOT_MET': 'HIGH',
|
|
1795
|
+
'CRITICAL_AGENT_FAILED': 'HIGH',
|
|
1796
|
+
'INVALID_STATE_TRANSITION': 'HIGH',
|
|
1797
|
+
|
|
1798
|
+
// MEDIUM - Warning
|
|
1799
|
+
'LINT_WARNINGS': 'MEDIUM',
|
|
1800
|
+
'DEPRECATION_WARNING': 'MEDIUM',
|
|
1801
|
+
'COVERAGE_LOW': 'MEDIUM',
|
|
1802
|
+
'UNUSED_FILES': 'MEDIUM',
|
|
1803
|
+
'OPTIONAL_AGENT_FAILED': 'MEDIUM',
|
|
1804
|
+
'SLOW_OPERATION': 'MEDIUM',
|
|
1805
|
+
'PARTIAL_SUCCESS': 'MEDIUM',
|
|
1806
|
+
|
|
1807
|
+
// LOW - Informativo
|
|
1808
|
+
'RETRY_SUCCESS': 'LOW',
|
|
1809
|
+
'OPERATION_COMPLETE': 'LOW',
|
|
1810
|
+
'SUGGESTION': 'LOW',
|
|
1811
|
+
'IMPROVEMENT_HINT': 'LOW'
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
// Patrones de error para clasificacion dinamica
|
|
1815
|
+
this.errorPatterns = [
|
|
1816
|
+
{ pattern: /corrupt|invalid.*state|missing.*critical/i, severity: 'CRITICAL' },
|
|
1817
|
+
{ pattern: /test.*fail|build.*fail|timeout|exhausted/i, severity: 'HIGH' },
|
|
1818
|
+
{ pattern: /warning|deprecated|slow/i, severity: 'MEDIUM' },
|
|
1819
|
+
{ pattern: /info|success|hint/i, severity: 'LOW' }
|
|
1820
|
+
];
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* Clasifica un error
|
|
1825
|
+
* @param {Object|string} error - Error a clasificar
|
|
1826
|
+
* @returns {Object} Error clasificado con severidad
|
|
1827
|
+
*/
|
|
1828
|
+
classify(error) {
|
|
1829
|
+
const errorCode = this.extractErrorCode(error);
|
|
1830
|
+
const errorMessage = this.extractErrorMessage(error);
|
|
1831
|
+
|
|
1832
|
+
// Buscar en mapa de errores conocidos
|
|
1833
|
+
let severity = this.errorSeverityMap[errorCode];
|
|
1834
|
+
|
|
1835
|
+
// Si no esta en el mapa, usar patrones
|
|
1836
|
+
if (!severity) {
|
|
1837
|
+
severity = this.classifyByPattern(errorMessage);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// Fail-safe: errores desconocidos se tratan como HIGH
|
|
1841
|
+
if (!severity) {
|
|
1842
|
+
severity = 'HIGH';
|
|
1843
|
+
console.warn(`[SEVERITY] Error desconocido clasificado como HIGH: ${errorMessage}`);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
const severityConfig = this.severities[severity];
|
|
1847
|
+
|
|
1848
|
+
return {
|
|
1849
|
+
code: errorCode,
|
|
1850
|
+
message: errorMessage,
|
|
1851
|
+
severity: severity,
|
|
1852
|
+
level: severityConfig.level,
|
|
1853
|
+
canContinue: severityConfig.canContinue,
|
|
1854
|
+
notifyImmediately: severityConfig.notifyImmediately,
|
|
1855
|
+
requiresUserAction: severityConfig.requiresUserAction,
|
|
1856
|
+
timestamp: new Date().toISOString()
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Extrae codigo de error
|
|
1862
|
+
*/
|
|
1863
|
+
extractErrorCode(error) {
|
|
1864
|
+
if (typeof error === 'string') {
|
|
1865
|
+
return error.toUpperCase().replace(/\s+/g, '_');
|
|
1866
|
+
}
|
|
1867
|
+
return error.code || error.type || 'UNKNOWN_ERROR';
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
/**
|
|
1871
|
+
* Extrae mensaje de error
|
|
1872
|
+
*/
|
|
1873
|
+
extractErrorMessage(error) {
|
|
1874
|
+
if (typeof error === 'string') {
|
|
1875
|
+
return error;
|
|
1876
|
+
}
|
|
1877
|
+
return error.message || error.description || String(error);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/**
|
|
1881
|
+
* Clasifica por patron de mensaje
|
|
1882
|
+
*/
|
|
1883
|
+
classifyByPattern(message) {
|
|
1884
|
+
for (const { pattern, severity } of this.errorPatterns) {
|
|
1885
|
+
if (pattern.test(message)) {
|
|
1886
|
+
return severity;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* Genera display visual del error
|
|
1894
|
+
* @param {Object} classifiedError - Error ya clasificado
|
|
1895
|
+
* @returns {string} Display ASCII
|
|
1896
|
+
*/
|
|
1897
|
+
generateDisplay(classifiedError) {
|
|
1898
|
+
const severity = classifiedError.severity;
|
|
1899
|
+
const severityConfig = this.severities[severity];
|
|
1900
|
+
|
|
1901
|
+
if (severity === 'CRITICAL' || severity === 'HIGH') {
|
|
1902
|
+
return this.generateBoxDisplay(classifiedError);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
if (severity === 'MEDIUM') {
|
|
1906
|
+
return this.generateWarningDisplay(classifiedError);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
return this.generateInfoDisplay(classifiedError);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/**
|
|
1913
|
+
* Display para errores CRITICAL/HIGH
|
|
1914
|
+
*/
|
|
1915
|
+
generateBoxDisplay(error) {
|
|
1916
|
+
const icon = error.severity === 'CRITICAL' ? 'ERROR' : 'ERROR';
|
|
1917
|
+
const lines = [
|
|
1918
|
+
'+======================================================+',
|
|
1919
|
+
`| ${icon}: ${error.code}`.padEnd(55) + '|',
|
|
1920
|
+
`| Severity: ${error.severity}`.padEnd(55) + '|',
|
|
1921
|
+
'+======================================================+',
|
|
1922
|
+
'|'.padEnd(55) + '|'
|
|
1923
|
+
];
|
|
1924
|
+
|
|
1925
|
+
// Dividir mensaje en lineas de 50 caracteres
|
|
1926
|
+
const messageLines = this.wrapText(error.message, 50);
|
|
1927
|
+
for (const line of messageLines) {
|
|
1928
|
+
lines.push(`| ${line}`.padEnd(55) + '|');
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
lines.push('|'.padEnd(55) + '|');
|
|
1932
|
+
|
|
1933
|
+
if (error.requiresUserAction) {
|
|
1934
|
+
lines.push('+------------------------------------------------------+');
|
|
1935
|
+
|
|
1936
|
+
if (error.severity === 'CRITICAL') {
|
|
1937
|
+
lines.push('| ACCION REQUERIDA:'.padEnd(55) + '|');
|
|
1938
|
+
lines.push('| -> Resolver el error antes de continuar'.padEnd(55) + '|');
|
|
1939
|
+
} else {
|
|
1940
|
+
lines.push('| Opciones:'.padEnd(55) + '|');
|
|
1941
|
+
lines.push('| [d] Debug con /elsabro:debug'.padEnd(55) + '|');
|
|
1942
|
+
lines.push('| [s] Saltar (no recomendado)'.padEnd(55) + '|');
|
|
1943
|
+
lines.push('| [a] Abortar'.padEnd(55) + '|');
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
lines.push('+======================================================+');
|
|
1948
|
+
|
|
1949
|
+
return lines.join('\n');
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
/**
|
|
1953
|
+
* Display para warnings (MEDIUM)
|
|
1954
|
+
*/
|
|
1955
|
+
generateWarningDisplay(error) {
|
|
1956
|
+
const lines = [
|
|
1957
|
+
'+------------------------------------------------------+',
|
|
1958
|
+
`| WARNING: ${error.code}`.padEnd(55) + '|',
|
|
1959
|
+
'+------------------------------------------------------+'
|
|
1960
|
+
];
|
|
1961
|
+
|
|
1962
|
+
const messageLines = this.wrapText(error.message, 50);
|
|
1963
|
+
for (const line of messageLines) {
|
|
1964
|
+
lines.push(`| ${line}`.padEnd(55) + '|');
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
lines.push('| Continuando automaticamente...'.padEnd(55) + '|');
|
|
1968
|
+
lines.push('+------------------------------------------------------+');
|
|
1969
|
+
|
|
1970
|
+
return lines.join('\n');
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* Display para info (LOW)
|
|
1975
|
+
*/
|
|
1976
|
+
generateInfoDisplay(error) {
|
|
1977
|
+
return `[INFO] ${error.message}`;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
/**
|
|
1981
|
+
* Wrap texto a ancho especifico
|
|
1982
|
+
*/
|
|
1983
|
+
wrapText(text, width) {
|
|
1984
|
+
const words = text.split(' ');
|
|
1985
|
+
const lines = [];
|
|
1986
|
+
let currentLine = '';
|
|
1987
|
+
|
|
1988
|
+
for (const word of words) {
|
|
1989
|
+
if ((currentLine + ' ' + word).trim().length <= width) {
|
|
1990
|
+
currentLine = (currentLine + ' ' + word).trim();
|
|
1991
|
+
} else {
|
|
1992
|
+
if (currentLine) lines.push(currentLine);
|
|
1993
|
+
currentLine = word;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
if (currentLine) lines.push(currentLine);
|
|
1998
|
+
|
|
1999
|
+
return lines;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/**
|
|
2003
|
+
* Compara severidades
|
|
2004
|
+
* @param {string} sev1 - Primera severidad
|
|
2005
|
+
* @param {string} sev2 - Segunda severidad
|
|
2006
|
+
* @returns {number} -1, 0, o 1
|
|
2007
|
+
*/
|
|
2008
|
+
compareSeverity(sev1, sev2) {
|
|
2009
|
+
const level1 = this.severities[sev1]?.level || 0;
|
|
2010
|
+
const level2 = this.severities[sev2]?.level || 0;
|
|
2011
|
+
return level2 - level1; // Mayor severidad primero
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Obtiene la severidad mas alta de una lista
|
|
2016
|
+
*/
|
|
2017
|
+
getHighestSeverity(errors) {
|
|
2018
|
+
let highest = 'LOW';
|
|
2019
|
+
|
|
2020
|
+
for (const error of errors) {
|
|
2021
|
+
const classified = this.classify(error);
|
|
2022
|
+
if (this.severities[classified.severity].level > this.severities[highest].level) {
|
|
2023
|
+
highest = classified.severity;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
return highest;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// Ejemplo de uso
|
|
2032
|
+
const classifier = new ContractSeverityClassifier();
|
|
2033
|
+
|
|
2034
|
+
// Clasificar varios errores
|
|
2035
|
+
const errors = [
|
|
2036
|
+
{ code: 'STATE_CORRUPTED', message: 'El archivo state.json esta corrupto' },
|
|
2037
|
+
{ code: 'TESTS_FAILED', message: '3 tests fallaron en el modulo de auth' },
|
|
2038
|
+
{ code: 'LINT_WARNINGS', message: '5 lint warnings encontrados' },
|
|
2039
|
+
{ code: 'RETRY_SUCCESS', message: 'Operacion exitosa despues de 2 reintentos' }
|
|
2040
|
+
];
|
|
2041
|
+
|
|
2042
|
+
for (const error of errors) {
|
|
2043
|
+
const classified = classifier.classify(error);
|
|
2044
|
+
console.log(classifier.generateDisplay(classified));
|
|
2045
|
+
console.log('');
|
|
2046
|
+
}
|
|
2047
|
+
```
|
|
2048
|
+
|
|
2049
|
+
---
|
|
2050
|
+
|
|
2051
|
+
## Contrato 7: ContractSessionValidator
|
|
2052
|
+
|
|
2053
|
+
### Proposito
|
|
2054
|
+
|
|
2055
|
+
Validar la integridad del archivo de estado `.elsabro/state.json` y detectar corrupcion o inconsistencias.
|
|
2056
|
+
|
|
2057
|
+
### Cuando se Activa
|
|
2058
|
+
|
|
2059
|
+
- Al inicio de cualquier comando ELSABRO
|
|
2060
|
+
- Al escribir estado
|
|
2061
|
+
- Periodicamente durante operaciones largas
|
|
2062
|
+
|
|
2063
|
+
### Comportamiento en Fallo
|
|
2064
|
+
|
|
2065
|
+
```
|
|
2066
|
+
SI estado corrupto:
|
|
2067
|
+
1. Intentar recuperar de backup
|
|
2068
|
+
2. Si no hay backup, reinicializar
|
|
2069
|
+
3. Preservar contexto que se pueda salvar
|
|
2070
|
+
4. Notificar al usuario
|
|
2071
|
+
```
|
|
2072
|
+
|
|
2073
|
+
### Notificacion al Usuario
|
|
2074
|
+
|
|
2075
|
+
```
|
|
2076
|
+
+======================================================+
|
|
2077
|
+
| ERROR: SESSION_CORRUPTED |
|
|
2078
|
+
| Severity: CRITICAL |
|
|
2079
|
+
+======================================================+
|
|
2080
|
+
| |
|
|
2081
|
+
| El archivo .elsabro/state.json esta corrupto |
|
|
2082
|
+
| o tiene estructura invalida |
|
|
2083
|
+
| |
|
|
2084
|
+
| Detalles: |
|
|
2085
|
+
| - Campo 'version' faltante |
|
|
2086
|
+
| - Campo 'current_flow' tiene tipo invalido |
|
|
2087
|
+
| |
|
|
2088
|
+
+------------------------------------------------------+
|
|
2089
|
+
| Opciones de recuperacion: |
|
|
2090
|
+
| [b] Restaurar desde backup |
|
|
2091
|
+
| [r] Reinicializar estado (perdera historial) |
|
|
2092
|
+
| [m] Editar manualmente |
|
|
2093
|
+
+======================================================+
|
|
2094
|
+
```
|
|
2095
|
+
|
|
2096
|
+
### Codigo Ejecutable
|
|
2097
|
+
|
|
2098
|
+
```javascript
|
|
2099
|
+
/**
|
|
2100
|
+
* ContractSessionValidator
|
|
2101
|
+
* Valida integridad del estado de sesion ELSABRO
|
|
2102
|
+
*/
|
|
2103
|
+
class ContractSessionValidator {
|
|
2104
|
+
constructor() {
|
|
2105
|
+
// Schema de validacion para state.json
|
|
2106
|
+
this.stateSchema = {
|
|
2107
|
+
required: ['version', 'updated_at'],
|
|
2108
|
+
optional: ['created_at', 'current_flow', 'context', 'history', 'pending_tasks', 'blocked_on', 'errors', 'suggested_next'],
|
|
2109
|
+
types: {
|
|
2110
|
+
version: 'string',
|
|
2111
|
+
created_at: 'string',
|
|
2112
|
+
updated_at: 'string',
|
|
2113
|
+
current_flow: ['object', 'null'],
|
|
2114
|
+
context: 'object',
|
|
2115
|
+
history: 'array',
|
|
2116
|
+
pending_tasks: 'array',
|
|
2117
|
+
blocked_on: ['string', 'null'],
|
|
2118
|
+
errors: 'object',
|
|
2119
|
+
suggested_next: ['string', 'null']
|
|
2120
|
+
},
|
|
2121
|
+
nested: {
|
|
2122
|
+
current_flow: {
|
|
2123
|
+
required: ['command', 'phase', 'started_at'],
|
|
2124
|
+
types: {
|
|
2125
|
+
command: 'string',
|
|
2126
|
+
phase: 'string',
|
|
2127
|
+
started_at: 'string'
|
|
2128
|
+
}
|
|
2129
|
+
},
|
|
2130
|
+
context: {
|
|
2131
|
+
optional: ['project_type', 'tech_stack', 'current_feature', 'plan_file'],
|
|
2132
|
+
types: {
|
|
2133
|
+
project_type: 'string',
|
|
2134
|
+
tech_stack: 'array',
|
|
2135
|
+
current_feature: 'string',
|
|
2136
|
+
plan_file: 'string'
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
|
|
2142
|
+
// Fases validas por comando
|
|
2143
|
+
this.validPhases = {
|
|
2144
|
+
start: ['initializing', 'detecting_context', 'greeting', 'transitioning', 'done'],
|
|
2145
|
+
plan: ['initializing', 'exploring', 'researching', 'planning', 'validating', 'done'],
|
|
2146
|
+
execute: ['initializing', 'exploring', 'executing_wave_1', 'executing_wave_2', 'executing_wave_3', 'verifying', 'done'],
|
|
2147
|
+
'verify-work': ['initializing', 'verifying', 'aggregating', 'done'],
|
|
2148
|
+
debug: ['initializing', 'diagnosing', 'fixing', 'verifying', 'done'],
|
|
2149
|
+
quick: ['initializing', 'executing', 'done']
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
// Path al estado
|
|
2153
|
+
this.statePath = '.elsabro/state.json';
|
|
2154
|
+
this.backupPath = '.elsabro/state.backup.json';
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* Valida el estado completo
|
|
2159
|
+
* @param {Object|string} state - Estado a validar (objeto o JSON string)
|
|
2160
|
+
* @returns {Object} Resultado de validacion
|
|
2161
|
+
*/
|
|
2162
|
+
validate(state) {
|
|
2163
|
+
const errors = [];
|
|
2164
|
+
|
|
2165
|
+
// Parsear si es string
|
|
2166
|
+
if (typeof state === 'string') {
|
|
2167
|
+
try {
|
|
2168
|
+
state = JSON.parse(state);
|
|
2169
|
+
} catch (e) {
|
|
2170
|
+
return {
|
|
2171
|
+
valid: false,
|
|
2172
|
+
error: {
|
|
2173
|
+
code: 'JSON_PARSE_ERROR',
|
|
2174
|
+
severity: 'CRITICAL',
|
|
2175
|
+
message: 'El estado no es JSON valido',
|
|
2176
|
+
parseError: e.message
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// Validar tipo base
|
|
2183
|
+
if (typeof state !== 'object' || state === null) {
|
|
2184
|
+
return {
|
|
2185
|
+
valid: false,
|
|
2186
|
+
error: {
|
|
2187
|
+
code: 'INVALID_STATE_TYPE',
|
|
2188
|
+
severity: 'CRITICAL',
|
|
2189
|
+
message: 'El estado debe ser un objeto'
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// Validar campos requeridos
|
|
2195
|
+
for (const field of this.stateSchema.required) {
|
|
2196
|
+
if (!(field in state)) {
|
|
2197
|
+
errors.push({
|
|
2198
|
+
field: field,
|
|
2199
|
+
type: 'MISSING_REQUIRED',
|
|
2200
|
+
message: `Campo requerido '${field}' faltante`
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// Validar tipos
|
|
2206
|
+
for (const [field, expectedType] of Object.entries(this.stateSchema.types)) {
|
|
2207
|
+
if (field in state) {
|
|
2208
|
+
const typeError = this.validateType(state[field], expectedType, field);
|
|
2209
|
+
if (typeError) {
|
|
2210
|
+
errors.push(typeError);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// Validar current_flow si existe
|
|
2216
|
+
if (state.current_flow !== null && state.current_flow !== undefined) {
|
|
2217
|
+
const flowErrors = this.validateCurrentFlow(state.current_flow);
|
|
2218
|
+
errors.push(...flowErrors);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// Validar historial si existe
|
|
2222
|
+
if (state.history) {
|
|
2223
|
+
const historyErrors = this.validateHistory(state.history);
|
|
2224
|
+
errors.push(...historyErrors);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
// Validar timestamps
|
|
2228
|
+
const timestampErrors = this.validateTimestamps(state);
|
|
2229
|
+
errors.push(...timestampErrors);
|
|
2230
|
+
|
|
2231
|
+
if (errors.length > 0) {
|
|
2232
|
+
return {
|
|
2233
|
+
valid: false,
|
|
2234
|
+
errors: errors,
|
|
2235
|
+
error: {
|
|
2236
|
+
code: 'SESSION_VALIDATION_FAILED',
|
|
2237
|
+
severity: errors.some(e => e.type === 'MISSING_REQUIRED') ? 'CRITICAL' : 'HIGH',
|
|
2238
|
+
message: `${errors.length} errores de validacion encontrados`,
|
|
2239
|
+
details: errors
|
|
2240
|
+
}
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
return {
|
|
2245
|
+
valid: true,
|
|
2246
|
+
state: state
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
/**
|
|
2251
|
+
* Valida el tipo de un valor
|
|
2252
|
+
*/
|
|
2253
|
+
validateType(value, expectedType, field) {
|
|
2254
|
+
const types = Array.isArray(expectedType) ? expectedType : [expectedType];
|
|
2255
|
+
|
|
2256
|
+
for (const type of types) {
|
|
2257
|
+
if (type === 'null' && value === null) return null;
|
|
2258
|
+
if (type === 'array' && Array.isArray(value)) return null;
|
|
2259
|
+
if (type === 'object' && typeof value === 'object' && value !== null && !Array.isArray(value)) return null;
|
|
2260
|
+
if (typeof value === type) return null;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
return {
|
|
2264
|
+
field: field,
|
|
2265
|
+
type: 'TYPE_MISMATCH',
|
|
2266
|
+
message: `Campo '${field}' tiene tipo invalido. Esperado: ${types.join(' | ')}, recibido: ${typeof value}`
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
/**
|
|
2271
|
+
* Valida current_flow
|
|
2272
|
+
*/
|
|
2273
|
+
validateCurrentFlow(flow) {
|
|
2274
|
+
const errors = [];
|
|
2275
|
+
const schema = this.stateSchema.nested.current_flow;
|
|
2276
|
+
|
|
2277
|
+
// Validar campos requeridos
|
|
2278
|
+
for (const field of schema.required) {
|
|
2279
|
+
if (!(field in flow)) {
|
|
2280
|
+
errors.push({
|
|
2281
|
+
field: `current_flow.${field}`,
|
|
2282
|
+
type: 'MISSING_REQUIRED',
|
|
2283
|
+
message: `Campo requerido 'current_flow.${field}' faltante`
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// Validar fase valida para el comando
|
|
2289
|
+
if (flow.command && flow.phase) {
|
|
2290
|
+
const validPhases = this.validPhases[flow.command];
|
|
2291
|
+
if (validPhases && !validPhases.includes(flow.phase)) {
|
|
2292
|
+
errors.push({
|
|
2293
|
+
field: 'current_flow.phase',
|
|
2294
|
+
type: 'INVALID_PHASE',
|
|
2295
|
+
message: `Fase '${flow.phase}' invalida para comando '${flow.command}'. Validas: ${validPhases.join(', ')}`
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
return errors;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
/**
|
|
2304
|
+
* Valida el historial
|
|
2305
|
+
*/
|
|
2306
|
+
validateHistory(history) {
|
|
2307
|
+
const errors = [];
|
|
2308
|
+
|
|
2309
|
+
if (!Array.isArray(history)) {
|
|
2310
|
+
errors.push({
|
|
2311
|
+
field: 'history',
|
|
2312
|
+
type: 'TYPE_MISMATCH',
|
|
2313
|
+
message: 'El historial debe ser un array'
|
|
2314
|
+
});
|
|
2315
|
+
return errors;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
for (let i = 0; i < history.length; i++) {
|
|
2319
|
+
const entry = history[i];
|
|
2320
|
+
|
|
2321
|
+
if (!entry.command) {
|
|
2322
|
+
errors.push({
|
|
2323
|
+
field: `history[${i}].command`,
|
|
2324
|
+
type: 'MISSING_REQUIRED',
|
|
2325
|
+
message: `Entrada de historial ${i} sin campo 'command'`
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
if (!entry.completed_at) {
|
|
2330
|
+
errors.push({
|
|
2331
|
+
field: `history[${i}].completed_at`,
|
|
2332
|
+
type: 'MISSING_REQUIRED',
|
|
2333
|
+
message: `Entrada de historial ${i} sin campo 'completed_at'`
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
return errors;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Valida timestamps
|
|
2343
|
+
*/
|
|
2344
|
+
validateTimestamps(state) {
|
|
2345
|
+
const errors = [];
|
|
2346
|
+
|
|
2347
|
+
const timestamps = ['created_at', 'updated_at'];
|
|
2348
|
+
|
|
2349
|
+
for (const field of timestamps) {
|
|
2350
|
+
if (state[field]) {
|
|
2351
|
+
const date = new Date(state[field]);
|
|
2352
|
+
if (isNaN(date.getTime())) {
|
|
2353
|
+
errors.push({
|
|
2354
|
+
field: field,
|
|
2355
|
+
type: 'INVALID_TIMESTAMP',
|
|
2356
|
+
message: `Timestamp '${field}' no es una fecha valida: ${state[field]}`
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// Verificar que updated_at >= created_at
|
|
2363
|
+
if (state.created_at && state.updated_at) {
|
|
2364
|
+
const created = new Date(state.created_at);
|
|
2365
|
+
const updated = new Date(state.updated_at);
|
|
2366
|
+
|
|
2367
|
+
if (updated < created) {
|
|
2368
|
+
errors.push({
|
|
2369
|
+
field: 'timestamps',
|
|
2370
|
+
type: 'INVALID_TIMESTAMP_ORDER',
|
|
2371
|
+
message: 'updated_at es anterior a created_at'
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
return errors;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
/**
|
|
2380
|
+
* Intenta reparar un estado corrupto
|
|
2381
|
+
* @param {Object} corruptState - Estado corrupto
|
|
2382
|
+
* @returns {Object} Estado reparado o error
|
|
2383
|
+
*/
|
|
2384
|
+
attemptRepair(corruptState) {
|
|
2385
|
+
const repaired = {};
|
|
2386
|
+
const repairs = [];
|
|
2387
|
+
|
|
2388
|
+
// Asegurar version
|
|
2389
|
+
repaired.version = corruptState?.version || '1.0.0';
|
|
2390
|
+
if (!corruptState?.version) {
|
|
2391
|
+
repairs.push('Agregado version: 1.0.0');
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// Asegurar timestamps
|
|
2395
|
+
const now = new Date().toISOString();
|
|
2396
|
+
repaired.created_at = corruptState?.created_at || now;
|
|
2397
|
+
repaired.updated_at = now;
|
|
2398
|
+
if (!corruptState?.created_at) {
|
|
2399
|
+
repairs.push('Agregado created_at');
|
|
2400
|
+
}
|
|
2401
|
+
repairs.push('Actualizado updated_at');
|
|
2402
|
+
|
|
2403
|
+
// Preservar o inicializar current_flow
|
|
2404
|
+
repaired.current_flow = null; // Limpiar flujo corrupto
|
|
2405
|
+
|
|
2406
|
+
// Preservar contexto si es valido
|
|
2407
|
+
if (corruptState?.context && typeof corruptState.context === 'object') {
|
|
2408
|
+
repaired.context = corruptState.context;
|
|
2409
|
+
} else {
|
|
2410
|
+
repaired.context = {};
|
|
2411
|
+
repairs.push('Reinicializado context');
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// Preservar historial si es valido
|
|
2415
|
+
if (Array.isArray(corruptState?.history)) {
|
|
2416
|
+
repaired.history = corruptState.history.filter(h => h.command && h.completed_at);
|
|
2417
|
+
if (repaired.history.length < corruptState.history.length) {
|
|
2418
|
+
repairs.push(`Filtradas ${corruptState.history.length - repaired.history.length} entradas de historial invalidas`);
|
|
2419
|
+
}
|
|
2420
|
+
} else {
|
|
2421
|
+
repaired.history = [];
|
|
2422
|
+
repairs.push('Reinicializado history');
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// Inicializar campos opcionales
|
|
2426
|
+
repaired.pending_tasks = [];
|
|
2427
|
+
repaired.blocked_on = null;
|
|
2428
|
+
repaired.errors = { count: 0 };
|
|
2429
|
+
repaired.suggested_next = null;
|
|
2430
|
+
|
|
2431
|
+
// Validar estado reparado
|
|
2432
|
+
const validation = this.validate(repaired);
|
|
2433
|
+
|
|
2434
|
+
if (validation.valid) {
|
|
2435
|
+
return {
|
|
2436
|
+
success: true,
|
|
2437
|
+
state: repaired,
|
|
2438
|
+
repairs: repairs
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
return {
|
|
2443
|
+
success: false,
|
|
2444
|
+
error: {
|
|
2445
|
+
code: 'REPAIR_FAILED',
|
|
2446
|
+
severity: 'CRITICAL',
|
|
2447
|
+
message: 'No se pudo reparar el estado',
|
|
2448
|
+
validationErrors: validation.errors
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
/**
|
|
2454
|
+
* Crea un estado inicial valido
|
|
2455
|
+
*/
|
|
2456
|
+
createInitialState() {
|
|
2457
|
+
const now = new Date().toISOString();
|
|
2458
|
+
|
|
2459
|
+
return {
|
|
2460
|
+
version: '1.0.0',
|
|
2461
|
+
created_at: now,
|
|
2462
|
+
updated_at: now,
|
|
2463
|
+
current_flow: null,
|
|
2464
|
+
context: {},
|
|
2465
|
+
history: [],
|
|
2466
|
+
pending_tasks: [],
|
|
2467
|
+
blocked_on: null,
|
|
2468
|
+
errors: { count: 0 },
|
|
2469
|
+
suggested_next: null
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
/**
|
|
2474
|
+
* Genera display de error de validacion
|
|
2475
|
+
*/
|
|
2476
|
+
generateValidationErrorDisplay(validationResult) {
|
|
2477
|
+
if (validationResult.valid) {
|
|
2478
|
+
return '[OK] Estado valido';
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
const error = validationResult.error;
|
|
2482
|
+
const details = validationResult.errors || [];
|
|
2483
|
+
|
|
2484
|
+
const lines = [
|
|
2485
|
+
'+======================================================+',
|
|
2486
|
+
`| ERROR: ${error.code}`.padEnd(55) + '|',
|
|
2487
|
+
`| Severity: ${error.severity}`.padEnd(55) + '|',
|
|
2488
|
+
'+======================================================+',
|
|
2489
|
+
'|'.padEnd(55) + '|',
|
|
2490
|
+
`| ${error.message}`.padEnd(55) + '|',
|
|
2491
|
+
'|'.padEnd(55) + '|'
|
|
2492
|
+
];
|
|
2493
|
+
|
|
2494
|
+
if (details.length > 0) {
|
|
2495
|
+
lines.push('| Detalles:'.padEnd(55) + '|');
|
|
2496
|
+
for (const detail of details.slice(0, 5)) { // Mostrar max 5
|
|
2497
|
+
lines.push(`| - ${detail.field}: ${detail.type}`.padEnd(55) + '|');
|
|
2498
|
+
}
|
|
2499
|
+
if (details.length > 5) {
|
|
2500
|
+
lines.push(`| ... y ${details.length - 5} errores mas`.padEnd(55) + '|');
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
lines.push('|'.padEnd(55) + '|');
|
|
2505
|
+
lines.push('+------------------------------------------------------+');
|
|
2506
|
+
lines.push('| Opciones de recuperacion:'.padEnd(55) + '|');
|
|
2507
|
+
lines.push('| [b] Restaurar desde backup'.padEnd(55) + '|');
|
|
2508
|
+
lines.push('| [r] Reinicializar estado (perdera historial)'.padEnd(55) + '|');
|
|
2509
|
+
lines.push('| [m] Editar manualmente'.padEnd(55) + '|');
|
|
2510
|
+
lines.push('+======================================================+');
|
|
2511
|
+
|
|
2512
|
+
return lines.join('\n');
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
/**
|
|
2516
|
+
* Carga y valida el estado de sesion desde el archivo
|
|
2517
|
+
* @returns {Object} Resultado con session y validacion
|
|
2518
|
+
*/
|
|
2519
|
+
async load() {
|
|
2520
|
+
try {
|
|
2521
|
+
const content = await Read(this.statePath);
|
|
2522
|
+
const session = JSON.parse(content);
|
|
2523
|
+
const validation = await this.validate(session);
|
|
2524
|
+
return { success: true, session, validation };
|
|
2525
|
+
} catch (error) {
|
|
2526
|
+
return {
|
|
2527
|
+
success: false,
|
|
2528
|
+
reason: error.code === 'ENOENT' ? 'SESSION_NOT_FOUND' : 'SESSION_CORRUPTED',
|
|
2529
|
+
error: error.message
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
/**
|
|
2535
|
+
* Valida y guarda el estado de sesion
|
|
2536
|
+
* @param {Object} session - Estado de sesion a guardar
|
|
2537
|
+
* @returns {Object} Resultado de la operacion
|
|
2538
|
+
*/
|
|
2539
|
+
async save(session) {
|
|
2540
|
+
const validation = await this.validate(session);
|
|
2541
|
+
if (!validation.valid) {
|
|
2542
|
+
return { success: false, reason: "SESSION_INVALID", errors: validation.errors };
|
|
2543
|
+
}
|
|
2544
|
+
session.updatedAt = new Date().toISOString();
|
|
2545
|
+
await Write(this.statePath, JSON.stringify(session, null, 2));
|
|
2546
|
+
return { success: true, savedAt: session.updatedAt };
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// Ejemplo de uso
|
|
2551
|
+
const sessionValidator = new ContractSessionValidator();
|
|
2552
|
+
|
|
2553
|
+
// Estado valido
|
|
2554
|
+
const validState = {
|
|
2555
|
+
version: '1.0.0',
|
|
2556
|
+
created_at: '2024-01-20T10:00:00Z',
|
|
2557
|
+
updated_at: '2024-01-20T15:30:00Z',
|
|
2558
|
+
current_flow: {
|
|
2559
|
+
command: 'execute',
|
|
2560
|
+
phase: 'executing_wave_1',
|
|
2561
|
+
started_at: '2024-01-20T15:00:00Z'
|
|
2562
|
+
},
|
|
2563
|
+
context: {
|
|
2564
|
+
project_type: 'brownfield',
|
|
2565
|
+
tech_stack: ['nextjs', 'prisma']
|
|
2566
|
+
},
|
|
2567
|
+
history: [
|
|
2568
|
+
{ command: 'plan', completed_at: '2024-01-20T14:50:00Z', result: 'created' }
|
|
2569
|
+
]
|
|
2570
|
+
};
|
|
2571
|
+
|
|
2572
|
+
const validResult = sessionValidator.validate(validState);
|
|
2573
|
+
console.log('Estado valido:', validResult.valid);
|
|
2574
|
+
|
|
2575
|
+
// Estado corrupto
|
|
2576
|
+
const corruptState = {
|
|
2577
|
+
version: '1.0.0',
|
|
2578
|
+
// Falta updated_at
|
|
2579
|
+
current_flow: {
|
|
2580
|
+
command: 'execute',
|
|
2581
|
+
phase: 'invalid_phase' // Fase invalida
|
|
2582
|
+
// Falta started_at
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
const corruptResult = sessionValidator.validate(corruptState);
|
|
2587
|
+
if (!corruptResult.valid) {
|
|
2588
|
+
console.log(sessionValidator.generateValidationErrorDisplay(corruptResult));
|
|
2589
|
+
|
|
2590
|
+
// Intentar reparar
|
|
2591
|
+
const repairResult = sessionValidator.attemptRepair(corruptState);
|
|
2592
|
+
if (repairResult.success) {
|
|
2593
|
+
console.log('Estado reparado:', repairResult.state);
|
|
2594
|
+
console.log('Reparaciones:', repairResult.repairs);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
```
|
|
2598
|
+
|
|
2599
|
+
---
|
|
2600
|
+
|
|
2601
|
+
## Integracion
|
|
2602
|
+
|
|
2603
|
+
### Flujo de Ejecucion Completo
|
|
2604
|
+
|
|
2605
|
+
```
|
|
2606
|
+
+============================================================================+
|
|
2607
|
+
| FLUJO DE CONTRATOS EN EJECUCION |
|
|
2608
|
+
+============================================================================+
|
|
2609
|
+
| |
|
|
2610
|
+
| 1. Usuario ejecuta /elsabro:execute |
|
|
2611
|
+
| |
|
|
2612
|
+
| 2. [ContractSessionValidator] |
|
|
2613
|
+
| Validar .elsabro/state.json |
|
|
2614
|
+
| -> OK: Continuar |
|
|
2615
|
+
| -> ERROR: Reparar o abortar |
|
|
2616
|
+
| |
|
|
2617
|
+
| 3. [ContractRegistryValidator] |
|
|
2618
|
+
| Validar agentes necesarios existen |
|
|
2619
|
+
| -> OK: Continuar |
|
|
2620
|
+
| -> ERROR: AGENT_NOT_FOUND - abortar |
|
|
2621
|
+
| |
|
|
2622
|
+
| 4. [ContractTaskLifecycle] |
|
|
2623
|
+
| Crear Tasks para cada agente |
|
|
2624
|
+
| INIT -> IN_PROGRESS |
|
|
2625
|
+
| |
|
|
2626
|
+
| 5. [ContractTimeoutHandler] |
|
|
2627
|
+
| Iniciar tracking de timeout |
|
|
2628
|
+
| Health checks cada 5s |
|
|
2629
|
+
| |
|
|
2630
|
+
| 6. EJECUTAR AGENTES EN PARALELO |
|
|
2631
|
+
| Task(haiku) | Task(haiku) | Task(haiku) |
|
|
2632
|
+
| |
|
|
2633
|
+
| 7. [ContractRetryPolicy] (por cada agente) |
|
|
2634
|
+
| Si falla: retry con backoff |
|
|
2635
|
+
| 1s -> 2s -> 4s |
|
|
2636
|
+
| |
|
|
2637
|
+
| 8. [ContractSeverityClassifier] |
|
|
2638
|
+
| Clasificar errores: CRITICAL/HIGH/MEDIUM/LOW |
|
|
2639
|
+
| |
|
|
2640
|
+
| 9. [ContractErrorAggregator] |
|
|
2641
|
+
| Colectar resultados |
|
|
2642
|
+
| Aplicar politica (quorum) |
|
|
2643
|
+
| -> QUORUM MET: Continuar |
|
|
2644
|
+
| -> QUORUM NOT MET: Abortar |
|
|
2645
|
+
| |
|
|
2646
|
+
| 10. [ContractTaskLifecycle] |
|
|
2647
|
+
| Marcar tasks: COMPLETED o ERROR |
|
|
2648
|
+
| |
|
|
2649
|
+
| 11. [ContractSessionValidator] |
|
|
2650
|
+
| Actualizar .elsabro/state.json |
|
|
2651
|
+
| |
|
|
2652
|
+
+============================================================================+
|
|
2653
|
+
```
|
|
2654
|
+
|
|
2655
|
+
### Dependencias entre Contratos
|
|
2656
|
+
|
|
2657
|
+
```
|
|
2658
|
+
ContractSessionValidator
|
|
2659
|
+
|
|
|
2660
|
+
v (estado valido)
|
|
2661
|
+
ContractRegistryValidator
|
|
2662
|
+
|
|
|
2663
|
+
v (agentes validos)
|
|
2664
|
+
ContractTaskLifecycle
|
|
2665
|
+
|
|
|
2666
|
+
v (tasks creadas)
|
|
2667
|
+
ContractTimeoutHandler
|
|
2668
|
+
|
|
|
2669
|
+
v (tracking activo)
|
|
2670
|
+
|
|
|
2671
|
+
+--> ContractRetryPolicy (por agente)
|
|
2672
|
+
| |
|
|
2673
|
+
| v (resultado o retry exhausted)
|
|
2674
|
+
|
|
|
2675
|
+
+--> ContractSeverityClassifier
|
|
2676
|
+
|
|
|
2677
|
+
v (errores clasificados)
|
|
2678
|
+
ContractErrorAggregator
|
|
2679
|
+
|
|
|
2680
|
+
v (decision de continuar/abortar)
|
|
2681
|
+
ContractTaskLifecycle
|
|
2682
|
+
|
|
|
2683
|
+
v (tasks actualizadas)
|
|
2684
|
+
ContractSessionValidator
|
|
2685
|
+
|
|
|
2686
|
+
v (estado persistido)
|
|
2687
|
+
```
|
|
2688
|
+
|
|
2689
|
+
### Ejemplo Completo de Integracion
|
|
2690
|
+
|
|
2691
|
+
```javascript
|
|
2692
|
+
/**
|
|
2693
|
+
* Ejemplo completo de integracion de todos los contratos
|
|
2694
|
+
*/
|
|
2695
|
+
class ElsabroErrorContracts {
|
|
2696
|
+
constructor() {
|
|
2697
|
+
this.sessionValidator = new ContractSessionValidator();
|
|
2698
|
+
this.registryValidator = new ContractRegistryValidator();
|
|
2699
|
+
this.taskLifecycle = new ContractTaskLifecycle();
|
|
2700
|
+
this.timeoutHandler = new ContractTimeoutHandler();
|
|
2701
|
+
this.retryPolicy = new ContractRetryPolicy();
|
|
2702
|
+
this.severityClassifier = new ContractSeverityClassifier();
|
|
2703
|
+
this.errorAggregator = new ContractErrorAggregator();
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
/**
|
|
2707
|
+
* Ejecuta una operacion paralela con todos los contratos
|
|
2708
|
+
*/
|
|
2709
|
+
async executeParallelOperation(agents, operations, policy = 'quorum') {
|
|
2710
|
+
// 1. Validar sesion
|
|
2711
|
+
const sessionResult = this.sessionValidator.validate(
|
|
2712
|
+
this.readState()
|
|
2713
|
+
);
|
|
2714
|
+
|
|
2715
|
+
if (!sessionResult.valid) {
|
|
2716
|
+
const repair = this.sessionValidator.attemptRepair(sessionResult);
|
|
2717
|
+
if (!repair.success) {
|
|
2718
|
+
return this.handleCriticalError(sessionResult.error);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// 2. Validar agentes
|
|
2723
|
+
const registryResult = this.registryValidator.validateAgents(agents);
|
|
2724
|
+
if (!registryResult.valid) {
|
|
2725
|
+
return this.handleCriticalError(registryResult.aggregatedError);
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
// 3. Crear tasks
|
|
2729
|
+
const tasks = [];
|
|
2730
|
+
for (let i = 0; i < agents.length; i++) {
|
|
2731
|
+
const taskResult = this.taskLifecycle.registerTask(
|
|
2732
|
+
`task-${Date.now()}-${i}`,
|
|
2733
|
+
{ agent: agents[i] }
|
|
2734
|
+
);
|
|
2735
|
+
tasks.push(taskResult.task);
|
|
2736
|
+
this.taskLifecycle.transition(taskResult.task.id, 'in_progress');
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// 4. Configurar agregador
|
|
2740
|
+
this.errorAggregator.reset();
|
|
2741
|
+
this.errorAggregator.setPolicy(policy);
|
|
2742
|
+
|
|
2743
|
+
// 5. Ejecutar operaciones en paralelo
|
|
2744
|
+
const promises = tasks.map(async (task, index) => {
|
|
2745
|
+
// Iniciar timeout tracking
|
|
2746
|
+
this.timeoutHandler.startTracking(task.id);
|
|
2747
|
+
|
|
2748
|
+
try {
|
|
2749
|
+
// Ejecutar con retry
|
|
2750
|
+
const result = await this.retryPolicy.executeWithRetry(
|
|
2751
|
+
() => operations[index]()
|
|
2752
|
+
);
|
|
2753
|
+
|
|
2754
|
+
// Marcar completado
|
|
2755
|
+
this.timeoutHandler.markComplete(task.id, result);
|
|
2756
|
+
this.taskLifecycle.transition(task.id, 'completed');
|
|
2757
|
+
|
|
2758
|
+
// Agregar resultado
|
|
2759
|
+
this.errorAggregator.addResult({
|
|
2760
|
+
taskId: task.id,
|
|
2761
|
+
agent: agents[index],
|
|
2762
|
+
status: result.success ? 'success' : 'error',
|
|
2763
|
+
duration: result.totalDuration,
|
|
2764
|
+
error: result.error
|
|
2765
|
+
});
|
|
2766
|
+
|
|
2767
|
+
return result;
|
|
2768
|
+
|
|
2769
|
+
} catch (error) {
|
|
2770
|
+
// Clasificar error
|
|
2771
|
+
const classified = this.severityClassifier.classify(error);
|
|
2772
|
+
|
|
2773
|
+
// Marcar error
|
|
2774
|
+
this.taskLifecycle.transition(task.id, 'error');
|
|
2775
|
+
|
|
2776
|
+
// Agregar al agregador
|
|
2777
|
+
this.errorAggregator.addResult({
|
|
2778
|
+
taskId: task.id,
|
|
2779
|
+
agent: agents[index],
|
|
2780
|
+
status: 'error',
|
|
2781
|
+
error: classified
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
return { success: false, error: classified };
|
|
2785
|
+
}
|
|
2786
|
+
});
|
|
2787
|
+
|
|
2788
|
+
// 6. Esperar todos los resultados
|
|
2789
|
+
const results = await Promise.all(promises);
|
|
2790
|
+
|
|
2791
|
+
// 7. Evaluar politica
|
|
2792
|
+
const evaluation = this.errorAggregator.evaluatePolicy();
|
|
2793
|
+
|
|
2794
|
+
// 8. Generar reporte
|
|
2795
|
+
const report = this.errorAggregator.generateReport();
|
|
2796
|
+
console.log(report);
|
|
2797
|
+
|
|
2798
|
+
// 9. Persistir estado
|
|
2799
|
+
this.persistState(evaluation);
|
|
2800
|
+
|
|
2801
|
+
return {
|
|
2802
|
+
success: evaluation.shouldContinue,
|
|
2803
|
+
results: results,
|
|
2804
|
+
evaluation: evaluation,
|
|
2805
|
+
report: report
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
handleCriticalError(error) {
|
|
2810
|
+
const display = this.severityClassifier.generateDisplay(
|
|
2811
|
+
this.severityClassifier.classify(error)
|
|
2812
|
+
);
|
|
2813
|
+
console.log(display);
|
|
2814
|
+
|
|
2815
|
+
return {
|
|
2816
|
+
success: false,
|
|
2817
|
+
error: error,
|
|
2818
|
+
aborted: true
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
readState() {
|
|
2823
|
+
// En implementacion real, leer de .elsabro/state.json
|
|
2824
|
+
return this.sessionValidator.createInitialState();
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
persistState(evaluation) {
|
|
2828
|
+
// En implementacion real, escribir a .elsabro/state.json
|
|
2829
|
+
console.log('Estado persistido:', evaluation);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// Uso
|
|
2834
|
+
const contracts = new ElsabroErrorContracts();
|
|
2835
|
+
|
|
2836
|
+
// Simular operacion paralela
|
|
2837
|
+
contracts.executeParallelOperation(
|
|
2838
|
+
['elsabro-executor', 'feature-dev:code-explorer', 'elsabro-verifier'],
|
|
2839
|
+
[
|
|
2840
|
+
async () => ({ data: 'result1' }),
|
|
2841
|
+
async () => ({ data: 'result2' }),
|
|
2842
|
+
async () => { throw { code: 'TIMEOUT' }; }
|
|
2843
|
+
],
|
|
2844
|
+
'quorum'
|
|
2845
|
+
).then(result => {
|
|
2846
|
+
console.log('Resultado final:', result.success ? 'EXITO' : 'FALLO');
|
|
2847
|
+
});
|
|
2848
|
+
```
|
|
2849
|
+
|
|
2850
|
+
---
|
|
2851
|
+
|
|
2852
|
+
## Escenarios de Fallo Exhaustivos
|
|
2853
|
+
|
|
2854
|
+
### Escenario 1: Estado Corrupto al Inicio
|
|
2855
|
+
|
|
2856
|
+
```
|
|
2857
|
+
SITUACION:
|
|
2858
|
+
Usuario ejecuta /elsabro:execute
|
|
2859
|
+
.elsabro/state.json tiene JSON invalido
|
|
2860
|
+
|
|
2861
|
+
CONTRATOS ACTIVADOS:
|
|
2862
|
+
1. ContractSessionValidator -> JSON_PARSE_ERROR
|
|
2863
|
+
|
|
2864
|
+
FLUJO:
|
|
2865
|
+
1. Detectar corrupcion
|
|
2866
|
+
2. Intentar leer backup
|
|
2867
|
+
3. Si backup existe -> restaurar
|
|
2868
|
+
4. Si no hay backup -> reinicializar
|
|
2869
|
+
5. Notificar al usuario
|
|
2870
|
+
6. Continuar con estado limpio
|
|
2871
|
+
|
|
2872
|
+
DISPLAY:
|
|
2873
|
+
+======================================================+
|
|
2874
|
+
| ERROR: SESSION_CORRUPTED |
|
|
2875
|
+
| Severity: CRITICAL |
|
|
2876
|
+
+======================================================+
|
|
2877
|
+
| El archivo .elsabro/state.json esta corrupto |
|
|
2878
|
+
| Restaurando desde backup... |
|
|
2879
|
+
+======================================================+
|
|
2880
|
+
```
|
|
2881
|
+
|
|
2882
|
+
### Escenario 2: Agente No Encontrado
|
|
2883
|
+
|
|
2884
|
+
```
|
|
2885
|
+
SITUACION:
|
|
2886
|
+
Plan referencia agente "elsabro-nonexistent"
|
|
2887
|
+
|
|
2888
|
+
CONTRATOS ACTIVADOS:
|
|
2889
|
+
1. ContractRegistryValidator -> AGENT_NOT_FOUND
|
|
2890
|
+
|
|
2891
|
+
FLUJO:
|
|
2892
|
+
1. Validar agentes antes de lanzar
|
|
2893
|
+
2. Detectar agente invalido
|
|
2894
|
+
3. Buscar sugerencias similares
|
|
2895
|
+
4. Abortar operacion
|
|
2896
|
+
5. Mostrar alternativas al usuario
|
|
2897
|
+
|
|
2898
|
+
DISPLAY:
|
|
2899
|
+
+======================================================+
|
|
2900
|
+
| ERROR: AGENT_NOT_FOUND |
|
|
2901
|
+
| Severity: CRITICAL |
|
|
2902
|
+
+======================================================+
|
|
2903
|
+
| El agente "elsabro-nonexistent" no existe |
|
|
2904
|
+
| |
|
|
2905
|
+
| Agentes similares: |
|
|
2906
|
+
| - elsabro-executor |
|
|
2907
|
+
| - elsabro-verifier |
|
|
2908
|
+
+======================================================+
|
|
2909
|
+
```
|
|
2910
|
+
|
|
2911
|
+
### Escenario 3: Timeout en Agente
|
|
2912
|
+
|
|
2913
|
+
```
|
|
2914
|
+
SITUACION:
|
|
2915
|
+
Agente tarda mas de 30 minutos
|
|
2916
|
+
|
|
2917
|
+
CONTRATOS ACTIVADOS:
|
|
2918
|
+
1. ContractTimeoutHandler -> TASK_TIMEOUT
|
|
2919
|
+
2. ContractSeverityClassifier -> HIGH
|
|
2920
|
+
3. ContractErrorAggregator -> Evaluar quorum
|
|
2921
|
+
|
|
2922
|
+
FLUJO:
|
|
2923
|
+
1. Health check detecta timeout inminente
|
|
2924
|
+
2. Warning al usuario
|
|
2925
|
+
3. Timeout alcanzado
|
|
2926
|
+
4. Terminacion graceful
|
|
2927
|
+
5. Clasificar como HIGH
|
|
2928
|
+
6. Agregar al agregador
|
|
2929
|
+
7. Evaluar si quorum se mantiene
|
|
2930
|
+
|
|
2931
|
+
DISPLAY:
|
|
2932
|
+
+======================================================+
|
|
2933
|
+
| WARNING: TASK_TIMEOUT |
|
|
2934
|
+
| Severity: HIGH |
|
|
2935
|
+
+======================================================+
|
|
2936
|
+
| La tarea "explore-codebase" excedio el timeout |
|
|
2937
|
+
| |
|
|
2938
|
+
| Opciones: |
|
|
2939
|
+
| [e] Extender timeout (+15 min) |
|
|
2940
|
+
| [t] Terminar y usar output parcial |
|
|
2941
|
+
+======================================================+
|
|
2942
|
+
```
|
|
2943
|
+
|
|
2944
|
+
### Escenario 4: Retry Exhausted
|
|
2945
|
+
|
|
2946
|
+
```
|
|
2947
|
+
SITUACION:
|
|
2948
|
+
Operacion falla 3 veces consecutivas
|
|
2949
|
+
|
|
2950
|
+
CONTRATOS ACTIVADOS:
|
|
2951
|
+
1. ContractRetryPolicy -> RETRY_EXHAUSTED
|
|
2952
|
+
2. ContractSeverityClassifier -> HIGH
|
|
2953
|
+
3. ContractErrorAggregator -> Agregar error
|
|
2954
|
+
|
|
2955
|
+
FLUJO:
|
|
2956
|
+
1. Intento 1: Fallo (1s delay)
|
|
2957
|
+
2. Intento 2: Fallo (2s delay)
|
|
2958
|
+
3. Intento 3: Fallo
|
|
2959
|
+
4. Crear error RETRY_EXHAUSTED
|
|
2960
|
+
5. Clasificar severidad
|
|
2961
|
+
6. Escalar a usuario
|
|
2962
|
+
|
|
2963
|
+
DISPLAY:
|
|
2964
|
+
+------------------------------------------------------+
|
|
2965
|
+
| RETRY: npm test |
|
|
2966
|
+
+------------------------------------------------------+
|
|
2967
|
+
| Attempt 1/3: FAILED (timeout) |
|
|
2968
|
+
| Waiting 1s... |
|
|
2969
|
+
| Attempt 2/3: FAILED (exit code 1) |
|
|
2970
|
+
| Waiting 2s... |
|
|
2971
|
+
| Attempt 3/3: FAILED (exit code 1) |
|
|
2972
|
+
| |
|
|
2973
|
+
| X All attempts exhausted |
|
|
2974
|
+
+------------------------------------------------------+
|
|
2975
|
+
```
|
|
2976
|
+
|
|
2977
|
+
### Escenario 5: Quorum No Alcanzado
|
|
2978
|
+
|
|
2979
|
+
```
|
|
2980
|
+
SITUACION:
|
|
2981
|
+
3 de 4 agentes fallan
|
|
2982
|
+
|
|
2983
|
+
CONTRATOS ACTIVADOS:
|
|
2984
|
+
1. ContractErrorAggregator -> QUORUM_NOT_MET
|
|
2985
|
+
2. ContractSeverityClassifier -> HIGH
|
|
2986
|
+
|
|
2987
|
+
FLUJO:
|
|
2988
|
+
1. Esperar todos los agentes
|
|
2989
|
+
2. Contar exitos: 1/4 (25%)
|
|
2990
|
+
3. Umbral quorum: 50%
|
|
2991
|
+
4. Quorum NO alcanzado
|
|
2992
|
+
5. Abortar operacion
|
|
2993
|
+
6. Reportar fallos
|
|
2994
|
+
|
|
2995
|
+
DISPLAY:
|
|
2996
|
+
+======================================================+
|
|
2997
|
+
| PARALLEL EXECUTION COMPLETE |
|
|
2998
|
+
+======================================================+
|
|
2999
|
+
| Policy: quorum |
|
|
3000
|
+
| Total: 4 agents |
|
|
3001
|
+
| |
|
|
3002
|
+
| OK Agent 1 (code-reviewer): SUCCESS |
|
|
3003
|
+
| X Agent 2 (typescript-pro): FAILED (timeout) |
|
|
3004
|
+
| X Agent 3 (silent-failure): FAILED (error) |
|
|
3005
|
+
| X Agent 4 (test-analyzer): FAILED (timeout) |
|
|
3006
|
+
| |
|
|
3007
|
+
| Result: 1/4 (25%) - QUORUM NOT MET |
|
|
3008
|
+
| Status: STOPPING |
|
|
3009
|
+
+======================================================+
|
|
3010
|
+
```
|
|
3011
|
+
|
|
3012
|
+
### Escenario 6: Transicion de Estado Invalida
|
|
3013
|
+
|
|
3014
|
+
```
|
|
3015
|
+
SITUACION:
|
|
3016
|
+
Intento de marcar task como COMPLETED sin pasar por IN_PROGRESS
|
|
3017
|
+
|
|
3018
|
+
CONTRATOS ACTIVADOS:
|
|
3019
|
+
1. ContractTaskLifecycle -> INVALID_STATE_TRANSITION
|
|
3020
|
+
|
|
3021
|
+
FLUJO:
|
|
3022
|
+
1. Detectar transicion invalida
|
|
3023
|
+
2. Rechazar operacion
|
|
3024
|
+
3. Sugerir transicion correcta
|
|
3025
|
+
4. Mantener estado anterior
|
|
3026
|
+
|
|
3027
|
+
DISPLAY:
|
|
3028
|
+
+======================================================+
|
|
3029
|
+
| ERROR: INVALID_STATE_TRANSITION |
|
|
3030
|
+
| Severity: HIGH |
|
|
3031
|
+
+======================================================+
|
|
3032
|
+
| Transicion invalida: INIT -> COMPLETED |
|
|
3033
|
+
| |
|
|
3034
|
+
| Flujo correcto: |
|
|
3035
|
+
| INIT -> IN_PROGRESS -> COMPLETED |
|
|
3036
|
+
+======================================================+
|
|
3037
|
+
```
|
|
3038
|
+
|
|
3039
|
+
### Escenario 7: Multiples Errores Criticos Simultaneos
|
|
3040
|
+
|
|
3041
|
+
```
|
|
3042
|
+
SITUACION:
|
|
3043
|
+
Estado corrupto + Agente critico falla + Timeout
|
|
3044
|
+
|
|
3045
|
+
CONTRATOS ACTIVADOS:
|
|
3046
|
+
Todos los contratos en cascada
|
|
3047
|
+
|
|
3048
|
+
FLUJO:
|
|
3049
|
+
1. SessionValidator detecta corrupcion -> reparar
|
|
3050
|
+
2. Continuar con estado reparado
|
|
3051
|
+
3. Agente critico falla
|
|
3052
|
+
4. CriticalPath policy activada
|
|
3053
|
+
5. Abortar toda la operacion
|
|
3054
|
+
6. Persistir estado de error
|
|
3055
|
+
7. Reportar al usuario
|
|
3056
|
+
|
|
3057
|
+
DISPLAY:
|
|
3058
|
+
+======================================================+
|
|
3059
|
+
| MULTIPLE CRITICAL ERRORS |
|
|
3060
|
+
| Severity: CRITICAL |
|
|
3061
|
+
+======================================================+
|
|
3062
|
+
| |
|
|
3063
|
+
| Errores encontrados: |
|
|
3064
|
+
| 1. SESSION_CORRUPTED (reparado) |
|
|
3065
|
+
| 2. CRITICAL_AGENT_FAILED: elsabro-executor |
|
|
3066
|
+
| 3. TASK_TIMEOUT: explore-codebase |
|
|
3067
|
+
| |
|
|
3068
|
+
| La operacion fue abortada por errores criticos |
|
|
3069
|
+
| |
|
|
3070
|
+
+------------------------------------------------------+
|
|
3071
|
+
| Estado guardado para recuperacion: |
|
|
3072
|
+
| -> /elsabro:resume-work para continuar |
|
|
3073
|
+
+======================================================+
|
|
3074
|
+
```
|
|
3075
|
+
|
|
3076
|
+
---
|
|
3077
|
+
|
|
3078
|
+
## Checklist de Implementacion
|
|
3079
|
+
|
|
3080
|
+
Para implementar los contratos en un comando ELSABRO:
|
|
3081
|
+
|
|
3082
|
+
- [ ] Importar/instanciar ContractSessionValidator
|
|
3083
|
+
- [ ] Validar estado al inicio del comando
|
|
3084
|
+
- [ ] Importar/instanciar ContractRegistryValidator
|
|
3085
|
+
- [ ] Validar agentes antes de lanzar
|
|
3086
|
+
- [ ] Importar/instanciar ContractTaskLifecycle
|
|
3087
|
+
- [ ] Crear Tasks para cada agente
|
|
3088
|
+
- [ ] Respetar transiciones INIT -> ACTIVE -> COMPLETE
|
|
3089
|
+
- [ ] Importar/instanciar ContractTimeoutHandler
|
|
3090
|
+
- [ ] Configurar timeout apropiado
|
|
3091
|
+
- [ ] Iniciar tracking para cada tarea
|
|
3092
|
+
- [ ] Importar/instanciar ContractRetryPolicy
|
|
3093
|
+
- [ ] Configurar reintentos para operaciones fallibles
|
|
3094
|
+
- [ ] Importar/instanciar ContractSeverityClassifier
|
|
3095
|
+
- [ ] Clasificar todos los errores
|
|
3096
|
+
- [ ] Generar displays apropiados
|
|
3097
|
+
- [ ] Importar/instanciar ContractErrorAggregator
|
|
3098
|
+
- [ ] Configurar politica (quorum, fail_fast, etc)
|
|
3099
|
+
- [ ] Agregar todos los resultados
|
|
3100
|
+
- [ ] Evaluar politica para decidir continuar/abortar
|
|
3101
|
+
- [ ] Persistir estado al finalizar
|
|
3102
|
+
- [ ] Generar reporte final
|