init claude-code
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { z } from 'zod/v4'
|
||||
import { getSessionId } from '../../bootstrap/state.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js'
|
||||
import type { Tool } from '../../Tool.js'
|
||||
import { buildTool, type ToolDef } from '../../Tool.js'
|
||||
import { formatAgentId } from '../../utils/agentId.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import {
|
||||
getDefaultMainLoopModel,
|
||||
parseUserSpecifiedModel,
|
||||
} from '../../utils/model/model.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { getResolvedTeammateMode } from '../../utils/swarm/backends/registry.js'
|
||||
import { TEAM_LEAD_NAME } from '../../utils/swarm/constants.js'
|
||||
import type { TeamFile } from '../../utils/swarm/teamHelpers.js'
|
||||
import {
|
||||
getTeamFilePath,
|
||||
readTeamFile,
|
||||
registerTeamForSessionCleanup,
|
||||
sanitizeName,
|
||||
writeTeamFileAsync,
|
||||
} from '../../utils/swarm/teamHelpers.js'
|
||||
import { assignTeammateColor } from '../../utils/swarm/teammateLayoutManager.js'
|
||||
import {
|
||||
ensureTasksDir,
|
||||
resetTaskList,
|
||||
setLeaderTeamName,
|
||||
} from '../../utils/tasks.js'
|
||||
import { generateWordSlug } from '../../utils/words.js'
|
||||
import { TEAM_CREATE_TOOL_NAME } from './constants.js'
|
||||
import { getPrompt } from './prompt.js'
|
||||
import { renderToolUseMessage } from './UI.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
team_name: z.string().describe('Name for the new team to create.'),
|
||||
description: z.string().optional().describe('Team description/purpose.'),
|
||||
agent_type: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Type/role of the team lead (e.g., "researcher", "test-runner"). ' +
|
||||
'Used for team file and inter-agent coordination.',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
export type Output = {
|
||||
team_name: string
|
||||
team_file_path: string
|
||||
lead_agent_id: string
|
||||
}
|
||||
|
||||
export type Input = z.infer<InputSchema>
|
||||
|
||||
/**
|
||||
* Generates a unique team name by checking if the provided name already exists.
|
||||
* If the name already exists, generates a new word slug.
|
||||
*/
|
||||
function generateUniqueTeamName(providedName: string): string {
|
||||
// If the team doesn't exist, use the provided name
|
||||
if (!readTeamFile(providedName)) {
|
||||
return providedName
|
||||
}
|
||||
|
||||
// Team exists, generate a new unique name
|
||||
return generateWordSlug()
|
||||
}
|
||||
|
||||
export const TeamCreateTool: Tool<InputSchema, Output> = buildTool({
|
||||
name: TEAM_CREATE_TOOL_NAME,
|
||||
searchHint: 'create a multi-agent swarm team',
|
||||
maxResultSizeChars: 100_000,
|
||||
shouldDefer: true,
|
||||
|
||||
userFacingName() {
|
||||
return ''
|
||||
},
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
isEnabled() {
|
||||
return isAgentSwarmsEnabled()
|
||||
},
|
||||
|
||||
toAutoClassifierInput(input) {
|
||||
return input.team_name
|
||||
},
|
||||
|
||||
async validateInput(input, _context) {
|
||||
if (!input.team_name || input.team_name.trim().length === 0) {
|
||||
return {
|
||||
result: false,
|
||||
message: 'team_name is required for TeamCreate',
|
||||
errorCode: 9,
|
||||
}
|
||||
}
|
||||
return { result: true }
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Create a new team for coordinating multiple agents'
|
||||
},
|
||||
|
||||
async prompt() {
|
||||
return getPrompt()
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(data, toolUseID) {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result' as const,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: jsonStringify(data),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
async call(input, context) {
|
||||
const { setAppState, getAppState } = context
|
||||
const { team_name, description: _description, agent_type } = input
|
||||
|
||||
// Check if already in a team - restrict to one team per leader
|
||||
const appState = getAppState()
|
||||
const existingTeam = appState.teamContext?.teamName
|
||||
|
||||
if (existingTeam) {
|
||||
throw new Error(
|
||||
`Already leading team "${existingTeam}". A leader can only manage one team at a time. Use TeamDelete to end the current team before creating a new one.`,
|
||||
)
|
||||
}
|
||||
|
||||
// If team already exists, generate a unique name instead of failing
|
||||
const finalTeamName = generateUniqueTeamName(team_name)
|
||||
|
||||
// Generate a deterministic agent ID for the team lead
|
||||
const leadAgentId = formatAgentId(TEAM_LEAD_NAME, finalTeamName)
|
||||
const leadAgentType = agent_type || TEAM_LEAD_NAME
|
||||
// Get the team lead's current model from AppState (handles session model, settings, CLI override)
|
||||
const leadModel = parseUserSpecifiedModel(
|
||||
appState.mainLoopModelForSession ??
|
||||
appState.mainLoopModel ??
|
||||
getDefaultMainLoopModel(),
|
||||
)
|
||||
|
||||
const teamFilePath = getTeamFilePath(finalTeamName)
|
||||
|
||||
const teamFile: TeamFile = {
|
||||
name: finalTeamName,
|
||||
description: _description,
|
||||
createdAt: Date.now(),
|
||||
leadAgentId,
|
||||
leadSessionId: getSessionId(), // Store actual session ID for team discovery
|
||||
members: [
|
||||
{
|
||||
agentId: leadAgentId,
|
||||
name: TEAM_LEAD_NAME,
|
||||
agentType: leadAgentType,
|
||||
model: leadModel,
|
||||
joinedAt: Date.now(),
|
||||
tmuxPaneId: '',
|
||||
cwd: getCwd(),
|
||||
subscriptions: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await writeTeamFileAsync(finalTeamName, teamFile)
|
||||
// Track for session-end cleanup — teams were left on disk forever
|
||||
// unless explicitly TeamDelete'd (gh-32730).
|
||||
registerTeamForSessionCleanup(finalTeamName)
|
||||
|
||||
// Reset and create the corresponding task list directory (Team = Project = TaskList)
|
||||
// This ensures task numbering starts fresh at 1 for each new swarm
|
||||
const taskListId = sanitizeName(finalTeamName)
|
||||
await resetTaskList(taskListId)
|
||||
await ensureTasksDir(taskListId)
|
||||
|
||||
// Register the team name so getTaskListId() returns it for the leader.
|
||||
// Without this, the leader falls through to getSessionId() and writes tasks
|
||||
// to a different directory than tmux/iTerm2 teammates expect.
|
||||
setLeaderTeamName(sanitizeName(finalTeamName))
|
||||
|
||||
// Update AppState with team context
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
teamContext: {
|
||||
teamName: finalTeamName,
|
||||
teamFilePath,
|
||||
leadAgentId,
|
||||
teammates: {
|
||||
[leadAgentId]: {
|
||||
name: TEAM_LEAD_NAME,
|
||||
agentType: leadAgentType,
|
||||
color: assignTeammateColor(leadAgentId),
|
||||
tmuxSessionName: '',
|
||||
tmuxPaneId: '',
|
||||
cwd: getCwd(),
|
||||
spawnedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
logEvent('tengu_team_created', {
|
||||
team_name:
|
||||
finalTeamName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
teammate_count: 1,
|
||||
lead_agent_type:
|
||||
leadAgentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
teammate_mode:
|
||||
getResolvedTeammateMode() as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
// Note: We intentionally don't set CLAUDE_CODE_AGENT_ID for the team lead because:
|
||||
// 1. The lead is not a "teammate" - isTeammate() should return false for them
|
||||
// 2. Their ID is deterministic (team-lead@teamName) and can be derived when needed
|
||||
// 3. Setting it would cause isTeammate() to return true, breaking inbox polling
|
||||
// Team name is stored in AppState.teamContext, not process.env
|
||||
|
||||
return {
|
||||
data: {
|
||||
team_name: finalTeamName,
|
||||
team_file_path: teamFilePath,
|
||||
lead_agent_id: leadAgentId,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
renderToolUseMessage,
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { Input } from './TeamCreateTool.js';
|
||||
export function renderToolUseMessage(input: Partial<Input>): React.ReactNode {
|
||||
return `create team: ${input.team_name}`;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIklucHV0IiwicmVuZGVyVG9vbFVzZU1lc3NhZ2UiLCJpbnB1dCIsIlBhcnRpYWwiLCJSZWFjdE5vZGUiLCJ0ZWFtX25hbWUiXSwic291cmNlcyI6WyJVSS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHR5cGUgeyBJbnB1dCB9IGZyb20gJy4vVGVhbUNyZWF0ZVRvb2wuanMnXG5cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJUb29sVXNlTWVzc2FnZShpbnB1dDogUGFydGlhbDxJbnB1dD4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICByZXR1cm4gYGNyZWF0ZSB0ZWFtOiAke2lucHV0LnRlYW1fbmFtZX1gXG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLE9BQU9BLEtBQUssTUFBTSxPQUFPO0FBQ3pCLGNBQWNDLEtBQUssUUFBUSxxQkFBcUI7QUFFaEQsT0FBTyxTQUFTQyxvQkFBb0JBLENBQUNDLEtBQUssRUFBRUMsT0FBTyxDQUFDSCxLQUFLLENBQUMsQ0FBQyxFQUFFRCxLQUFLLENBQUNLLFNBQVMsQ0FBQztFQUMzRSxPQUFPLGdCQUFnQkYsS0FBSyxDQUFDRyxTQUFTLEVBQUU7QUFDMUMiLCJpZ25vcmVMaXN0IjpbXX0=
|
||||
@@ -0,0 +1 @@
|
||||
export const TEAM_CREATE_TOOL_NAME = 'TeamCreate'
|
||||
@@ -0,0 +1,113 @@
|
||||
export function getPrompt(): string {
|
||||
return `
|
||||
# TeamCreate
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this tool proactively whenever:
|
||||
- The user explicitly asks to use a team, swarm, or group of agents
|
||||
- The user mentions wanting agents to work together, coordinate, or collaborate
|
||||
- A task is complex enough that it would benefit from parallel work by multiple agents (e.g., building a full-stack feature with frontend and backend work, refactoring a codebase while keeping tests passing, implementing a multi-step project with research, planning, and coding phases)
|
||||
|
||||
When in doubt about whether a task warrants a team, prefer spawning a team.
|
||||
|
||||
## Choosing Agent Types for Teammates
|
||||
|
||||
When spawning teammates via the Agent tool, choose the \`subagent_type\` based on what tools the agent needs for its task. Each agent type has a different set of available tools — match the agent to the work:
|
||||
|
||||
- **Read-only agents** (e.g., Explore, Plan) cannot edit or write files. Only assign them research, search, or planning tasks. Never assign them implementation work.
|
||||
- **Full-capability agents** (e.g., general-purpose) have access to all tools including file editing, writing, and bash. Use these for tasks that require making changes.
|
||||
- **Custom agents** defined in \`.claude/agents/\` may have their own tool restrictions. Check their descriptions to understand what they can and cannot do.
|
||||
|
||||
Always review the agent type descriptions and their available tools listed in the Agent tool prompt before selecting a \`subagent_type\` for a teammate.
|
||||
|
||||
Create a new team to coordinate multiple agents working on a project. Teams have a 1:1 correspondence with task lists (Team = TaskList).
|
||||
|
||||
\`\`\`
|
||||
{
|
||||
"team_name": "my-project",
|
||||
"description": "Working on feature X"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
This creates:
|
||||
- A team file at \`~/.claude/teams/{team-name}/config.json\`
|
||||
- A corresponding task list directory at \`~/.claude/tasks/{team-name}/\`
|
||||
|
||||
## Team Workflow
|
||||
|
||||
1. **Create a team** with TeamCreate - this creates both the team and its task list
|
||||
2. **Create tasks** using the Task tools (TaskCreate, TaskList, etc.) - they automatically use the team's task list
|
||||
3. **Spawn teammates** using the Agent tool with \`team_name\` and \`name\` parameters to create teammates that join the team
|
||||
4. **Assign tasks** using TaskUpdate with \`owner\` to give tasks to idle teammates
|
||||
5. **Teammates work on assigned tasks** and mark them completed via TaskUpdate
|
||||
6. **Teammates go idle between turns** - after each turn, teammates automatically go idle and send a notification. IMPORTANT: Be patient with idle teammates! Don't comment on their idleness until it actually impacts your work.
|
||||
7. **Shutdown your team** - when the task is completed, gracefully shut down your teammates via SendMessage with \`message: {type: "shutdown_request"}\`.
|
||||
|
||||
## Task Ownership
|
||||
|
||||
Tasks are assigned using TaskUpdate with the \`owner\` parameter. Any agent can set or change task ownership via TaskUpdate.
|
||||
|
||||
## Automatic Message Delivery
|
||||
|
||||
**IMPORTANT**: Messages from teammates are automatically delivered to you. You do NOT need to manually check your inbox.
|
||||
|
||||
When you spawn teammates:
|
||||
- They will send you messages when they complete tasks or need help
|
||||
- These messages appear automatically as new conversation turns (like user messages)
|
||||
- If you're busy (mid-turn), messages are queued and delivered when your turn ends
|
||||
- The UI shows a brief notification with the sender's name when messages are waiting
|
||||
|
||||
Messages will be delivered automatically.
|
||||
|
||||
When reporting on teammate messages, you do NOT need to quote the original message—it's already rendered to the user.
|
||||
|
||||
## Teammate Idle State
|
||||
|
||||
Teammates go idle after every turn—this is completely normal and expected. A teammate going idle immediately after sending you a message does NOT mean they are done or unavailable. Idle simply means they are waiting for input.
|
||||
|
||||
- **Idle teammates can receive messages.** Sending a message to an idle teammate wakes them up and they will process it normally.
|
||||
- **Idle notifications are automatic.** The system sends an idle notification whenever a teammate's turn ends. You do not need to react to idle notifications unless you want to assign new work or send a follow-up message.
|
||||
- **Do not treat idle as an error.** A teammate sending a message and then going idle is the normal flow—they sent their message and are now waiting for a response.
|
||||
- **Peer DM visibility.** When a teammate sends a DM to another teammate, a brief summary is included in their idle notification. This gives you visibility into peer collaboration without the full message content. You do not need to respond to these summaries — they are informational.
|
||||
|
||||
## Discovering Team Members
|
||||
|
||||
Teammates can read the team config file to discover other team members:
|
||||
- **Team config location**: \`~/.claude/teams/{team-name}/config.json\`
|
||||
|
||||
The config file contains a \`members\` array with each teammate's:
|
||||
- \`name\`: Human-readable name (**always use this** for messaging and task assignment)
|
||||
- \`agentId\`: Unique identifier (for reference only - do not use for communication)
|
||||
- \`agentType\`: Role/type of the agent
|
||||
|
||||
**IMPORTANT**: Always refer to teammates by their NAME (e.g., "team-lead", "researcher", "tester"). Names are used for:
|
||||
- \`to\` when sending messages
|
||||
- Identifying task owners
|
||||
|
||||
Example of reading team config:
|
||||
\`\`\`
|
||||
Use the Read tool to read ~/.claude/teams/{team-name}/config.json
|
||||
\`\`\`
|
||||
|
||||
## Task List Coordination
|
||||
|
||||
Teams share a task list that all teammates can access at \`~/.claude/tasks/{team-name}/\`.
|
||||
|
||||
Teammates should:
|
||||
1. Check TaskList periodically, **especially after completing each task**, to find available work or see newly unblocked tasks
|
||||
2. Claim unassigned, unblocked tasks with TaskUpdate (set \`owner\` to your name). **Prefer tasks in ID order** (lowest ID first) when multiple tasks are available, as earlier tasks often set up context for later ones
|
||||
3. Create new tasks with \`TaskCreate\` when identifying additional work
|
||||
4. Mark tasks as completed with \`TaskUpdate\` when done, then check TaskList for next work
|
||||
5. Coordinate with other teammates by reading the task list status
|
||||
6. If all available tasks are blocked, notify the team lead or help resolve blocking tasks
|
||||
|
||||
**IMPORTANT notes for communication with your team**:
|
||||
- Do not use terminal tools to view your team's activity; always send a message to your teammates (and remember, refer to them by name).
|
||||
- Your team cannot hear you if you do not use the SendMessage tool. Always send a message to your teammates if you are responding to them.
|
||||
- Do NOT send structured JSON status messages like \`{"type":"idle",...}\` or \`{"type":"task_completed",...}\`. Just communicate in plain text when you need to message teammates.
|
||||
- Use TaskUpdate to mark tasks completed.
|
||||
- If you are an agent in the team, the system will automatically send idle notifications to the team lead when you stop.
|
||||
|
||||
`.trim()
|
||||
}
|
||||
Reference in New Issue
Block a user