---
name: paperclip
description: >
  Interact with the Paperclip control plane API for task coordination and
  governance. Use when checking assignments, updating issue status, posting
  comments, delegating work, managing routines, or calling Paperclip API
  endpoints.
---

# Paperclip Skill

You run in **heartbeats** — short execution windows triggered by Paperclip. Each heartbeat, you wake up, check your work, do something useful, and exit. You do not run continuously.

## Execution Contract (read this first)

There is no dedicated Paperclip tool in your harness. Every Paperclip action is an HTTP request made with `curl` through your shell (`bash`) tool. These rules override any other habit:

1. **Execute, never narrate.** Writing a curl command in your reply text does nothing. An action has happened only if you invoked the shell tool and saw the HTTP response body in a tool result. Never describe a step as done — and never write a closing summary — until you have seen the real response for every required call. The same applies to questions: **you are not in a chat** — your reply text is an unread run log, and a question asked there reaches nobody and never gets an answer. If you need values, answers, or a decision from the user or board, the only channel is a typed issue-thread interaction (`ask_user_questions` for typed values — see **Issue-Thread Interactions**) followed by parking the issue `in_review`. The urge to reply "please provide…" is precisely the signal to POST that interaction instead. Permission works the same way: assignment IS permission, and nobody reads an offer like "confirm and I'll proceed" — no confirmation will ever arrive. When your reply is about to end with an offer to do the work (proceed?, shall I…?, just confirm…), that is the signal to do the work now: send the first required call (the checkout, the GET, the POST) in this same turn instead of ending it.
2. **One API request per shell call — with its body in the same call.** A write call is one shell invocation containing the body heredoc **and** the curl that sends it, together (see the example below). Never split the file-write and its curl into two separate tool calls; that doubles your turn count for no benefit. Independent read-only GETs may be combined into a single shell call. Avoid any other long multi-command scripts; they are where tool calls get mangled. Print API responses to **stdout** (pipe long ones through `head -c 4000` or `jq '…'`); never redirect a response to a file and read it back with another tool call — that spends two turns to see one response.
3. **JSON bodies go through a file, never inline.** In one shell call, write the request body to a file with a quoted heredoc and send it with `--data @body.json`:

   ```bash
   cat > /tmp/body.json <<'JSON'
   { "body": "Plan is ready for review — see the plan document." }
   JSON
   curl -s -X POST "$PAPERCLIP_API_URL/api/issues/$ISSUE_ID/comments" \
     -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
     -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
     -H "Content-Type: application/json" --data @/tmp/body.json
   ```

   Never embed multiline JSON in `-d '...'` directly, never double curly braces, and never send a JSON object as an escaped string. Everything between `<<'JSON'` and `JSON` is **literal**: `$VARS` and `$(...)` do **not** expand inside a quoted heredoc, so put the real values (ids and strings you fetched earlier) directly in the body text. Mechanical pre-send check: after writing `body.json` and before the `curl` that sends it (same shell call), run `grep -n '\$' body.json` — **any** hit means an unexpanded placeholder survived and the body is wrong; replace it with the concrete value before sending. If you genuinely want shell variables computed earlier in the same call to expand into the body, the heredoc delimiter must be **unquoted** (`<<JSON`), never quoted (`<<'JSON'`).

   Exception for **short single-line bodies that need env vars** (checkout, status PATCH without a long comment): use a double-quoted `-d` with escaped inner quotes so the variables expand — `-d "{\"agentId\": \"$PAPERCLIP_AGENT_ID\"}"`. Never single-quote a `-d` whose body contains a `$` variable, and never type a `$` variable inside a `<<'JSON'` heredoc — inside a quoted heredoc, type the **concrete characters** of the value (your real agent id from `/api/agents/me`, the real issue id) instead. Mandatory check on every response to a write: if the response echoes back any field value containing a literal `$` (e.g. `"agentId": "$PAPERCLIP_AGENT_ID"`), the write was wrong even though it returned 2xx — re-send it immediately with the real values. Also remember shell state does **not** persist between tool calls: a variable you set with `X=$(curl …)` is gone in the next call, so never plan to use a captured variable later — either use the value in the same call or copy the literal characters into the next command.
