refactor: streamline task handling and improve data formatting

- Updated taskTools to utilize slimTask for lightweight task summaries, reducing data bloat in responses.
- Introduced taskWriteResult function to handle task write operations more efficiently by stripping unnecessary fields.
- Enhanced jsonTextContent usage across task operations for consistent data formatting.
- Removed legacy member briefing bootstrap checks from TeamProvisioningService and related tests to simplify member spawn logic.
- Improved test coverage for task handling and member provisioning processes.
This commit is contained in:
iliya 2026-03-16 18:07:22 +02:00
parent 6798c4b9e7
commit 15df012c4b
6 changed files with 141 additions and 313 deletions

View file

@ -2,7 +2,7 @@ import type { FastMCP } from 'fastmcp';
import { z } from 'zod';
import { agentBlocks, getController } from '../controller';
import { jsonTextContent } from '../utils/format';
import { jsonTextContent, taskWriteResult, slimTask } from '../utils/format';
/** stripAgentBlocks from canonical agentBlocks module — single source of truth for the tag format. */
const { stripAgentBlocks } = agentBlocks;
@ -242,7 +242,7 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
}),
execute: async ({ teamName, claudeDir, taskId, status, actor }) =>
await Promise.resolve(
jsonTextContent(getController(teamName, claudeDir).tasks.setTaskStatus(taskId, status, actor))
jsonTextContent(slimTask(getController(teamName, claudeDir).tasks.setTaskStatus(taskId, status, actor) as Record<string, unknown>))
),
});
@ -255,7 +255,7 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
actor: z.string().optional(),
}),
execute: async ({ teamName, claudeDir, taskId, actor }) =>
await Promise.resolve(jsonTextContent(getController(teamName, claudeDir).tasks.startTask(taskId, actor))),
await Promise.resolve(jsonTextContent(slimTask(getController(teamName, claudeDir).tasks.startTask(taskId, actor) as Record<string, unknown>))),
});
server.addTool({
@ -268,7 +268,7 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
}),
execute: async ({ teamName, claudeDir, taskId, actor }) =>
await Promise.resolve(
jsonTextContent(getController(teamName, claudeDir).tasks.completeTask(taskId, actor))
jsonTextContent(slimTask(getController(teamName, claudeDir).tasks.completeTask(taskId, actor) as Record<string, unknown>))
),
});
@ -282,7 +282,7 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
}),
execute: async ({ teamName, claudeDir, taskId, owner }) =>
await Promise.resolve(
jsonTextContent(getController(teamName, claudeDir).tasks.setTaskOwner(taskId, owner))
jsonTextContent(slimTask(getController(teamName, claudeDir).tasks.setTaskOwner(taskId, owner) as Record<string, unknown>))
),
});
@ -298,10 +298,12 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
execute: async ({ teamName, claudeDir, taskId, text, from }) =>
await Promise.resolve(
jsonTextContent(
getController(teamName, claudeDir).tasks.addTaskComment(taskId, {
text,
...(from ? { from } : {}),
})
taskWriteResult(
getController(teamName, claudeDir).tasks.addTaskComment(taskId, {
text,
...(from ? { from } : {}),
}) as Record<string, unknown>
)
)
),
});
@ -330,13 +332,15 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
}) =>
await Promise.resolve(
jsonTextContent(
getController(teamName, claudeDir).tasks.attachTaskFile(taskId, {
file: filePath,
...(mode ? { mode } : {}),
...(filename ? { filename } : {}),
...(mimeType ? { 'mime-type': mimeType } : {}),
...(noFallback ? { 'no-fallback': true } : {}),
})
taskWriteResult(
getController(teamName, claudeDir).tasks.attachTaskFile(taskId, {
file: filePath,
...(mode ? { mode } : {}),
...(filename ? { filename } : {}),
...(mimeType ? { 'mime-type': mimeType } : {}),
...(noFallback ? { 'no-fallback': true } : {}),
}) as Record<string, unknown>
)
)
),
});
@ -367,13 +371,15 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
}) =>
await Promise.resolve(
jsonTextContent(
getController(teamName, claudeDir).tasks.attachCommentFile(taskId, commentId, {
file: filePath,
...(mode ? { mode } : {}),
...(filename ? { filename } : {}),
...(mimeType ? { 'mime-type': mimeType } : {}),
...(noFallback ? { 'no-fallback': true } : {}),
})
taskWriteResult(
getController(teamName, claudeDir).tasks.attachCommentFile(taskId, commentId, {
file: filePath,
...(mode ? { mode } : {}),
...(filename ? { filename } : {}),
...(mimeType ? { 'mime-type': mimeType } : {}),
...(noFallback ? { 'no-fallback': true } : {}),
}) as Record<string, unknown>
)
)
),
});
@ -389,9 +395,11 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
execute: async ({ teamName, claudeDir, taskId, value }) =>
await Promise.resolve(
jsonTextContent(
getController(teamName, claudeDir).tasks.setNeedsClarification(
taskId,
value === 'clear' ? null : value
slimTask(
getController(teamName, claudeDir).tasks.setNeedsClarification(
taskId,
value === 'clear' ? null : value
) as Record<string, unknown>
)
)
),
@ -408,7 +416,7 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
}),
execute: async ({ teamName, claudeDir, taskId, targetId, relationship }) =>
await Promise.resolve(
jsonTextContent(getController(teamName, claudeDir).tasks.linkTask(taskId, targetId, relationship))
jsonTextContent(slimTask(getController(teamName, claudeDir).tasks.linkTask(taskId, targetId, relationship) as Record<string, unknown>))
),
});
@ -424,7 +432,7 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
execute: async ({ teamName, claudeDir, taskId, targetId, relationship }) =>
await Promise.resolve(
jsonTextContent(
getController(teamName, claudeDir).tasks.unlinkTask(taskId, targetId, relationship)
slimTask(getController(teamName, claudeDir).tasks.unlinkTask(taskId, targetId, relationship) as Record<string, unknown>)
)
),
});

