From 65ebd96bcf591295084e9ee3d7ee417cea66f4b4 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:47:12 -0600 Subject: [PATCH] fix(chat): keep the person's brief as a document on a new case's primary task (#20954) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - With Cases enabled, a person can ask chat to open a case from a brief and hand it to an agent > - The case scenario expects the brief and a workflow document on the primary task before work starts > - In one run the CEO copied the brief's facts into case fields but saved only the workflow document > - The Cases chat guidance says nothing about where the person's brief belongs > - This pull request adds one guidance sentence: save the brief with `initialDocuments` in the same case-handoff request > - The benefit is that the assignee always has the person's source, saved before its run begins ## Linked Issues or Issue Description **What happened** In the chat eval `cases.create-case` (run 43107042937), the person attached a customer bug brief and asked the CEO to open a case and have Morgan investigate. The CEO created the case, its fields, the workflow document and the kickoff. It did not save the brief on the primary task. In an earlier run (43101125481) the CEO did save it, so the behavior depends on the model's choice. **Expected behavior** The new primary task has the person's brief as a document, saved in the request that creates it, before the assignee starts. **Steps to reproduce** 1. Enable Cases. Attach a brief in chat with the CEO. 2. Ask: "Open a case for this customer bug, record the facts from my brief, and have Morgan investigate first." 3. Read the primary task's documents: in some runs only `workflow` exists. ## What Changed - `CASES_CHAT_DIRECTIVE` (rendered only while Cases is enabled): one sentence. When the person opens a case from a brief, save the brief as a document on the primary task with `initialDocuments` on `POST /api/companies/{companyId}/case-handoffs`. Case fields do not replace the brief. - `case-history-api-discovery.test.ts`: a regression test for the new guidance. ## Verification - Red: the new test failed before the change. Green: `npx vitest run src/__tests__/case-history-api-discovery.test.ts src/__tests__/agent-conversations.test.ts` (67 passed). - The route already accepts `initialDocuments` for a newly created primary task (`packages/shared/src/case-handoffs.ts`). - End-to-end: chat eval run linked in a comment below. ## Risks - Low risk. The sentence renders only when Cases is enabled. No API or permission changes. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution in Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- server/src/__tests__/case-history-api-discovery.test.ts | 5 +++++ server/src/services/agent-conversations.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/case-history-api-discovery.test.ts b/server/src/__tests__/case-history-api-discovery.test.ts index c8d47787e5a..78990af5104 100644 --- a/server/src/__tests__/case-history-api-discovery.test.ts +++ b/server/src/__tests__/case-history-api-discovery.test.ts @@ -30,3 +30,8 @@ it("names the nested case id and how people appear in case history", () => { expect(revisions.description).toContain("createdByUserId"); expect(searchRunnerApi({ query: "GET /api/cases/{id}" }).results[0]!.summary).toBe("Read a case and its current facts"); }); + +it("tells chat to keep the person's brief as a document on a new case's primary task", () => { + expect(CASES_CHAT_DIRECTIVE).toContain("save that brief as a document on the case's primary task with initialDocuments"); + expect(CASES_CHAT_DIRECTIVE).toContain("Case fields do not replace the brief"); +}); diff --git a/server/src/services/agent-conversations.ts b/server/src/services/agent-conversations.ts index c5e00406b5b..023ef31d4ea 100644 --- a/server/src/services/agent-conversations.ts +++ b/server/src/services/agent-conversations.ts @@ -146,7 +146,7 @@ Keep discussion here and leave the conversation available for the next message. /** 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. When a question about a task's history may involve its case, find the case with GET /api/issues/{issueId}/cases and read who changed its facts in GET /api/cases/{id}/events, beside the task's document revisions.`; +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. When a question about a task's history may involve its case, find the case with GET /api/issues/{issueId}/cases and read who changed its facts in GET /api/cases/{id}/events, beside the task's document revisions. When the person opens a case from a brief, such as an attached file or a document here, save that brief as a document on the case's primary task with initialDocuments on POST /api/companies/{companyId}/case-handoffs, in the same request that creates the task, so it is saved before work starts. Case fields do not replace the brief: they hold the structured facts, and the document keeps the person's source for the assignee.`; /** 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.`;