fix(status-cards): link listed cards and report the real refresh outcome in chat

The card list now carries each card's href, so an agent that reuses an
existing card can link it. The chat directive tells the agent to report what
the refresh response says (queued, already running, deferred by policy, or a
concurrent refresh that already finished) instead of always saying queued.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-09 05:26:45 -06:00
1 parent b9f106f1b1
commit 97ad08935d
4 files changed
+7 -3

No files matched your search

@@ -281,6 +281,8 @@ describeEmbeddedPostgres("status card routes", () => {
// The person's restore route travels with the archive receipt, so an agent can link it.
expect(archived.body).toMatchObject({ archivedAt: expect.any(String), generatingIssueId: null, href: `/${company.issuePrefix}/status/${created.body.id}` });
expect((await request(app).get(`/api/status-cards/${created.body.id}`)).body.href).toBe(`/${company.issuePrefix}/status/${created.body.id}`);
// A card found by listing carries the same link, so an agent reusing it can reply with it.
expect((await request(app).get(`/api/companies/${company.id}/status-cards?archived=true`)).body[0]?.href).toBe(`/${company.issuePrefix}/status/${created.body.id}`);
expect(await db.select().from(issues).where(eq(issues.id, created.body.generatingIssueId)).then((rows) => rows[0]?.status)).toBe("cancelled");
expect((await request(app).get(`/api/companies/${company.id}/status-cards`)).body).toEqual([]);
expect((await request(app).get(`/api/companies/${company.id}/status-cards?archived=true`)).body).toHaveLength(1);
+2 -1
View File
@@ -161,9 +161,10 @@ export function statusCardRoutes(db: Db, opts: { heartbeat?: IssueAssignmentWake
assertCompanyAccess(req, companyId);
await assertStatusCardsEnabled();
const query = listStatusCardsQuerySchema.parse(req.query);
const issuePrefix = await companyIssuePrefix(companyId);
res.json((await service.list(companyId, query.archived, {
issueReadCondition: await issueReadSqlCondition(db, req.actor),
})).map(publicCard));
})).map((card) => withHref(publicCard(card), issuePrefix)));
});
router.post("/companies/:companyId/status-cards", validate(createStatusCardSchema), async (req, res) => {
+1 -1
View File
@@ -131,7 +131,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 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 /<prefix>/status (listed as Status in the sidebar), each card at /<prefix>/status/<cardId>. 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. Reply now with what you queued and the card's returned href as a Markdown link, such as [Launch blockers](/<prefix>/status/<cardId>), 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.`;
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 /<prefix>/status (listed as Status in the sidebar), each card at /<prefix>/status/<cardId>. 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](/<prefix>/status/<cardId>), 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.`;
/** Rendered only while the instance enables the Conference Room experiment. */
export const CONFERENCE_ROOM_CHAT_DIRECTIVE = `This instance has the Conference Room enabled: a separate board-level discussion surface in this company at /<prefix>/board-chat, listed as Conference Room in the sidebar. It is not this conversation and you do not answer there. A separate board assistant replies, running through the operator's local Claude CLI, so it works only on a local single-operator instance. Its messages are kept on the company's standing Board Operations task. It does not carry over this conversation's history, and its actions are not runs or tasks of yours. When the person asks to talk in the conference room or with the board assistant, link it as a Markdown link, such as [Open the Conference Room](/<prefix>/board-chat), with this conversation's company prefix. Say in a sentence what it is and that this chat's history and tools do not carry over, and offer to keep discussing here instead.`;
@@ -81,7 +81,8 @@ describe("heartbeat task markdown rendering", () => {
expect(enabled).toContain("A status card is a saved Paperclip feature, not a document or comment");
expect(enabled).toContain("POST /api/companies/{companyId}/status-cards");
expect(enabled).toContain("POST /api/status-cards/{id}/refresh");
expect(enabled).toContain("Do not sleep or poll the card. Reply now");
expect(enabled).toContain("Do not sleep or poll the card. Report the outcome the response gives");
expect(enabled).toContain("enqueued false otherwise means no run started, so say why from card.state");
expect(enabled).toContain("[Launch blockers](/<prefix>/status/<cardId>)");
// The named pages are the board's real routes.
const app = readFileSync(new URL("../../../../ui/src/App.tsx", import.meta.url), "utf8");