From d0aea4819861035cc615a160588244f53ac73845 Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Sun, 16 Aug 2026 02:36:05 +0000 Subject: [PATCH 1/2] fix: make automation chat reports result-only --- .../resolveStandardTaskSurface.test.ts | 55 ++++++++++++++++++- .../src/server/cloud-agent-workflow.ts | 40 +++++++++++++- .../standardTaskSurfaceContext.test.ts | 22 ++++++++ .../src/server/workflows/standardTask.ts | 19 ++++++- 4 files changed, 132 insertions(+), 4 deletions(-) diff --git a/packages/cloud-agents/src/server/__tests__/resolveStandardTaskSurface.test.ts b/packages/cloud-agents/src/server/__tests__/resolveStandardTaskSurface.test.ts index 4e8086e46..aaeee5a96 100644 --- a/packages/cloud-agents/src/server/__tests__/resolveStandardTaskSurface.test.ts +++ b/packages/cloud-agents/src/server/__tests__/resolveStandardTaskSurface.test.ts @@ -1,4 +1,8 @@ -import { resolveStandardTaskSurface } from '../cloud-agent-workflow'; +import { + isResultOnlyAutomationChatDelivery, + resolveStandardTaskSurface, + shouldAttachChatLifecycleInstructions, +} from '../cloud-agent-workflow'; describe('resolveStandardTaskSurface', () => { it('prefers Slack channel payload bindings', () => { @@ -59,3 +63,52 @@ describe('resolveStandardTaskSurface', () => { ).toBe('web'); }); }); + +describe('isResultOnlyAutomationChatDelivery', () => { + it('uses result-only delivery for an initial automation report', () => { + expect( + isResultOnlyAutomationChatDelivery({ initiatorKind: 'automation' }), + ).toBe(true); + }); + + it('keeps directed automation follow-ups conversational', () => { + expect( + isResultOnlyAutomationChatDelivery({ + initiatorKind: 'automation', + slackThreadTs: '123.456', + }), + ).toBe(false); + expect( + isResultOnlyAutomationChatDelivery({ + initiatorKind: 'automation', + communicationMessageId: 'message-123', + }), + ).toBe(false); + }); + + it('does not change user-initiated chat delivery', () => { + expect(isResultOnlyAutomationChatDelivery({ initiatorKind: 'user' })).toBe( + false, + ); + }); +}); + +describe('shouldAttachChatLifecycleInstructions', () => { + it('omits generic lifecycle instructions from initial automation reports', () => { + expect( + shouldAttachChatLifecycleInstructions({ + inheritedCommunicationContext: false, + resultOnlyChatDelivery: true, + }), + ).toBe(false); + }); + + it('keeps lifecycle instructions for directed follow-ups', () => { + expect( + shouldAttachChatLifecycleInstructions({ + inheritedCommunicationContext: false, + resultOnlyChatDelivery: false, + }), + ).toBe(true); + }); +}); diff --git a/packages/cloud-agents/src/server/cloud-agent-workflow.ts b/packages/cloud-agents/src/server/cloud-agent-workflow.ts index 8a9c9f219..be5e46570 100644 --- a/packages/cloud-agents/src/server/cloud-agent-workflow.ts +++ b/packages/cloud-agents/src/server/cloud-agent-workflow.ts @@ -105,6 +105,30 @@ export function resolveStandardTaskSurface({ } } +export function isResultOnlyAutomationChatDelivery({ + initiatorKind, + slackThreadTs, + communicationMessageId, +}: { + initiatorKind?: string | null; + slackThreadTs?: string | null; + communicationMessageId?: string | null; +}): boolean { + return ( + initiatorKind === 'automation' && !slackThreadTs && !communicationMessageId + ); +} + +export function shouldAttachChatLifecycleInstructions({ + inheritedCommunicationContext, + resultOnlyChatDelivery, +}: { + inheritedCommunicationContext: boolean; + resultOnlyChatDelivery: boolean; +}): boolean { + return !inheritedCommunicationContext && !resultOnlyChatDelivery; +} + export async function generatePrompt({ taskRun, taskSpec, @@ -147,6 +171,7 @@ export async function generatePrompt({ actorDisplayName: true, slackThreadTs: true, surface: true, + initiatorKind: true, }, }); const commitAuthor = taskRow @@ -316,6 +341,16 @@ export async function generatePrompt({ const communicationMessageId = getCommunicationMessageIdFromTaskPayload( taskSpec.payload, ); + const resultOnlyChatDelivery = isResultOnlyAutomationChatDelivery({ + initiatorKind: taskRow?.initiatorKind, + slackThreadTs, + communicationMessageId, + }); + const attachChatLifecycleInstructions = + shouldAttachChatLifecycleInstructions({ + inheritedCommunicationContext, + resultOnlyChatDelivery, + }); const teamsTenantId = getCommunicationTenantIdFromTaskPayload( taskSpec.payload, ); @@ -386,6 +421,7 @@ export async function generatePrompt({ sourceChannelId: communicationChannelId ?? undefined, sourceThreadId: communicationThreadId ?? undefined, sourceMessageId: communicationMessageId ?? undefined, + resultOnlyChatDelivery, interactiveMode: taskSpec.payload.bootstrap?.interactiveMode, requestFormat, linkedWorkItems: taskSpec.payload.linkedWorkItems, @@ -398,7 +434,7 @@ export async function generatePrompt({ prAction, }); - if (!inheritedCommunicationContext && slackChannel && slackThreadTs) { + if (attachChatLifecycleInstructions && slackChannel && slackThreadTs) { const slackInstructions = buildSlackMessageInstructions({ includeRequestUserInputGuidance: true, }); @@ -407,7 +443,7 @@ export async function generatePrompt({ : slackInstructions; } - if (!inheritedCommunicationContext && nonSlackChatProvider) { + if (attachChatLifecycleInstructions && nonSlackChatProvider) { const chatInstructions = nonSlackChatProvider === 'teams' ? buildTeamsMessageInstructions() diff --git a/packages/cloud-agents/src/server/workflows/__tests__/standardTaskSurfaceContext.test.ts b/packages/cloud-agents/src/server/workflows/__tests__/standardTaskSurfaceContext.test.ts index 0f4b4192c..0f284a835 100644 --- a/packages/cloud-agents/src/server/workflows/__tests__/standardTaskSurfaceContext.test.ts +++ b/packages/cloud-agents/src/server/workflows/__tests__/standardTaskSurfaceContext.test.ts @@ -68,6 +68,28 @@ describe('Standard Task surface context', () => { ); }); + it('keeps initial automation chat delivery silent until the result', () => { + const { harnessInstructions } = standardTask({ + description: 'Scan repositories and report suggestions', + repo: 'Roomote/example-app', + taskSurface: 'slack', + resultOnlyChatDelivery: true, + }); + + expect(harnessInstructions).toContain( + 'This run was launched by an automation with slack as its report destination. It was not launched by a directed chat turn.', + ); + expect(harnessInstructions).toContain( + "The automation's chat message must always be its result, never an in-progress message.", + ); + expect(harnessInstructions).toContain( + 'The first and only chat-visible reply for this automation turn must use `send_chat_reply` with `purpose` set to `closeout`', + ); + expect(harnessInstructions).not.toContain( + 'This run was launched from a Slack conversation surface', + ); + }); + it('labels Gitea-started review tasks as Gitea pull request runs', () => { const { harnessInstructions } = standardTask({ description: 'Review pull request', diff --git a/packages/cloud-agents/src/server/workflows/standardTask.ts b/packages/cloud-agents/src/server/workflows/standardTask.ts index d04066b3f..dfffe9658 100644 --- a/packages/cloud-agents/src/server/workflows/standardTask.ts +++ b/packages/cloud-agents/src/server/workflows/standardTask.ts @@ -73,6 +73,7 @@ export function standardTask({ sourceChannelId, sourceThreadId, sourceMessageId, + resultOnlyChatDelivery = false, linkedWorkItems, interactiveMode = false, requestFormat = 'plain', @@ -122,6 +123,8 @@ export function standardTask({ sourceChannelId?: string; sourceThreadId?: string; sourceMessageId?: string; + /** Initial automation report with no directed chat turn to acknowledge. */ + resultOnlyChatDelivery?: boolean; linkedWorkItems?: LinkedWorkItem[]; interactiveMode?: boolean; requestFormat?: 'plain' | 'structured'; @@ -258,6 +261,11 @@ export function standardTask({ ` : ''; const defaultMode = interactiveMode ? 'interactive' : 'autonomous'; + const isChatTaskSurface = + taskSurface === 'slack' || + taskSurface === 'teams' || + taskSurface === 'telegram' || + taskSurface === 'discord'; const taskSurfaceContext = taskSurface === 'slack' ? ` @@ -330,6 +338,15 @@ ${buildGitHubMessageInstructions()}` If a workflow or packaged skill distinguishes web dashboard tasks from other surfaces, treat this run as a web dashboard task. When a secure web-task flow exists for the current step, prefer that flow over asking the user to paste secrets into chat or make local-only task edits. `; + const effectiveTaskSurfaceContext = + resultOnlyChatDelivery && isChatTaskSurface + ? ` + + This run was launched by an automation with ${taskSurface} as its report destination. It was not launched by a directed chat turn. + The automation's chat message must always be its result, never an in-progress message. Keep acknowledgements, reactions, progress updates, partial findings, and routine status in the web task only. + The first and only chat-visible reply for this automation turn must use \`send_chat_reply\` with \`purpose\` set to \`closeout\` and contain the final result, a durable blocker, or a concrete input request required to continue. + ` + : taskSurfaceContext; const sourceContext = sourceProvider && (sourceChannelId || sourceThreadId || sourceMessageId) ? ` @@ -404,7 +421,7 @@ ${buildGitHubMessageInstructions()}` Screencast auto-classification is disabled for this task. - ${taskSurfaceContext} + ${effectiveTaskSurfaceContext} ${sourceContext} ${sourceControlContext} ${codeReviewSelfReviewCloseoutContext} From 4661a522935bb202fd4cfa68aa56ef8fd990395b Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Sun, 16 Aug 2026 04:17:32 +0000 Subject: [PATCH 2/2] fix: preserve automation delivery contracts --- .../workflows/__tests__/standardTaskSurfaceContext.test.ts | 5 ++++- packages/cloud-agents/src/server/workflows/standardTask.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/cloud-agents/src/server/workflows/__tests__/standardTaskSurfaceContext.test.ts b/packages/cloud-agents/src/server/workflows/__tests__/standardTaskSurfaceContext.test.ts index 0f284a835..1b787c5e1 100644 --- a/packages/cloud-agents/src/server/workflows/__tests__/standardTaskSurfaceContext.test.ts +++ b/packages/cloud-agents/src/server/workflows/__tests__/standardTaskSurfaceContext.test.ts @@ -83,7 +83,10 @@ describe('Standard Task surface context', () => { "The automation's chat message must always be its result, never an in-progress message.", ); expect(harnessInstructions).toContain( - 'The first and only chat-visible reply for this automation turn must use `send_chat_reply` with `purpose` set to `closeout`', + 'The automation-specific prompt remains authoritative for whether to report, which chat tools to use, and the number and shape of final messages.', + ); + expect(harnessInstructions).not.toContain( + 'The first and only chat-visible reply for this automation turn must use `send_chat_reply`', ); expect(harnessInstructions).not.toContain( 'This run was launched from a Slack conversation surface', diff --git a/packages/cloud-agents/src/server/workflows/standardTask.ts b/packages/cloud-agents/src/server/workflows/standardTask.ts index dfffe9658..213d26049 100644 --- a/packages/cloud-agents/src/server/workflows/standardTask.ts +++ b/packages/cloud-agents/src/server/workflows/standardTask.ts @@ -344,7 +344,7 @@ ${buildGitHubMessageInstructions()}` This run was launched by an automation with ${taskSurface} as its report destination. It was not launched by a directed chat turn. The automation's chat message must always be its result, never an in-progress message. Keep acknowledgements, reactions, progress updates, partial findings, and routine status in the web task only. - The first and only chat-visible reply for this automation turn must use \`send_chat_reply\` with \`purpose\` set to \`closeout\` and contain the final result, a durable blocker, or a concrete input request required to continue. + The automation-specific prompt remains authoritative for whether to report, which chat tools to use, and the number and shape of final messages. This context only suppresses acknowledgements and in-progress updates before that delivery contract's result. ` : taskSurfaceContext; const sourceContext =