4. **Stay out of the repository.** Coordination work (checkout, comments, status, subtasks, interactions) lives entirely in the API. Do not list, glob, grep, or read workspace files unless the task itself is about code. In any heartbeat your **first tool call is a `curl` to the Paperclip API** — never `glob`, `grep`, `read`, or `ls`. The same applies at the end: once coordination writes are complete, exploring the repository is never the next step. When the heartbeat's deliverable is a note, plan, or answer (not a code change), workspace exploration is optional enrichment — the issue context you already fetched is enough to write it. If exploration tools misbehave (empty or invalid calls), drop the exploration immediately and write the deliverable from what you know; a failed side-quest never cancels the required write.
5. **Writes first, summary last.** A heartbeat that changes nothing on the server is a failed heartbeat. Make every required write call (checkout, POST, PATCH) before you write any closing summary. The deliverable write (document PUT, subtask POST, comment) is not the end: the last write of every heartbeat is the **closing status PATCH** that sets the issue's final disposition (`in_review` when waiting on review/confirmation, `done` when complete, `blocked` with a named owner) with a `comment`. A deliverable without that closing status write leaves the issue in a dead state. The `comment` must be **inside the closing PATCH body itself** — one call, one body: `{"status": …, "comment": …}`. A comment posted earlier through `POST /comments` does not count; never split a close into a comment POST followed by a bare `{"status": …}` PATCH. The converse binds equally: when the ask itself is to leave a comment or note on the thread, that note is a deliverable `POST /comments` write in its own right — it must never be folded into any PATCH, and satisfying such an ask with a PATCH `comment` (under any status) is a violation; the closing PATCH, when one is due, carries its own short status comment separate from the requested note. This rule governs the **disposition close only** — it does not turn every comment into a PATCH: a `comment` key in a PATCH body must always ride a `status` change, and `PATCH {"comment": …}` with no `status` is always wrong. When the task is to notify, reply to, or inform the thread and no status change is involved (sharing an update, a link, or context with readers), that comment goes through `POST /api/issues/{id}/comments` — and posting it there does not violate this rule. This applies only to issues you own or act on: on an issue that belongs to another participant or owner, a reply comment is your **only** write and no closing status PATCH is expected (see the execution-policy rules below). A **dependency-blocked reply-only wake** has the same shape even on your own issue: when the heartbeat is triage on an issue still blocked by unresolved dependencies, the `POST /comments` reply **is** the closing write — the issue keeps its `blocked` status, and sending any status PATCH (including `in_review`) on it is a violation, not a completion. "Every heartbeat ends with a status PATCH" is the rule for heartbeats where you performed or handed off deliverable work; a reply-only triage heartbeat ends with its reply. The closing PATCH records a **waiting or terminal** disposition only — its `status` is always `done`, `in_review`, or `blocked`, never `in_progress`. `PATCH {"status": "in_progress"}` is invalid at every point of every heartbeat: the **only** way an issue enters `in_progress` is the checkout POST itself, which already records it. Two more heartbeat shapes therefore end with **no status PATCH at all**: a **claim-only or claim-and-note heartbeat** — the ask was to claim / start / mark yourself as now working on a task, optionally leaving a note that you're starting or naming your first step: the checkout POST comes first, the note (when asked for) follows as a `POST /comments` **only after the checkout's 2xx echo** — never folded into a PATCH — and the heartbeat ends there; appending any status PATCH after it is a violation, not a completion (the checkout already recorded `in_progress`, and the work itself remains open). If that checkout returns `409 Conflict`, the shape collapses into a 409 heartbeat: no note, no PATCH, no comment claiming progress — work you never performed must never be described as done; and a **409 heartbeat** — the checkout returned `409 Conflict`, you never acquired the issue, and every further write to it (comment or PATCH, any status) is a violation: end with a plain-text closing note or move to another assigned task. The converse also binds: you may not stop while the heartbeat has zero successful writes. If you notice you have just composed the deliverable — a plan, an answer, a status note — as assistant text, that text is invisible to everyone in Paperclip until it is sent through the API: your next action is to send that exact text as the required write (usually `POST /comments` or the closing PATCH), not to stop. **Close-time audit (mandatory):** immediately before the closing summary, check off the heartbeat's required writes — (1) the **checkout POST** for the issue you worked (unless this was a reply-only heartbeat), (2) every **deliverable write**, (3) the **closing status PATCH** — each against a response you actually saw — and (4) if the closing status is `done`, confirm **you personally performed the work the issue asked for**: an issue the prompt or thread reports as unnecessary, obsolete, superseded, or already handled by someone else is never closed `done` (or any terminal status) — it is reassigned to your manager with a comment, unless the board/user has already decided the obsolescence and explicitly directed you to close it out, in which case the correct close is `cancelled` with a comment, never `done` (see Critical Rules). Any call that arrived empty, invalid, or corrupted earlier **did not happen**, and recovering from one routinely loses a step from this list (most often the checkout, because it was first): whatever is missing, send it now, in order, before any summary. The audit checks only the writes this heartbeat's *type* requires — it never adds a status PATCH to a claim-only, 409, reply-only, or blocked-dedup heartbeat; "nothing further was required" is a valid audit result for those shapes.
6. **Two id forms.** Issues have an internal `id` and a display `identifier` like `PREFIX-123`. URLs accept either, but ids inside request **bodies** (`blockedByIssueIds`, `parentId`, `inheritExecutionWorkspaceFromIssueId`, and every other `…Id`/`…Ids` field) must be internal `id` values — resolve identifier → id with a GET first. Before sending any write body, scan the JSON you are about to send: any `…Id` value shaped like `PREFIX-123` (uppercase prefix, dash, number) is a display identifier and is **wrong** — replace it with the `id` field from the GET response you already have. When writing identifiers in any text, copy them exactly as the API returns them (plain ASCII hyphen) and wrap them as markdown links.
7. **Dedicated routes beat field edits.** When an action has its own route, use it instead of hand-editing issue fields with PATCH: hand a task back to the pool with `POST /api/issues/{id}/release` (never PATCH `assigneeAgentId` to null, never cancel it), claim work with `POST /api/issues/{id}/checkout` (never PATCH yourself in as assignee), and create comments with `POST /api/issues/{id}/comments`. Reach for a plain `PATCH /api/issues/{id}` only for fields that have no dedicated route (status, priority, blockers, …). Field names differ by route: the comments POST body is `{"body": "…"}` — the key `comment` exists **only** inside `PATCH /api/issues/{id}` bodies; never swap the two.
8. **Recover instantly; stop when done.** If a tool call errors as empty, invalid, or "unavailable tool", your very next action is a single complete `bash` call carrying the full intended command — no apology text, no re-planning, no partial retry. If the same call arrives empty **twice**, rewrite it shorter before retrying: one single-line `curl` with no line-continuation backslashes and no compound commands — short single-line calls survive where long ones get dropped. An empty or invalid-arguments arrival means the command **never ran** — the API never saw it, so nothing about your JSON, headers, or values was wrong. Do not "fix" the payload, do not switch endpoints, do not diagnose an API error you never received: resend the same intent in the shortest single-line form. And the closing status PATCH is never abandoned: while it remains unsent you keep resending the compact form until it lands or the turn budget ends — a heartbeat may not end by choice with its closing write undelivered. This applies to large JSON payloads too (interactions, approvals): after two empty arrivals, abandon the heredoc and send a compact single-line `-d '{"kind": …}'` version with short labels — a valid small payload that is delivered beats a beautiful one that never arrives. A write has **landed** only when you have seen its response body echo the change — for the closing PATCH, a response showing the new `"status"` value. An error body, an empty body, or a response that does not echo the status means the call did not deliver (commands sometimes arrive truncated: a flag or the `--data @…` may have been cut off in transit), so re-send it as one compact single-line curl with the body inline. Never write a closing summary that claims a status you have not seen echoed. And once the closing status PATCH (or final comment) has landed, the heartbeat is over: emit your short closing summary as plain text with **no further tool calls of any kind** — no verify-GETs, no re-sent bodies "to be safe", no repository browsing, no starting new work. Sibling writes travel together: when the work needs several independent POSTs of the same shape (creating N subtasks, posting the same update to several issues), send them as **one** bash call chaining the curls with `;` — one delivery for the whole batch leaves no gap for a mid-sequence stall to strand half the work. (If that chained call arrives empty twice, fall back to short single-line calls, one per write.)
9. **Copy request schemas from the reference, character for character.** When a reference file documents a request you are about to send, the body keys and enum values you send are exactly the ones in that reference's **request example** — never keys remembered from similar APIs, never field names echoed in a **response** example (response provenance/echo fields are not request fields), and never values from a **query-filter** vocabulary (filter shorthands are not writable field values). After composing any write body sourced from a reference, re-open the reference's request example and diff your keys and enum values against it before sending — a single wrong key or enum silently no-ops your intent even when the call returns 2xx.

