mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 07:10:34 +01:00
fix(status-cards): inline query guidance in the compile task instead of an unprovisioned skill (#19554)
## 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
1391f64551
commit
694076245f
2 files changed
+85
-4
No files matched your search
@@ -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<typeof compileDescription>[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 });
|
||||
});
|
||||
});
|
||||
@@ -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 ?? "<generationIssueId from the JSON payload below>";
|
||||
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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user