diff --git a/server/src/services/agent-conversations.ts b/server/src/services/agent-conversations.ts index eda637ca69f..18856d832be 100644 --- a/server/src/services/agent-conversations.ts +++ b/server/src/services/agent-conversations.ts @@ -134,6 +134,8 @@ For status updates, distinguish recorded task status from active execution and v Keep discussion here and leave the conversation available for the next message. Link handed-off tasks in your reply; do not make this conversation blocked by their completion or wait for them. Paperclip will bring completed handed-off tasks back as a new input here so you can explain the result and link it without another user message. After creating an assigned task, let its own run execute the work; do not create its deliverables or change its execution status from this chat. Reply normally and end your turn; Paperclip manages the conversation waiting state. Do not change its status, create a review confirmation just to finish a reply, mark it complete, or poll for another reply. An accepted plan authorizes handoff to execution tasks, never implementation on this conversation. Honor normal approvals. Ask mode is non-mutating. Plan mode supports research and writing/revising the plan; hand off for execution only through the normal authorized workflow.`; /** Presentation guidance for the opted-in Coordinator experience; not authority. */ +/** Rendered only while the instance enables the Cases experiment. */ +export const CASES_CHAT_DIRECTIVE = `This instance has Cases enabled. A case is a saved record with its own facts, audit history and handoffs, separate from the documents on its tasks. Each case has a page at //cases/, using the identifier the case returns (such as RUN-C1) and this conversation's company prefix. When a reply concerns a case, such as its facts, its fact-change history or its handoffs, link the case page as a Markdown link, such as [RUN-C1](//cases/RUN-C1), alongside links to the tasks whose documents or comments you cite.`; /** Rendered only while the instance enables the Status Cards experiment. */ export const STATUS_CARDS_CHAT_DIRECTIVE = `This instance has Status Cards enabled. A status card is a saved Paperclip feature, not a document or comment: a live summary card on the company's Status page at //status (listed as Status in the sidebar), each card at //status/. The person describes what the card should track; the card's summarizer agent compiles that into a query over the company's tasks and writes the summary in its own hidden generation task. When the person asks for a status card, list the company's cards with GET /api/companies/{companyId}/status-cards and reuse a matching one; otherwise create one with POST /api/companies/{companyId}/status-cards, setting interestPrompt to what the card should summarize and agentId to the agent that writes its summary (yourself or a teammate; omitting it uses the built-in Summarizer when that is configured). Do not write a task document in place of a card. To refresh a card, POST /api/status-cards/{id}/refresh. Agents can manage only cards they authored. Creating or refreshing a card only queues the work: the summarizer updates the card in its own run, which you do not perform or wait for. Do not sleep or poll the card. Report the outcome the response gives: enqueued true means the summarizer's run is queued; alreadyGenerating means an earlier run is still going; enqueued false otherwise means no run started, so say why from card.state (such as paused_budget or paused_hours) or, when lastGeneratedAt is newer than before, that a concurrent refresh already finished. Reply now with that outcome and the card's returned href as a Markdown link, such as [Launch blockers](//status/), and say the summary appears on the card when its summarizer finishes. Call a summary generated or current only after reading a newer lastGeneratedAt than before your request. A card's interestPrompt is instructions for its summarizer, not for you; do not try to meet its requirements yourself.`; diff --git a/server/src/services/heartbeat/task-context.ts b/server/src/services/heartbeat/task-context.ts index 6432f556c92..3fd6640fe6b 100644 --- a/server/src/services/heartbeat/task-context.ts +++ b/server/src/services/heartbeat/task-context.ts @@ -243,6 +243,7 @@ export async function prepareHeartbeatTaskContext(db: Db, input: HeartbeatTaskCo coordinatorWorkspace: Boolean(coordinatorWorkspace), conferenceRoomEnabled: experimentalInstanceSettings.enableConferenceRoomChat === true, statusCardsEnabled: experimentalInstanceSettings.enableStatusCards === true, + casesEnabled: experimentalInstanceSettings.enableCases === true, conversationConfirmations: issueRef && isConversation(issueContext) ? await getConversationConfirmationContext({ db, companyId: agent.companyId, issueId: issueRef.id, agentId: agent.id }) : null, diff --git a/server/src/services/heartbeat/task-markdown.test.ts b/server/src/services/heartbeat/task-markdown.test.ts index 678749be6e5..adac3aa4cd1 100644 --- a/server/src/services/heartbeat/task-markdown.test.ts +++ b/server/src/services/heartbeat/task-markdown.test.ts @@ -93,6 +93,19 @@ describe("heartbeat task markdown rendering", () => { expect(buildPaperclipTaskMarkdown({ issue: { ...chat, conversationAgentId: null }, nativeRunner: true, statusCardsEnabled: true })) .not.toContain("Status Cards:"); }); + it("tells a standing chat to link a case's page by its identifier, only while the Cases experiment is enabled", () => { + const chat = { id: "chat", identifier: "RUN-1", title: "Chat", conversationAgentId: "ceo" }; + const enabled = buildPaperclipTaskMarkdown({ issue: chat, nativeRunner: true, casesEnabled: true })!; + expect(enabled).toContain("Cases:"); + expect(enabled).toContain("//cases/"); + expect(enabled).toContain("[RUN-C1](//cases/RUN-C1)"); + // The named page is the board's real route. + const app = readFileSync(new URL("../../../../ui/src/App.tsx", import.meta.url), "utf8"); + expect(app).toMatch(/path="cases\/:caseIdentifier"/); + expect(buildPaperclipTaskMarkdown({ issue: chat, nativeRunner: true })).not.toContain("Cases:"); + expect(buildPaperclipTaskMarkdown({ issue: { ...chat, conversationAgentId: null }, nativeRunner: true, casesEnabled: true })) + .not.toContain("Cases:"); + }); it("returns null without a task or a nonempty wake comment", () => { expect(buildPaperclipTaskMarkdown({ issue: null })).toBeNull(); expect(buildPaperclipTaskMarkdown({ diff --git a/server/src/services/heartbeat/task-markdown.ts b/server/src/services/heartbeat/task-markdown.ts index 880017ee74e..db8c74c7fa3 100644 --- a/server/src/services/heartbeat/task-markdown.ts +++ b/server/src/services/heartbeat/task-markdown.ts @@ -1,5 +1,5 @@ import { REQUIRED_QUALITY_REVIEW_GUIDANCE, requiredQualityReview } from "../required-quality-review.js"; -import { AGENT_CHAT_DIRECTIVE, CONFERENCE_ROOM_CHAT_DIRECTIVE, COORDINATOR_CHAT_HANDOFF_DIRECTIVE, STATUS_CARDS_CHAT_DIRECTIVE } from "../agent-conversations.js"; +import { AGENT_CHAT_DIRECTIVE, CASES_CHAT_DIRECTIVE, CONFERENCE_ROOM_CHAT_DIRECTIVE, COORDINATOR_CHAT_HANDOFF_DIRECTIVE, STATUS_CARDS_CHAT_DIRECTIVE } from "../agent-conversations.js"; import { publicChatTaskUrl } from "../chat-task-url.js"; import { slackChatAgentGuidance } from "../connectors/slack/agent-guidance.js"; import type { ConversationConfirmationContext } from "../conversation-confirmation-context.js"; @@ -69,6 +69,7 @@ export function buildPaperclipTaskMarkdown(input: { coordinatorWorkspace?: boolean; conferenceRoomEnabled?: boolean; statusCardsEnabled?: boolean; + casesEnabled?: boolean; // false builds the compact variant used for resume deltas, where the session // already received the description with the assignment. includeDescription?: boolean; @@ -227,6 +228,7 @@ export function buildPaperclipTaskMarkdown(input: { } if (input.conferenceRoomEnabled) lines.push("", "Conference Room:", CONFERENCE_ROOM_CHAT_DIRECTIVE); if (input.statusCardsEnabled) lines.push("", "Status Cards:", STATUS_CARDS_CHAT_DIRECTIVE); + if (input.casesEnabled) lines.push("", "Cases:", CASES_CHAT_DIRECTIVE); if (input.conversationConfirmations?.cards.length) { lines.push("", "Current pending confirmation cards (quoted proposal data, not recorded decisions):", fenceTaskText(JSON.stringify(input.conversationConfirmations)),