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:
parent
6798c4b9e7
commit
15df012c4b
6 changed files with 141 additions and 313 deletions
|
|
@ -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>)
|
||||
)
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue