refactor: remove task comment forwarding logic and clean up related code

- Deleted the TaskCommentForwarding module and its associated functions to simplify the codebase.
- Updated TeamDataService and TeamProvisioningService to remove references to task comment forwarding, ensuring a more straightforward implementation.
- Adjusted unit tests to reflect the removal of task comment forwarding functionality, enhancing clarity and maintainability.
This commit is contained in:
iliya 2026-03-15 11:27:12 +02:00
parent 96c9b00d92
commit f22a43854e
5 changed files with 19 additions and 217 deletions

View file

@ -35,7 +35,6 @@ import { TeamMemberResolver } from './TeamMemberResolver';
import { TeamMembersMetaStore } from './TeamMembersMetaStore';
import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { TeamTaskCommentNotificationJournal } from './TeamTaskCommentNotificationJournal';
import { getTaskCommentForwardingMode } from './TeamTaskCommentForwarding';
import { TeamTaskReader } from './TeamTaskReader';
import { TeamTaskWriter } from './TeamTaskWriter';
@ -1341,9 +1340,6 @@ export class TeamDataService {
recoverPending?: boolean;
}
): Promise<void> {
const mode = getTaskCommentForwardingMode();
if (mode === 'off') return;
const seedHistoricalIfJournalMissing = options?.seedHistoricalIfJournalMissing === true;
const recoverPending = options?.recoverPending === true;
let config: TeamConfig | null = null;
@ -1358,18 +1354,13 @@ export class TeamDataService {
const leadSessionId = config.leadSessionId;
if (!leadName.trim()) return;
const mutateLiveJournal = mode === 'on';
const journalExists = mutateLiveJournal
? await this.taskCommentNotificationJournal.exists(teamName)
: false;
if (mutateLiveJournal && !journalExists) {
const journalExists = await this.taskCommentNotificationJournal.exists(teamName);
if (!journalExists) {
await this.taskCommentNotificationJournal.ensureFile(teamName);
}
const leadInboxMessageIds =
mode === 'on' ? await this.getLeadInboxMessageIds(teamName, leadName) : new Set<string>();
const shouldSeedHistorical =
seedHistoricalIfJournalMissing && mutateLiveJournal && !journalExists;
const leadInboxMessageIds = await this.getLeadInboxMessageIds(teamName, leadName);
const shouldSeedHistorical = seedHistoricalIfJournalMissing && !journalExists;
const tasks = await this.taskReader.getTasks(teamName);
const scopedTasks =
taskId && !shouldSeedHistorical ? tasks.filter((task) => task.id === taskId) : tasks;
@ -1388,15 +1379,6 @@ export class TeamDataService {
);
if (notifications.length === 0) continue;
if (mode === 'dry-run') {
for (const notification of notifications) {
logger.info(
`[TeamDataService] Dry-run would forward task comment for ${teamName}#${notification.taskRef.displayId}:${notification.comment.id}`
);
}
continue;
}
const pending = await this.taskCommentNotificationJournal.withEntries(teamName, (entries) => {
const toSend: EligibleTaskCommentNotification[] = [];
let changed = false;
@ -1413,7 +1395,7 @@ export class TeamDataService {
author: notification.comment.author,
commentCreatedAt: notification.comment.createdAt,
messageId: notification.messageId,
state: shouldSeedHistorical || mode !== 'on' ? 'seeded' : 'pending_send',
state: shouldSeedHistorical ? 'seeded' : 'pending_send',
createdAt: now,
updatedAt: now,
});
@ -1422,7 +1404,7 @@ export class TeamDataService {
logger.info(
`[TeamDataService] Seeded historical task comment notification for ${teamName}#${notification.taskRef.displayId}:${notification.comment.id}`
);
} else if (mode === 'on') {
} else {
logger.info(
`[TeamDataService] Queued task comment notification for ${teamName}#${notification.taskRef.displayId}:${notification.comment.id}`
);

View file

@ -53,7 +53,6 @@ import { TeamInboxReader } from './TeamInboxReader';
import { TeamMcpConfigBuilder } from './TeamMcpConfigBuilder';
import { TeamMembersMetaStore } from './TeamMembersMetaStore';
import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { isTaskCommentForwardingLive } from './TeamTaskCommentForwarding';
import { TeamTaskReader } from './TeamTaskReader';
/**
@ -458,11 +457,8 @@ After member_briefing succeeds:
- 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.
- If a newly assigned task cannot be started immediately because you are still busy on another task, leave a short task comment on that waiting task right away with the reason and your best ETA, keep it in pending/TODO, and only move it to in_progress with task_start when you truly begin.
- CRITICAL: If a task gets a new comment and you are going to do additional implementation/fix/follow-up work on that same task, FIRST leave a short task comment saying what you are about to do, THEN move it to in_progress with task_start, THEN do the work, and when finished leave a short result comment and move it to done with task_complete. Never skip this comment -> reopen -> work -> comment -> done cycle.
- Direct messages to your team lead are only for urgent attention, no-task situations, or when the lead explicitly asked for a direct reply.${
isTaskCommentForwardingLive()
? '\n- If a task-scoped update is already recorded in a task comment, do NOT send a duplicate SendMessage to the lead with the same content unless you need urgent non-task attention.'
: ''
}
- Direct messages to your team lead are only for urgent attention, no-task situations, or when the lead explicitly asked for a direct reply.
- If a task-scoped update is already recorded in a task comment, do NOT send a duplicate SendMessage to the lead with the same content unless you need urgent non-task attention.
${buildTeammateAgentBlockReminder()}
${actionModeProtocol}`;
}
@ -538,11 +534,8 @@ ${actionModeProtocol}
- 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.
- Direct messages to your team lead are only for urgent attention, no-task situations, or when the lead explicitly asked for a direct reply.${
isTaskCommentForwardingLive()
? '\n - If a task-scoped update is already recorded in a task comment, do NOT send a duplicate SendMessage to the lead with the same content unless you need urgent non-task attention.'
: ''
}
- Direct messages to your team lead are only for urgent attention, no-task situations, or when the lead explicitly asked for a direct reply.
- If a task-scoped update is already recorded in a task comment, do NOT send a duplicate SendMessage to the lead with the same content unless you need urgent non-task attention.
- If you have no tasks, wait for new assignments.`;
}
@ -647,11 +640,8 @@ function buildTaskStatusProtocol(teamName: string): string {
{ teamName: "${teamName}", taskId: "<taskId>", text: "<your reply>", from: "<your-name>" }
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.${
isTaskCommentForwardingLive()
? '\n When task-comment forwarding is enabled in this runtime, do NOT send a duplicate SendMessage to the lead for the same task-scoped update unless you need urgent non-task attention.'
: ''
}
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.
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").
@ -760,11 +750,7 @@ function buildTeamCtlOpsInstructions(teamName: string, leadName: string): string
`- Task assignment notifications are handled by the board runtime, so do NOT send a separate SendMessage for the same assignment unless you have extra context that is not already on the task.`,
`- Review requests are also handled by the board runtime: review_request already notifies the reviewer, so do NOT send a second manual SendMessage for the same review request unless you are adding materially new context that is not already on the task.`,
`- If you receive a task-scoped system notification like "Comment on #...", treat the task as the source of truth and prefer replying via task_add_comment instead of continuing the same task discussion in direct messages.`,
`${
isTaskCommentForwardingLive()
? '- In this runtime, teammate task comments may already be auto-forwarded to you. When that happens, respond on-task first; use direct messages only for urgent wake-up pings or clearly non-task coordination.'
: '- Unless a runtime message explicitly says task-comment forwarding is active, do NOT assume task comments automatically notify you. Existing clarification/escalation paths still apply when someone needs guaranteed lead attention.'
}`,
`- Teammate task comments are auto-forwarded to you. When that happens, respond on-task first; use direct messages only for urgent wake-up pings or clearly non-task coordination.`,
`- Ownership must reflect the person actually doing the implementation/fix work. If someone takes over execution, update the owner immediately before they start. Do NOT leave the lead/planner as owner when another member is doing the work.`,
`- Set createdBy when creating tasks so workflow history shows who created the task.`,
``,

View file

@ -1,15 +0,0 @@
export const TASK_COMMENT_FORWARDING_ENV = 'CLAUDE_TEAM_TASK_COMMENT_FORWARDING';
export type TaskCommentForwardingMode = 'off' | 'dry-run' | 'on';
export function getTaskCommentForwardingMode(): TaskCommentForwardingMode {
const raw = process.env[TASK_COMMENT_FORWARDING_ENV]?.trim().toLowerCase();
if (raw === 'dry-run' || raw === 'on') {
return raw;
}
return 'off';
}
export function isTaskCommentForwardingLive(): boolean {
return getTaskCommentForwardingMode() === 'on';
}

View file

@ -1,10 +1,11 @@
import { describe, expect, it, vi } from 'vitest';
import { TeamDataService } from '../../../../src/main/services/team/TeamDataService';
import { TASK_COMMENT_FORWARDING_ENV } from '../../../../src/main/services/team/TeamTaskCommentForwarding';
import type { TeamTask } from '../../../../src/shared/types/team';
const TASK_COMMENT_FORWARDING_ENV = 'CLAUDE_TEAM_TASK_COMMENT_FORWARDING';
function createForwardingJournalStore(initialEntries: Array<Record<string, unknown>> = []) {
const journalEntries = initialEntries;
const journal = {
@ -763,147 +764,6 @@ describe('TeamDataService', () => {
}
});
it('does not mutate the live journal or send inbox rows in dry-run mode', async () => {
const previous = process.env[TASK_COMMENT_FORWARDING_ENV];
process.env[TASK_COMMENT_FORWARDING_ENV] = 'dry-run';
const journalEntries: Array<Record<string, unknown>> = [];
const inboxWriter = { sendMessage: vi.fn() };
const journal = {
exists: vi.fn(async () => true),
ensureFile: vi.fn(async () => undefined),
withEntries: vi.fn(async (_teamName: string, fn: (entries: unknown[]) => Promise<{ result: unknown }>) => {
const outcome = await fn(journalEntries);
return outcome.result;
}),
};
try {
const service = new TeamDataService(
{
listTeams: vi.fn(),
getConfig: vi.fn(async () => ({
name: 'My team',
members: [{ name: 'team-lead', role: 'Lead' }],
})),
} as never,
{
getTasks: vi.fn(async () => [
{
id: 'task-1',
displayId: 'abcd1234',
subject: 'Investigate',
status: 'pending',
owner: 'alice',
comments: [
{
id: 'comment-1',
author: 'alice',
text: 'Would forward in live mode.',
createdAt: '2026-03-14T10:00:00.000Z',
type: 'regular',
},
],
},
]),
} as never,
{
listInboxNames: vi.fn(async () => []),
getMessages: vi.fn(async () => []),
getMessagesFor: vi.fn(async () => []),
} as never,
inboxWriter as never,
{} as never,
{} as never,
{} as never,
{} as never,
{} as never,
{} as never,
(() => ({}) as never) as never,
journal as never
);
await service.notifyLeadOnTeammateTaskComment('my-team', 'task-1');
expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
expect(journal.withEntries).not.toHaveBeenCalled();
expect(journalEntries).toHaveLength(0);
} finally {
if (previous === undefined) delete process.env[TASK_COMMENT_FORWARDING_ENV];
else process.env[TASK_COMMENT_FORWARDING_ENV] = previous;
}
});
it('keeps feature-flag off mode as a no-op for the live journal', async () => {
const previous = process.env[TASK_COMMENT_FORWARDING_ENV];
process.env[TASK_COMMENT_FORWARDING_ENV] = 'off';
const journalEntries: Array<Record<string, unknown>> = [];
const inboxWriter = { sendMessage: vi.fn() };
const journal = {
exists: vi.fn(async () => true),
ensureFile: vi.fn(async () => undefined),
withEntries: vi.fn(async (_teamName: string, fn: (entries: unknown[]) => Promise<{ result: unknown }>) => {
const outcome = await fn(journalEntries);
return outcome.result;
}),
};
try {
const service = new TeamDataService(
{
listTeams: vi.fn(),
getConfig: vi.fn(async () => ({
name: 'My team',
members: [{ name: 'team-lead', role: 'Lead' }],
})),
} as never,
{
getTasks: vi.fn(async () => [
{
id: 'task-1',
displayId: 'abcd1234',
subject: 'Investigate',
status: 'pending',
owner: 'alice',
comments: [
{
id: 'comment-1',
author: 'alice',
text: 'Should stay untouched while off.',
createdAt: '2026-03-14T10:00:00.000Z',
type: 'regular',
},
],
},
]),
} as never,
{
listInboxNames: vi.fn(async () => []),
getMessages: vi.fn(async () => []),
getMessagesFor: vi.fn(async () => []),
} as never,
inboxWriter as never,
{} as never,
{} as never,
{} as never,
{} as never,
{} as never,
{} as never,
(() => ({}) as never) as never,
journal as never
);
await service.notifyLeadOnTeammateTaskComment('my-team', 'task-1');
expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
expect(journal.exists).not.toHaveBeenCalled();
expect(journal.withEntries).not.toHaveBeenCalled();
expect(journalEntries).toHaveLength(0);
} finally {
if (previous === undefined) delete process.env[TASK_COMMENT_FORWARDING_ENV];
else process.env[TASK_COMMENT_FORWARDING_ENV] = previous;
}
});
it('seeds historical eligible comments across the whole team on the first observed event when the journal is missing', async () => {
const previous = process.env[TASK_COMMENT_FORWARDING_ENV];
process.env[TASK_COMMENT_FORWARDING_ENV] = 'on';

View file

@ -6,13 +6,11 @@ import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN } from '@shared/constants/agentBlocks';
import { TASK_COMMENT_FORWARDING_ENV } from '@main/services/team/TeamTaskCommentForwarding';
let tempClaudeRoot = '';
let tempTeamsBase = '';
let tempTasksBase = '';
let originalMemberBriefingBootstrapEnv: string | undefined;
let originalTaskCommentForwardingEnv: string | undefined;
vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
ClaudeBinaryResolver: { resolve: vi.fn() },
@ -74,9 +72,7 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
beforeEach(() => {
vi.clearAllMocks();
originalMemberBriefingBootstrapEnv = process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV];
originalTaskCommentForwardingEnv = process.env[TASK_COMMENT_FORWARDING_ENV];
process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV] = '1';
process.env[TASK_COMMENT_FORWARDING_ENV] = 'off';
tempClaudeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-team-prompts-'));
tempTeamsBase = path.join(tempClaudeRoot, 'teams');
tempTasksBase = path.join(tempClaudeRoot, 'tasks');
@ -90,11 +86,6 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
} else {
process.env[MEMBER_BRIEFING_BOOTSTRAP_ENV] = originalMemberBriefingBootstrapEnv;
}
if (originalTaskCommentForwardingEnv === undefined) {
delete process.env[TASK_COMMENT_FORWARDING_ENV];
} else {
process.env[TASK_COMMENT_FORWARDING_ENV] = originalTaskCommentForwardingEnv;
}
// Best-effort cleanup of temp dir (per-test)
try {
fs.rmSync(tempClaudeRoot, { recursive: true, force: true });
@ -265,17 +256,16 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
expect(prompt).toContain(
'Direct messages to your team lead are only for urgent attention, no-task situations, or when the lead explicitly asked for a direct reply.'
);
expect(prompt).toContain(
'do NOT send a duplicate SendMessage to the lead with the same content unless you need urgent non-task attention.'
);
expect(prompt).not.toContain('Include the following agent-only instructions verbatim in the prompt:');
expect(prompt).not.toContain('runtime forwards task comments to the lead automatically');
expect(prompt).not.toContain(
'do NOT send a duplicate SendMessage to the lead for the same task-scoped update'
);
await svc.cancelProvisioning(runId);
});
it('includes live task-comment forwarding wording only when live forwarding is enabled', async () => {
process.env[TASK_COMMENT_FORWARDING_ENV] = 'on';
it('includes task-comment forwarding wording by default', async () => {
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude');
const { child, writeSpy } = createFakeChild();
vi.mocked(spawnCli).mockReturnValue(child as any);
@ -299,7 +289,6 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
);
const prompt = extractPromptFromWrite(writeSpy);
expect(prompt).toContain('task comments may already be auto-forwarded to you');
expect(prompt).toContain(
'do NOT send a duplicate SendMessage to the lead with the same content unless you need urgent non-task attention.'
);