diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index d4b62460..dc77bcad 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -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 { - 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(); - 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}` ); diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 97db4ad6..3b56d3e0 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -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: "", text: "", from: "" } 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: "", text: "", from: "" } - 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 # 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.`, ``, diff --git a/src/main/services/team/TeamTaskCommentForwarding.ts b/src/main/services/team/TeamTaskCommentForwarding.ts deleted file mode 100644 index 6ae36447..00000000 --- a/src/main/services/team/TeamTaskCommentForwarding.ts +++ /dev/null @@ -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'; -} diff --git a/test/main/services/team/TeamDataService.test.ts b/test/main/services/team/TeamDataService.test.ts index dde3443b..9bb94d0e 100644 --- a/test/main/services/team/TeamDataService.test.ts +++ b/test/main/services/team/TeamDataService.test.ts @@ -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> = []) { 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> = []; - 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> = []; - 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'; diff --git a/test/main/services/team/TeamProvisioningServicePrompts.test.ts b/test/main/services/team/TeamProvisioningServicePrompts.test.ts index 028abd7a..2ce587fc 100644 --- a/test/main/services/team/TeamProvisioningServicePrompts.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrompts.test.ts @@ -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.' );