/ utils / swarm / teammateInit.ts
teammateInit.ts
  1  /**
  2   * Teammate Initialization Module
  3   *
  4   * Handles initialization for Claude Code instances running as teammates in a swarm.
  5   * Registers a Stop hook to notify the team leader when the teammate becomes idle.
  6   */
  7  
  8  import type { AppState } from '../../state/AppState.js'
  9  import { logForDebugging } from '../debug.js'
 10  import { addFunctionHook } from '../hooks/sessionHooks.js'
 11  import { applyPermissionUpdate } from '../permissions/PermissionUpdate.js'
 12  import { jsonStringify } from '../slowOperations.js'
 13  import { getTeammateColor } from '../teammate.js'
 14  import {
 15    createIdleNotification,
 16    getLastPeerDmSummary,
 17    writeToMailbox,
 18  } from '../teammateMailbox.js'
 19  import { readTeamFile, setMemberActive } from './teamHelpers.js'
 20  
 21  /**
 22   * Initializes hooks for a teammate running in a swarm.
 23   * Should be called early in session startup after AppState is available.
 24   *
 25   * Registers a Stop hook that sends an idle notification to the team leader
 26   * when this teammate's session stops.
 27   */
 28  export function initializeTeammateHooks(
 29    setAppState: (updater: (prev: AppState) => AppState) => void,
 30    sessionId: string,
 31    teamInfo: { teamName: string; agentId: string; agentName: string },
 32  ): void {
 33    const { teamName, agentId, agentName } = teamInfo
 34  
 35    // Read team file to get leader ID
 36    const teamFile = readTeamFile(teamName)
 37    if (!teamFile) {
 38      logForDebugging(`[TeammateInit] Team file not found for team: ${teamName}`)
 39      return
 40    }
 41  
 42    const leadAgentId = teamFile.leadAgentId
 43  
 44    // Apply team-wide allowed paths if any exist
 45    if (teamFile.teamAllowedPaths && teamFile.teamAllowedPaths.length > 0) {
 46      logForDebugging(
 47        `[TeammateInit] Found ${teamFile.teamAllowedPaths.length} team-wide allowed path(s)`,
 48      )
 49  
 50      for (const allowedPath of teamFile.teamAllowedPaths) {
 51        // For absolute paths (starting with /), prepend one / to create //path/** pattern
 52        // For relative paths, just use path/**
 53        const ruleContent = allowedPath.path.startsWith('/')
 54          ? `/${allowedPath.path}/**`
 55          : `${allowedPath.path}/**`
 56  
 57        logForDebugging(
 58          `[TeammateInit] Applying team permission: ${allowedPath.toolName} allowed in ${allowedPath.path} (rule: ${ruleContent})`,
 59        )
 60  
 61        setAppState(prev => ({
 62          ...prev,
 63          toolPermissionContext: applyPermissionUpdate(
 64            prev.toolPermissionContext,
 65            {
 66              type: 'addRules',
 67              rules: [
 68                {
 69                  toolName: allowedPath.toolName,
 70                  ruleContent,
 71                },
 72              ],
 73              behavior: 'allow',
 74              destination: 'session',
 75            },
 76          ),
 77        }))
 78      }
 79    }
 80  
 81    // Find the leader's name from the members array
 82    const leadMember = teamFile.members.find(m => m.agentId === leadAgentId)
 83    const leadAgentName = leadMember?.name || 'team-lead'
 84  
 85    // Don't register hook if this agent is the leader
 86    if (agentId === leadAgentId) {
 87      logForDebugging(
 88        '[TeammateInit] This agent is the team leader - skipping idle notification hook',
 89      )
 90      return
 91    }
 92  
 93    logForDebugging(
 94      `[TeammateInit] Registering Stop hook for teammate ${agentName} to notify leader ${leadAgentName}`,
 95    )
 96  
 97    // Register Stop hook to notify leader when this teammate stops
 98    addFunctionHook(
 99      setAppState,
100      sessionId,
101      'Stop',
102      '', // No matcher - applies to all Stop events
103      async (messages, _signal) => {
104        // Mark this teammate as idle in the team config (fire and forget)
105        void setMemberActive(teamName, agentName, false)
106  
107        // Send idle notification to the team leader using agent name (not UUID)
108        // Must await to ensure the write completes before process shutdown
109        const notification = createIdleNotification(agentName, {
110          idleReason: 'available',
111          summary: getLastPeerDmSummary(messages),
112        })
113        await writeToMailbox(leadAgentName, {
114          from: agentName,
115          text: jsonStringify(notification),
116          timestamp: new Date().toISOString(),
117          color: getTeammateColor(),
118        })
119        logForDebugging(
120          `[TeammateInit] Sent idle notification to leader ${leadAgentName}`,
121        )
122        return true // Don't block the Stop
123      },
124      'Failed to send idle notification to team leader',
125      {
126        timeout: 10000,
127      },
128    )
129  }