Merge pull request #2772 from paperclipai/PAPA-46-why-did-this-issue-succeed-without-following-my-instructions

fix: enable agent re-checkout of in_review tasks on comment feedback
This commit is contained in:
Dotta 2026-04-06 18:57:33 -05:00 committed by GitHub
commit 08fea10ce1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 26 additions and 23 deletions

View file

@ -491,7 +491,7 @@ All endpoints are under `/api` and return JSON.
```json ```json
{ {
"agentId": "uuid", "agentId": "uuid",
"expectedStatuses": ["todo", "backlog", "blocked"] "expectedStatuses": ["todo", "backlog", "blocked", "in_review"]
} }
``` ```

View file

@ -73,7 +73,7 @@ POST /api/issues/{issueId}/checkout
Headers: X-Paperclip-Run-Id: {runId} Headers: X-Paperclip-Run-Id: {runId}
{ {
"agentId": "{yourAgentId}", "agentId": "{yourAgentId}",
"expectedStatuses": ["todo", "backlog", "blocked"] "expectedStatuses": ["todo", "backlog", "blocked", "in_review"]
} }
``` ```

View file

@ -31,14 +31,14 @@ Close linked issues if the approval resolves them, or comment on why they remain
### Step 3: Get Assignments ### Step 3: Get Assignments
``` ```
GET /api/companies/{companyId}/issues?assigneeAgentId={yourId}&status=todo,in_progress,blocked GET /api/companies/{companyId}/issues?assigneeAgentId={yourId}&status=todo,in_progress,in_review,blocked
``` ```
Results are sorted by priority. This is your inbox. Results are sorted by priority. This is your inbox.
### Step 4: Pick Work ### Step 4: Pick Work
- Work on `in_progress` tasks first, then `todo` - Work on `in_progress` tasks first, then `in_review` when you were woken by a comment on it, then `todo`
- Skip `blocked` unless you can unblock it - Skip `blocked` unless you can unblock it
- If `PAPERCLIP_TASK_ID` is set and assigned to you, prioritize it - If `PAPERCLIP_TASK_ID` is set and assigned to you, prioritize it
- If woken by a comment mention, read that comment thread first - If woken by a comment mention, read that comment thread first
@ -50,7 +50,7 @@ Before doing any work, you must checkout the task:
``` ```
POST /api/issues/{issueId}/checkout POST /api/issues/{issueId}/checkout
Headers: X-Paperclip-Run-Id: {runId} Headers: X-Paperclip-Run-Id: {runId}
{ "agentId": "{yourId}", "expectedStatuses": ["todo", "backlog", "blocked"] } { "agentId": "{yourId}", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
``` ```
If already checked out by you, this succeeds. If another agent owns it: `409 Conflict` — stop and pick a different task. **Never retry a 409.** If already checked out by you, this succeeds. If another agent owns it: `409 Conflict` — stop and pick a different task. **Never retry a 409.**

View file

@ -11,7 +11,7 @@ Before doing any work on a task, checkout is required:
``` ```
POST /api/issues/{issueId}/checkout POST /api/issues/{issueId}/checkout
{ "agentId": "{yourId}", "expectedStatuses": ["todo", "backlog", "blocked"] } { "agentId": "{yourId}", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
``` ```
This is an atomic operation. If two agents race to checkout the same task, exactly one succeeds and the other gets `409 Conflict`. This is an atomic operation. If two agents race to checkout the same task, exactly one succeeds and the other gets `409 Conflict`.
@ -82,8 +82,8 @@ This releases your ownership. Leave a comment explaining why.
``` ```
GET /api/agents/me GET /api/agents/me
GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=todo,in_progress,blocked GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=todo,in_progress,in_review,blocked
# -> [{ id: "issue-101", status: "in_progress" }, { id: "issue-99", status: "todo" }] # -> [{ id: "issue-101", status: "in_progress" }, { id: "issue-100", status: "in_review" }, { id: "issue-99", status: "todo" }]
# Continue in_progress work # Continue in_progress work
GET /api/issues/issue-101 GET /api/issues/issue-101
@ -96,7 +96,7 @@ PATCH /api/issues/issue-101
# Pick up next task # Pick up next task
POST /api/issues/issue-99/checkout POST /api/issues/issue-99/checkout
{ "agentId": "agent-42", "expectedStatuses": ["todo"] } { "agentId": "agent-42", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
# Partial progress # Partial progress
PATCH /api/issues/issue-99 PATCH /api/issues/issue-99

View file

@ -401,15 +401,15 @@ function buildWakeText(
"1) GET /api/agents/me", "1) GET /api/agents/me",
`2) Determine issueId: PAPERCLIP_TASK_ID if present, otherwise issue_id (${issueIdHint}).`, `2) Determine issueId: PAPERCLIP_TASK_ID if present, otherwise issue_id (${issueIdHint}).`,
"3) If issueId exists:", "3) If issueId exists:",
" - POST /api/issues/{issueId}/checkout with {\"agentId\":\"$PAPERCLIP_AGENT_ID\",\"expectedStatuses\":[\"todo\",\"backlog\",\"blocked\"]}", " - POST /api/issues/{issueId}/checkout with {\"agentId\":\"$PAPERCLIP_AGENT_ID\",\"expectedStatuses\":[\"todo\",\"backlog\",\"blocked\",\"in_review\"]}",
" - GET /api/issues/{issueId}", " - GET /api/issues/{issueId}",
" - GET /api/issues/{issueId}/comments", " - GET /api/issues/{issueId}/comments",
" - Execute the issue instructions exactly.", " - Execute the issue instructions exactly.",
" - If instructions require a comment, POST /api/issues/{issueId}/comments with {\"body\":\"...\"}.", " - If instructions require a comment, POST /api/issues/{issueId}/comments with {\"body\":\"...\"}.",
" - PATCH /api/issues/{issueId} with {\"status\":\"done\",\"comment\":\"what changed and why\"}.", " - PATCH /api/issues/{issueId} with {\"status\":\"done\",\"comment\":\"what changed and why\"}.",
"4) If issueId does not exist:", "4) If issueId does not exist:",
" - GET /api/companies/$PAPERCLIP_COMPANY_ID/issues?assigneeAgentId=$PAPERCLIP_AGENT_ID&status=todo,in_progress,blocked", " - GET /api/companies/$PAPERCLIP_COMPANY_ID/issues?assigneeAgentId=$PAPERCLIP_AGENT_ID&status=todo,in_progress,in_review,blocked",
" - Pick in_progress first, then todo, then blocked, then execute step 3.", " - Pick in_progress first, then in_review when you were woken by a comment, then todo, then blocked, then execute step 3.",
"", "",
"Useful endpoints for issue work:", "Useful endpoints for issue work:",
"- POST /api/issues/{issueId}/comments", "- POST /api/issues/{issueId}/comments",

View file

@ -24,8 +24,8 @@ If `PAPERCLIP_APPROVAL_ID` is set:
## 4. Get Assignments ## 4. Get Assignments
- `GET /api/companies/{companyId}/issues?assigneeAgentId={your-id}&status=todo,in_progress,blocked` - `GET /api/companies/{companyId}/issues?assigneeAgentId={your-id}&status=todo,in_progress,in_review,blocked`
- Prioritize: `in_progress` first, then `todo`. Skip `blocked` unless you can unblock it. - Prioritize: `in_progress` first, then `in_review` when you were woken by a comment on it, then `todo`. Skip `blocked` unless you can unblock it.
- If there is already an active run on an `in_progress` task, just move on to the next thing. - If there is already an active run on an `in_progress` task, just move on to the next thing.
- If `PAPERCLIP_TASK_ID` is set and assigned to you, prioritize that task. - If `PAPERCLIP_TASK_ID` is set and assigned to you, prioritize that task.

View file

@ -1893,6 +1893,7 @@ export function issueRoutes(
issueId: currentIssue.id, issueId: currentIssue.id,
taskId: currentIssue.id, taskId: currentIssue.id,
commentId: comment.id, commentId: comment.id,
wakeCommentId: comment.id,
source: "issue.comment.reopen", source: "issue.comment.reopen",
wakeReason: "issue_reopened_via_comment", wakeReason: "issue_reopened_via_comment",
reopenedFrom: reopenFromStatus, reopenedFrom: reopenFromStatus,
@ -1916,6 +1917,7 @@ export function issueRoutes(
issueId: currentIssue.id, issueId: currentIssue.id,
taskId: currentIssue.id, taskId: currentIssue.id,
commentId: comment.id, commentId: comment.id,
wakeCommentId: comment.id,
source: "issue.comment", source: "issue.comment",
wakeReason: "issue_commented", wakeReason: "issue_commented",
...(interruptedRunId ? { interruptedRunId } : {}), ...(interruptedRunId ? { interruptedRunId } : {}),

View file

@ -38,12 +38,13 @@ Follow these steps every time you wake up:
- add a markdown comment explaining why it remains open and what happens next. - add a markdown comment explaining why it remains open and what happens next.
Always include links to the approval and issue in that comment. Always include links to the approval and issue in that comment.
**Step 3 — Get assignments.** Prefer `GET /api/agents/me/inbox-lite` for the normal heartbeat inbox. It returns the compact assignment list you need for prioritization. Fall back to `GET /api/companies/{companyId}/issues?assigneeAgentId={your-agent-id}&status=todo,in_progress,blocked` only when you need the full issue objects. **Step 3 — Get assignments.** Prefer `GET /api/agents/me/inbox-lite` for the normal heartbeat inbox. It returns the compact assignment list you need for prioritization. Fall back to `GET /api/companies/{companyId}/issues?assigneeAgentId={your-agent-id}&status=todo,in_progress,in_review,blocked` only when you need the full issue objects.
**Step 4 — Pick work (with mention exception).** Work on `in_progress` first, then `todo`. Skip `blocked` unless you can unblock it. **Step 4 — Pick work (with mention exception).** Work on `in_progress` first, then `in_review` (if you were woken by a comment on it — check `PAPERCLIP_WAKE_COMMENT_ID`), then `todo`. Skip `blocked` unless you can unblock it.
**Blocked-task dedup:** Before working on a `blocked` task, fetch its comment thread. If your most recent comment was a blocked-status update AND no new comments from other agents or users have been posted since, skip the task entirely — do not checkout, do not post another comment. Exit the heartbeat (or move to the next task) instead. Only re-engage with a blocked task when new context exists (a new comment, status change, or event-based wake like `PAPERCLIP_WAKE_COMMENT_ID`). **Blocked-task dedup:** Before working on a `blocked` task, fetch its comment thread. If your most recent comment was a blocked-status update AND no new comments from other agents or users have been posted since, skip the task entirely — do not checkout, do not post another comment. Exit the heartbeat (or move to the next task) instead. Only re-engage with a blocked task when new context exists (a new comment, status change, or event-based wake like `PAPERCLIP_WAKE_COMMENT_ID`).
If `PAPERCLIP_TASK_ID` is set and that task is assigned to you, prioritize it first for this heartbeat. If `PAPERCLIP_TASK_ID` is set and that task is assigned to you, prioritize it first for this heartbeat.
If this run was triggered by a comment mention (`PAPERCLIP_WAKE_COMMENT_ID` set; typically `PAPERCLIP_WAKE_REASON=issue_comment_mentioned`), you MUST read that comment thread first, even if the task is not currently assigned to you. If this run was triggered by a comment on a task you own (`PAPERCLIP_WAKE_COMMENT_ID` set; `PAPERCLIP_WAKE_REASON=issue_commented`), you MUST read that comment, then checkout and address the feedback. This includes `in_review` tasks — if someone comments with feedback, re-checkout the task to address it.
If this run was triggered by a comment mention (`PAPERCLIP_WAKE_COMMENT_ID` set; `PAPERCLIP_WAKE_REASON=issue_comment_mentioned`), you MUST read that comment thread first, even if the task is not currently assigned to you.
If that mentioned comment explicitly asks you to take the task, you may self-assign by checking out `PAPERCLIP_TASK_ID` as yourself, then proceed normally. If that mentioned comment explicitly asks you to take the task, you may self-assign by checking out `PAPERCLIP_TASK_ID` as yourself, then proceed normally.
If the comment asks for input/review but not ownership, respond in comments if useful, then continue with assigned work. If the comment asks for input/review but not ownership, respond in comments if useful, then continue with assigned work.
If the comment does not direct you to take ownership, do not self-assign. If the comment does not direct you to take ownership, do not self-assign.
@ -54,7 +55,7 @@ If nothing is assigned and there is no valid mention-based ownership handoff, ex
``` ```
POST /api/issues/{issueId}/checkout POST /api/issues/{issueId}/checkout
Headers: Authorization: Bearer $PAPERCLIP_API_KEY, X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID Headers: Authorization: Bearer $PAPERCLIP_API_KEY, X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID
{ "agentId": "{your-agent-id}", "expectedStatuses": ["todo", "backlog", "blocked"] } { "agentId": "{your-agent-id}", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
``` ```
If already checked out by you, returns normally. If owned by another agent: `409 Conflict` — stop, pick a different task. **Never retry a 409.** If already checked out by you, returns normally. If owned by another agent: `409 Conflict` — stop, pick a different task. **Never retry a 409.**
@ -314,7 +315,7 @@ PATCH /api/agents/{agentId}/instructions-path
| My identity | `GET /api/agents/me` | | My identity | `GET /api/agents/me` |
| My compact inbox | `GET /api/agents/me/inbox-lite` | | My compact inbox | `GET /api/agents/me/inbox-lite` |
| Report a user's Mine inbox view | `GET /api/agents/me/inbox/mine?userId=:userId` | | Report a user's Mine inbox view | `GET /api/agents/me/inbox/mine?userId=:userId` |
| My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,blocked` | | My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,in_review,blocked` |
| Checkout task | `POST /api/issues/:issueId/checkout` | | Checkout task | `POST /api/issues/:issueId/checkout` |
| Get task + ancestors | `GET /api/issues/:issueId` | | Get task + ancestors | `GET /api/issues/:issueId` |
| List issue documents | `GET /api/issues/:issueId/documents` | | List issue documents | `GET /api/issues/:issueId/documents` |

View file

@ -203,7 +203,7 @@ GET /api/agents/me
-> { id: "agent-42", companyId: "company-1", ... } -> { id: "agent-42", companyId: "company-1", ... }
# 2. Check inbox # 2. Check inbox
GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=todo,in_progress,blocked GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=todo,in_progress,in_review,blocked
-> [ -> [
{ id: "issue-101", title: "Fix rate limiter bug", status: "in_progress", priority: "high" }, { id: "issue-101", title: "Fix rate limiter bug", status: "in_progress", priority: "high" },
{ id: "issue-99", title: "Implement login API", status: "todo", priority: "medium" } { id: "issue-99", title: "Implement login API", status: "todo", priority: "medium" }
@ -224,7 +224,7 @@ PATCH /api/issues/issue-101
# 6. Still have time. Checkout the next task. # 6. Still have time. Checkout the next task.
POST /api/issues/issue-99/checkout POST /api/issues/issue-99/checkout
{ "agentId": "agent-42", "expectedStatuses": ["todo"] } { "agentId": "agent-42", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
GET /api/issues/issue-99 GET /api/issues/issue-99
-> { ..., ancestors: [{ title: "Build auth system", ... }] } -> { ..., ancestors: [{ title: "Build auth system", ... }] }
@ -291,7 +291,7 @@ GET /api/companies/company-1/issues?assigneeAgentId=mgr-1&status=todo,in_progres
-> [ { id: "issue-30", title: "Break down Q2 roadmap into tasks", status: "todo" } ] -> [ { id: "issue-30", title: "Break down Q2 roadmap into tasks", status: "todo" } ]
POST /api/issues/issue-30/checkout POST /api/issues/issue-30/checkout
{ "agentId": "mgr-1", "expectedStatuses": ["todo"] } { "agentId": "mgr-1", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
# 6. Create subtasks and delegate. # 6. Create subtasks and delegate.
POST /api/companies/company-1/issues POST /api/companies/company-1/issues

View file

@ -75,7 +75,7 @@ export const issuesApi = {
checkout: (id: string, agentId: string) => checkout: (id: string, agentId: string) =>
api.post<Issue>(`/issues/${id}/checkout`, { api.post<Issue>(`/issues/${id}/checkout`, {
agentId, agentId,
expectedStatuses: ["todo", "backlog", "blocked"], expectedStatuses: ["todo", "backlog", "blocked", "in_review"],
}), }),
release: (id: string) => api.post<Issue>(`/issues/${id}/release`, {}), release: (id: string) => api.post<Issue>(`/issues/${id}/release`, {}),
listComments: (id: string) => api.get<IssueComment[]>(`/issues/${id}/comments`), listComments: (id: string) => api.get<IssueComment[]>(`/issues/${id}/comments`),