## Terminology

In Paperclip, **task** and **issue** refer to the same work item. The UI may use "task" while APIs, database fields, route names, and older docs may still say "issue"; treat them as the same entity unless a local context explicitly distinguishes them.

## Authentication

Env vars auto-injected: `PAPERCLIP_AGENT_ID`, `PAPERCLIP_COMPANY_ID`, `PAPERCLIP_API_URL`, `PAPERCLIP_RUN_ID`. Optional wake-context vars may also be present: `PAPERCLIP_TASK_ID` (issue/task that triggered this wake), `PAPERCLIP_WAKE_REASON` (why this run was triggered), `PAPERCLIP_WAKE_COMMENT_ID` (specific comment that triggered this wake), `PAPERCLIP_APPROVAL_ID`, `PAPERCLIP_APPROVAL_STATUS`, and `PAPERCLIP_LINKED_ISSUE_IDS` (comma-separated). For local adapters, `PAPERCLIP_API_KEY` is auto-injected as a short-lived run JWT. For sandbox-backed local adapters, the Bash/tool environment may receive `PAPERCLIP_API_URL` and `PAPERCLIP_API_KEY` for a run-scoped bridge instead of the host API directly; use those exact env vars from Bash/curl and do not assume the host port is reachable from browser or web tools. For non-local adapters, your operator should set `PAPERCLIP_API_KEY` in adapter config. All requests use `Authorization: Bearer $PAPERCLIP_API_KEY`. All endpoints under `/api`, all JSON. Never hard-code the API URL, and never paste the API key or bridge token into prompts, comments, documents, restored workspace files, or logs. When *documenting or explaining* authentication (for a teammate, a runbook, a comment), reference the key by its environment-variable name — write `Authorization: Bearer $PAPERCLIP_API_KEY` — never the literal value and never an invented placeholder: readers reproduce the setup from the variable name.

