@agent-link/agent 0.1.83 → 0.1.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/team.js CHANGED
@@ -7,22 +7,13 @@
7
7
  * while the Lead Claude process drives all planning/execution autonomously.
8
8
  */
9
9
  import { randomUUID } from 'crypto';
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, renameSync, unlinkSync } from 'fs';
11
- import { join } from 'path';
12
- import { CONFIG_DIR } from './config.js';
13
- // ── Color palette for auto-assigning agent colors ──────────────────────
14
- const AGENT_COLORS = [
15
- '#EF4444', // red (Lead)
16
- '#EAB308', // yellow
17
- '#3B82F6', // blue
18
- '#10B981', // emerald
19
- '#8B5CF6', // violet
20
- '#F97316', // orange
21
- '#EC4899', // pink
22
- '#06B6D4', // cyan
23
- '#84CC16', // lime
24
- '#6366F1', // indigo
25
- ];
10
+ export { buildAgentsDef, buildLeadPrompt } from './team-templates.js';
11
+ export { getNextAgentColor, classifyRole, pickCharacter, deriveAgentDisplayName, deriveTaskTitle } from './team-naming.js';
12
+ export { serializeTeam, deserializeTeam, persistTeam, persistTeamDebounced, loadTeam, listTeams, deleteTeam, renameTeam, } from './team-persistence.js';
13
+ import { getNextAgentColor, deriveAgentDisplayName, deriveTaskTitle } from './team-naming.js';
14
+ import { buildAgentsDef, buildLeadPrompt } from './team-templates.js';
15
+ import { serializeTeam, persistTeam, flushPendingPersists as _flushPendingPersists, } from './team-persistence.js';
16
+ // ── Module state ───────────────────────────────────────────────────────
26
17
  let activeTeam = null;
27
18
  let sendFn = () => { };
28
19
  let handleChatFn = null;
@@ -32,7 +23,6 @@ let clearOutputObserverFn = null;
32
23
  let setCloseObserverFn = null;
33
24
  let clearCloseObserverFn = null;
34
25
  let agentMessageIdCounter = 0;
35
- const TEAMS_DIR = join(CONFIG_DIR, 'teams');
36
26
  // ── Public API ─────────────────────────────────────────────────────────