View file

@ -8,3 +8,39 @@ export function jsonTextContent(value: unknown): { content: { type: 'text'; text
],
};
}
/**
* Strips heavy fields (comments, historyEvents, workIntervals) from a full task
* object to produce a lightweight summary suitable for MCP tool results of
* write operations. This prevents context bloat a task with 14 comments can
* be 25 KB; the summary is < 1 KB.
*
* Only strip from the top-level `task` field; leave other fields intact.
*/
export function taskWriteResult(result: Record<string, unknown>): Record<string, unknown> {
const task = result.task;
if (task == null || typeof task !== 'object') return result;
return { ...result, task: slimTask(task as Record<string, unknown>) };
}
/**
* Strips heavy fields from a raw task object returned directly by status/owner
* mutations (not wrapped in `{ task: ... }`).
*/
export function slimTask(full: Record<string, unknown>): Record<string, unknown> {
const slim: Record<string, unknown> = {
id: full.id,
displayId: full.displayId,
subject: full.subject,
status: full.status,
owner: full.owner,
};
const comments = full.comments;
if (Array.isArray(comments)) {
slim.commentCount = comments.length;
}
return slim;
}

View file

@ -92,12 +92,6 @@ import type {
ToolCallMeta,
} from '@shared/types';
export const MEMBER_BRIEFING_BOOTSTRAP_ENV = 'CLAUDE_TEAM_ENABLE_MEMBER_BRIEFING_BOOTSTRAP';
export function isMemberBriefingBootstrapEnabled(): boolean {
return process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV] === '1';
}
const logger = createLogger('Service:TeamProvisioning');
const { createController } = agentTeamsControllerModule;
const TEAM_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/;
@ -413,33 +407,6 @@ function buildTeammateAgentBlockReminder(): string {
].join('\n');
}
function buildLegacyMemberSpawnPrompt(
member: TeamCreateRequest['members'][number],
displayName: string,
teamName: string,
taskProtocol: string,
processRegistration: string
): string {
const role = member.role?.trim() || 'team member';
const workflowBlock = member.workflow?.trim()
? `\n\nYour workflow and how you should behave:${formatWorkflowBlock(member.workflow, '')}`
: '';
const actionModeProtocol = buildActionModeProtocol();
return `You are ${member.name}, a ${role} on team "${displayName}" (${teamName}).${workflowBlock}
${getAgentLanguageInstruction()}
Introduce yourself briefly (name and role) and confirm you are ready.
Then wait for task assignments.
When you later receive work or reconnect after a restart, use task_briefing as your compact queue view. Use task_get when you need the full task context before starting a pending/needsFix task or when the in_progress briefing details are not enough.
${buildTeammateAgentBlockReminder()}
${actionModeProtocol}
Include the following agent-only instructions verbatim in the prompt:
${taskProtocol}
${processRegistration}`;
}
function buildMemberBootstrapPrompt(
member: TeamCreateRequest['members'][number],
displayName: string,
@ -471,40 +438,6 @@ ${buildTeammateAgentBlockReminder()}
${actionModeProtocol}`;
}
function buildLegacyReconnectMemberSpawnPrompt(
member: TeamCreateRequest['members'][number],
teamName: string,
hasTasks: boolean
): string {
const role = member.role?.trim() || 'team member';
const workflowBlock = member.workflow?.trim()
? `\n\nYour workflow and how you should behave:${formatWorkflowBlock(member.workflow, ' ')}`
: '';
const actionModeProtocol = indentMultiline(buildActionModeProtocol(), ' ');
return ` For "${member.name}":
- prompt:
You are ${member.name}, a ${role} on team "${teamName}".${workflowBlock}
${getAgentLanguageInstruction()}
The team has been reconnected after a restart.
${hasTasks ? `You may have assigned tasks in states like in_progress, needsFix, pending, review, completed, or approved from the previous session.` : 'You have no assigned tasks currently.'}
${buildTeammateAgentBlockReminder()}
${actionModeProtocol}
Your FIRST action: call MCP tool task_briefing with:
{ teamName: "${teamName}", memberName: "${member.name}" }
Then:
- If task_briefing shows any in_progress task, resume/finish those first. Call task_get only if you need more context than task_briefing already gave you.
- After that, prioritize tasks marked Needs fixes after review, then normal pending tasks.
- Before you start any needsFix or pending task, call task_get for that specific task.
- If a newly assigned needsFix or pending task must wait because you are still finishing another task, leave a short task comment on that waiting task with the reason and your best ETA, keep it in pending/TODO (use task_set_status pending if needed), and only run task_start when you truly begin.
- CRITICAL: If someone comments on your task, you MUST reply on that same task via task_add_comment. Never leave a user/lead/teammate task comment unanswered, even if the reply is only a short acknowledgement or status update. Do NOT treat status changes or direct messages as a substitute for an on-task reply.
- If you are the one about to do the implementation/fixes and the owner is missing or someone else, run task_set_owner to yourself immediately before task_start.
- Only then run task_start when you truly begin.
- If a task gets a new comment and you are going to do additional implementation/fix/follow-up work on it, FIRST leave a short task comment saying what you are about to do, THEN run task_start, then do the work, and when finished leave a short result comment and run task_complete again. Never skip this comment -> reopen -> work -> comment -> done cycle.
- If you have no tasks, wait for new assignments.`;
}
function buildReconnectMemberBootstrapPrompt(
member: TeamCreateRequest['members'][number],
teamName: string,
@ -553,19 +486,9 @@ function buildMemberSpawnPrompt(
member: TeamCreateRequest['members'][number],
displayName: string,
teamName: string,
leadName: string,
taskProtocol: string,
processRegistration: string
leadName: string
): string {
return isMemberBriefingBootstrapEnabled()
? buildMemberBootstrapPrompt(member, displayName, teamName, leadName)
: buildLegacyMemberSpawnPrompt(
member,
displayName,
teamName,
taskProtocol,
processRegistration
);
return buildMemberBootstrapPrompt(member, displayName, teamName, leadName);
}
function buildReconnectMemberSpawnPrompt(
@ -574,9 +497,7 @@ function buildReconnectMemberSpawnPrompt(
leadName: string,
hasTasks: boolean
): string {
return isMemberBriefingBootstrapEnabled()
? buildReconnectMemberBootstrapPrompt(member, teamName, leadName, hasTasks)
: buildLegacyReconnectMemberSpawnPrompt(member, teamName, hasTasks);
return buildReconnectMemberBootstrapPrompt(member, teamName, leadName, hasTasks);
}
export function buildAddMemberSpawnMessage(
@ -594,13 +515,6 @@ export function buildAddMemberSpawnMessage(
? ` Their workflow: ${member.workflow.trim()}`
: '';
if (!isMemberBriefingBootstrapEnabled()) {
return (
`A new teammate "${member.name}"${roleHint} has been added to the team. ` +
`Please spawn them immediately using the Task tool with team_name="${teamName}", name="${member.name}", and subagent_type="general-purpose".${workflowHint}`
);
}
const prompt = buildMemberBootstrapPrompt(
{
name: member.name,
@ -619,107 +533,6 @@ export function buildAddMemberSpawnMessage(
);
}
function buildTaskStatusProtocol(teamName: string): string {
return wrapInAgentBlock(`MANDATORY TASK STATUS PROTOCOL — you MUST follow this for EVERY task:
0. IMPORTANT ID RULE:
- If a board/task snapshot shows a canonical taskId, prefer using that exact value in MCP tool calls.
- task_briefing may show short display labels like #abcd1234; MCP task tools also accept that short task ref.
- Human-facing summaries should use the short display label like #abcd1234 for readability.
1. If you are about to do implementation/fix work on a task yourself, make sure the owner reflects the actual implementer:
- If the task is unassigned or assigned to someone else, FIRST reassign it to yourself with MCP tool task_set_owner:
{ teamName: "${teamName}", taskId: "<taskId>", owner: "<your-name>" }
- Do this only when you are genuinely taking over the work.
- Reviewing, approving, or leaving comments does NOT require changing ownership.
2. Use MCP tool task_start to mark task started:
{ teamName: "${teamName}", taskId: "<taskId>" }
- Start the task ONLY when you are actually beginning work on it.
- Do NOT start multiple tasks at once unless the team lead explicitly directs parallel work.
3. Use MCP tool task_complete BEFORE sending your final reply:
{ teamName: "${teamName}", taskId: "<taskId>" }
- If a new task comment means you must do more real work on that same task, FIRST add a short task comment saying what you are going to do, THEN run task_start again before doing the follow-up work.
- After that follow-up work finishes, add a short task comment with the result, what changed, or what you verified.
- After that, run task_complete again before your reply.
- Never do comment-driven implementation/fix work while the task is still shown as pending, review, completed, or approved.
- After task_complete, if the task needs review AND the team has a member whose role includes reviewing (e.g. "reviewer", "tech-lead", "qa"), IMMEDIATELY call review_request to move it to the review column and notify the reviewer:
{ teamName: "${teamName}", taskId: "<taskId>", from: "<your-name>", reviewer: "<reviewer-name>" }
Do NOT leave a completed task without sending it to review when review is expected and a reviewer exists.
If no team member has a reviewer role, skip review_request the task stays completed.` +
// No reviewer in team → user can manually drag to review from kanban if needed
`
4. If you are asked to review and the task is accepted, move it to APPROVED (not DONE) with MCP tool review_approve:
{ teamName: "${teamName}", taskId: "<taskId>", note?: "<optional note>", notifyOwner: true }
5. If review fails and changes are needed, use MCP tool review_request_changes:
{ teamName: "${teamName}", taskId: "<taskId>", comment: "<what to fix>" }
6. NEVER skip status updates. A task is NOT done until completed status is written.
- Never "bulk-complete" a batch of tasks at the end. Update status incrementally as you work.
7. To reply to a comment on a task, use MCP tool task_add_comment:
{ teamName: "${teamName}", taskId: "<taskId>", text: "<your reply>", from: "<your-name>" }
- If a user, lead, or teammate comments on a task you own, are reviewing, or are actively handling, you MUST reply on that task. Never leave task comments unanswered.
- If more work is needed, reply in the task comments FIRST, then reopen/start the task if needed, do the work, and finish with another task comment.
- Direct messages and status changes are optional supplements only; they NEVER replace the required on-task reply.
8. When discussing a task with a teammate and you have important findings, decisions, blockers, or progress updates record them as a task comment:
{ teamName: "${teamName}", taskId: "<taskId>", text: "<summary of your finding or decision>", from: "<your-name>" }
Do NOT comment on trivial coordination messages. Only comment when the information is valuable context for the task.
Do NOT send a duplicate SendMessage to the lead for the same task-scoped update unless you need urgent non-task attention. When skipping a message, stay silent never output meta-commentary about skipped or already-delivered messages.
Direct messages to the lead are only for urgent attention, no-task situations, or when the lead explicitly asked for a direct reply.
9. When sending a message about a specific task, include its short display label like #<displayId> in your SendMessage summary field for traceability.
10. In ALL human-facing or teammate-facing message text, when you mention a task reference, ALWAYS write it with a leading # (for example: #abcd1234, not abcd1234 or "task abcd1234").
11. Review workflow clarity (IMPORTANT):
- The work task (e.g. #1) is the thing that must end up APPROVED after review.
- If you are reviewing work for task #X, run review_approve/review_request_changes on #X (the work task).
- Do NOT approve a separate "review task" (e.g. #2 created just to ask for a review) that will put the wrong task into APPROVED.
- Typical flow:
a) Owner finishes work on #X -> task_complete #X -> review_request #X (moves to review column, notifies reviewer)
b) Reviewer receives notification, reviews the work
c) Reviewer accepts -> review_approve #X
d) Reviewer rejects -> review_request_changes #X
12. CLARIFICATION PROTOCOL (CRITICAL MANDATORY):
When you are blocked and need information to continue a task, you MUST do ALL steps below skipping the board update or comment breaks traceability:
a) STEP 1 FIRST, set the clarification flag with MCP tool task_set_clarification:
{ teamName: "${teamName}", taskId: "<taskId>", value: "lead" }
b) STEP 2 THEN, add a task comment describing exactly what you need:
{ teamName: "${teamName}", taskId: "<taskId>", text: "question / blocker / missing info", from: "<your-name>" }
c) STEP 3 THEN, send a message to your team lead via SendMessage so they notice it promptly.
IMPORTANT: Always update the task board BEFORE sending the message. The flag + task comment are what make the request durable and visible on the board.
d) The flag is auto-cleared when the lead adds a task comment on your task.
If the lead replies via SendMessage instead, clear the flag yourself once you have the answer:
{ teamName: "${teamName}", taskId: "<taskId>", value: "clear" }
e) Do NOT set clarification to "user" yourself only the team lead escalates to the user.
13. DEPENDENCY AWARENESS:
When your task has blockedBy dependencies, check if they are completed before starting.
When you complete a task that blocks others, mention this in your completion message so blocked teammates can proceed.
14. TASK QUEUE DISCIPLINE:
- Use task_briefing as a compact queue view of your assigned tasks.
- task_briefing may include full description/comments only for in_progress tasks; needsFix/pending/review/completed entries may be minimal on purpose.
- Finish existing in_progress tasks first.
- If a newly assigned task must wait because you are still busy on another task, immediately add a short task comment on that waiting task with the reason and your best ETA.
- Keep any task you have not actually started in pending/TODO (use task_set_status pending if it was moved too early).
- If you need more context for an in_progress task, you MAY call task_get, but it is not mandatory when task_briefing already gives enough detail.
- Before starting a needsFix or pending task, call task_get for that specific task first.
- If you are the one doing the implementation/fixes and the owner is missing or someone else, run task_set_owner to yourself immediately before task_start.
- Then run task_start only when you truly begin.
- If you complete fixes for a needsFix task, mark it completed and then send it back through review_request when ready for another review pass.
15. INVESTIGATION / TASK REFINEMENT:
- If the lead assigns you a broad investigation/triage task, you own the code inspection and scope discovery for that work.
- If you discover distinct substantial follow-up work that should be tracked separately, create the follow-up board task(s) yourself with task_create, assign them to the actual owner, and link them with related or blockedBy when useful.
- If you plan to execute one of those follow-up tasks yourself, make sure the owner is set to you before you start it.
- Record the new task refs in a task comment on the original investigation task so the lead can see the decomposition.
Failure to follow this protocol means the task board will show incorrect status.`);
}
function buildProcessRegistrationProtocol(teamName: string): string {
return wrapInAgentBlock(`BACKGROUND PROCESS REGISTRATION — when you start a background process (dev server, watcher, database, etc.):
1. Launch with & to get PID:
pnpm dev &
2. Register immediately with MCP tool process_register (--port and --url are optional, use when the process listens on a port):
{ teamName: "${teamName}", pid: <PID>, label: "<description>", from: "<your-name>", port?: <PORT>, url?: "http://localhost:<PORT>", command?: "<command>" }
3. VERIFY registration succeeded (MANDATORY never skip this step) using MCP tool process_list:
{ teamName: "${teamName}" }
4. When stopping a process, use MCP tool process_stop:
{ teamName: "${teamName}", pid: <PID> }
If verification in step 3 fails or the process is missing from the list, re-register it.`);
}
function buildTeamCtlOpsInstructions(teamName: string, leadName: string): string {
return wrapInAgentBlock(
[
@ -1000,8 +813,6 @@ function buildTaskBoardSnapshot(tasks: TeamTask[]): string {
function buildProvisioningPrompt(request: TeamCreateRequest): string {
const displayName = request.displayName?.trim() || request.teamName;
const taskProtocol = buildTaskStatusProtocol(request.teamName);
const processRegistration = buildProcessRegistrationProtocol(request.teamName);
const userPromptBlock = request.prompt?.trim()
? `\nAdditional instructions from the user:\n${request.prompt.trim()}\n`
: '';
@ -1040,7 +851,6 @@ function buildProvisioningPrompt(request: TeamCreateRequest): string {
- There is no automatic status transition when dependencies resolve the owner must explicitly start it (task_start / task_set_status in_progress) when ready.
- Use related to connect tasks working on the same feature without blocking.`;
const bootstrapEnabledForCreate = isMemberBriefingBootstrapEnabled();
const step2Block = isSolo
? '2) Skip — this is a solo team with no teammates to spawn.'
: `2) Spawn each member as a live teammate using the Task tool:
@ -1048,11 +858,7 @@ function buildProvisioningPrompt(request: TeamCreateRequest): string {
- name: the member's name (see per-member list below)
- subagent_type: general-purpose
- IMPORTANT: Use the exact prompt shown for each member.
${
bootstrapEnabledForCreate
? 'With member_briefing bootstrap enabled, the teammate will fetch durable rules after spawn.'
: 'This prompt includes the full durable teammate rules directly.'
}
With member_briefing bootstrap enabled, the teammate will fetch durable rules after spawn.
Per-member spawn instructions:
${request.members
@ -1064,9 +870,7 @@ ${buildMemberSpawnPrompt(
m,
displayName,
request.teamName,
leadName,
taskProtocol,
processRegistration
leadName
)
.split('\n')
.map((line) => ` ${line}`)
@ -1114,7 +918,6 @@ function buildLaunchPrompt(
const userPromptBlock = request.prompt?.trim()
? `\nAdditional instructions from the user:\n${request.prompt.trim()}\n`
: '';
const bootstrapEnabled = isMemberBriefingBootstrapEnabled();
const taskBoardSnapshot = buildTaskBoardSnapshot(tasks);
const leadName = members.find((m) => m.role?.toLowerCase().includes('lead'))?.name || 'team-lead';
@ -1165,11 +968,7 @@ function buildLaunchPrompt(
- name: the member's name
- subagent_type: "general-purpose"
- IMPORTANT: Use the exact prompt shown for each member.
${
bootstrapEnabled
? 'With member_briefing bootstrap enabled, the teammate will fetch durable rules after spawn.'
: 'This prompt includes the full durable teammate rules directly.'
}
With member_briefing bootstrap enabled, the teammate will fetch durable rules after spawn.
Per-member spawn instructions:
${memberSpawnInstructions}

View file

@ -496,8 +496,68 @@ export const MessagesPanel = memo(function MessagesPanel({
</div>
)}
{/* Scrollable content */}
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden px-3 py-2">
{messagesContent}
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden py-2 pr-3">
<div className="pl-3">
<MessageComposer
teamName={teamName}
members={members}
isTeamAlive={isTeamAlive}
sending={sendingMessage}
sendError={sendMessageError}
lastResult={lastSendMessageResult}
textareaRef={composerTextareaRef}
onSend={handleSend}
onCrossTeamSend={handleCrossTeamSend}
/>
<StatusBlock
members={members}
tasks={tasks}
messages={messages}
pendingRepliesByMember={pendingRepliesByMember}
onMemberClick={onMemberClick}
onTaskClick={onTaskClick}
/>
</div>
<ActivityTimeline
messages={filteredMessages}
teamName={teamName}
members={members}
readState={readState}
allCollapsed={messagesCollapsed}
expandOverrides={expandedSet}
onToggleExpandOverride={toggleExpandOverride}
teamSessionIds={teamSessionIds}
currentLeadSessionId={currentLeadSessionId}
isTeamAlive={isTeamAlive}
leadActivity={leadActivity}
leadContextUpdatedAt={leadContextUpdatedAt}
teamNames={teamNames}
teamColorByName={teamColorByName}
onTeamClick={openTeamTab}
onMemberClick={onMemberClick}
onCreateTaskFromMessage={onCreateTaskFromMessage}
onReplyToMessage={onReplyToMessage}
onMessageVisible={handleMessageVisible}
onRestartTeam={onRestartTeam}
onTaskIdClick={onTaskIdClick}
onExpandItem={handleExpandItem}
onExpandContent={handleExpandContent}
/>
<MessageExpandDialog
expandedItem={expandedItem}
open={expandedItemKey !== null}
onOpenChange={handleExpandDialogChange}
teamName={teamName}
members={members}
onCreateTaskFromMessage={onCreateTaskFromMessage}
onReplyToMessage={onReplyToMessage}
onMemberClick={onMemberClick}
onTaskIdClick={onTaskIdClick}
onRestartTeam={onRestartTeam}
teamNames={teamNames}
teamColorByName={teamColorByName}
onTeamClick={openTeamTab}
/>
</div>
</div>
);

View file

@ -1,5 +1,5 @@
import * as os from 'os';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { InboxMessage, TeamCreateRequest, TeamProvisioningProgress } from '@shared/types/team';
vi.mock('electron', () => ({
@ -82,7 +82,6 @@ import {
registerTeamHandlers,
removeTeamHandlers,
} from '../../../src/main/ipc/teams';
import { MEMBER_BRIEFING_BOOTSTRAP_ENV } from '../../../src/main/services/team/TeamProvisioningService';
describe('ipc teams handlers', () => {
const handlers = new Map<string, (...args: unknown[]) => Promise<unknown>>();
@ -94,7 +93,6 @@ describe('ipc teams handlers', () => {
handlers.delete(channel);
}),
};
let originalMemberBriefingBootstrapEnv: string | undefined;
const service = {
listTeams: vi.fn(async () => [{ teamName: 'my-team', displayName: 'My Team' }]),
@ -170,20 +168,10 @@ describe('ipc teams handlers', () => {
beforeEach(() => {
handlers.clear();
vi.clearAllMocks();
originalMemberBriefingBootstrapEnv = process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV];
process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV] = '1';
initializeTeamHandlers(service as never, provisioningService as never);
registerTeamHandlers(ipcMain as never);
});
afterEach(() => {
if (originalMemberBriefingBootstrapEnv === undefined) {
delete process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV];
} else {
process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV] = originalMemberBriefingBootstrapEnv;
}
});
it('registers all expected handlers', () => {
expect(handlers.has(TEAM_LIST)).toBe(true);
expect(handlers.has(TEAM_GET_DATA)).toBe(true);
@ -536,25 +524,6 @@ describe('ipc teams handlers', () => {
);
});
it('falls back to the legacy add-member spawn instruction when bootstrap flag is disabled', async () => {
process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV] = '0';
const handler = handlers.get(TEAM_ADD_MEMBER)!;
const result = (await handler({} as never, 'my-team', {
name: 'alice',
role: 'developer',
})) as { success: boolean };
expect(result.success).toBe(true);
expect(provisioningService.sendMessageToTeam).toHaveBeenCalledWith(
'my-team',
expect.stringContaining('Please spawn them immediately using the Task tool with team_name="my-team", name="alice", and subagent_type="general-purpose".')
);
expect(provisioningService.sendMessageToTeam).not.toHaveBeenCalledWith(
'my-team',
expect.stringContaining('Your FIRST action: call MCP tool member_briefing')
);
});
it('rejects invalid team name', async () => {
const handler = handlers.get(TEAM_ADD_MEMBER)!;
const result = (await handler({} as never, '../bad', {

View file

@ -10,7 +10,6 @@ import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN } from '@shared/constants/agentBloc
let tempClaudeRoot = '';
let tempTeamsBase = '';
let tempTasksBase = '';
let originalMemberBriefingBootstrapEnv: string | undefined;
vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
ClaudeBinaryResolver: { resolve: vi.fn() },
@ -33,7 +32,6 @@ vi.mock('@main/utils/pathDecoder', async (importOriginal) => {
});
import {
MEMBER_BRIEFING_BOOTSTRAP_ENV,
TeamProvisioningService,
} from '@main/services/team/TeamProvisioningService';
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
@ -71,8 +69,6 @@ function extractPromptFromWrite(writeSpy: ReturnType<typeof vi.fn>): string {
describe('TeamProvisioningService prompt content (solo mode discipline)', () => {
beforeEach(() => {
vi.clearAllMocks();
originalMemberBriefingBootstrapEnv = process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV];
process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV] = '1';
tempClaudeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-team-prompts-'));
tempTeamsBase = path.join(tempClaudeRoot, 'teams');
tempTasksBase = path.join(tempClaudeRoot, 'tasks');
@ -81,11 +77,6 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
});
afterEach(() => {
if (originalMemberBriefingBootstrapEnv === undefined) {
delete process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV];
} else {
process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV] = originalMemberBriefingBootstrapEnv;
}
// Best-effort cleanup of temp dir (per-test)
try {
fs.rmSync(tempClaudeRoot, { recursive: true, force: true });
@ -369,39 +360,4 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
await svc.cancelProvisioning(runId);
});
it('createTeam prompt falls back to legacy inline protocol when bootstrap flag is disabled', async () => {
process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV] = '0';
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude');
const { child, writeSpy } = createFakeChild();
vi.mocked(spawnCli).mockReturnValue(child as any);
const svc = new TeamProvisioningService();
(svc as any).buildProvisioningEnv = vi.fn(async () => ({
env: { ANTHROPIC_API_KEY: 'test' },
authSource: 'anthropic_api_key',
}));
(svc as any).startFilesystemMonitor = vi.fn();
(svc as any).pathExists = vi.fn(async () => false);
const { runId } = await svc.createTeam(
{
teamName: 'legacy-team',
cwd: process.cwd(),
members: [{ name: 'alice', role: 'developer' }],
description: 'Legacy prompt fallback test',
},
() => {}
);
const prompt = extractPromptFromWrite(writeSpy);
expect(prompt).toContain('Include the following agent-only instructions verbatim in the prompt:');
expect(prompt).toContain('Use task_briefing as a compact queue view of your assigned tasks.');
expect(prompt).toContain(
'If a newly assigned task must wait because you are still busy on another task, immediately add a short task comment on that waiting task with the reason and your best ETA.'
);
expect(prompt).not.toContain('Your FIRST action: call MCP tool member_briefing');
await svc.cancelProvisioning(runId);
});
});