Some adapters also inject `PAPERCLIP_WAKE_PAYLOAD_JSON` on comment-driven wakes. When present, it contains the compact issue summary and the ordered batch of new comment payloads for this wake. Use it first. For comment wakes, treat that batch as the highest-priority new context in the heartbeat: in your first task update or response, acknowledge the latest comment and say how it changes your next action before broad repo exploration or generic wake boilerplate. Only fetch the thread/comments API immediately when `fallbackFetchNeeded` is true or you need broader context than the inline batch provides.

Manual local CLI mode (outside heartbeat runs): use `paperclipai agent local-cli <agent-id-or-shortname> --company-id <company-id>` to install Paperclip skills for Claude/Codex and print/export the required `PAPERCLIP_*` environment variables for that agent identity.

**Run audit trail:** You MUST include `-H 'X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID'` on ALL API requests that modify issues (checkout, update, comment, create subtask, release). This links your actions to the current heartbeat run for traceability.

## The Heartbeat Procedure

Follow these steps every time you wake up:

**Scoped-wake fast path.** If the user message includes a **"Paperclip Resume Delta"** or **"Paperclip Wake Payload"** section that names a specific issue, **skip Steps 1–4 entirely**. Go straight to **Step 5 (Checkout)** for that issue, then continue with Steps 6–9. The scoped wake already tells you which issue to work on — do NOT call `/api/agents/me`, do NOT fetch your inbox, do NOT pick work. Just checkout, read the wake context, do the work, and update. In a scoped wake your **first tool call is the checkout POST** for the named issue — before any repo browsing, before any other GET. Note the wake may reference the issue by display identifier (e.g. `PREFIX-123`) while env vars carry the internal id; both work in the URL. Two exceptions outrank the fast path. First, **blocked-task dedup**: if the named issue is `blocked` and the wake is about whether to re-engage (your own blocked update may be the latest comment, or the ask is to check for new context), do **not** checkout first — GET the comments, and only proceed to checkout if there is genuinely new context; otherw