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/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}Intentando actualizar de todas formas...${reset}\n`);
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
- // Use specific version from registry to avoid cache issues
251
- // Don't pass --update to avoid infinite recursion
252
- const versionToInstall = latestVersion || 'latest';
253
- const packageSpec = `elsabro@${versionToInstall}`;
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
- // npm exec with specific version bypasses cache
261
- execFileSync(getExecutable('npm'), ['exec', '--yes', '--', packageSpec, '--global'], { stdio: 'inherit' });
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
- } else {
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
- // Además de implementar, validar arquitectura en paralelo
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
- // Ejecutar agentes en sus worktrees
1066
- for (const agent of agents) {
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: agent,
1069
- prompt: `Trabaja en worktree: ../elsabro-worktrees/${agent}-wt/`
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
- // Merge y cleanup al completar
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(elsabro-executor, plan))
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
- onAgentTurn: async ({ agent, topic, round, totalRounds, history, instruction }) => {
222
- // Each agent turn delegates to the real ELSABRO agent via Task()
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) 💡 Tengo otra idea (cuéntame)
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) Otra cosa (cuéntame)
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) 🆕 Empezar algo diferente
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
@@ -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 – (params) => string response
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
- for (const agent of agents) {
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
- for (const agent of agents) {
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 42 nodes', async () => {
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, 42);
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, 42);
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 42 nodes from development-flow.json', () => {
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(), 42);
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, 42);
72
- assert.equal(meta.sync_metadata.audit_result.implemented, 40);
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, 40);
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('40 implemented nodes exist', () => {
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, 40);
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 ───────────────────────────────────────────────────────