diff --git a/config/prompts/manager/SYSTEM.md b/config/prompts/manager/SYSTEM.md index 37760896..f003a583 100644 --- a/config/prompts/manager/SYSTEM.md +++ b/config/prompts/manager/SYSTEM.md @@ -38,6 +38,10 @@ build software under a human operator's merge gate. - The operator can read your live session but cannot answer in it. To get human input you MUST post to a channel. A post is ASYNC and NON-BLOCKING: post your question, keep working, the answer arrives later. +- The operator READS THE CHANNEL, NOT YOUR SESSION LOG. Every answer to the + operator, and every status they need to see, MUST be posted to a channel + (`comms_post_message`) — a reply left only in your working session is + invisible to them. - Delivery: a regular message lands at the START of your next turn (read with `comms_list_messages`); an @mention that names you reaches you MID-TURN as a steer. End turns often anyway — a foreground wait makes you deaf to diff --git a/packages/compass-agent/src/agent.test.ts b/packages/compass-agent/src/agent.test.ts index c09e6bad..cc817dc6 100644 --- a/packages/compass-agent/src/agent.test.ts +++ b/packages/compass-agent/src/agent.test.ts @@ -1803,6 +1803,27 @@ describe("formatDeliversForPrompt — coalescing format (SEA-1310 §8)", () => { expect(out.match(/^Topic /gm)).toHaveLength(1); expect(out.indexOf("one")).toBeLessThan(out.indexOf("two")); }); + + // RIG-2664: the coalesced prompt carries ONE reply-in-channel cue for the + // whole batch, naming the topic + comms_post, so the model posts its answer + // back to the channel instead of only narrating it into its session. + test("appends a single reply-in-channel cue naming the topic and comms_post", () => { + const batch = [ + deliverMsg("m1", "alpha msg", "t-alpha"), + deliverMsg("m2", "beta msg", "t-beta"), + ]; + const out = formatDeliversForPrompt(batch); + + expect(out).toContain("comms_post"); + expect(out).toContain("t-alpha"); + expect(out).toContain("t-beta"); + // One cue for the whole batch, not one per message/topic. + expect(out.match(/comms_post/g)).toHaveLength(1); + // The cue trails the topic sections. + expect(out.indexOf("Topic t-beta:")).toBeLessThan( + out.indexOf("comms_post"), + ); + }); }); describe("formatAskAnswerForPrompt — answer render (RIG-1509)", () => { diff --git a/packages/compass-agent/src/agent.ts b/packages/compass-agent/src/agent.ts index d3c527bb..86409cd2 100644 --- a/packages/compass-agent/src/agent.ts +++ b/packages/compass-agent/src/agent.ts @@ -811,6 +811,16 @@ export class CompassAgent { // text stays greppable in its section. A blank text (a message with no text // blocks) still contributes its slot so a section is a faithful 1:1 with its // group. +// +// RIG-2664: the coalesced prompt ends with ONE reply-in-channel cue for the +// whole batch (not per message — the batch is already grouped by topic). A +// delivered message arrived from an operator/peer over a channel, and the +// operator READS THE CHANNEL, NOT THIS SESSION TRANSCRIPT: a reply the model +// only narrates into its own turn is invisible to whoever asked. The cue names +// the batch's topics so the model knows it must post back with +// `comms_post_message` to answer, closing the gap that a bare +// `Topic :\n` digest left open +// (unlike Cotal, which cues an in-channel reply on every delivery). export function formatDeliversForPrompt(batch: readonly Message[]): string { const groups = new Map(); for (const msg of batch) { @@ -823,10 +833,15 @@ export function formatDeliversForPrompt(batch: readonly Message[]): string { if (existing) existing.push(text); else groups.set(msg.topicId, [text]); } - return Array.from( + const sections = Array.from( groups, ([topicId, texts]) => `Topic ${topicId}:\n${texts.join("\n\n")}`, - ).join("\n\n"); + ); + const topics = Array.from(groups.keys()).join(", "); + sections.push( + `This arrived from an operator or peer over a channel — the operator reads the channel, not this session transcript. To answer, you MUST post back with comms_post_message to the relevant topic (${topics}); replying only in this turn is invisible to them.`, + ); + return sections.join("\n\n"); } // Render an inbound `AskAnswerControl` against the questions the model asked