elsabro 7.0.1 → 7.2.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/commands/elsabro/execute.md +121 -10
- 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/flow-engine/tests/skill-install.test.js +374 -0
- package/flows/development-flow.json +42 -5
- package/flows/quick-flow.json +0 -1
- package/hooks/skill-discovery.sh +6 -4
- package/hooks/skill-install.sh +224 -0
- package/package.json +1 -1
- package/references/command-flow.md +25 -20
|
@@ -172,10 +172,67 @@ Los skills proveen patrones de código verificados, configuraciones de setup, y
|
|
|
172
172
|
const recommendedSkills = state.context.available_skills || [];
|
|
173
173
|
|
|
174
174
|
if (recommendedSkills.length > 0) {
|
|
175
|
-
// 2. Cargar contenido de los top-3 skills
|
|
175
|
+
// 2. Cargar contenido de los top-3 skills (local → global → install)
|
|
176
176
|
const loadedSkills = [];
|
|
177
|
+
let registryDown = false;
|
|
177
178
|
for (const skill of recommendedSkills.slice(0, 3)) {
|
|
178
|
-
|
|
179
|
+
// 2a. Try local ELSABRO skill first
|
|
180
|
+
let content = Read(`skills/${skill.id}.md`);
|
|
181
|
+
|
|
182
|
+
// 2b. Try global installed skill
|
|
183
|
+
if (!content) {
|
|
184
|
+
content = Read(`${HOME}/.claude/skills/${skill.id}.md`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 2c. If not found locally and has install_cmd → offer to install
|
|
188
|
+
if (!content && skill.install_cmd && skill.source === "skills-registry" && !registryDown) {
|
|
189
|
+
// Check registry accessibility
|
|
190
|
+
const checkResult = Bash(`bash ./hooks/skill-install.sh check`, { timeout: 20000 });
|
|
191
|
+
const check = JSON.parse(checkResult);
|
|
192
|
+
|
|
193
|
+
if (check.status === "ok") {
|
|
194
|
+
// Ask user permission
|
|
195
|
+
const answer = AskUserQuestion({
|
|
196
|
+
questions: [{
|
|
197
|
+
question: `Skill "${skill.id}" no esta instalado localmente pero esta disponible en el registry. Instalarlo?`,
|
|
198
|
+
header: "Skill Install",
|
|
199
|
+
options: [
|
|
200
|
+
{ label: "Instalar", description: `Ejecuta: ${skill.install_cmd}` },
|
|
201
|
+
{ label: "Omitir", description: "Continuar sin este skill" }
|
|
202
|
+
],
|
|
203
|
+
multiSelect: false
|
|
204
|
+
}]
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (answer === "Instalar") {
|
|
208
|
+
// Execute install
|
|
209
|
+
const installResult = Bash(`bash ./hooks/skill-install.sh install "${skill.install_cmd}"`, { timeout: 45000 });
|
|
210
|
+
const install = JSON.parse(installResult);
|
|
211
|
+
|
|
212
|
+
if (install.status === "ok") {
|
|
213
|
+
// Validate installed file
|
|
214
|
+
const validateResult = Bash(`bash ./hooks/skill-install.sh validate "${skill.id}"`, { timeout: 5000 });
|
|
215
|
+
const validation = JSON.parse(validateResult);
|
|
216
|
+
|
|
217
|
+
if (validation.status === "ok") {
|
|
218
|
+
content = Read(validation.path);
|
|
219
|
+
output(` + Skill "${skill.id}" instalado y cargado`);
|
|
220
|
+
} else {
|
|
221
|
+
output(` ! Skill "${skill.id}" instalado pero formato invalido — omitido`);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
output(` ! Instalacion de "${skill.id}" fallo: ${install.message} — omitido`);
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
output(` - Skill "${skill.id}" omitido por usuario`);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
output(` ! Registry no disponible — omitiendo instalacion de skills externos`);
|
|
231
|
+
registryDown = true; // Skip install attempts for remaining skills
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 2d. Load content if available (from any source)
|
|
179
236
|
if (content) {
|
|
180
237
|
loadedSkills.push({
|
|
181
238
|
name: skill.id,
|
|
@@ -666,19 +723,34 @@ TaskUpdate({ taskId: "plan-A-id", status: "completed" })
|
|
|
666
723
|
|
|
667
724
|
### Validación arquitectónica en paralelo (OPUS x2)
|
|
668
725
|
```javascript
|
|
669
|
-
//
|
|
726
|
+
// OBLIGATORIO: Crear Agent Team para validación arquitectónica (Rule 8)
|
|
727
|
+
TeamCreate({
|
|
728
|
+
team_name: "elsabro-arch-validate",
|
|
729
|
+
description: "Architectural validation: implementation + architecture review"
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
// Spawn 2 teammates OPUS en paralelo
|
|
670
733
|
Task({
|
|
671
734
|
subagent_type: "elsabro-executor",
|
|
672
735
|
model: "opus",
|
|
736
|
+
team_name: "elsabro-arch-validate",
|
|
737
|
+
name: "impl-1",
|
|
673
738
|
description: "Implementar cambios",
|
|
674
739
|
prompt: "..."
|
|
675
740
|
}) |
|
|
676
741
|
Task({
|
|
677
742
|
subagent_type: "feature-dev:code-architect",
|
|
678
743
|
model: "opus", // ← OPUS para decisiones arquitectónicas
|
|
744
|
+
team_name: "elsabro-arch-validate",
|
|
745
|
+
name: "architect-1",
|
|
679
746
|
description: "Validar arquitectura",
|
|
680
747
|
prompt: "Verifica que la implementación sigue los patrones del codebase..."
|
|
681
748
|
})
|
|
749
|
+
|
|
750
|
+
// Shutdown y cleanup
|
|
751
|
+
SendMessage({ type: "shutdown_request", recipient: "impl-1", content: "Validation complete" })
|
|
752
|
+
SendMessage({ type: "shutdown_request", recipient: "architect-1", content: "Validation complete" })
|
|
753
|
+
TeamDelete()
|
|
682
754
|
```
|
|
683
755
|
|
|
684
756
|
### Si solo hay un plan → Secuencial (OPUS)
|
|
@@ -1062,15 +1134,33 @@ if (shouldUseWorktrees(wave, profile)) {
|
|
|
1062
1134
|
// Crear worktrees
|
|
1063
1135
|
Bash(`./scripts/setup-parallel-worktrees.sh create ${agents.join(' ')}`);
|
|
1064
1136
|
|
|
1065
|
-
//
|
|
1066
|
-
|
|
1137
|
+
// OBLIGATORIO: Crear Agent Team para worktree paralelo (Rule 8)
|
|
1138
|
+
TeamCreate({
|
|
1139
|
+
team_name: "elsabro-worktree",
|
|
1140
|
+
description: "Parallel worktree execution for wave " + wave.id
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
// Ejecutar agentes como teammates en sus worktrees
|
|
1144
|
+
for (let i = 0; i < agents.length; i++) {
|
|
1067
1145
|
Task({
|
|
1068
|
-
subagent_type:
|
|
1069
|
-
|
|
1146
|
+
subagent_type: agents[i],
|
|
1147
|
+
team_name: "elsabro-worktree",
|
|
1148
|
+
name: `wt-agent-${i + 1}`,
|
|
1149
|
+
prompt: `Trabaja en worktree: ../elsabro-worktrees/${agents[i]}-wt/`
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Shutdown teammates y cleanup team
|
|
1154
|
+
for (let i = 0; i < agents.length; i++) {
|
|
1155
|
+
SendMessage({
|
|
1156
|
+
type: "shutdown_request",
|
|
1157
|
+
recipient: `wt-agent-${i + 1}`,
|
|
1158
|
+
content: "Worktree work complete"
|
|
1070
1159
|
});
|
|
1071
1160
|
}
|
|
1161
|
+
TeamDelete();
|
|
1072
1162
|
|
|
1073
|
-
// Merge y cleanup al completar
|
|
1163
|
+
// Merge y cleanup worktrees al completar
|
|
1074
1164
|
Bash(`./scripts/setup-parallel-worktrees.sh complete ${agents.join(' ')}`);
|
|
1075
1165
|
}
|
|
1076
1166
|
```
|
|
@@ -1142,12 +1232,33 @@ errorAggregator.setPolicy("quorum");
|
|
|
1142
1232
|
for (const wave of waves) {
|
|
1143
1233
|
timeoutHandler.startTimeout(wave.id, 60 * 60 * 1000); // 60min por wave
|
|
1144
1234
|
|
|
1235
|
+
// OBLIGATORIO: Crear Agent Team para wave de implementacion (Rule 8)
|
|
1236
|
+
TeamCreate({
|
|
1237
|
+
team_name: "elsabro-wave-impl",
|
|
1238
|
+
description: "Implementation wave " + wave.id
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1145
1241
|
const results = await Promise.all(
|
|
1146
|
-
wave.plans.map(plan =>
|
|
1147
|
-
executeWithRetry(plan.id, () => Task(
|
|
1242
|
+
wave.plans.map((plan, i) =>
|
|
1243
|
+
executeWithRetry(plan.id, () => Task({
|
|
1244
|
+
subagent_type: "elsabro-executor",
|
|
1245
|
+
team_name: "elsabro-wave-impl",
|
|
1246
|
+
name: `wave-executor-${i + 1}`,
|
|
1247
|
+
prompt: plan
|
|
1248
|
+
}))
|
|
1148
1249
|
)
|
|
1149
1250
|
);
|
|
1150
1251
|
|
|
1252
|
+
// Shutdown teammates y cleanup team
|
|
1253
|
+
wave.plans.forEach((_, i) => {
|
|
1254
|
+
SendMessage({
|
|
1255
|
+
type: "shutdown_request",
|
|
1256
|
+
recipient: `wave-executor-${i + 1}`,
|
|
1257
|
+
content: "Wave complete"
|
|
1258
|
+
});
|
|
1259
|
+
});
|
|
1260
|
+
TeamDelete();
|
|
1261
|
+
|
|
1151
1262
|
const summary = await errorAggregator.aggregate(wave.id, results);
|
|
1152
1263
|
|
|
1153
1264
|
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 ───────────────────────────────────────────────────────
|