From ac3475d3beee6e4cd5ca75ae7e72e9caa2b5dfd1 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 7 Apr 2026 01:33:04 +0300 Subject: [PATCH] fix(team): support runtime bootstrap prompt sanitizing --- .../utils/bootstrapPromptSanitizer.ts | 29 ++++++++--- .../utils/bootstrapPromptSanitizer.test.ts | 52 +++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 test/renderer/utils/bootstrapPromptSanitizer.test.ts diff --git a/src/renderer/utils/bootstrapPromptSanitizer.ts b/src/renderer/utils/bootstrapPromptSanitizer.ts index 2d70260b..6fa03a42 100644 --- a/src/renderer/utils/bootstrapPromptSanitizer.ts +++ b/src/renderer/utils/bootstrapPromptSanitizer.ts @@ -2,15 +2,28 @@ import { displayMemberName } from '@renderer/utils/memberHelpers'; import type { InboxMessage } from '@shared/types'; -const BOOTSTRAP_REQUIRED_MARKERS = [ - 'Your FIRST action: call MCP tool member_briefing', - 'Do NOT start work, claim tasks, or improvise workflow/task/process rules before member_briefing succeeds.', +const BOOTSTRAP_REQUIRED_MARKER_SETS = [ + [ + 'Your FIRST action: call MCP tool member_briefing', + 'Do NOT start work, claim tasks, or improvise workflow/task/process rules before member_briefing succeeds.', + ], + [ + 'Your FIRST action: call MCP tool member_briefing', + 'The team has already been created and you are being attached as a persistent teammate.', + ], + [ + 'Your FIRST action: call MCP tool member_briefing', + 'The team has already been reconnected and you are being re-attached as a persistent teammate.', + ], ] as const; const BOOTSTRAP_SUPPORTING_MARKERS = [ 'If member_briefing fails, send', 'member_briefing is expected to be available in your initial MCP tool list.', 'IMPORTANT: When sending messages to the team lead', + 'Call member_briefing directly yourself. Do NOT use Agent', + 'wait for instructions from the lead and use team mailbox/task tools normally', + 'resume your queue normally and prioritize already-assigned board work', ] as const; type TeamProviderId = 'anthropic' | 'codex' | 'gemini'; @@ -136,7 +149,9 @@ export function getBootstrapPromptDisplay( message: Pick ): BootstrapPromptDisplay | null { const text = typeof message.text === 'string' ? message.text.trim() : ''; - const hasRequiredMarkers = BOOTSTRAP_REQUIRED_MARKERS.every((marker) => text.includes(marker)); + const hasRequiredMarkers = BOOTSTRAP_REQUIRED_MARKER_SETS.some((markerSet) => + markerSet.every((marker) => text.includes(marker)) + ); const hasSupportingMarker = BOOTSTRAP_SUPPORTING_MARKERS.some((marker) => text.includes(marker)); if (!text.startsWith('You are ') || !hasRequiredMarkers || !hasSupportingMarker) { return null; @@ -147,10 +162,10 @@ export function getBootstrapPromptDisplay( (typeof message.to === 'string' ? message.to.trim() : undefined); const teamName = matchField(text, /on team "([^"]+)"/); const providerId = parseProviderId( - matchField(text, /Provider override for this teammate:\s*([^\.\n]+)/i) + matchField(text, /Provider override(?: for this teammate)?:\s*([^\.\n]+)/i) ); - const model = matchField(text, /Model override for this teammate:\s*([^\.\n]+)/i); - const effort = matchField(text, /Effort override for this teammate:\s*([^\.\n]+)/i); + const model = matchField(text, /Model override(?: for this teammate)?:\s*([^\.\n]+)/i); + const effort = matchField(text, /Effort override(?: for this teammate)?:\s*([^\.\n]+)/i); const runtime = buildRuntimeSummary(providerId, model, effort); const displayName = teammateName ? displayMemberName(teammateName) : 'teammate'; const summary = `Starting ${displayName}`; diff --git a/test/renderer/utils/bootstrapPromptSanitizer.test.ts b/test/renderer/utils/bootstrapPromptSanitizer.test.ts new file mode 100644 index 00000000..509d08dd --- /dev/null +++ b/test/renderer/utils/bootstrapPromptSanitizer.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; + +import { + getBootstrapPromptDisplay, + getSanitizedInboxMessageText, +} from '@renderer/utils/bootstrapPromptSanitizer'; + +import type { InboxMessage } from '@shared/types'; + +function makeMessage(text: string, overrides: Partial = {}): InboxMessage { + return { + from: 'team-lead', + to: 'alice', + text, + timestamp: '2026-04-07T10:00:00.000Z', + read: false, + messageId: 'msg-1', + ...overrides, + }; +} + +describe('bootstrapPromptSanitizer', () => { + it('sanitizes legacy verbose bootstrap prompts', () => { + const message = makeMessage(`You are alice, a reviewer on team "forge-labs" (forge-labs). +Your FIRST action: call MCP tool member_briefing with: +{ teamName: "forge-labs", memberName: "alice" } +member_briefing is expected to be available in your initial MCP tool list. +Do NOT start work, claim tasks, or improvise workflow/task/process rules before member_briefing succeeds. +If member_briefing fails, send one short natural-language message to your team lead "team-lead". +IMPORTANT: When sending messages to the team lead, always use the exact name "team-lead".`); + + const display = getBootstrapPromptDisplay(message); + expect(display?.summary).toBe('Starting alice'); + expect(getSanitizedInboxMessageText(message)).toContain('Lead is starting `alice` as a teammate.'); + }); + + it('sanitizes new runtime-generated bootstrap prompts', () => { + const message = makeMessage(`You are alice, a reviewer on team "forge-labs" (forge-labs). +IMPORTANT: Communicate in English. All messages, summaries, and task descriptions MUST be in English. +The team has already been created and you are being attached as a persistent teammate. +Your FIRST action: call MCP tool member_briefing with: +{ teamName: "forge-labs", memberName: "alice" } +Call member_briefing directly yourself. Do NOT use Agent, any subagent, or a delegated helper for this bootstrap step. +If member_briefing fails, send one short natural-language message to "team-lead" with the exact error text. +After member_briefing succeeds, wait for instructions from the lead and use team mailbox/task tools normally. +Do NOT send acknowledgement-only messages such as "ready" or "online".`); + + const display = getBootstrapPromptDisplay(message); + expect(display?.summary).toBe('Starting alice'); + expect(getSanitizedInboxMessageText(message)).toContain('Startup instructions are hidden in the UI.'); + }); +});