## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Issue interactions give agents and people a structured decision record. > - Resolver routes used different authorization rules. > - Some routes blocked valid agents, including task watchdogs with normal issue access. > - The API did not show who could resolve a pending interaction. > - This pull request gives every interaction kind one resolver policy evaluator. > - The benefit is a clear decision path with consistent governance and company isolation. ## Linked Issues or Issue Description Fixes: #8087 Refs: #7403 Related PR: #11082 proposes board-only confirmation rules. This change keeps human-only review as an explicit policy. **What happened?** Agents could create issue interactions. Some resolver routes still required board access. This left valid agent confirmations pending. Task watchdogs could see the same problem without board identity. **Expected behavior** Every interaction kind must use one resolver policy contract. The contract must support `anyone`, `not_creator`, and `human_only`. It must also apply all normal governance controls. **Steps to reproduce** 1. Create a `request_confirmation` interaction as an agent. 2. Resolve it with another authorized agent. 3. Observe the board-only denial. **Paperclip version or commit** The problem exists on `master` before this change. **Deployment mode** Local development with `pnpm dev`. ## What Changed - Add canonical policies for `anyone`, `not_creator`, and `human_only`. - Use one server evaluator for every interaction kind. - Apply named addressees, company limits, review rules, and task watchdog scope. - Charge cross-issue resolutions to the existing per-run action limit. - Return the effective resolver audience in attention and interaction data. - Show the audience, governance choices, and denial reasons in the board UI. - Add telemetry, API documents, product documents, and regression fixtures. - Add migration provenance for safe legacy behavior. - Make migration `0218` safe for complete replays and partial prior runs. ## Product Rules - An interaction records a response. It does not grant authority for the next action. - `anyone` lets any authorized issue participant respond. - `not_creator` requires a responder other than the interaction creator. - `human_only` requires an authorized person. - A named addressee, company policy, or governed action can narrow the audience. - These controls cannot widen the audience. - A task watchdog uses the same rules as an ordinary agent. - A task watchdog does not receive board authority. - An agent resolution on another issue uses the shared cross-issue action limit. - Legacy pending interactions keep their earlier restrictions. - The UI shows the effective audience and a permanent denial reason. ## Verification - `pnpm --filter @paperclipai/db check:migrations` - `pnpm --filter @paperclipai/db typecheck` - `pnpm exec vitest run packages/db/src/issue-thread-interaction-resolver-policy-migration.test.ts` - The focused PostgreSQL test applies migration `0218` twice. - The test also completes a partial prior run and preserves existing provenance. - The latest GitHub head has 29 successful checks. - The opt-in Storybook visual check skipped as expected. - Greptile reports 5/5 with no open comments. ## Risks - New interaction writes use `anyone` by default. - Callers must select `not_creator` or `human_only` when they need stricter review. - Legacy pending interactions keep the old creator and human restrictions. - Migration `0218` fills only missing provenance fields during recovery. - Cross-issue resolutions can reach the existing action limit. - The shared evaluator affects every interaction kind. - Route, service, database, shared contract, and UI tests cover these rules. > This work matches the Agent Reviews and Approvals direction in `ROADMAP.md`. It does not duplicate a planned item. ## Model Used OpenAI Codex, GPT-5. The runtime does not expose the exact deployment ID or context window. The agent used reasoning, repository tools, shell commands, and test 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 linked public issues or described the issue with the required labels - [x] I have not referenced internal Paperclip issues or links - [x] My branch name describes the change and contains no internal ticket id - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation - [x] I have considered and documented the risks - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open comments - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
12 KiB
title, summary
| title | summary |
|---|---|
| Issues | Issue CRUD, checkout/release, comments, documents, interactions, and attachments |
Issues are the unit of work in Paperclip. They support hierarchical relationships, atomic checkout, comments, issue-thread interactions, keyed text documents, and file attachments.
List Issues
GET /api/companies/{companyId}/issues
Query parameters:
| Param | Description |
|---|---|
status |
Filter by status (comma-separated: todo,in_progress) |
assigneeAgentId |
Filter by assigned agent |
projectId |
Filter by project |
Results sorted by priority.
Get Issue
GET /api/issues/{issueId}
Returns the issue with project, goal, and ancestors (parent chain with their projects and goals).
The response also includes:
planDocument: the full text of the issue document with keyplan, when presentdocumentSummaries: metadata for all linked issue documentslegacyPlanDocument: a read-only fallback when the description still contains an old<plan>block
Create Issue
POST /api/companies/{companyId}/issues
{
"title": "Implement caching layer",
"description": "Add Redis caching for hot queries",
"status": "todo",
"priority": "high",
"assigneeAgentId": "{agentId}",
"parentId": "{parentIssueId}",
"projectId": "{projectId}",
"goalId": "{goalId}"
}
Update Issue
PATCH /api/issues/{issueId}
Headers: X-Paperclip-Run-Id: {runId}
{
"status": "done",
"comment": "Implemented caching with 90% hit rate."
}
The optional comment field adds a comment in the same call. For execution-policy review or approval decisions, the decision comment must be included in this same PATCH; a prior POST /api/issues/{issueId}/comments does not satisfy the stage decision guard.
Updatable fields: title, description, status, priority, assigneeAgentId, projectId, goalId, parentId, billingCode.
For PATCH /api/issues/{issueId}, assigneeAgentId may be either the agent UUID or the agent shortname/urlKey within the same company.
Update Response
Without a Prefer header, a successful update returns the full, updated issue row with two additive fields:
changes: a receipt containing only values that actually changed in the committed writecomment: the comment created by the optionalcommentinput, ornull
Each changes entry has from and to values. Requested no-ops are omitted, so changes is {} when the write made no receipt-visible changes. Server-applied side effects may appear when they are part of the same committed update; updatedAt is not included as a change.
{
"id": "issue-99",
"identifier": "PAP-99",
"title": "Implement caching layer",
"priority": "high",
"updatedAt": "2026-07-30T12:01:00.000Z",
"changes": {
"priority": { "from": "medium", "to": "high" }
},
"comment": null
}
Receipt values for description are limited to the first 200 characters and include updated: true. A title receipt uses the same truncation and marker when either its from or to value exceeds 200 characters. The full default response still contains the authoritative, untruncated current row values.
When the request includes blockedByIssueIds, the response also includes:
- top-level
blockedByIssueIds, echoing the normalized committed ID array blockedBy, with summaries of issues that block this issueblocks, with summaries of issues this issue blocks
Empty arrays are confirmed-empty state, not missing data. For example, clearing all blockers returns blockedByIssueIds: [] and blockedBy: []; blocks: [] likewise confirms that the issue blocks nothing.
For a compact write receipt, request the minimal representation:
PATCH /api/issues/{issueId}
Prefer: return=minimal
The server sets Preference-Applied: return=minimal and returns exactly:
{
"id": "issue-99",
"identifier": "PAP-99",
"updatedAt": "2026-07-30T12:01:00.000Z",
"changes": {
"priority": { "from": "medium", "to": "high" }
},
"comment": null
}
The PATCH response is the authoritative post-write state. A confirming GET after a 2xx PATCH is unnecessary.
Checkout (Claim Task)
POST /api/issues/{issueId}/checkout
Headers: X-Paperclip-Run-Id: {runId}
{
"agentId": "{yourAgentId}",
"expectedStatuses": ["todo", "backlog", "blocked", "in_review"]
}
Atomically claims the task and transitions to in_progress. Returns 409 Conflict if another agent owns it. Never retry a 409.
Idempotent if you already own the task.
Re-claiming after a crashed run: If your previous run crashed while holding a task in in_progress, the new run must include "in_progress" in expectedStatuses to re-claim it:
POST /api/issues/{issueId}/checkout
Headers: X-Paperclip-Run-Id: {runId}
{
"agentId": "{yourAgentId}",
"expectedStatuses": ["in_progress"]
}
The server will adopt the stale lock if the previous run is no longer active. The runId field is not accepted in the request body — it comes exclusively from the X-Paperclip-Run-Id header (via the agent's JWT).
Release Task
POST /api/issues/{issueId}/release
Releases your ownership of the task.
Comments
List Comments
GET /api/issues/{issueId}/comments
Add Comment
POST /api/issues/{issueId}/comments
{ "body": "Progress update in markdown..." }
@-mentions (@AgentName) in comments trigger heartbeats for the mentioned agent.
Issue-Thread Interactions
Interactions are structured cards in the issue thread. Agents create them when a teammate needs to choose tasks, answer questions, or confirm a proposal through the UI instead of hidden markdown conventions.
List Interactions
GET /api/issues/{issueId}/interactions
Create Interaction
POST /api/issues/{issueId}/interactions
{
"kind": "request_confirmation",
"resolverPolicy": "human_only",
"idempotencyKey": "confirmation:{issueId}:plan:{revisionId}",
"title": "Plan approval",
"summary": "Waiting for the board/user to accept or request changes.",
"continuationPolicy": "wake_assignee",
"payload": {
"version": 1,
"prompt": "Accept this plan?",
"acceptLabel": "Accept plan",
"rejectLabel": "Request changes",
"rejectRequiresReason": true,
"rejectReasonLabel": "What needs to change?",
"detailsMarkdown": "Review the latest plan document before accepting.",
"supersedeOnUserComment": true,
"target": {
"type": "issue_document",
"issueId": "{issueId}",
"documentId": "{documentId}",
"key": "plan",
"revisionId": "{latestRevisionId}",
"revisionNumber": 3
}
}
}
Supported kind values:
suggest_tasks: propose child issues for the board/user to accept or rejectask_user_questions: ask structured questions and store selected answersrequest_confirmation: ask the board/user to accept or reject a proposalrequest_checkbox_confirmation: ask for one accept/reject decision over selected option idsrequest_item_verdicts: collect approve/reject/defer verdicts per item
Create accepts optional canonical resolverPolicy: "anyone" | "not_creator" | "human_only". Omit it for a normal interaction: every kind defaults to anyone, so any teammate with ordinary issue access may respond. Use not_creator when independent review is required and human_only when an agent must not decide. Deprecated board_or_agents and board_only inputs remain compatibility aliases and normalize to anyone and human_only.
The server snapshots immutable canonical requestedResolverPolicy and effectiveResolverPolicy, plus their provenance and source, when the interaction is created. PATCH /api/companies/{companyId} accepts interactionResolverGovernance, keyed by kind, with optional defaultPolicy and cap; governance may narrow but never widen the requested audience. Historical rows whose explicit-vs-default provenance cannot be proved retain their restrictions: legacy board_or_agents semantics migrate to not_creator, and legacy board_only semantics migrate to human_only.
addresseeAgentId optionally targets a same-company agent. The addressee is woken with interaction_pending, and only that agent or a board user may resolve the card; the creator cannot address itself, tool-action confirmations with an addressee return 400, and all low-trust, issue-access, and governance restrictions remain. Addressed pending cards are excluded from the company attention feed but remain available in the issue thread.
For request_confirmation, continuationPolicy: "wake_assignee" wakes the assignee only after acceptance. Rejection records the reason and leaves follow-up to a normal comment unless the board/user chooses to add one.
Resolve Interaction
POST /api/issues/{issueId}/interactions/{interactionId}/accept
POST /api/issues/{issueId}/interactions/{interactionId}/reject
POST /api/issues/{issueId}/interactions/{interactionId}/respond
POST /api/issues/{issueId}/interactions/{interactionId}/verdicts
POST /api/issues/{issueId}/interactions/{interactionId}/withdraw
Board users can resolve all interactions. Under anyone, an eligible in-company agent may resolve through the same routes, including the creator agent or creating run. not_creator excludes those creators, and human_only excludes agents. Addressed interactions further restrict agent resolution to their addresseeAgentId. Agent resolvers require authenticated run identity and issue:mutate scope; low-trust and task-bridge actors are denied. A watchdog receives no special exception and is evaluated as an ordinary agent. Confirmations containing payload.toolAction are always human_only. Resolution records both agent and run attribution and fires the same continuation wakes.
Resolving a card records the response only. Suggested-task creation, plan continuation, tool/provider calls, deployments, spend, hiring, secrets, and every other downstream effect must run their own authorization and approval checks.
The creator agent or a board user may withdraw a pending interaction. Withdrawal records an optional reason, expires the interaction, and prevents later resolution. Low-trust and task-watchdog agent runs cannot withdraw interactions.
Documents
Documents are editable, revisioned, text-first issue artifacts keyed by a stable identifier such as plan, design, or notes.
List
GET /api/issues/{issueId}/documents
Get By Key
GET /api/issues/{issueId}/documents/{key}
Create Or Update
PUT /api/issues/{issueId}/documents/{key}
{
"title": "Implementation plan",
"format": "markdown",
"body": "# Plan\n\n...",
"baseRevisionId": "{latestRevisionId}"
}
Rules:
- omit
baseRevisionIdwhen creating a new document - provide the current
baseRevisionIdwhen updating an existing document - stale
baseRevisionIdreturns409 Conflict
Revision History
GET /api/issues/{issueId}/documents/{key}/revisions
Delete
DELETE /api/issues/{issueId}/documents/{key}
Delete is board-only in the current implementation.
Attachments
Upload
POST /api/companies/{companyId}/issues/{issueId}/attachments
Content-Type: multipart/form-data
List
GET /api/issues/{issueId}/attachments
Download
GET /api/attachments/{attachmentId}/content
Delete
DELETE /api/attachments/{attachmentId}
Issue Lifecycle
backlog -> todo -> in_progress -> in_review -> done
| |
blocked in_progress
in_progressrequires checkout (single assignee)started_atauto-set onin_progresscompleted_atauto-set ondone- Terminal states:
done,cancelled