37
27
  export function setTeamSendFn(fn) {
38
28
  sendFn = fn;
@@ -82,7 +72,7 @@ export function createTeamState(config, conversationId) {
82
72
  };
83
73
  // Register Lead as the first agent
84
74
  team.agents.set('lead', {
85
- role: { id: 'lead', name: 'Lead', color: AGENT_COLORS[0] },
75
+ role: { id: 'lead', name: 'Lead', color: getNextAgentColor(team) },
86
76
  toolUseId: null,
87
77
  agentTaskId: null,
88
78
  status: 'working',
@@ -100,78 +90,13 @@ export function createTeamState(config, conversationId) {
100
90
  export function clearActiveTeam() {
101
91
  activeTeam = null;
102
92
  }
103
- /**
104
- * Get the next color for an agent (based on current count).
105
- */
106
- export function getNextAgentColor(team) {
107
- const idx = team.agents.size % AGENT_COLORS.length;
108
- return AGENT_COLORS[idx];
109
- }
110
- /**
111
- * Derive a human-readable display name for a subagent from the Agent tool input.
112
- * Priority: descriptive input.name → short description → colon-prefixed description → prompt extraction → "Agent N"
113
- */
114
- function deriveAgentDisplayName(input, agentIndex) {
115
- // If input.name is descriptive (not a generic ID like "worker-1", "agent-2"), use it directly
116
- if (input.name && !/^(worker|agent|hypothesis)-\d+$/i.test(input.name)) {
117
- return input.name;
118
- }
119
- // Short description → use directly (e.g. "Implement the login page")
120
- if (input.description && input.description.length <= 60) {
121
- return input.description;
122
- }
123
- // Colon-prefixed description → use prefix (e.g. "Designer: review design doc" → "Designer")
124
- if (input.description) {
125
- const colonIdx = input.description.indexOf(':');
126
- if (colonIdx > 0 && colonIdx <= 30) {
127
- return input.description.slice(0, colonIdx).trim();
128
- }
129
- }
130
- // Extract role from prompt text
131
- const text = input.prompt || input.description || '';
132
- if (text) {
133
- // "You are tasked with writing/creating/implementing/..." pattern
134
- const taskMatch = text.match(/\btasked with\s+(writing|creating|implementing|reviewing|testing|designing|building|debugging|analyzing|refactoring|fixing|optimizing|setting up|configuring)\b\s*(.*)/i);
135
- if (taskMatch) {
136
- const verb = taskMatch[1].toLowerCase();
137
- const rest = taskMatch[2];
138
- const roleMap = {
139
- 'writing': 'Writer', 'creating': 'Creator', 'implementing': 'Implementer',
140
- 'reviewing': 'Reviewer', 'testing': 'Tester', 'designing': 'Designer',
141
- 'building': 'Builder', 'debugging': 'Debugger', 'analyzing': 'Analyst',
142
- 'refactoring': 'Refactorer', 'fixing': 'Fixer', 'optimizing': 'Optimizer',
143
- 'setting up': 'Setup', 'configuring': 'Config',
144
- };
145
- let roleName = roleMap[verb] || verb.charAt(0).toUpperCase() + verb.slice(1);
146
- // Extract object for specificity (e.g. "writing tests" → "Tester", "writing a design document" → "Designer")
147
- const objectMatch = rest.match(/^(?:a\s+|the\s+)?(\w+)/i);
148
- if (objectMatch) {
149
- const obj = objectMatch[1].toLowerCase();
150
- if (obj === 'tests' || obj === 'test')
151
- roleName = 'Tester';
152
- else if (obj === 'design')
153
- roleName = 'Designer';
154
- else if (obj === 'docs' || obj === 'documentation')
155
- roleName = 'Docs Writer';
156
- else
157
- roleName = obj.charAt(0).toUpperCase() + obj.slice(1) + ' ' + roleName;
158
- }
159
- return roleName;
160
- }
161
- // "You are a [role]..." pattern
162
- const roleMatch = text.match(/\bYou are (?:a |an |the )?([A-Z][\w\s]{2,30}?)(?:\.|,|\s+(?:who|that|tasked|responsible|focused))/);
163
- if (roleMatch) {
164
- return roleMatch[1].trim();
165
- }
166
- }
167
- return input.name || `Agent ${agentIndex}`;
168
- }
169
93
  /**
170
94
  * Register a subagent when Lead calls the Agent tool.
171
95
  */
172
96
  export function registerSubagent(team, toolUseId, input) {
173
97
  const agentId = (input.name || `agent-${team.agents.size}`).toLowerCase().replace(/\s+/g, '-');
174
- const displayName = deriveAgentDisplayName(input, team.agents.size);
98
+ const displayName = deriveAgentDisplayName(team, input);
99
+ const taskTitle = deriveTaskTitle(input);
175
100
  const color = getNextAgentColor(team);
176
101
  const teammate = {
177
102
  role: { id: agentId, name: displayName, color },
@@ -185,7 +110,7 @@ export function registerSubagent(team, toolUseId, input) {
185
110
  // Auto-create a task for this agent
186
111
  const task = {
187
112
  id: `task-${randomUUID().slice(0, 8)}`,
188
- title: displayName,
113
+ title: taskTitle,
189
114
  description: input.prompt || input.description || '',
190
115
  status: 'pending',
191
116
  assignee: agentId,
@@ -294,325 +219,73 @@ export function allSubagentsDone(team) {
294
219
  return subagents.every(a => a.status === 'done' || a.status === 'error');
295
220
  }
296
221
  /**
297
- * Serialize TeamState for persistence/transmission (Map array).
222
+ * Wrapper for flushPendingPersists that passes activeTeam.
298
223
  */
299
- export function serializeTeam(team, includeMessages = false) {
300
- return {
224
+ export function flushPendingPersists() {
225
+ _flushPendingPersists(activeTeam);
226
+ }
227
+ // ── Shared helpers (deduplicated) ────────────────────────────────────────
228
+ /**
229
+ * Emit agent status + task update + feed entry to web client.
230
+ * Consolidates the 3-message broadcast pattern used in multiple places.
231
+ */
232
+ function emitAgentUpdate(team, agent, feedType, feedMessage) {
233
+ sendFn({
234
+ type: 'team_agent_status',
301
235
  teamId: team.teamId,
302
- title: team.title,
303
- config: team.config,
304
- conversationId: team.conversationId,
305
- claudeSessionId: team.claudeSessionId,
306
- agents: [...team.agents.entries()].map(([, agent]) => ({
236
+ agent: {
307
237
  id: agent.role.id,
308
238
  name: agent.role.name,
309
239
  color: agent.role.color,
310
- toolUseId: agent.toolUseId,
311
- agentTaskId: agent.agentTaskId,
312
240
  status: agent.status,
313
- currentTaskId: agent.currentTaskId,
314
- ...(includeMessages ? { messages: agent.messages } : {}),
315
- })),
316
- tasks: team.tasks,
317
- feed: team.feed,
318
- status: team.status,
319
- leadStatus: team.leadStatus,
320
- summary: team.summary,
321
- totalCost: team.totalCost,
322
- durationMs: team.durationMs,
323
- createdAt: team.createdAt,
324
- };
325
- }
326
- // ── Persistence ──────────────────────────────────────────────────────────
327
- function ensureTeamsDir() {
328
- if (!existsSync(TEAMS_DIR)) {
329
- mkdirSync(TEAMS_DIR, { recursive: true });
330
- }
331
- }
332
- /**
333
- * Deserialize a TeamStateSerialized back into a live TeamState.
334
- */
335
- function deserializeTeam(data) {
336
- const agents = new Map();
337
- for (const a of data.agents) {
338
- agents.set(a.id, {
339
- role: { id: a.id, name: a.name, color: a.color },
340
- toolUseId: a.toolUseId,
341
- agentTaskId: a.agentTaskId,
342
- status: a.status,
343
- currentTaskId: a.currentTaskId,
344
- messages: a.messages || [],
241
+ taskId: agent.currentTaskId,
242
+ },
243
+ });
244
+ const task = team.tasks.find(t => t.assignee === agent.role.id);
245
+ if (task) {
246
+ sendFn({
247
+ type: 'team_task_update',
248
+ teamId: team.teamId,
249
+ task,
345
250
  });
346
251
  }
347
- return {
348
- teamId: data.teamId,
349
- title: data.title,
350
- config: data.config,
351
- conversationId: data.conversationId,
352
- claudeSessionId: data.claudeSessionId,
353
- agents,
354
- tasks: data.tasks,
355
- feed: data.feed,
356
- status: data.status,
357
- leadStatus: data.leadStatus || '',
358
- summary: data.summary,
359
- totalCost: data.totalCost,
360
- durationMs: data.durationMs,
361
- createdAt: data.createdAt,
362
- };
363
- }
364
- /**
365
- * Persist team state to disk (atomic write: tmp → rename).
366
- */
367
- export function persistTeam(team) {
368
- ensureTeamsDir();
369
- const serialized = serializeTeam(team, true);
370
- const filePath = join(TEAMS_DIR, `${team.teamId}.json`);
371
- const tmpPath = filePath + '.tmp';
372
- writeFileSync(tmpPath, JSON.stringify(serialized, null, 2), 'utf-8');
373
- renameSync(tmpPath, filePath);
374
- }
375
- // Debounce timers for persist calls per team
376
- const persistTimers = new Map();
377
- /**
378
- * Debounced persist — coalesces rapid state changes into a single write.
379
- * Flushes after 500ms of quiet.
380
- */
381
- export function persistTeamDebounced(team) {
382
- const existing = persistTimers.get(team.teamId);
383
- if (existing)
384
- clearTimeout(existing);
385
- persistTimers.set(team.teamId, setTimeout(() => {
386
- persistTimers.delete(team.teamId);
387
- persistTeam(team);
388
- }, 500));
389
- }
390
- /**
391
- * Flush all pending debounced persists immediately.
392
- */
393
- export function flushPendingPersists() {
394
- for (const [teamId, timer] of persistTimers.entries()) {
395
- clearTimeout(timer);
396
- persistTimers.delete(teamId);
397
- if (activeTeam?.teamId === teamId) {
398
- persistTeam(activeTeam);
399
- }
400
- }
401
- }
402
- /**
403
- * Load a team from disk by teamId.
404
- */
405
- export function loadTeam(teamId) {
406
- const filePath = join(TEAMS_DIR, `${teamId}.json`);
407
- try {
408
- const raw = readFileSync(filePath, 'utf-8');
409
- const data = JSON.parse(raw);
410
- return deserializeTeam(data);
411
- }
412
- catch {
413
- return null;
414
- }
252
+ addFeedEntry(team, agent.role.id, feedType, feedMessage);
253
+ sendFn({
254
+ type: 'team_feed',
255
+ teamId: team.teamId,
256
+ entry: team.feed[team.feed.length - 1],
257
+ });
415
258
  }
416
259
  /**
417
- * List all persisted teams, sorted by createdAt descending (newest first).
260
+ * Mark all active agents and tasks to a final status.
261
+ * Used by both dissolveTeam and completeTeam.
418
262
  */
419
- export function listTeams() {
420
- ensureTeamsDir();
421
- const files = readdirSync(TEAMS_DIR).filter(f => f.endsWith('.json'));
422
- const teams = [];
423
- for (const file of files) {
424
- try {
425
- const raw = readFileSync(join(TEAMS_DIR, file), 'utf-8');
426
- const data = JSON.parse(raw);
427
- teams.push({
428
- teamId: data.teamId,
429
- title: data.title,
430
- status: data.status,
431
- template: data.config.template,
432
- agentCount: data.agents.length,
433
- taskCount: data.tasks.length,
434
- totalCost: data.totalCost,
435
- createdAt: data.createdAt,
436
- });
263
+ function finalizeAgentsAndTasks(team, agentStatus, taskStatus) {
264
+ for (const agent of team.agents.values()) {
265
+ if (agentStatus === 'done') {
266
+ if (agent.status !== 'error')
267
+ agent.status = 'done';
437
268
  }
438
- catch {
439
- // Skip corrupted files
269
+ else {
270
+ if (agent.status === 'starting' || agent.status === 'working')
271
+ agent.status = 'error';
440
272
  }
441
273
  }
442
- teams.sort((a, b) => b.createdAt - a.createdAt);
443
- return teams;
444
- }
445
- /**
446
- * Delete a persisted team file.
447
- */
448
- export function deleteTeam(teamId) {
449
- const filePath = join(TEAMS_DIR, `${teamId}.json`);
450
- try {
451
- unlinkSync(filePath);
452
- return true;
453
- }
454
- catch {
455
- return false;
456
- }
457
- }
458
- export function renameTeam(teamId, newTitle) {
459
- const filePath = join(TEAMS_DIR, `${teamId}.json`);
460
- try {
461
- const raw = readFileSync(filePath, 'utf-8');
462
- const data = JSON.parse(raw);
463
- data.title = newTitle;
464
- const tmpPath = filePath + '.tmp';
465
- writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
466
- renameSync(tmpPath, filePath);
467
- return true;
468
- }
469
- catch {
470
- return false;
274
+ for (const task of team.tasks) {
275
+ if (task.status === 'pending' || task.status === 'active') {
276
+ task.status = taskStatus;
277
+ task.updatedAt = Date.now();
278
+ }
471
279
  }
472
280
  }
473
- const TEMPLATE_AGENTS = {
474
- 'code-review': {
475
- 'security-reviewer': {
476
- description: 'Security expert focused on cryptographic, auth, and injection vulnerabilities',
477
- prompt: 'You are a security reviewer. Analyze code for vulnerabilities including injection attacks, authentication/authorization flaws, cryptographic issues, and data exposure risks. Provide specific file/line references and severity ratings.',
478
- tools: ['Read', 'Grep', 'Glob'],
479
- },
480
- 'quality-reviewer': {
481
- description: 'Code quality expert focused on maintainability, patterns, and best practices',
482
- prompt: 'You are a code quality reviewer. Analyze code structure, naming conventions, error handling, test coverage, and adherence to best practices. Identify code smells, unnecessary complexity, and improvement opportunities.',
483
- tools: ['Read', 'Grep', 'Glob'],
484
- },
485
- 'performance-reviewer': {
486
- description: 'Performance expert focused on efficiency, resource usage, and scalability',
487
- prompt: 'You are a performance reviewer. Identify performance bottlenecks, memory leaks, inefficient algorithms, unnecessary allocations, and scalability concerns. Suggest concrete optimizations with benchmarks where possible.',
488
- tools: ['Read', 'Grep', 'Glob'],
489
- },
490
- },
491
- 'full-stack': {
492
- 'backend-dev': {
493
- description: 'Backend developer for API endpoints, database, and server-side logic',
494
- prompt: 'You are a backend developer. Implement server-side features including API endpoints, data models, business logic, and integrations. Write clean, tested, production-ready code.',
495
- tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
496
- },
497
- 'frontend-dev': {
498
- description: 'Frontend developer for UI components, styling, and client-side logic',
499
- prompt: 'You are a frontend developer. Build user interface components, handle state management, implement responsive layouts, and ensure good UX. Follow the project\'s existing patterns and framework conventions.',
500
- tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
501
- },
502
- 'test-engineer': {
503
- description: 'Test engineer for unit tests, integration tests, and quality assurance',
504
- prompt: 'You are a test engineer. Write comprehensive tests (unit, integration, E2E) for new and existing code. Ensure edge cases are covered, mocks are appropriate, and tests are maintainable.',
505
- tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
506
- },
507
- },
508
- 'debug': {
509
- 'hypothesis-a': {
510
- description: 'Debug investigator exploring the first hypothesis',
511
- prompt: 'You are a debugging specialist. Investigate the bug by exploring one specific hypothesis. Read relevant code, trace execution paths, check logs, and report your findings with evidence.',
512
- tools: ['Read', 'Grep', 'Glob', 'Bash'],
513
- },
514
- 'hypothesis-b': {
515
- description: 'Debug investigator exploring an alternative hypothesis',
516
- prompt: 'You are a debugging specialist. Investigate the bug by exploring an alternative hypothesis different from other investigators. Read relevant code, trace execution paths, check logs, and report your findings with evidence.',
517
- tools: ['Read', 'Grep', 'Glob', 'Bash'],
518
- },
519
- 'hypothesis-c': {
520
- description: 'Debug investigator exploring a third hypothesis',
521
- prompt: 'You are a debugging specialist. Investigate the bug by exploring yet another hypothesis different from the other investigators. Think creatively about less obvious causes. Report findings with evidence.',
522
- tools: ['Read', 'Grep', 'Glob', 'Bash'],
523
- },
524
- },
525
- 'custom': {
526
- 'worker-1': {
527
- description: 'General-purpose development agent',
528
- prompt: 'You are a skilled software engineer. Complete the assigned task thoroughly and report your results.',
529
- tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
530
- },
531
- 'worker-2': {
532
- description: 'General-purpose development agent',
533
- prompt: 'You are a skilled software engineer. Complete the assigned task thoroughly and report your results.',
534
- tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
535
- },
536
- 'worker-3': {
537
- description: 'General-purpose development agent',
538
- prompt: 'You are a skilled software engineer. Complete the assigned task thoroughly and report your results.',
539
- tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
540
- },
541
- },
542
- };
543
- const TEMPLATE_LEAD_INSTRUCTIONS = {
544
- 'code-review': `You are a team lead coordinating a code review.
545
-
546
- Instructions:
547
- 1. First, analyze the codebase to understand its structure and what needs reviewing
548
- 2. Use the Agent tool to spawn each reviewer IN PARALLEL (multiple Agent calls simultaneously)
549
- 3. Give each reviewer specific, detailed instructions referencing exact files and directories to review
550
- 4. After all reviewers complete, synthesize their findings into a unified summary with prioritized action items
551
-
552
- Important:
553
- - When calling the Agent tool, use a descriptive role-based name (e.g., "Security Reviewer", "Quality Analyst", "Performance Auditor") instead of generic names. The name should reflect the agent's specialty.
554
- - Spawn agents in parallel for efficiency. Each agent should focus on their specialty area.`,
555
- 'full-stack': `You are a team lead coordinating full-stack development.
556
-
557
- Instructions:
558
- 1. First, analyze the codebase to understand the architecture, existing patterns, and what needs building
559
- 2. Break the task into backend, frontend, and test subtasks, and analyze dependencies between them
560
- 3. Define clear interfaces, API contracts, and data schemas before spawning any agents
561
- 4. Spawn independent subtasks IN PARALLEL using the Agent tool. If a subtask depends on another's output (e.g., frontend needs the API built first, tests need the implementation), wait for the dependency to complete, then spawn the dependent agent with the prior result as context
562
- 5. Provide each agent with specific, detailed instructions including file paths and shared contracts
563
- 6. After all agents complete, review their work and provide a summary of what was built
564
-
565
- Important:
566
- - When calling the Agent tool, use a descriptive role-based name (e.g., "Backend Engineer", "Frontend Engineer", "Test Engineer") instead of generic names. The name should reflect the agent's responsibility.
567
- - Maximize parallelism for truly independent tasks, but respect dependencies. Do not spawn all agents simultaneously if some need others' output first.`,
568
- 'debug': `You are a team lead coordinating a debugging investigation.
569
-
570
- Instructions:
571
- 1. First, analyze the bug report and relevant code to understand the problem space
572
- 2. Formulate 3 distinct hypotheses about the root cause
573
- 3. Use the Agent tool to assign each hypothesis to a different investigator IN PARALLEL
574
- 4. Give each investigator specific areas of code to examine and tests to run
575
- 5. After all investigators complete, compare their findings and synthesize a diagnosis with a recommended fix
576
-
577
- Important:
578
- - When calling the Agent tool, use a descriptive name that reflects the hypothesis being investigated (e.g., "Race Condition Investigator", "Memory Leak Analyst", "Config Error Detective") instead of generic names.
579
- - Each investigator should explore a DIFFERENT hypothesis. Avoid overlap.`,
580
- 'custom': `You are a team lead coordinating a development task.
581
-
582
- Instructions:
583
- 1. First, analyze the codebase and the user's request to understand what needs to be done
584
- 2. Break the task into subtasks and analyze dependencies between them
585
- 3. Spawn independent tasks IN PARALLEL using the Agent tool for efficiency
586
- 4. If a task depends on another's output (e.g., implementation needs a design doc, tests need the implementation), wait for the dependency to complete first, then spawn the dependent task with the prior result as context
587
- 5. Give each worker specific, detailed instructions
588
- 6. After all workers complete, review their work and provide a summary
589
-
590
- Important:
591
- - When calling the Agent tool, use a descriptive role-based name (e.g., "Designer", "Developer", "Tester", "Architect") instead of generic names like "Agent 1". The name should reflect what the agent does.
592
- - Maximize parallelism for truly independent tasks, but respect dependencies. For example, if one agent writes a design doc and another implements it, spawn the doc agent first, wait for its result, then spawn the implementation agent with the doc content.`,
593
- };
594
- /**
595
- * Build the agents definition JSON for the --agents CLI flag.
596
- */
597
- export function buildAgentsDef(template) {
598
- const key = template && TEMPLATE_AGENTS[template] ? template : 'custom';
599
- return { ...TEMPLATE_AGENTS[key] };
600
- }
601
281
  /**
602
- * Build the lead prompt that instructs the Lead to use Agent tool.
282
+ * Remove output and close observers.
603
283
  */
604
- export function buildLeadPrompt(config, agentsDef) {
605
- const template = config.template || 'custom';
606
- const instructions = TEMPLATE_LEAD_INSTRUCTIONS[template] || TEMPLATE_LEAD_INSTRUCTIONS['custom'];
607
- const agentList = Object.entries(agentsDef)
608
- .map(([id, def]) => `- ${id}: ${def.description}`)
609
- .join('\n');
610
- return `${instructions}
611
-
612
- Available agents (use the Agent tool to delegate to them):
613
- ${agentList}
614
-
615
- User's request: "${config.instruction}"`;
284
+ function cleanupObservers() {
285
+ if (clearOutputObserverFn)
286
+ clearOutputObserverFn();
287
+ if (clearCloseObserverFn)
288
+ clearCloseObserverFn();
616
289
  }
617
290
  // ── Output stream parser (observer callback) ────────────────────────────
618
291
  /**
@@ -674,32 +347,7 @@ export function onLeadOutput(conversationId, msg) {
674
347
  const input = (block.input || {});
675
348
  const toolUseId = block.id;
676
349
  const agent = registerSubagent(team, toolUseId, input);
677
- // Emit agent status + task update to web
678
- sendFn({
679
- type: 'team_agent_status',
680
- teamId: team.teamId,
681
- agent: {
682
- id: agent.role.id,
683
- name: agent.role.name,
684
- color: agent.role.color,
685
- status: agent.status,
686
- taskId: agent.currentTaskId,
687
- },
688
- });
689
- const task = team.tasks.find(t => t.assignee === agent.role.id);
690
- if (task) {
691
- sendFn({
692
- type: 'team_task_update',
693
- teamId: team.teamId,
694
- task,
695
- });
696
- }
697
- addFeedEntry(team, agent.role.id, 'status_change', `${agent.role.name} has joined and is getting ready`);
698
- sendFn({
699
- type: 'team_feed',
700
- teamId: team.teamId,
701
- entry: team.feed[team.feed.length - 1],
702
- });
350
+ emitAgentUpdate(team, agent, 'status_change', `${agent.role.name} has joined and is getting ready`);
703
351
  }
704
352
  }
705
353
  }
@@ -715,31 +363,7 @@ export function onLeadOutput(conversationId, msg) {
715
363
  const taskId = msg.task_id;
716
364
  const agent = linkSubagentTaskId(team, toolUseId, taskId);
717
365
  if (agent) {
718
- addFeedEntry(team, agent.role.id, 'task_started', `${agent.role.name} started working on the task`);
719
- sendFn({
720
- type: 'team_agent_status',
721
- teamId: team.teamId,
722
- agent: {
723
- id: agent.role.id,
724
- name: agent.role.name,
725
- color: agent.role.color,
726
- status: agent.status,
727
- taskId: agent.currentTaskId,
728
- },
729
- });
730
- sendFn({
731
- type: 'team_feed',
732
- teamId: team.teamId,
733
- entry: team.feed[team.feed.length - 1],
734
- });
735
- const task = team.tasks.find(t => t.assignee === agent.role.id);
736
- if (task) {
737
- sendFn({
738
- type: 'team_task_update',
739
- teamId: team.teamId,
740
- task,
741
- });
742
- }
366
+ emitAgentUpdate(team, agent, 'task_started', `${agent.role.name} started working on the task`);
743
367
  }
744
368
  return true; // suppress system.task_started from normal forwarding
745
369
  }
@@ -765,31 +389,7 @@ export function onLeadOutput(conversationId, msg) {
765
389
  if (agent.currentTaskId) {
766
390
  updateTaskStatus(team, agent.currentTaskId, isError ? 'failed' : 'done');
767
391
  }
768
- addFeedEntry(team, agent.role.id, isError ? 'task_failed' : 'task_completed', isError ? `${agent.role.name} ran into an issue and stopped` : `${agent.role.name} finished the task successfully`);
769
- sendFn({
770
- type: 'team_agent_status',
771
- teamId: team.teamId,
772
- agent: {
773
- id: agent.role.id,
774
- name: agent.role.name,
775
- color: agent.role.color,
776
- status: agent.status,
777
- taskId: agent.currentTaskId,
778
- },
779
- });
780
- const task = team.tasks.find(t => t.assignee === agent.role.id);
781
- if (task) {
782
- sendFn({
783
- type: 'team_task_update',
784
- teamId: team.teamId,
785
- task,
786
- });
787
- }
788
- sendFn({
789
- type: 'team_feed',
790
- teamId: team.teamId,
791
- entry: team.feed[team.feed.length - 1],
792
- });
392
+ emitAgentUpdate(team, agent, isError ? 'task_failed' : 'task_completed', isError ? `${agent.role.name} ran into an issue and stopped` : `${agent.role.name} finished the task successfully`);
793
393
  // Check if all subagents are done → transition to summarizing
794
394
  if (allSubagentsDone(team) && team.status === 'running') {
795
395
  team.status = 'summarizing';
@@ -829,27 +429,11 @@ export function onLeadClose(conversationId, _exitCode, resultReceived) {
829
429
  // Lead process crashed without producing a result — dissolve the team
830
430
  console.log(`[Team] Lead process exited without result — marking team as failed`);
831
431
  const team = activeTeam;
832
- // Mark remaining agents as error
833
- for (const agent of team.agents.values()) {
834
- if (agent.status === 'starting' || agent.status === 'working') {
835
- agent.status = 'error';
836
- }
837
- }
838
- // Mark remaining tasks as failed
839
- for (const task of team.tasks) {
840
- if (task.status === 'pending' || task.status === 'active') {
841
- task.status = 'failed';
842
- task.updatedAt = Date.now();
843
- }
844
- }
432
+ finalizeAgentsAndTasks(team, 'error', 'failed');
845
433
  team.status = 'failed';
846
434
  team.durationMs = Date.now() - team.createdAt;
847
435
  persistTeam(team);
848
- // Remove observers
849
- if (clearOutputObserverFn)
850
- clearOutputObserverFn();
851
- if (clearCloseObserverFn)
852
- clearCloseObserverFn();
436
+ cleanupObservers();
853
437
  // Notify clients
854
438
  sendFn({
855
439
  type: 'team_completed',
@@ -1025,29 +609,10 @@ export function dissolveTeam() {
1025
609
  if (cancelExecutionFn) {
1026
610
  cancelExecutionFn(team.conversationId);
1027
611
  }
1028
- // Mark remaining agents as error
1029
- for (const agent of team.agents.values()) {
1030
- if (agent.status === 'starting' || agent.status === 'working') {
1031
- agent.status = 'error';
1032
- }
1033
- }
1034
- // Mark remaining tasks as failed
1035
- for (const task of team.tasks) {
1036
- if (task.status === 'pending' || task.status === 'active') {
1037
- task.status = 'failed';
1038
- task.updatedAt = Date.now();
1039
- }
1040
- }
612
+ finalizeAgentsAndTasks(team, 'error', 'failed');
1041
613
  team.status = 'failed';
1042
614
  persistTeam(team);
1043
- // Remove output observer
1044
- if (clearOutputObserverFn) {
1045
- clearOutputObserverFn();
1046
- }
1047
- // Remove close observer
1048
- if (clearCloseObserverFn) {
1049
- clearCloseObserverFn();
1050
- }
615
+ cleanupObservers();
1051
616
  // Notify clients
1052
617
  sendFn({
1053
618
  type: 'team_completed',
@@ -1069,28 +634,9 @@ export function completeTeam(summary) {
1069
634
  if (summary) {
1070
635
  team.summary = summary;
1071
636
  }
1072
- // Mark all agents as done (unless they already errored)
1073
- for (const agent of team.agents.values()) {
1074
- if (agent.status !== 'error') {
1075
- agent.status = 'done';
1076
- }
1077
- }
1078
- // Mark remaining active/pending tasks as done
1079
- for (const task of team.tasks) {
1080
- if (task.status === 'active' || task.status === 'pending') {
1081
- task.status = 'done';
1082
- task.updatedAt = Date.now();
1083
- }
1084
- }
637
+ finalizeAgentsAndTasks(team, 'done', 'done');
1085
638
  persistTeam(team);
1086
- // Remove output observer
1087
- if (clearOutputObserverFn) {
1088
- clearOutputObserverFn();
1089
- }
1090
- // Remove close observer
1091
- if (clearCloseObserverFn) {
1092
- clearCloseObserverFn();
1093
- }
639
+ cleanupObservers();
1094
640
  // Notify clients
1095
641
  sendFn({
1096
642
  type: 'team_completed',