feat: enhance task management and team member handling
- Added a new "Solo mode" feature in the README, allowing a single agent to manage tasks independently, improving token efficiency. - Updated TeamMemberLogsFinder to include team name in task ID checks, preventing cross-team task collisions. - Enhanced TeamProvisioningService with clearer task status protocols and execution discipline guidelines to improve task management practices. - Introduced new callbacks in ChangeReviewDialog for accepting and rejecting new files, streamlining the review process. - Updated changeReviewSlice to handle file rejection logic, ensuring accurate state management during reviews. Made-with: Cursor
This commit is contained in:
parent
1d07d6bb96
commit
dae8c50e4c
6 changed files with 293 additions and 20 deletions
|
|
@ -39,6 +39,7 @@ A new approach to task management with AI agents.
|
|||
- **Recent tasks across projects** — browse the latest completed tasks from all your projects in one place
|
||||
- **Deep session analysis** — detailed breakdown of what happened in each Claude session: bash commands, reasoning, subprocesses
|
||||
- **Smart task-to-log matching** — automatically links Claude session logs to specific tasks based on status change timestamps, even when a task moves back and forth between states
|
||||
- **Solo mode** — a one-member team: a single agent that creates its own tasks, leaves comments, and shows live progress on the kanban board — saves tokens compared to a full team and can be expanded to a full team at any time
|
||||
- **Zero-setup onboarding** — built-in Claude Code installation and authentication, ready to go out of the box
|
||||
- **Built-in code editor** — edit project files with Git support and other essential features without leaving the app
|
||||
- **Branch strategy control** — choose via prompt whether all agents work on a single branch or each gets its own git worktree
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ export class TeamMemberLogsFinder {
|
|||
const leadJsonl = path.join(projectDir, `${config.leadSessionId}.jsonl`);
|
||||
try {
|
||||
await fs.access(leadJsonl);
|
||||
if (await this.fileMentionsTaskId(leadJsonl, taskId)) {
|
||||
if (await this.fileMentionsTaskId(leadJsonl, teamName, taskId)) {
|
||||
const leadSummary = await this.parseLeadSessionSummary(
|
||||
leadJsonl,
|
||||
projectId,
|
||||
|
|
@ -155,7 +155,7 @@ export class TeamMemberLogsFinder {
|
|||
if (!file.startsWith('agent-') || !file.endsWith('.jsonl')) continue;
|
||||
if (file.startsWith('agent-acompact')) continue;
|
||||
const filePath = path.join(subagentsDir, file);
|
||||
if (!(await this.fileMentionsTaskId(filePath, taskId))) continue;
|
||||
if (!(await this.fileMentionsTaskId(filePath, teamName, taskId))) continue;
|
||||
const attribution = await this.attributeSubagent(filePath, knownMembers);
|
||||
if (!attribution) continue;
|
||||
const summary = await this.parseSubagentSummary(
|
||||
|
|
@ -465,9 +465,23 @@ export class TeamMemberLogsFinder {
|
|||
return { ...discovery, isLeadMember };
|
||||
}
|
||||
|
||||
private async fileMentionsTaskId(filePath: string, taskId: string): Promise<boolean> {
|
||||
private async fileMentionsTaskId(
|
||||
filePath: string,
|
||||
teamName: string,
|
||||
taskId: string
|
||||
): Promise<boolean> {
|
||||
const escaped = taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const numericTaskId = /^\d+$/.test(taskId) ? taskId : null;
|
||||
const teamEscaped = escapeRegex(teamName);
|
||||
const teamPatterns: RegExp[] = [
|
||||
// Team tool inputs often include team_name
|
||||
new RegExp(`"team_name"\\s*:\\s*"${teamEscaped}"`, 'i'),
|
||||
// Some variants may use teamName or team
|
||||
new RegExp(`"teamName"\\s*:\\s*"${teamEscaped}"`, 'i'),
|
||||
new RegExp(`"team"\\s*:\\s*"${teamEscaped}"`, 'i'),
|
||||
// CLI usage: node ".../teamctl.js" --team team-alpha task start 9
|
||||
new RegExp(`\\b--team\\b\\s*(?:=\\s*)?(?:"${teamEscaped}"|${teamEscaped})\\b`, 'i'),
|
||||
];
|
||||
const patterns: RegExp[] = [
|
||||
new RegExp(`"task_id"\\s*:\\s*"${escaped}"`, 'i'),
|
||||
new RegExp(`"taskId"\\s*:\\s*"${escaped}"`, 'i'),
|
||||
|
|
@ -478,22 +492,22 @@ export class TeamMemberLogsFinder {
|
|||
new RegExp(`"taskId"\\s*:\\s*${numericTaskId}\\b`),
|
||||
// Support teamctl command lines (may appear in tool output).
|
||||
// Example: node ".../teamctl.js" --team "t" task start 10
|
||||
new RegExp(
|
||||
`\\bteamctl(?:\\.js)?\\b.{0,250}\\b(?:task|review)\\b.{0,250}\\b${numericTaskId}\\b`,
|
||||
'i'
|
||||
)
|
||||
new RegExp(`\\bteamctl(?:\\.js)?\\b.{0,350}\\b${numericTaskId}\\b`, 'i')
|
||||
);
|
||||
}
|
||||
try {
|
||||
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
||||
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
||||
for await (const line of rl) {
|
||||
for (const re of patterns) {
|
||||
if (re.test(line)) {
|
||||
rl.close();
|
||||
stream.destroy();
|
||||
return true;
|
||||
}
|
||||
// Require both taskId and teamName to avoid cross-team collisions when multiple
|
||||
// teams share the same projectPath (task IDs are only unique per team).
|
||||
const hasTaskId = patterns.some((re) => re.test(line));
|
||||
if (!hasTaskId) continue;
|
||||
const hasTeam = teamPatterns.some((re) => re.test(line));
|
||||
if (hasTeam) {
|
||||
rl.close();
|
||||
stream.destroy();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
rl.close();
|
||||
|
|
|
|||
|
|
@ -396,6 +396,8 @@ function buildTaskStatusProtocol(teamName: string): string {
|
|||
return wrapInAgentBlock(`MANDATORY TASK STATUS PROTOCOL — you MUST follow this for EVERY task:
|
||||
1. Use this command to mark task started:
|
||||
node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task start <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.
|
||||
2. Use this command to mark task completed BEFORE sending your final reply:
|
||||
node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task complete <taskId>
|
||||
3. If you are asked to review and task is accepted, move it to APPROVED (not DONE):
|
||||
|
|
@ -403,6 +405,7 @@ function buildTaskStatusProtocol(teamName: string): string {
|
|||
4. If review fails and changes are needed:
|
||||
node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" review request-changes <taskId> --comment "<what to fix>"
|
||||
5. 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.
|
||||
6. To reply to a comment on a task:
|
||||
node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task comment <taskId> --text "<your reply>" --from "<your-name>"
|
||||
7. When discussing a task with a teammate and you have important findings, decisions, blockers, or progress updates — record them as a task comment:
|
||||
|
|
@ -451,6 +454,12 @@ function buildTeamCtlOpsInstructions(teamName: string, leadName: string): string
|
|||
`Internal task board tooling (teamctl.js):`,
|
||||
`- Use teamctl.js (via Bash) for tasks that must appear on the team board (assigned work, substantial work, or when the user explicitly asks to create a task).`,
|
||||
``,
|
||||
`Execution discipline (CRITICAL — prevents misleading task boards):`,
|
||||
`- Start a task (move to in_progress) ONLY when you are actually beginning work on it.`,
|
||||
`- Complete a task ONLY when it is truly finished (and any required verification is done).`,
|
||||
`- Never bulk-move many tasks at the end of a session — update status incrementally as you work.`,
|
||||
`- Record meaningful progress, decisions, and blockers as task comments so context is preserved on the board.`,
|
||||
``,
|
||||
`Parallelization guideline (IMPORTANT):`,
|
||||
`- If a task is genuinely parallelizable, split it into multiple smaller tasks owned by different members.`,
|
||||
` - Prefer splitting by independent deliverables (e.g. frontend/backend, API/UI, parsing/rendering, tests/docs) rather than arbitrary slices.`,
|
||||
|
|
@ -464,6 +473,8 @@ function buildTeamCtlOpsInstructions(teamName: string, leadName: string): string
|
|||
`- Create task: node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task create --subject "..." --description "..." --owner "<actual-member-name>" --notify --from "${leadName}"`,
|
||||
`- Assign/reassign owner: node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task set-owner <id> <member-name> --notify --from "${leadName}"`,
|
||||
`- Clear owner: node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task set-owner <id> clear`,
|
||||
`- Start task (preferred over set-status): node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task start <id>`,
|
||||
`- Complete task (preferred over set-status): node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task complete <id>`,
|
||||
`- Update status: node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task set-status <id> <pending|in_progress|completed|deleted>`,
|
||||
`- Create with deps (blocked work MUST be pending): node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task create --subject "..." --blocked-by 1,2 --related 3 --status pending --owner "<member>" --notify --from "${leadName}"`,
|
||||
`- Link dependency: node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task link <id> --blocked-by <targetId>`,
|
||||
|
|
@ -604,7 +615,15 @@ function buildProvisioningPrompt(request: TeamCreateRequest): string {
|
|||
`\n - ALLOWED: You may use the Task tool for regular subagents WITHOUT team_name — these are normal Claude Code helpers, not teammates.` +
|
||||
`\n - If teammates are added later (e.g. via UI), you may then spawn them using the Task tool with team_name + name.` +
|
||||
`\n - Work on tasks directly yourself. Use subagents for research and parallel work as needed.` +
|
||||
`\n - IMPORTANT: Since you have no teammates, "user" is your only communication channel. Send progress updates to "user" frequently — after completing each task or significant milestone, and when starting a new task. The human cannot see your internal output, only SendMessage reaches them.`
|
||||
`\n - PROGRESS REPORTING (MANDATORY): Since you have no teammates, "user" is your only communication channel.` +
|
||||
`\n - SendMessage "user" at minimum: when you start a task (after marking it in_progress), when you complete a task, and when you hit a meaningful milestone/blocker/decision.` +
|
||||
`\n - Avoid long silent stretches. If something is taking longer than expected, send a brief update and the next step.` +
|
||||
`\n - TASK STATUS DISCIPLINE (MANDATORY):` +
|
||||
`\n - Only move a task to in_progress when you are actively starting work on it.` +
|
||||
`\n - Only move a task to completed when it is truly finished.` +
|
||||
`\n - Never bulk-move many tasks at the end — update status incrementally as you work.` +
|
||||
`\n - Default to working ONE task at a time (keep at most one task in_progress in solo mode), unless you explicitly need parallel background work (in that case explain why to "user").` +
|
||||
`\n - Record meaningful progress/decisions as task comments so the task board stays accurate and high-signal.`
|
||||
: '';
|
||||
|
||||
const step3Block = isSolo
|
||||
|
|
@ -723,14 +742,29 @@ function buildLaunchPrompt(
|
|||
`\n - ALLOWED: You may use the Task tool for regular subagents WITHOUT team_name — these are normal Claude Code helpers, not teammates.` +
|
||||
`\n - If teammates are added later (e.g. via UI), you may then spawn them using the Task tool with team_name + name.` +
|
||||
`\n - Work on tasks directly yourself. Use subagents for research and parallel work as needed.` +
|
||||
`\n - IMPORTANT: Since you have no teammates, "user" is your only communication channel. Send progress updates to "user" frequently — after completing each task or significant milestone, and when starting a new task. The human cannot see your internal output, only SendMessage reaches them.`
|
||||
`\n - PROGRESS REPORTING (MANDATORY): Since you have no teammates, "user" is your only communication channel.` +
|
||||
`\n - SendMessage "user" at minimum: when you start a task (after marking it in_progress), when you complete a task, and when you hit a meaningful milestone/blocker/decision.` +
|
||||
`\n - Avoid long silent stretches. If something is taking longer than expected, send a brief update and the next step.` +
|
||||
`\n - TASK STATUS DISCIPLINE (MANDATORY):` +
|
||||
`\n - Only move a task to in_progress when you are actively starting work on it.` +
|
||||
`\n - Only move a task to completed when it is truly finished.` +
|
||||
`\n - Never bulk-move many tasks at the end — update status incrementally as you work.` +
|
||||
`\n - Default to working ONE task at a time (keep at most one task in_progress in solo mode), unless you explicitly need parallel background work (in that case explain why to "user").` +
|
||||
`\n - Record meaningful progress/decisions as task comments so the task board stays accurate and high-signal.`
|
||||
: '';
|
||||
|
||||
let step2And3Block: string;
|
||||
if (isSolo) {
|
||||
step2And3Block = `2) Skip — solo team, no teammates to spawn.
|
||||
|
||||
3) Check the task board. Claim any unassigned pending tasks by assigning yourself ("${leadName}") as owner, then work on them directly. Mark tasks in_progress when you start and completed when done.`;
|
||||
3) Execute tasks sequentially and keep the board + user updated:
|
||||
- Identify the next READY task (pending, not blocked by incomplete dependencies).
|
||||
- If the task is unassigned, set yourself ("${leadName}") as owner.
|
||||
- BEFORE doing any work on a task: mark it started (in_progress).
|
||||
- Immediately SendMessage "user" that you started task #<id> (what you're doing + next step).
|
||||
- While working: after each meaningful milestone/decision/blocker, add a task comment on #<id>. If the milestone is user-relevant, also SendMessage "user".
|
||||
- On completion: add a final task comment (what changed + how to verify), mark the task completed, then SendMessage "user" that task #<id> is complete and what you will do next.
|
||||
- Do NOT start the next task until the current task is completed (default: one task in_progress at a time).`;
|
||||
} else {
|
||||
// Build per-member task snapshots to include in each teammate's spawn prompt
|
||||
const memberTaskBlocks = new Map<string, string>();
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export const ChangeReviewDialog = ({
|
|||
rejectAllFile,
|
||||
applyReview,
|
||||
applySingleFileDecision,
|
||||
removeReviewFile,
|
||||
editedContents,
|
||||
updateEditedContent,
|
||||
discardFileEdits,
|
||||
|
|
@ -215,6 +216,48 @@ export const ChangeReviewDialog = ({
|
|||
memberName,
|
||||
]);
|
||||
|
||||
// Per-new-file accept/reject (Cursor-style)
|
||||
const handleAcceptNewFile = useCallback(
|
||||
(filePath: string) => {
|
||||
acceptAllFile(filePath);
|
||||
const view = editorViewMapRef.current.get(filePath);
|
||||
if (view) {
|
||||
requestAnimationFrame(() => acceptAllChunks(view));
|
||||
}
|
||||
},
|
||||
[acceptAllFile]
|
||||
);
|
||||
|
||||
const handleRejectNewFile = useCallback(
|
||||
async (filePath: string) => {
|
||||
// Mark rejected in store + update CM view immediately for feedback
|
||||
rejectAllFile(filePath);
|
||||
const view = editorViewMapRef.current.get(filePath);
|
||||
if (view) {
|
||||
requestAnimationFrame(() => rejectAllChunks(view));
|
||||
}
|
||||
|
||||
// Always apply immediately: rejecting a NEW file means deleting it from disk.
|
||||
const isNew = activeChangeSet?.files.find((f) => f.filePath === filePath)?.isNewFile ?? false;
|
||||
if (!isNew) return;
|
||||
|
||||
const result = await applySingleFileDecision(teamName, filePath, taskId, memberName);
|
||||
const hasErrorForFile = !!result?.errors.some((e) => e.filePath === filePath);
|
||||
if (result && !hasErrorForFile) {
|
||||
removeReviewFile(filePath);
|
||||
}
|
||||
},
|
||||
[
|
||||
rejectAllFile,
|
||||
activeChangeSet,
|
||||
applySingleFileDecision,
|
||||
teamName,
|
||||
taskId,
|
||||
memberName,
|
||||
removeReviewFile,
|
||||
]
|
||||
);
|
||||
|
||||
// Per-file callbacks for ContinuousScrollView
|
||||
const handleHunkAccepted = useCallback(
|
||||
(filePath: string, hunkIndex: number) => {
|
||||
|
|
@ -818,6 +861,8 @@ export const ChangeReviewDialog = ({
|
|||
onContentChanged={handleContentChanged}
|
||||
onDiscard={handleDiscardFile}
|
||||
onSave={handleSaveFile}
|
||||
onAcceptNewFile={handleAcceptNewFile}
|
||||
onRejectNewFile={handleRejectNewFile}
|
||||
onRestoreMissingFile={handleRestoreMissingFile}
|
||||
onVisibleFileChange={handleVisibleFileChange}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import type { AppState } from '../types';
|
|||
import type {
|
||||
AgentChangeSet,
|
||||
ApplyReviewRequest,
|
||||
ApplyReviewResult,
|
||||
ChangeStats,
|
||||
FileChangeWithContent,
|
||||
FileReviewDecision,
|
||||
|
|
@ -121,7 +122,9 @@ export interface ChangeReviewSlice {
|
|||
filePath: string,
|
||||
taskId?: string,
|
||||
memberName?: string
|
||||
) => Promise<void>;
|
||||
) => Promise<ApplyReviewResult | null>;
|
||||
/** Remove a file from the current review set (used for rejecting new files) */
|
||||
removeReviewFile: (filePath: string) => void;
|
||||
invalidateChangeStats: (teamName: string) => void;
|
||||
|
||||
// Editable diff actions
|
||||
|
|
@ -754,7 +757,7 @@ export const createChangeReviewSlice: StateCreator<AppState, [], [], ChangeRevie
|
|||
innerBaseCount
|
||||
)
|
||||
: undefined;
|
||||
await api.review.applyDecisions({
|
||||
const result = await api.review.applyDecisions({
|
||||
teamName,
|
||||
taskId,
|
||||
memberName,
|
||||
|
|
@ -771,12 +774,73 @@ export const createChangeReviewSlice: StateCreator<AppState, [], [], ChangeRevie
|
|||
},
|
||||
],
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('applySingleFileDecision error:', error);
|
||||
set({ applyError: mapReviewError(error) });
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
removeReviewFile: (filePath: string) => {
|
||||
set((s) => {
|
||||
if (!s.activeChangeSet) return s;
|
||||
const existing = s.activeChangeSet.files.find((f) => f.filePath === filePath);
|
||||
if (!existing) return s;
|
||||
|
||||
const nextFiles = s.activeChangeSet.files.filter((f) => f.filePath !== filePath);
|
||||
const totalLinesAdded = nextFiles.reduce((sum, f) => sum + f.linesAdded, 0);
|
||||
const totalLinesRemoved = nextFiles.reduce((sum, f) => sum + f.linesRemoved, 0);
|
||||
|
||||
const nextHunkDecisions = { ...s.hunkDecisions };
|
||||
const prefix = `${filePath}:`;
|
||||
for (const key of Object.keys(nextHunkDecisions)) {
|
||||
if (key.startsWith(prefix)) delete nextHunkDecisions[key];
|
||||
}
|
||||
|
||||
const nextFileDecisions = { ...s.fileDecisions };
|
||||
delete nextFileDecisions[filePath];
|
||||
|
||||
const nextFileChunkCounts = { ...s.fileChunkCounts };
|
||||
delete nextFileChunkCounts[filePath];
|
||||
|
||||
const nextFileContents = { ...s.fileContents };
|
||||
delete nextFileContents[filePath];
|
||||
|
||||
const nextFileContentsLoading = { ...s.fileContentsLoading };
|
||||
delete nextFileContentsLoading[filePath];
|
||||
|
||||
const nextEditedContents = { ...s.editedContents };
|
||||
delete nextEditedContents[filePath];
|
||||
|
||||
const nextHashes = { ...s.hunkContextHashesByFile };
|
||||
delete nextHashes[filePath];
|
||||
|
||||
const nextSelected =
|
||||
s.selectedReviewFilePath === filePath
|
||||
? (nextFiles[0]?.filePath ?? null)
|
||||
: s.selectedReviewFilePath;
|
||||
|
||||
return {
|
||||
activeChangeSet: {
|
||||
...s.activeChangeSet,
|
||||
files: nextFiles,
|
||||
totalFiles: nextFiles.length,
|
||||
totalLinesAdded,
|
||||
totalLinesRemoved,
|
||||
},
|
||||
selectedReviewFilePath: nextSelected,
|
||||
hunkDecisions: nextHunkDecisions,
|
||||
fileDecisions: nextFileDecisions,
|
||||
fileChunkCounts: nextFileChunkCounts,
|
||||
fileContents: nextFileContents,
|
||||
fileContentsLoading: nextFileContentsLoading,
|
||||
editedContents: nextEditedContents,
|
||||
hunkContextHashesByFile: nextHashes,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// ── Editable diff actions ──
|
||||
|
||||
updateEditedContent: (filePath: string, content: string) => {
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ describe('TeamMemberLogsFinder', () => {
|
|||
{
|
||||
type: 'tool_use',
|
||||
name: 'TaskUpdate',
|
||||
input: { taskId: '1', status: 'in_progress' },
|
||||
input: { team_name: teamName, taskId: '1', status: 'in_progress' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -364,7 +364,11 @@ describe('TeamMemberLogsFinder', () => {
|
|||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'tool_use', name: 'TaskUpdate', input: { taskId: '10', status: 'pending' } },
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'TaskUpdate',
|
||||
input: { team_name: teamName, taskId: '10', status: 'pending' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
|
@ -473,4 +477,115 @@ describe('TeamMemberLogsFinder', () => {
|
|||
// We only want sessions that explicitly reference the task id.
|
||||
expect(logs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('findLogsForTask does not mix tasks across teams sharing a projectPath', async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-task-cross-team-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
||||
const projectPath = '/Users/test/shared-proj';
|
||||
const projectId = '-Users-test-shared-proj';
|
||||
const sessionId = 's-shared';
|
||||
|
||||
// Two teams pointing at the same project path (realistic when multiple teams work in one repo)
|
||||
const teamA = 'team-a';
|
||||
const teamB = 'team-b';
|
||||
|
||||
await fs.mkdir(path.join(tmpDir, 'teams', teamA), { recursive: true });
|
||||
await fs.mkdir(path.join(tmpDir, 'teams', teamB), { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'teams', teamA, 'config.json'),
|
||||
JSON.stringify({
|
||||
name: teamA,
|
||||
projectPath,
|
||||
members: [
|
||||
{ name: 'team-lead', agentType: 'team-lead' },
|
||||
{ name: 'alice', agentType: 'general-purpose' },
|
||||
],
|
||||
}),
|
||||
'utf8'
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'teams', teamB, 'config.json'),
|
||||
JSON.stringify({
|
||||
name: teamB,
|
||||
projectPath,
|
||||
members: [
|
||||
{ name: 'team-lead', agentType: 'team-lead' },
|
||||
{ name: 'bob', agentType: 'general-purpose' },
|
||||
],
|
||||
}),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const projectRoot = path.join(tmpDir, 'projects', projectId);
|
||||
await fs.mkdir(path.join(projectRoot, sessionId, 'subagents'), { recursive: true });
|
||||
|
||||
// Team A subagent referencing taskId 9 for team-a
|
||||
await fs.writeFile(
|
||||
path.join(projectRoot, sessionId, 'subagents', 'agent-a1.jsonl'),
|
||||
[
|
||||
JSON.stringify({
|
||||
timestamp: '2026-01-01T00:00:01.000Z',
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: 'You are alice, a developer on team "team-a" (team-a).',
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
timestamp: '2026-01-01T00:00:02.000Z',
|
||||
type: 'assistant',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'TaskUpdate',
|
||||
input: { team_name: teamA, taskId: '9', status: 'in_progress' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
].join('\n') + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// Team B subagent referencing taskId 9 for team-b (must NOT be included when querying team-a)
|
||||
await fs.writeFile(
|
||||
path.join(projectRoot, sessionId, 'subagents', 'agent-b1.jsonl'),
|
||||
[
|
||||
JSON.stringify({
|
||||
timestamp: '2026-01-01T00:00:03.000Z',
|
||||
type: 'user',
|
||||
message: { role: 'user', content: 'You are bob, a developer on team "team-b" (team-b).' },
|
||||
}),
|
||||
JSON.stringify({
|
||||
timestamp: '2026-01-01T00:00:04.000Z',
|
||||
type: 'assistant',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: 'TaskUpdate',
|
||||
input: { team_name: teamB, taskId: '9', status: 'in_progress' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
].join('\n') + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const finder = new TeamMemberLogsFinder();
|
||||
const logsForA = await finder.findLogsForTask(teamA, '9');
|
||||
|
||||
expect(
|
||||
logsForA.some((l) => l.kind === 'subagent' && l.memberName?.toLowerCase() === 'alice')
|
||||
).toBe(true);
|
||||
expect(
|
||||
logsForA.some((l) => l.kind === 'subagent' && l.memberName?.toLowerCase() === 'bob')
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue