elsabro 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -887
- package/bin/install.js +20 -15
- package/commands/elsabro/execute.md +62 -8
- package/commands/elsabro/party.md +87 -2
- package/commands/elsabro/start.md +9 -3
- package/flow-engine/src/party.js +29 -3
- package/flow-engine/tests/cli.test.js +2 -2
- package/flow-engine/tests/graph.test.js +1 -1
- package/flow-engine/tests/integration.test.js +7 -7
- package/flow-engine/tests/party.test.js +57 -0
- package/flow-engine/tests/runner.test.js +457 -0
- package/flows/development-flow.json +42 -5
- package/flows/quick-flow.json +0 -1
- package/package.json +1 -1
- package/references/command-flow.md +25 -20
package/bin/install.js
CHANGED
|
@@ -234,10 +234,10 @@ if (hasUpdate) {
|
|
|
234
234
|
console.log(` ${cyan}Verificando actualizaciones...${reset}\n`);
|
|
235
235
|
|
|
236
236
|
// Check if there's a newer version first to prevent infinite loops
|
|
237
|
-
checkLatestVersion().then((latestVersion) => {
|
|
237
|
+
checkLatestVersion().then(async (latestVersion) => {
|
|
238
238
|
if (!latestVersion) {
|
|
239
239
|
console.log(` ${yellow}⚠${reset} No se pudo verificar la última versión`);
|
|
240
|
-
console.log(` ${dim}
|
|
240
|
+
console.log(` ${dim}Reinstalando archivos locales...${reset}\n`);
|
|
241
241
|
} else if (latestVersion === pkg.version) {
|
|
242
242
|
console.log(` ${green}✓${reset} Versión actual: ${pkg.version} (ya es la última)`);
|
|
243
243
|
console.log(` ${dim}Reinstalando archivos locales...${reset}\n`);
|
|
@@ -247,27 +247,31 @@ if (hasUpdate) {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (packageManager === 'pnpm') {
|
|
256
|
-
execFileSync(getExecutable('pnpm'), ['dlx', packageSpec, '--global'], { stdio: 'inherit' });
|
|
257
|
-
} else if (packageManager === 'bun') {
|
|
258
|
-
execFileSync(getExecutable('bunx'), [packageSpec, '--global'], { stdio: 'inherit' });
|
|
250
|
+
if (!latestVersion || latestVersion === pkg.version) {
|
|
251
|
+
// Already up to date — reinstall from local package directly
|
|
252
|
+
// (avoids npm CDN cache lag returning a stale version)
|
|
253
|
+
await install();
|
|
254
|
+
console.log(`\n ${green}✓${reset} ELSABRO reinstalado v${pkg.version}\n`);
|
|
259
255
|
} else {
|
|
260
|
-
//
|
|
261
|
-
|
|
256
|
+
// Newer version available — download from npm
|
|
257
|
+
const packageSpec = `elsabro@${latestVersion}`;
|
|
258
|
+
|
|
259
|
+
if (packageManager === 'pnpm') {
|
|
260
|
+
execFileSync(getExecutable('pnpm'), ['dlx', packageSpec, '--global'], { stdio: 'inherit' });
|
|
261
|
+
} else if (packageManager === 'bun') {
|
|
262
|
+
execFileSync(getExecutable('bunx'), [packageSpec, '--global'], { stdio: 'inherit' });
|
|
263
|
+
} else {
|
|
264
|
+
execFileSync(getExecutable('npm'), ['exec', '--yes', '--', packageSpec, '--global'], { stdio: 'inherit' });
|
|
265
|
+
}
|
|
266
|
+
console.log(`\n ${green}✓${reset} ELSABRO actualizado a v${latestVersion}\n`);
|
|
262
267
|
}
|
|
263
|
-
console.log(`\n ${green}✓${reset} ELSABRO actualizado a v${versionToInstall}\n`);
|
|
264
268
|
} catch (error) {
|
|
265
269
|
console.error(`\n ${red}✗${reset} Error al actualizar: ${error.message}\n`);
|
|
266
270
|
process.exit(1);
|
|
267
271
|
}
|
|
268
272
|
process.exit(0);
|
|
269
273
|
});
|
|
270
|
-
}
|
|
274
|
+
}
|
|
271
275
|
|
|
272
276
|
// Interactive prompt for location if not specified
|
|
273
277
|
async function promptLocation() {
|
|
@@ -502,6 +506,7 @@ async function uninstall() {
|
|
|
502
506
|
}
|
|
503
507
|
|
|
504
508
|
// Main (note: update handling is above with its own process.exit)
|
|
509
|
+
if (!hasUpdate) {
|
|
505
510
|
if (hasUninstall) {
|
|
506
511
|
uninstall().catch((error) => {
|
|
507
512
|
console.error(` ${red}✗${reset} Error: ${error.message}`);
|
|
@@ -666,19 +666,34 @@ TaskUpdate({ taskId: "plan-A-id", status: "completed" })
|
|
|
666
666
|
|
|
667
667
|
### Validación arquitectónica en paralelo (OPUS x2)
|
|
668
668
|
```javascript
|
|
669
|
-
//
|
|
669
|
+
// OBLIGATORIO: Crear Agent Team para validación arquitectónica (Rule 8)
|
|
670
|
+
TeamCreate({
|
|
671
|
+
team_name: "elsabro-arch-validate",
|
|
672
|
+
description: "Architectural validation: implementation + architecture review"
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
// Spawn 2 teammates OPUS en paralelo
|
|
670
676
|
Task({
|
|
671
677
|
subagent_type: "elsabro-executor",
|
|
672
678
|
model: "opus",
|
|
679
|
+
team_name: "elsabro-arch-validate",
|
|
680
|
+
name: "impl-1",
|
|
673
681
|
description: "Implementar cambios",
|
|
674
682
|
prompt: "..."
|
|
675
683
|
}) |
|
|
676
684
|
Task({
|
|
677
685
|
subagent_type: "feature-dev:code-architect",
|
|
678
686
|
model: "opus", // ← OPUS para decisiones arquitectónicas
|
|
687
|
+
team_name: "elsabro-arch-validate",
|
|
688
|
+
name: "architect-1",
|
|
679
689
|
description: "Validar arquitectura",
|
|
680
690
|
prompt: "Verifica que la implementación sigue los patrones del codebase..."
|
|
681
691
|
})
|
|
692
|
+
|
|
693
|
+
// Shutdown y cleanup
|
|
694
|
+
SendMessage({ type: "shutdown_request", recipient: "impl-1", content: "Validation complete" })
|
|
695
|
+
SendMessage({ type: "shutdown_request", recipient: "architect-1", content: "Validation complete" })
|
|
696
|
+
TeamDelete()
|
|
682
697
|
```
|
|
683
698
|
|
|
684
699
|
### Si solo hay un plan → Secuencial (OPUS)
|
|
@@ -1062,15 +1077,33 @@ if (shouldUseWorktrees(wave, profile)) {
|
|
|
1062
1077
|
// Crear worktrees
|
|
1063
1078
|
Bash(`./scripts/setup-parallel-worktrees.sh create ${agents.join(' ')}`);
|
|
1064
1079
|
|
|
1065
|
-
//
|
|
1066
|
-
|
|
1080
|
+
// OBLIGATORIO: Crear Agent Team para worktree paralelo (Rule 8)
|
|
1081
|
+
TeamCreate({
|
|
1082
|
+
team_name: "elsabro-worktree",
|
|
1083
|
+
description: "Parallel worktree execution for wave " + wave.id
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
// Ejecutar agentes como teammates en sus worktrees
|
|
1087
|
+
for (let i = 0; i < agents.length; i++) {
|
|
1067
1088
|
Task({
|
|
1068
|
-
subagent_type:
|
|
1069
|
-
|
|
1089
|
+
subagent_type: agents[i],
|
|
1090
|
+
team_name: "elsabro-worktree",
|
|
1091
|
+
name: `wt-agent-${i + 1}`,
|
|
1092
|
+
prompt: `Trabaja en worktree: ../elsabro-worktrees/${agents[i]}-wt/`
|
|
1070
1093
|
});
|
|
1071
1094
|
}
|
|
1072
1095
|
|
|
1073
|
-
//
|
|
1096
|
+
// Shutdown teammates y cleanup team
|
|
1097
|
+
for (let i = 0; i < agents.length; i++) {
|
|
1098
|
+
SendMessage({
|
|
1099
|
+
type: "shutdown_request",
|
|
1100
|
+
recipient: `wt-agent-${i + 1}`,
|
|
1101
|
+
content: "Worktree work complete"
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
TeamDelete();
|
|
1105
|
+
|
|
1106
|
+
// Merge y cleanup worktrees al completar
|
|
1074
1107
|
Bash(`./scripts/setup-parallel-worktrees.sh complete ${agents.join(' ')}`);
|
|
1075
1108
|
}
|
|
1076
1109
|
```
|
|
@@ -1142,12 +1175,33 @@ errorAggregator.setPolicy("quorum");
|
|
|
1142
1175
|
for (const wave of waves) {
|
|
1143
1176
|
timeoutHandler.startTimeout(wave.id, 60 * 60 * 1000); // 60min por wave
|
|
1144
1177
|
|
|
1178
|
+
// OBLIGATORIO: Crear Agent Team para wave de implementacion (Rule 8)
|
|
1179
|
+
TeamCreate({
|
|
1180
|
+
team_name: "elsabro-wave-impl",
|
|
1181
|
+
description: "Implementation wave " + wave.id
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1145
1184
|
const results = await Promise.all(
|
|
1146
|
-
wave.plans.map(plan =>
|
|
1147
|
-
executeWithRetry(plan.id, () => Task(
|
|
1185
|
+
wave.plans.map((plan, i) =>
|
|
1186
|
+
executeWithRetry(plan.id, () => Task({
|
|
1187
|
+
subagent_type: "elsabro-executor",
|
|
1188
|
+
team_name: "elsabro-wave-impl",
|
|
1189
|
+
name: `wave-executor-${i + 1}`,
|
|
1190
|
+
prompt: plan
|
|
1191
|
+
}))
|
|
1148
1192
|
)
|
|
1149
1193
|
);
|
|
1150
1194
|
|
|
1195
|
+
// Shutdown teammates y cleanup team
|
|
1196
|
+
wave.plans.forEach((_, i) => {
|
|
1197
|
+
SendMessage({
|
|
1198
|
+
type: "shutdown_request",
|
|
1199
|
+
recipient: `wave-executor-${i + 1}`,
|
|
1200
|
+
content: "Wave complete"
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
TeamDelete();
|
|
1204
|
+
|
|
1151
1205
|
const summary = await errorAggregator.aggregate(wave.id, results);
|
|
1152
1206
|
|
|
1153
1207
|
if (summary.decision === "STOP") {
|
|
@@ -193,6 +193,63 @@ Al final, generar consenso y puntos de debate:
|
|
|
193
193
|
|
|
194
194
|
Genera `.planning/PARTY-{topic-slug}-SUMMARY.md` usando template.
|
|
195
195
|
|
|
196
|
+
<agent_teams_gate>
|
|
197
|
+
## Agent Teams Gate (OBLIGATORIO)
|
|
198
|
+
|
|
199
|
+
Party Mode uses 2-3 agents per round in parallel → Agent Teams is OBLIGATORIO.
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
Each round with 2+ agents?
|
|
203
|
+
│
|
|
204
|
+
├─ YES → TeamCreate("elsabro-party-rN")
|
|
205
|
+
│ Task(team_name: "elsabro-party-rN", name: "agent-K") per agent
|
|
206
|
+
│ ... round execution ...
|
|
207
|
+
│ SendMessage(shutdown_request) per agent
|
|
208
|
+
│ TeamDelete()
|
|
209
|
+
│
|
|
210
|
+
└─ Synthesis (1 agent only) → Single Task(orchestrator) — no team needed
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**VIOLACION CRITICA**: Lanzar 2+ agentes de party sin Agent Teams = ABORTAR
|
|
214
|
+
</agent_teams_gate>
|
|
215
|
+
|
|
216
|
+
<IMPERATIVO_AGENT_TEAMS>
|
|
217
|
+
## ⛔ STOP — LEER ANTES DE LANZAR AGENTES DE PARTY
|
|
218
|
+
|
|
219
|
+
**REGLA NO NEGOCIABLE (Rule 8):** Cada round de Party Mode con 2+ agentes DEBE usar Agent Teams.
|
|
220
|
+
|
|
221
|
+
1. **PRIMERO** → `TeamCreate({ team_name: "elsabro-party-rN", description: "Party round N" })`
|
|
222
|
+
2. **DESPUES** → `Task({ ..., team_name: "elsabro-party-rN", name: "agent-K" })` for each agent
|
|
223
|
+
3. **AL FINAL** → `SendMessage({ type: "shutdown_request", ... })` for EACH teammate
|
|
224
|
+
4. **ULTIMO** → `TeamDelete()`
|
|
225
|
+
|
|
226
|
+
**Synthesis is exempt:** Only 1 agent (orchestrator), no team needed.
|
|
227
|
+
|
|
228
|
+
**NUNCA hagas esto:**
|
|
229
|
+
```javascript
|
|
230
|
+
// ⛔ PROHIBIDO — bare Task() per agent in a round
|
|
231
|
+
onAgentTurn: async ({ agent }) => {
|
|
232
|
+
return Task({ subagent_type: `elsabro-${agent.id}`, prompt: "..." });
|
|
233
|
+
}
|
|
234
|
+
// 3 agents = 3 bare Task() calls = Rule 8 VIOLATION
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**SIEMPRE haz esto:**
|
|
238
|
+
```javascript
|
|
239
|
+
// ✅ CORRECTO — team per round (via onBeforeRound/onAfterRound callbacks)
|
|
240
|
+
onBeforeRound: ({ round, agents }) => {
|
|
241
|
+
TeamCreate({ team_name: `elsabro-party-r${round}`, description: `Party round ${round}` });
|
|
242
|
+
}
|
|
243
|
+
onAgentTurn: ({ agent, round, agentIndex }) => {
|
|
244
|
+
return Task({ subagent_type: `elsabro-${agent.id}`, team_name: `elsabro-party-r${round}`, name: `agent-${agentIndex + 1}`, prompt: "..." });
|
|
245
|
+
}
|
|
246
|
+
onAfterRound: ({ round, agents }) => {
|
|
247
|
+
agents.forEach((_, i) => SendMessage({ type: "shutdown_request", recipient: `agent-${i + 1}`, content: "Done" }));
|
|
248
|
+
TeamDelete();
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
</IMPERATIVO_AGENT_TEAMS>
|
|
252
|
+
|
|
196
253
|
## Implementacion Tecnica (PartyEngine v7.0.0)
|
|
197
254
|
|
|
198
255
|
Party Mode is powered by `PartyEngine` from `flow-engine/src/party.js`. The engine handles agent selection, round management, checkpointing, and synthesis through callbacks.
|
|
@@ -213,16 +270,30 @@ const agents = engine.selectAgents(topic, {
|
|
|
213
270
|
// Display agent cards with emoji, name, focus
|
|
214
271
|
|
|
215
272
|
// 3. Run party session with callbacks
|
|
273
|
+
// NOTE: Team lifecycle is managed PER ROUND via onBeforeRound/onAfterRound.
|
|
274
|
+
// Each round creates a team, spawns agents as teammates, then destroys the team.
|
|
216
275
|
const result = await engine.run(topic, {
|
|
217
276
|
agents: inputs.agents || undefined,
|
|
218
277
|
maxRounds: inputs.rounds || 3,
|
|
219
278
|
projectContext: state.context
|
|
220
279
|
}, {
|
|
221
|
-
|
|
222
|
-
//
|
|
280
|
+
onBeforeRound: async ({ round, agents }) => {
|
|
281
|
+
// AGENT TEAMS (Rule 8): Create team for this round
|
|
282
|
+
// Each round has 2-3 agents in parallel → TeamCreate OBLIGATORIO
|
|
283
|
+
TeamCreate({
|
|
284
|
+
team_name: `elsabro-party-r${round}`,
|
|
285
|
+
description: `Party Mode round ${round} — ${agents.map(a => a.name).join(', ')}`
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
onAgentTurn: async ({ agent, topic, round, totalRounds, history, instruction, agentIndex }) => {
|
|
290
|
+
// Each agent is spawned as a teammate within the round's team
|
|
291
|
+
// team_name and name are REQUIRED for Rule 8 compliance
|
|
223
292
|
return Task({
|
|
224
293
|
subagent_type: `elsabro-${agent.id}`,
|
|
225
294
|
model: "sonnet",
|
|
295
|
+
team_name: `elsabro-party-r${round}`,
|
|
296
|
+
name: `agent-${agentIndex + 1}`,
|
|
226
297
|
prompt: `You are ${agent.name}. ${agent.style}.
|
|
227
298
|
Focus: ${agent.focus}. Tendency: ${agent.tendency}.
|
|
228
299
|
Topic: "${topic}". Round: ${round}/${totalRounds}.
|
|
@@ -232,8 +303,21 @@ const result = await engine.run(topic, {
|
|
|
232
303
|
});
|
|
233
304
|
},
|
|
234
305
|
|
|
306
|
+
onAfterRound: async ({ round, agents }) => {
|
|
307
|
+
// AGENT TEAMS (Rule 8): Shutdown all teammates and delete team
|
|
308
|
+
for (let i = 0; i < agents.length; i++) {
|
|
309
|
+
SendMessage({
|
|
310
|
+
type: "shutdown_request",
|
|
311
|
+
recipient: `agent-${i + 1}`,
|
|
312
|
+
content: `Round ${round} complete`
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
TeamDelete();
|
|
316
|
+
},
|
|
317
|
+
|
|
235
318
|
onSynthesize: async ({ history, topic, agents }) => {
|
|
236
319
|
// Orchestrator (Quantum) synthesizes the debate
|
|
320
|
+
// Single agent — NO team needed (Rule 8 exception: 1 agent)
|
|
237
321
|
return Task({
|
|
238
322
|
subagent_type: "elsabro-orchestrator",
|
|
239
323
|
model: "sonnet",
|
|
@@ -249,6 +333,7 @@ const result = await engine.run(topic, {
|
|
|
249
333
|
|
|
250
334
|
onRoundComplete: async ({ completedRound, totalRounds, lastResponses }) => {
|
|
251
335
|
// Ask user if they want to continue, stop, or add a round
|
|
336
|
+
// Single interaction — NO team needed (not parallel agents)
|
|
252
337
|
const answer = AskUserQuestion({
|
|
253
338
|
questions: [{
|
|
254
339
|
question: `Round ${completedRound}/${totalRounds} complete. Continue?`,
|
|
@@ -208,7 +208,8 @@ Veo que estás en una carpeta vacía. ¿Qué te gustaría crear?
|
|
|
208
208
|
2) 📱 Una app móvil (Expo/React Native)
|
|
209
209
|
3) 🔌 Una API o microservicio
|
|
210
210
|
4) 🧩 Una extensión de Chrome
|
|
211
|
-
5)
|
|
211
|
+
5) 🎨 Primero quiero ver cómo se vería (diseñar UI)
|
|
212
|
+
6) 💡 Tengo otra idea (cuéntame)
|
|
212
213
|
```
|
|
213
214
|
|
|
214
215
|
### Para Brownfield (código existente):
|
|
@@ -221,7 +222,8 @@ Veo que estás en una carpeta vacía. ¿Qué te gustaría crear?
|
|
|
221
222
|
2) 🐛 Arreglar algo que no funciona
|
|
222
223
|
3) 📖 Entender cómo funciona el código
|
|
223
224
|
4) 🔄 Refactorizar o mejorar algo
|
|
224
|
-
5)
|
|
225
|
+
5) 🎨 Diseñar o rediseñar la interfaz
|
|
226
|
+
6) ❓ Otra cosa (cuéntame)
|
|
225
227
|
```
|
|
226
228
|
|
|
227
229
|
### Para Continuation (proyecto ELSABRO existente):
|
|
@@ -236,7 +238,8 @@ Tu proyecto: [context.project_type]
|
|
|
236
238
|
1) ▶️ Continuar con la siguiente fase
|
|
237
239
|
2) ✅ Verificar el trabajo anterior
|
|
238
240
|
3) 📋 Ver el plan completo
|
|
239
|
-
4)
|
|
241
|
+
4) 🎨 Diseñar o ajustar la interfaz
|
|
242
|
+
5) 🆕 Empezar algo diferente
|
|
240
243
|
```
|
|
241
244
|
|
|
242
245
|
Usar `AskUserQuestion` con las opciones correspondientes.
|
|
@@ -292,6 +295,7 @@ TaskUpdate(id, status: "in_progress")
|
|
|
292
295
|
| App móvil | new + plan | `Skill("mobile-app")` luego `Skill("elsabro:plan")` |
|
|
293
296
|
| API | new + plan | `Skill("api-microservice")` luego `Skill("elsabro:plan")` |
|
|
294
297
|
| Chrome extension | new + plan | `Skill("chrome-extension")` luego `Skill("elsabro:plan")` |
|
|
298
|
+
| Diseñar UI | design-ui | `Skill("elsabro:design-ui")` |
|
|
295
299
|
| Otra idea | plan | `Skill("elsabro:plan")` con contexto |
|
|
296
300
|
|
|
297
301
|
#### Brownfield:
|
|
@@ -301,6 +305,7 @@ TaskUpdate(id, status: "in_progress")
|
|
|
301
305
|
| Arreglar bug | debug | `Skill("elsabro:debug")` |
|
|
302
306
|
| Entender código | map-codebase | `Skill("elsabro:map-codebase")` |
|
|
303
307
|
| Refactorizar | plan | `Skill("elsabro:plan")` con intent="refactor" |
|
|
308
|
+
| Diseñar UI | design-ui | `Skill("elsabro:design-ui")` |
|
|
304
309
|
|
|
305
310
|
#### Continuation:
|
|
306
311
|
| Opción Usuario | Comando | Skill a Invocar |
|
|
@@ -308,6 +313,7 @@ TaskUpdate(id, status: "in_progress")
|
|
|
308
313
|
| Continuar siguiente | execute/plan | Según `pending_tasks` |
|
|
309
314
|
| Verificar anterior | verify-work | `Skill("elsabro:verify-work")` |
|
|
310
315
|
| Ver plan | read | Mostrar `.planning/*.md` |
|
|
316
|
+
| Diseñar UI | design-ui | `Skill("elsabro:design-ui")` |
|
|
311
317
|
| Algo diferente | → Paso 3 | Mostrar opciones de brownfield |
|
|
312
318
|
|
|
313
319
|
### 4.4 Invocar comando con contexto
|
package/flow-engine/src/party.js
CHANGED
|
@@ -205,7 +205,9 @@ class PartyEngine {
|
|
|
205
205
|
* @param {string} topic – discussion topic
|
|
206
206
|
* @param {object} [options] – { agents, maxRounds, maxAgents, projectContext }
|
|
207
207
|
* @param {object} callbacks
|
|
208
|
-
* @param {function} callbacks.onAgentTurn – (
|
|
208
|
+
* @param {function} callbacks.onAgentTurn – ({ agent, topic, round, totalRounds, history, instruction, agentIndex, projectContext }) => string response
|
|
209
|
+
* @param {function} [callbacks.onBeforeRound] – ({ round, totalRounds, agents }) => void (e.g., create Agent Team)
|
|
210
|
+
* @param {function} [callbacks.onAfterRound] – ({ round, totalRounds, agents, responses }) => void (e.g., destroy Agent Team)
|
|
209
211
|
* @param {function} [callbacks.onSynthesize] – (params) => { consensus, debates, actionItems, keyInsights, suggestedNext }
|
|
210
212
|
* @param {function} [callbacks.onRoundComplete] – (params) => "continue"|"stop"|"add_round"
|
|
211
213
|
* @param {function} [callbacks.onFormatSummary] – (template, data) => string
|
|
@@ -224,7 +226,13 @@ class PartyEngine {
|
|
|
224
226
|
for (let round = 1; round <= totalRounds; round++) {
|
|
225
227
|
const roundResponses = [];
|
|
226
228
|
|
|
227
|
-
|
|
229
|
+
// Hook: before round starts (e.g., create Agent Team)
|
|
230
|
+
if (callbacks.onBeforeRound) {
|
|
231
|
+
await callbacks.onBeforeRound({ round, totalRounds, agents });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (let agentIdx = 0; agentIdx < agents.length; agentIdx++) {
|
|
235
|
+
const agent = agents[agentIdx];
|
|
228
236
|
const instruction = round === 1
|
|
229
237
|
? 'Share your initial perspective on this topic.'
|
|
230
238
|
: 'React to previous perspectives and refine your position.';
|
|
@@ -236,6 +244,7 @@ class PartyEngine {
|
|
|
236
244
|
totalRounds,
|
|
237
245
|
history: [...history],
|
|
238
246
|
instruction,
|
|
247
|
+
agentIndex: agentIdx,
|
|
239
248
|
projectContext: options.projectContext || null
|
|
240
249
|
});
|
|
241
250
|
|
|
@@ -250,6 +259,11 @@ class PartyEngine {
|
|
|
250
259
|
roundResponses.push(entry);
|
|
251
260
|
}
|
|
252
261
|
|
|
262
|
+
// Hook: after round completes (e.g., destroy Agent Team)
|
|
263
|
+
if (callbacks.onAfterRound) {
|
|
264
|
+
await callbacks.onAfterRound({ round, totalRounds, agents, responses: roundResponses });
|
|
265
|
+
}
|
|
266
|
+
|
|
253
267
|
// Checkpoint after each round
|
|
254
268
|
this.checkpoint.save(sessionId, {
|
|
255
269
|
topic,
|
|
@@ -323,7 +337,13 @@ class PartyEngine {
|
|
|
323
337
|
for (let round = startRound; round <= rounds; round++) {
|
|
324
338
|
const roundResponses = [];
|
|
325
339
|
|
|
326
|
-
|
|
340
|
+
// Hook: before round starts (e.g., create Agent Team)
|
|
341
|
+
if (callbacks.onBeforeRound) {
|
|
342
|
+
await callbacks.onBeforeRound({ round, totalRounds: rounds, agents });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (let agentIdx = 0; agentIdx < agents.length; agentIdx++) {
|
|
346
|
+
const agent = agents[agentIdx];
|
|
327
347
|
const instruction = round === 1
|
|
328
348
|
? 'Share your initial perspective on this topic.'
|
|
329
349
|
: 'React to previous perspectives and refine your position.';
|
|
@@ -335,6 +355,7 @@ class PartyEngine {
|
|
|
335
355
|
totalRounds: rounds,
|
|
336
356
|
history: [...history],
|
|
337
357
|
instruction,
|
|
358
|
+
agentIndex: agentIdx,
|
|
338
359
|
projectContext: (options && options.projectContext) || null
|
|
339
360
|
});
|
|
340
361
|
|
|
@@ -349,6 +370,11 @@ class PartyEngine {
|
|
|
349
370
|
roundResponses.push(entry);
|
|
350
371
|
}
|
|
351
372
|
|
|
373
|
+
// Hook: after round completes (e.g., destroy Agent Team)
|
|
374
|
+
if (callbacks.onAfterRound) {
|
|
375
|
+
await callbacks.onAfterRound({ round, totalRounds: rounds, agents, responses: roundResponses });
|
|
376
|
+
}
|
|
377
|
+
|
|
352
378
|
this.checkpoint.save(sessionId, {
|
|
353
379
|
topic,
|
|
354
380
|
agents: agentIds,
|
|
@@ -48,10 +48,10 @@ describe('CLI: helpers', () => {
|
|
|
48
48
|
// ---------- validate ----------
|
|
49
49
|
|
|
50
50
|
describe('CLI: validate', () => {
|
|
51
|
-
it('reports valid flow with
|
|
51
|
+
it('reports valid flow with 44 nodes', async () => {
|
|
52
52
|
const result = await main(['node', 'cli.js', 'validate', '--flow', FLOW_PATH]);
|
|
53
53
|
assert.equal(result.valid, true);
|
|
54
|
-
assert.equal(result.nodeCount,
|
|
54
|
+
assert.equal(result.nodeCount, 44);
|
|
55
55
|
assert.ok(result.parallelNodes.length >= 4);
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -154,7 +154,7 @@ describe('real flow loading', () => {
|
|
|
154
154
|
it('loads the full development-flow.json', () => {
|
|
155
155
|
const flow = require('../../flows/development-flow.json');
|
|
156
156
|
const graph = buildGraph(flow);
|
|
157
|
-
assert.equal(graph.nodes.size,
|
|
157
|
+
assert.equal(graph.nodes.size, 44);
|
|
158
158
|
assert.equal(graph.entryNode, 'start');
|
|
159
159
|
assert.equal(graph.meta.version, '5.3.0');
|
|
160
160
|
});
|
|
@@ -52,10 +52,10 @@ function makeMockCallbacks() {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
describe('Integration: Graph loading', () => {
|
|
55
|
-
it('loads all
|
|
55
|
+
it('loads all 44 nodes from development-flow.json', () => {
|
|
56
56
|
const engine = new FlowEngine();
|
|
57
57
|
engine.loadFlow(flow);
|
|
58
|
-
assert.equal(engine.getNodeCount(),
|
|
58
|
+
assert.equal(engine.getNodeCount(), 44);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it('finds the entry node at "start"', () => {
|
|
@@ -68,8 +68,8 @@ describe('Integration: Graph loading', () => {
|
|
|
68
68
|
const engine = new FlowEngine();
|
|
69
69
|
engine.loadFlow(flow);
|
|
70
70
|
const meta = engine.getFlowMetadata();
|
|
71
|
-
assert.equal(meta.sync_metadata.audit_result.total_nodes,
|
|
72
|
-
assert.equal(meta.sync_metadata.audit_result.implemented,
|
|
71
|
+
assert.equal(meta.sync_metadata.audit_result.total_nodes, 44);
|
|
72
|
+
assert.equal(meta.sync_metadata.audit_result.implemented, 42);
|
|
73
73
|
assert.equal(meta.sync_metadata.audit_result.partial, 0);
|
|
74
74
|
assert.equal(meta.sync_metadata.audit_result.not_implemented, 0);
|
|
75
75
|
assert.equal(meta.sync_metadata.audit_result.deprecated, 2);
|
|
@@ -79,7 +79,7 @@ describe('Integration: Graph loading', () => {
|
|
|
79
79
|
const engine = new FlowEngine();
|
|
80
80
|
engine.loadFlow(flow);
|
|
81
81
|
const implemented = engine.getNodesWhere(n => n.runtime_status === 'implemented');
|
|
82
|
-
assert.equal(implemented.length,
|
|
82
|
+
assert.equal(implemented.length, 42);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
it('counts not_implemented nodes correctly', () => {
|
|
@@ -871,10 +871,10 @@ describe('Integration: P5 Cleanup & Deprecation', () => {
|
|
|
871
871
|
assert.deepStrictEqual(ids, ['interrupt_teams_failed', 'teams_spawn']);
|
|
872
872
|
});
|
|
873
873
|
|
|
874
|
-
it('
|
|
874
|
+
it('42 implemented nodes exist', () => {
|
|
875
875
|
const engine = new FlowEngine();
|
|
876
876
|
engine.loadFlow(flow);
|
|
877
877
|
const implemented = engine.getNodesWhere(n => n.runtime_status === 'implemented');
|
|
878
|
-
assert.equal(implemented.length,
|
|
878
|
+
assert.equal(implemented.length, 42);
|
|
879
879
|
});
|
|
880
880
|
});
|
|
@@ -399,6 +399,63 @@ describe('Round Management', () => {
|
|
|
399
399
|
{ message: /requires callbacks.onAgentTurn/ }
|
|
400
400
|
);
|
|
401
401
|
});
|
|
402
|
+
|
|
403
|
+
it('calls onBeforeRound before each round', async () => {
|
|
404
|
+
const dir = tmpDir();
|
|
405
|
+
const engine = new PartyEngine({ checkpointDir: dir, maxRounds: 2, maxAgents: 2 });
|
|
406
|
+
const beforeCalls = [];
|
|
407
|
+
await engine.run('architecture', { maxRounds: 2 }, {
|
|
408
|
+
onAgentTurn: mockOnAgentTurn,
|
|
409
|
+
onBeforeRound: ({ round, agents }) => {
|
|
410
|
+
beforeCalls.push({ round, agentCount: agents.length });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
assert.equal(beforeCalls.length, 2);
|
|
414
|
+
assert.equal(beforeCalls[0].round, 1);
|
|
415
|
+
assert.equal(beforeCalls[1].round, 2);
|
|
416
|
+
assert.ok(beforeCalls[0].agentCount >= 2);
|
|
417
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('calls onAfterRound after each round', async () => {
|
|
421
|
+
const dir = tmpDir();
|
|
422
|
+
const engine = new PartyEngine({ checkpointDir: dir, maxRounds: 2, maxAgents: 2 });
|
|
423
|
+
const afterCalls = [];
|
|
424
|
+
await engine.run('architecture', { maxRounds: 2 }, {
|
|
425
|
+
onAgentTurn: mockOnAgentTurn,
|
|
426
|
+
onAfterRound: ({ round, responses }) => {
|
|
427
|
+
afterCalls.push({ round, responseCount: responses.length });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
assert.equal(afterCalls.length, 2);
|
|
431
|
+
assert.equal(afterCalls[0].round, 1);
|
|
432
|
+
assert.ok(afterCalls[0].responseCount >= 2);
|
|
433
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('provides agentIndex in onAgentTurn', async () => {
|
|
437
|
+
const dir = tmpDir();
|
|
438
|
+
const engine = new PartyEngine({ checkpointDir: dir, maxRounds: 1, maxAgents: 3 });
|
|
439
|
+
const indices = [];
|
|
440
|
+
await engine.run('architecture', { maxRounds: 1 }, {
|
|
441
|
+
onAgentTurn: ({ agentIndex }) => {
|
|
442
|
+
indices.push(agentIndex);
|
|
443
|
+
return 'response';
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
assert.deepEqual(indices, [0, 1, 2]);
|
|
447
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('onBeforeRound and onAfterRound are optional (backward compat)', async () => {
|
|
451
|
+
const dir = tmpDir();
|
|
452
|
+
const engine = new PartyEngine({ checkpointDir: dir, maxRounds: 1, maxAgents: 2 });
|
|
453
|
+
const result = await engine.run('testing', { maxRounds: 1 }, {
|
|
454
|
+
onAgentTurn: mockOnAgentTurn
|
|
455
|
+
});
|
|
456
|
+
assert.ok(result.history.length >= 2);
|
|
457
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
458
|
+
});
|
|
402
459
|
});
|
|
403
460
|
|
|
404
461
|
// ── Suite 4: Synthesis ───────────────────────────────────────────────────────
|