From 694076245fa38f6bc40768c970fd8ccab2f3a2a2 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:48:45 -0600 Subject: [PATCH] fix(status-cards): inline query guidance in the compile task instead of an unprovisioned skill (#19554) 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 > - Status cards let the board watch a slice of company work with a prose prompt > - A new card starts a compile task. The summarizer agent compiles the prompt into search queries and writes the first summary. > - The compile task told the agent to use the bundled `status-card-query` skill. Nothing gives that skill to the built-in Summarizer or to a per-card override agent. > - The agent saw no assigned skills, got `runner_skill_not_assigned`, and then searched the filesystem until the run deadline. It never wrote the card query. > - This pull request puts the query guidance directly in the compile task, so the task names only things the run has. > - The benefit is that any summarizer agent, on any model, can compile and save the card query without a missing dependency. ## Linked Issues or Issue Description **What happened?** Creating a status card with a summarizer on a smaller model (for example an OpenCode agent on a DeepSeek flash model) never finished. The compile run reported no assigned skills, then spent its full budget looking for `status-card-query`. No `PUT /api/status-cards/{id}/query` request was sent, and the run hit its deadline. Stronger models sometimes worked around the gap. **Expected behavior** The compile task contains everything the run needs. The agent writes the query, reads the compiled scope, and writes the first summary. **Steps to reproduce** 1. Enable status cards and configure a summarizer agent that does not have the `status-card-query` skill (the default Summarizer only installs `summarize-status`). 2. Create a status card. 3. Watch the compile run: it looks for the skill and does not write the query. ## What Changed - `server/src/services/status-cards.ts`: the compile task no longer says "Use the bundled `status-card-query` skill". It now inlines the query format (fields, allowed status and priority values from shared constants, limits), the project and label lookup endpoints, an example query body, and an explicit `GET /api/status-cards/{id}/dry-run` step before the summary write. - The example body uses a `text` fence, so the existing parser still reads the `json` generation payload. - New `server/src/__tests__/status-card-compile-prompt.test.ts`: asserts that the compile task names no skill, contains the full query format and write-back sequence, has an example that passes `companySearchQuerySchema`, and keeps a parseable generation payload. ## Verification - `cd server && npx vitest run src/__tests__/status-card-compile-prompt.test.ts src/__tests__/status-cards.test.ts src/__tests__/status-card-evaluation-work.test.ts`: 3 files, 74 tests pass. - `npx tsc --noEmit -p server`: no errors in the changed files. - `pnpm verify` selected 2224 test files. Most failed in this fresh worktree because `@paperclipai/plugin-sdk` was not built yet (`Failed to resolve entry for package "@paperclipai/plugin-sdk"`). After a build of that package, the status-card files above pass. Required CI supplies the broad coverage. ## Risks - Low risk. The change is prompt text only. The write-back routes, validators, and authorization do not change. - The compile prompt is longer. The added text is a small, bounded block. - The `status-card-query` catalog skill still exists for agents that author cards. The two texts can drift. The new test pins the prompt contract. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use and code execution. ## 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 - [x] All Paperclip CI gates are green - [x] 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 --- .../status-card-compile-prompt.test.ts | 45 +++++++++++++++++++ server/src/services/status-cards.ts | 44 ++++++++++++++++-- 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 server/src/__tests__/status-card-compile-prompt.test.ts diff --git a/server/src/__tests__/status-card-compile-prompt.test.ts b/server/src/__tests__/status-card-compile-prompt.test.ts new file mode 100644 index 00000000000..2faf1336314 --- /dev/null +++ b/server/src/__tests__/status-card-compile-prompt.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { writeStatusCardQuerySchema } from "@paperclipai/shared"; +import { compileDescription } from "../services/status-cards.js"; + +type CompileCard = Parameters[0]; + +const card = { + id: "11111111-1111-4111-8111-111111111111", + companyId: "22222222-2222-4222-8222-222222222222", + interestPrompt: "Blocked launch work updated this week", +} as CompileCard; + +describe("status-card compile prompt", () => { + const generationIssueId = "33333333-3333-4333-8333-333333333333"; + const description = compileDescription(card, generationIssueId, "hash"); + + // The compile task can be assigned to any summarizer agent (built-in or a + // per-card override), and no skill is provisioned for it. Pointing the run at + // a skill it cannot read made weaker models spend the whole run searching + // for it instead of writing the query. + it("does not depend on a skill the summarizer run is not given", () => { + expect(description).not.toContain("status-card-query"); + expect(description).not.toMatch(/\buse the [^.\n]*\bskill\b/i); + }); + + it("inlines the query format and the full write-back sequence", () => { + for (const field of ["scope", "q", "status", "priority", "assigneeAgentId", "projectId", "labelId", "updatedWithin", "sort", "limit", "offset"]) { + expect(description).toContain(`\`${field}\``); + } + expect(description).toContain(`GET /api/companies/${card.companyId}/projects`); + expect(description).toContain(`GET /api/companies/${card.companyId}/labels`); + expect(description).toContain(`PUT /api/status-cards/${card.id}/query`); + expect(description).toContain(`GET /api/status-cards/${card.id}/dry-run`); + expect(description).toContain(`PUT /api/status-cards/${card.id}/summary`); + }); + + it("gives a copy-pasteable example query body and a parseable generation payload", () => { + const example = description.match(/```text\n([\s\S]*?)\n```/)?.[1]; + expect(example).toBeDefined(); + const body = writeStatusCardQuerySchema.parse(JSON.parse(example!)); + expect(body.generationIssueId).toBe(generationIssueId); + const payload = description.match(/```json\n([\s\S]*?)\n```/)?.[1]; + expect(JSON.parse(payload!)).toMatchObject({ operation: "compile", statusCardId: card.id }); + }); +}); diff --git a/server/src/services/status-cards.ts b/server/src/services/status-cards.ts index 669e097c532..e286af843f8 100644 --- a/server/src/services/status-cards.ts +++ b/server/src/services/status-cards.ts @@ -22,7 +22,7 @@ import type { WriteStatusCardQuery, WriteStatusCardSummary, } from "@paperclipai/shared"; -import { companySearchQuerySchema, STATUS_CARD_AGENT_MAX_CARDS } from "@paperclipai/shared"; +import { companySearchQuerySchema, ISSUE_PRIORITIES, ISSUE_STATUSES, STATUS_CARD_AGENT_MAX_CARDS } from "@paperclipai/shared"; import { conflict, forbidden, notFound, unprocessable } from "../errors.js"; import { logger } from "../middleware/logger.js"; import { readBuiltInAgentMarker } from "../lib/built-in-agent-metadata.js"; @@ -119,11 +119,47 @@ function updateDescription(input: { return `Update this Paperclip status card.\n\n${UNTRUSTED_PROMPT_RULE}\n\n${task}${promptBlock}\n\n${mechanical}\n\n## Previous summary\n\n${untrustedPromptBlock("previous-summary", input.previousSummary ?? null)}\n\n## Changed issues\n\n${untrustedPromptBlock("changed-issues", input.changes.map(({ issueId, identifier, title, from, to, changeKind }) => ({ issueId, identifier, title, from, to, changeKind })))}\n\n${input.kind === "full" ? `## Bounded snapshot\n\n${untrustedPromptBlock("bounded-snapshot", input.snapshot.map(({ id, identifier, title, status }) => ({ id, identifier, title, status })))}` : ""}\n\n\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\``; } -function compileDescription(card: StatusCardRow, generationIssueId: string | null, hash: string) { +/** + * Query-compilation guidance inlined into every compile task. The compile task + * can be assigned to any company agent (the built-in Summarizer or a per-card + * override), and nothing provisions the `status-card-query` catalog skill to + * that agent, so the task must carry everything the run needs instead of + * pointing it at a skill it may not have. + */ +function compileQueryGuidance(card: StatusCardRow, generationIssueId: string | null) { + // The persisted task always carries its own issue id (the description is + // rewritten once the issue exists), so the example body is copy-pasteable. + const exampleGenerationIssueId = generationIssueId ?? ""; + return `Everything needed for this task is in this description; no extra skill is required, so do not search for one. + +## Query format + +\`queries\` is an array of 1–10 company-search query objects with union semantics: an issue matching any query belongs to the card. Prefer one narrow query; add another only for genuinely distinct populations. Each object accepts: + +- \`scope\`: always \`"issues"\`. +- \`q\`: optional short free text, only for concepts no structured filter covers. Never paste the whole prompt into it. +- \`status\`: array of ${ISSUE_STATUSES.map((status) => `\`${status}\``).join(", ")}. +- \`priority\`: array of ${ISSUE_PRIORITIES.map((priority) => `\`${priority}\``).join(", ")}. +- \`assigneeAgentId\` / \`assigneeUserId\`: one resolved assignee id. +- \`projectId\` / \`labelId\`: one resolved UUID each. Resolve names with \`GET /api/companies/${card.companyId}/projects\` and \`GET /api/companies/${card.companyId}/labels\`; never put a name in an id field. Use separate query objects for multiple projects or labels. +- \`updatedWithin\`: a bounded window such as \`24h\`, \`7d\`, \`4w\`, or \`3m\`. Add it whenever the prompt implies recency (recent, this week, lately, current). +- \`sort\`: \`relevance\`, \`updated\`, \`created\`, or \`priority\`. +- \`limit\`: 1–50, normally 20. \`offset\`: 0. + +Preserve the prompt's intent; do not broaden it. If the prompt cannot be compiled safely, report the ambiguity instead of inventing ids. + +Example \`PUT /api/status-cards/${card.id}/query\` body: + +\`\`\`text +{"queries":[{"scope":"issues","q":"launch","status":["in_progress","blocked","in_review"],"updatedWithin":"7d","sort":"updated","limit":20,"offset":0}],"title":"Launch work updated this week","changeSummary":"Compiled the prompt into one recent active-work query.","generationIssueId":${JSON.stringify(exampleGenerationIssueId)}} +\`\`\``; +} + +export function compileDescription(card: StatusCardRow, generationIssueId: string | null, hash: string) { const payload = compilePayload(card, generationIssueId, hash); return `Compile this status-card interest prompt into structured Paperclip company-search queries, then continue in the same run and write the first full summary. -Use the bundled \`status-card-query\` skill. Resolve named projects and labels to ids. Keep queries narrow, cap limits, and preserve union semantics across the query array. +${compileQueryGuidance(card, generationIssueId)} ${UNTRUSTED_PROMPT_RULE} @@ -134,7 +170,7 @@ ${untrustedPromptBlock("interest-prompt", card.interestPrompt)} ## Required write-back sequence 1. \`PUT /api/status-cards/${card.id}/query\` with \`queries\`, an auto-title, a non-empty \`changeSummary\`, and \`generationIssueId\`. -2. Execute the compiled scope and write the first full Markdown summary with \`PUT /api/status-cards/${card.id}/summary\` using the same \`generationIssueId\`. Do not create or wait for a second task. +2. Execute the compiled scope with \`GET /api/status-cards/${card.id}/dry-run\`, then write the first full Markdown summary with \`PUT /api/status-cards/${card.id}/summary\` (\`markdown\`, \`title\`, a non-empty \`changeSummary\`, the same \`generationIssueId\`, and the model id). Do not create or wait for a second task. Both writes must happen from this assigned issue run.