@penclipai/adapter-codex-local 2026.406.0 → 2026.408.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,6 +62,6 @@ export function isCodexUnknownSessionError(stdout, stderr) {
62
62
  .map((line) => line.trim())
63
63
  .filter(Boolean)
64
64
  .join("\n");
65
- return /unknown (session|thread)|session .* not found|thread .* not found|conversation .* not found|missing rollout path for thread|state db missing rollout path/i.test(haystack);
65
+ return /unknown (session|thread)|session .* not found|thread .* not found|conversation .* not found|missing rollout path for thread|state db missing rollout path|no rollout found for thread id/i.test(haystack);
66
66
  }
67
67
  //# sourceMappingURL=parse.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/server/parse.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/server/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,uCAAuC,CAAC;AAEnG,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,YAAY,GAAkB,IAAI,CAAC;IACvC,MAAM,KAAK,GAAG;QACZ,WAAW,EAAE,CAAC;QACd,iBAAiB,EAAE,CAAC;QACpB,YAAY,EAAE,CAAC;KAChB,CAAC;IAEF,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,KAAK;YAAE,SAAS;QAErB,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACtC,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;YAC9B,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,SAAS,IAAI,EAAE,CAAC,IAAI,SAAS,CAAC;YACpE,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/C,IAAI,GAAG;gBAAE,YAAY,GAAG,GAAG,CAAC;YAC5B,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,eAAe,EAAE,CAAC;gBAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBACrC,IAAI,IAAI;oBAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAChC,CAAC;YACD,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;YAC9B,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC1C,KAAK,CAAC,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;YACvE,KAAK,CAAC,iBAAiB,GAAG,QAAQ,CAAC,QAAQ,CAAC,mBAAmB,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;YAC1F,KAAK,CAAC,YAAY,GAAG,QAAQ,CAAC,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;YAC1E,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC7C,IAAI,GAAG;gBAAE,YAAY,GAAG,GAAG,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,OAAO;QACL,SAAS;QACT,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE;QACrC,KAAK;QACL,YAAY;KACb,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,MAAc,EAAE,MAAc;IACvE,MAAM,QAAQ,GAAG,GAAG,MAAM,KAAK,MAAM,EAAE;SACpC,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO,2LAA2L,CAAC,IAAI,CACrM,QAAQ,CACT,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parse.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.test.d.ts","sourceRoot":"","sources":["../../src/server/parse.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isCodexUnknownSessionError, parseCodexJsonl } from "./parse.js";
3
+ describe("parseCodexJsonl", () => {
4
+ it("captures session id, assistant summary, usage, and error message", () => {
5
+ const stdout = [
6
+ JSON.stringify({ type: "thread.started", thread_id: "thread_123" }),
7
+ JSON.stringify({
8
+ type: "item.completed",
9
+ item: { type: "agent_message", text: "Recovered response" },
10
+ }),
11
+ JSON.stringify({
12
+ type: "turn.completed",
13
+ usage: { input_tokens: 10, cached_input_tokens: 2, output_tokens: 4 },
14
+ }),
15
+ JSON.stringify({ type: "turn.failed", error: { message: "resume failed" } }),
16
+ ].join("\n");
17
+ expect(parseCodexJsonl(stdout)).toEqual({
18
+ sessionId: "thread_123",
19
+ summary: "Recovered response",
20
+ usage: {
21
+ inputTokens: 10,
22
+ cachedInputTokens: 2,
23
+ outputTokens: 4,
24
+ },
25
+ errorMessage: "resume failed",
26
+ });
27
+ });
28
+ });
29
+ describe("isCodexUnknownSessionError", () => {
30
+ it("detects the current missing-rollout thread error", () => {
31
+ expect(isCodexUnknownSessionError("", "Error: thread/resume: thread/resume failed: no rollout found for thread id d448e715-7607-4bcc-91fc-7a3c0c5a9632")).toBe(true);
32
+ });
33
+ it("still detects existing stale-session wordings", () => {
34
+ expect(isCodexUnknownSessionError("unknown thread id", "")).toBe(true);
35
+ expect(isCodexUnknownSessionError("", "state db missing rollout path for thread abc")).toBe(true);
36
+ });
37
+ it("does not classify unrelated Codex failures as stale sessions", () => {
38
+ expect(isCodexUnknownSessionError("", "model overloaded")).toBe(false);
39
+ });
40
+ });
41
+ //# sourceMappingURL=parse.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.test.js","sourceRoot":"","sources":["../../src/server/parse.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEzE,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,MAAM,GAAG;YACb,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;YACnE,IAAI,CAAC,SAAS,CAAC;gBACb,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,oBAAoB,EAAE;aAC5D,CAAC;YACF,IAAI,CAAC,SAAS,CAAC;gBACb,IAAI,EAAE,gBAAgB;gBACtB,KAAK,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,mBAAmB,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE;aACtE,CAAC;YACF,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE,CAAC;SAC7E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEb,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;YACtC,SAAS,EAAE,YAAY;YACvB,OAAO,EAAE,oBAAoB;YAC7B,KAAK,EAAE;gBACL,WAAW,EAAE,EAAE;gBACf,iBAAiB,EAAE,CAAC;gBACpB,YAAY,EAAE,CAAC;aAChB;YACD,YAAY,EAAE,eAAe;SAC9B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CACJ,0BAA0B,CACxB,EAAE,EACF,iHAAiH,CAClH,CACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,0BAA0B,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvE,MAAM,CAAC,0BAA0B,CAAC,EAAE,EAAE,8CAA8C,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,CAAC,0BAA0B,CAAC,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@penclipai/adapter-codex-local",
3
- "version": "2026.406.0",
3
+ "version": "2026.408.0-canary.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://github.com/penclipai/paperclip-cn",
6
6
  "bugs": {
@@ -38,7 +38,7 @@
38
38
  "skills"
39
39
  ],
40
40
  "dependencies": {
41
- "@penclipai/adapter-utils": "2026.406.0",
41
+ "@penclipai/adapter-utils": "2026.408.0-canary.0",
42
42
  "picocolors": "^1.1.1"
43
43
  },
44
44
  "devDependencies": {
@@ -40,12 +40,13 @@ Follow these steps every time you wake up:
40
40
  - add a markdown comment explaining why it remains open and what happens next.
41
41
  Always include links to the approval and issue in that comment.
42
42
 
43
- **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.
43
+ **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.
44
44
 
45
- **Step 4 — Pick work (with mention exception).** Work on `in_progress` first, then `todo`. Skip `blocked` unless you can unblock it.
45
+ **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.
46
46
  **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`).
47
47
  If `PAPERCLIP_TASK_ID` is set and that task is assigned to you, prioritize it first for this heartbeat.
48
- 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.
48
+ 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.
49
+ 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.
49
50
  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.
50
51
  If the comment asks for input/review but not ownership, respond in comments if useful, then continue with assigned work.
51
52
  If the comment does not direct you to take ownership, do not self-assign.
@@ -56,7 +57,7 @@ If nothing is assigned and there is no valid mention-based ownership handoff, ex
56
57
  ```
57
58
  POST /api/issues/{issueId}/checkout
58
59
  Headers: Authorization: Bearer $PAPERCLIP_API_KEY, X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID
59
- { "agentId": "{your-agent-id}", "expectedStatuses": ["todo", "backlog", "blocked"] }
60
+ { "agentId": "{your-agent-id}", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
60
61
  ```
61
62
 
62
63
  If already checked out by you, returns normally. If owned by another agent: `409 Conflict` — stop, pick a different task. **Never retry a 409.**
@@ -73,6 +74,35 @@ Use comments incrementally:
73
74
 
74
75
  Read enough ancestor/comment context to understand _why_ the task exists and what changed. Do not reflexively reload the whole thread on every heartbeat.
75
76
 
77
+ **Execution-policy review/approval wakes.** If the issue is in `in_review` and includes `executionState`, inspect these fields immediately:
78
+
79
+ - `executionState.currentStageType` tells you whether you are in a `review` or `approval` stage
80
+ - `executionState.currentParticipant` tells you who is currently allowed to act
81
+ - `executionState.returnAssignee` tells you who receives the task back if changes are requested
82
+ - `executionState.lastDecisionOutcome` tells you the latest review/approval outcome
83
+
84
+ If `currentParticipant` matches you, you are the active reviewer/approver for this heartbeat. There is **no separate execution-decision endpoint**. Submit your decision through the normal issue update route:
85
+
86
+ ```json
87
+ PATCH /api/issues/{issueId}
88
+ Headers: X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID
89
+ { "status": "done", "comment": "Approved: what you reviewed and why it passes." }
90
+ ```
91
+
92
+ That approves the current stage. If more stages remain, Paperclip keeps the issue in `in_review`, reassigns it to the next participant, and records the decision automatically.
93
+
94
+ To request changes, send a non-`done` status with a required comment. Prefer `in_progress`:
95
+
96
+ ```json
97
+ PATCH /api/issues/{issueId}
98
+ Headers: X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID
99
+ { "status": "in_progress", "comment": "Changes requested: exactly what must be fixed." }
100
+ ```
101
+
102
+ Paperclip converts that into a changes-requested decision, reassigns the issue to `returnAssignee`, and routes the task back through the same stage after the executor resubmits.
103
+
104
+ If `currentParticipant` does **not** match you, do not try to advance the stage. Only the active reviewer/approver can do that, and Paperclip will reject other actors with `422`.
105
+
76
106
  **Step 7 — Do the work.** Use your tools and capabilities.
77
107
 
78
108
  **Step 8 — Update status and communicate.** Always include the run ID header.
@@ -90,10 +120,81 @@ Headers: X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID
90
120
  { "status": "blocked", "comment": "What is blocked, why, and who needs to unblock it." }
91
121
  ```
92
122
 
93
- Status values: `backlog`, `todo`, `in_progress`, `in_review`, `done`, `blocked`, `cancelled`. Priority values: `critical`, `high`, `medium`, `low`. Other updatable fields: `title`, `description`, `priority`, `assigneeAgentId`, `projectId`, `goalId`, `parentId`, `billingCode`.
123
+ Status values: `backlog`, `todo`, `in_progress`, `in_review`, `done`, `blocked`, `cancelled`. Priority values: `critical`, `high`, `medium`, `low`. Other updatable fields: `title`, `description`, `priority`, `assigneeAgentId`, `projectId`, `goalId`, `parentId`, `billingCode`, `blockedByIssueIds`.
94
124
 
95
125
  **Step 9 — Delegate if needed.** Create subtasks with `POST /api/companies/{companyId}/issues`. Always set `parentId` and `goalId`. When a follow-up issue needs to stay on the same code change but is not a true child task, set `inheritExecutionWorkspaceFromIssueId` to the source issue. Set `billingCode` for cross-team work.
96
126
 
127
+ ## Issue Dependencies (Blockers)
128
+
129
+ Paperclip supports first-class blocker relationships between issues. Use these to express "issue A is blocked by issue B" so that dependent work automatically resumes when blockers are resolved.
130
+
131
+ ### Setting blockers
132
+
133
+ Pass `blockedByIssueIds` (an array of issue IDs) when creating or updating an issue:
134
+
135
+ ```json
136
+ // At creation time
137
+ POST /api/companies/{companyId}/issues
138
+ { "title": "Deploy to prod", "blockedByIssueIds": ["issue-id-1", "issue-id-2"], "status": "blocked", ... }
139
+
140
+ // After the fact
141
+ PATCH /api/issues/{issueId}
142
+ { "blockedByIssueIds": ["issue-id-1", "issue-id-2"] }
143
+ ```
144
+
145
+ The `blockedByIssueIds` array **replaces** the existing blocker set on each update. To add a blocker, include the full list. To remove all blockers, send `[]`.
146
+
147
+ Constraints: issues cannot block themselves, and circular blocker chains are rejected.
148
+
149
+ ### Reading blockers
150
+
151
+ `GET /api/issues/{issueId}` returns two relation arrays:
152
+
153
+ - `blockedBy` — issues that block this one (with `id`, `identifier`, `title`, `status`, `priority`, assignee info)
154
+ - `blocks` — issues that this one blocks
155
+
156
+ ### Automatic wake-on-dependency-resolved
157
+
158
+ Paperclip fires automatic wakes in two scenarios:
159
+
160
+ 1. **All blockers done** (`PAPERCLIP_WAKE_REASON=issue_blockers_resolved`): When every issue in the `blockedBy` set reaches `done`, the dependent issue's assignee is woken to resume work.
161
+ 2. **All children done** (`PAPERCLIP_WAKE_REASON=issue_children_completed`): When every direct child issue of a parent reaches a terminal state (`done` or `cancelled`), the parent issue's assignee is woken to finalize or close out.
162
+
163
+ If a blocker is moved to `cancelled`, it does **not** count as resolved for blocker wakeups. Remove or replace cancelled blockers explicitly before expecting `issue_blockers_resolved`.
164
+
165
+ When you receive one of these wake reasons, check the issue state and continue the work or mark it done.
166
+
167
+ ## Requesting Board Approval
168
+
169
+ Agents can create approval requests for arbitrary issue-linked work. Use this when you need the board to approve or deny a proposed action before continuing.
170
+
171
+ Recommended generic type:
172
+
173
+ - `request_board_approval` for open-ended approval requests like spend approval, vendor approval, launch approval, or other board decisions
174
+
175
+ Create the approval and link it to the relevant issue in one call:
176
+
177
+ ```json
178
+ POST /api/companies/{companyId}/approvals
179
+ {
180
+ "type": "request_board_approval",
181
+ "requestedByAgentId": "{your-agent-id}",
182
+ "issueIds": ["{issue-id}"],
183
+ "payload": {
184
+ "title": "Approve monthly hosting spend",
185
+ "summary": "Estimated cost is $42/month for provider X.",
186
+ "recommendedAction": "Approve provider X and continue setup.",
187
+ "risks": ["Costs may increase with usage."]
188
+ }
189
+ }
190
+ ```
191
+
192
+ Notes:
193
+
194
+ - `issueIds` links the approval into the issue thread/UI.
195
+ - When the board approves it, Paperclip wakes the requesting agent and includes `PAPERCLIP_APPROVAL_ID` / `PAPERCLIP_APPROVAL_STATUS`.
196
+ - Keep the payload concise and decision-ready: what you want approved, why, expected cost/impact, and what happens next.
197
+
97
198
  ## Project Setup Workflow (CEO/Manager Common Path)
98
199
 
99
200
  When asked to set up a new project with workspace config (local folder and/or GitHub repo), use:
@@ -168,6 +269,7 @@ If you are asked to create or manage routines you MUST read:
168
269
  - **Preserve workspace continuity for follow-ups.** Child issues inherit execution workspace linkage server-side from `parentId`. For non-child follow-ups tied to the same checkout/worktree, send `inheritExecutionWorkspaceFromIssueId` explicitly instead of relying on free-text references or memory.
169
270
  - **Never cancel cross-team tasks.** Reassign to your manager with a comment.
170
271
  - **Always update blocked issues explicitly.** If blocked, PATCH status to `blocked` with a blocker comment before exiting, then escalate. On subsequent heartbeats, do NOT repeat the same blocked comment — see blocked-task dedup in Step 4.
272
+ - **Use first-class blockers** when a task depends on other tasks. Set `blockedByIssueIds` on the dependent issue so Paperclip automatically wakes the assignee when all blockers are done. Prefer this over ad-hoc "blocked by X" comments.
171
273
  - **@-mentions** (`@AgentName` in comments) trigger heartbeats — use sparingly, they cost budget.
172
274
  - **Budget**: auto-paused at 100%. Above 80%, focus on critical tasks only.
173
275
  - **Escalate** via `chainOfCommand` when stuck. Reassign to manager or create a task for them.
@@ -275,7 +377,7 @@ PATCH /api/agents/{agentId}/instructions-path
275
377
  | My identity | `GET /api/agents/me` |
276
378
  | My compact inbox | `GET /api/agents/me/inbox-lite` |
277
379
  | Report a user's Mine inbox view | `GET /api/agents/me/inbox/mine?userId=:userId` |
278
- | My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,blocked` |
380
+ | My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,in_review,blocked` |
279
381
  | Checkout task | `POST /api/issues/:issueId/checkout` |
280
382
  | Get task + ancestors | `GET /api/issues/:issueId` |
281
383
  | List issue documents | `GET /api/issues/:issueId/documents` |
@@ -295,6 +397,7 @@ PATCH /api/agents/{agentId}/instructions-path
295
397
  | Set instructions path | `PATCH /api/agents/:agentId/instructions-path` |
296
398
  | Release task | `POST /api/issues/:issueId/release` |
297
399
  | List agents | `GET /api/companies/:companyId/agents` |
400
+ | Create approval | `POST /api/companies/:companyId/approvals` |
298
401
  | List company skills | `GET /api/companies/:companyId/skills` |
299
402
  | Import company skills | `POST /api/companies/:companyId/skills/import` |
300
403
  | Scan project workspaces for skills | `POST /api/companies/:companyId/skills/scan-projects` |
@@ -109,6 +109,8 @@ POST /api/companies/company-1/exports
109
109
 
110
110
  Includes the issue's `project` and `goal` (with descriptions), plus each ancestor's resolved `project` and `goal`. This gives agents full context about where the task sits in the project/goal hierarchy.
111
111
 
112
+ The response also includes `blockedBy` and `blocks` arrays showing first-class dependency relationships:
113
+
112
114
  ```json
113
115
  {
114
116
  "id": "issue-99",
@@ -116,6 +118,10 @@ Includes the issue's `project` and `goal` (with descriptions), plus each ancesto
116
118
  "parentId": "issue-50",
117
119
  "projectId": "proj-1",
118
120
  "goalId": null,
121
+ "blockedBy": [
122
+ { "id": "issue-80", "identifier": "PAP-80", "title": "Design auth schema", "status": "in_progress", "priority": "high", "assigneeAgentId": "agent-55", "assigneeUserId": null }
123
+ ],
124
+ "blocks": [],
119
125
  "project": {
120
126
  "id": "proj-1",
121
127
  "name": "Auth System",
@@ -183,6 +189,60 @@ Includes the issue's `project` and `goal` (with descriptions), plus each ancesto
183
189
  }
184
190
  ```
185
191
 
192
+ Blocker wake semantics are strict: `issue_blockers_resolved` only fires when every blocker reaches `done`. A blocker moved to `cancelled` still requires manual re-triage or relation cleanup.
193
+
194
+ ### Execution Policy Fields On An Issue
195
+
196
+ When an issue has review or approval gates, `GET /api/issues/:issueId` can also include `executionPolicy` and `executionState`:
197
+
198
+ ```json
199
+ {
200
+ "status": "in_review",
201
+ "executionPolicy": {
202
+ "mode": "normal",
203
+ "commentRequired": true,
204
+ "stages": [
205
+ {
206
+ "id": "stage-review",
207
+ "type": "review",
208
+ "approvalsNeeded": 1,
209
+ "participants": [
210
+ { "id": "participant-qa", "type": "agent", "agentId": "qa-agent-id" }
211
+ ]
212
+ },
213
+ {
214
+ "id": "stage-approval",
215
+ "type": "approval",
216
+ "approvalsNeeded": 1,
217
+ "participants": [
218
+ { "id": "participant-cto", "type": "user", "userId": "cto-user-id" }
219
+ ]
220
+ }
221
+ ]
222
+ },
223
+ "executionState": {
224
+ "status": "pending",
225
+ "currentStageId": "stage-review",
226
+ "currentStageIndex": 0,
227
+ "currentStageType": "review",
228
+ "currentParticipant": { "type": "agent", "agentId": "qa-agent-id" },
229
+ "returnAssignee": { "type": "agent", "agentId": "coder-agent-id" },
230
+ "completedStageIds": [],
231
+ "lastDecisionId": null,
232
+ "lastDecisionOutcome": null
233
+ }
234
+ }
235
+ ```
236
+
237
+ Interpretation:
238
+
239
+ - `currentStageType` tells you whether the active gate is `review` or `approval`
240
+ - `currentParticipant` is the only actor allowed to advance the stage
241
+ - `returnAssignee` is who gets the task back when changes are requested
242
+ - `lastDecisionOutcome` shows the latest gate decision
243
+
244
+ There is **no separate execution-decision endpoint**. Review and approval decisions are submitted through `PATCH /api/issues/:issueId`, and Paperclip records the decision row automatically.
245
+
186
246
  ---
187
247
 
188
248
  ## Worked Example: IC Heartbeat
@@ -195,7 +255,7 @@ GET /api/agents/me
195
255
  -> { id: "agent-42", companyId: "company-1", ... }
196
256
 
197
257
  # 2. Check inbox
198
- GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=todo,in_progress,blocked
258
+ GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=todo,in_progress,in_review,blocked
199
259
  -> [
200
260
  { id: "issue-101", title: "Fix rate limiter bug", status: "in_progress", priority: "high" },
201
261
  { id: "issue-99", title: "Implement login API", status: "todo", priority: "medium" }
@@ -216,7 +276,7 @@ PATCH /api/issues/issue-101
216
276
 
217
277
  # 6. Still have time. Checkout the next task.
218
278
  POST /api/issues/issue-99/checkout
219
- { "agentId": "agent-42", "expectedStatuses": ["todo"] }
279
+ { "agentId": "agent-42", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
220
280
 
221
281
  GET /api/issues/issue-99
222
282
  -> { ..., ancestors: [{ title: "Build auth system", ... }] }
@@ -254,6 +314,43 @@ PATCH /api/issues/issue-200
254
314
  { "comment": "Your Mine inbox has 1 unread issue: [PAP-310](/PAP/issues/PAP-310)." }
255
315
  ```
256
316
 
317
+ ### Worked Example: Reviewer / Approver Heartbeat
318
+
319
+ When you wake up on an issue in `in_review`, inspect `executionState` first:
320
+
321
+ ```
322
+ GET /api/issues/issue-77
323
+ -> {
324
+ id: "issue-77",
325
+ status: "in_review",
326
+ assigneeAgentId: "qa-agent-id",
327
+ executionState: {
328
+ status: "pending",
329
+ currentStageType: "review",
330
+ currentParticipant: { type: "agent", agentId: "qa-agent-id" },
331
+ returnAssignee: { type: "agent", agentId: "coder-agent-id" }
332
+ }
333
+ }
334
+ ```
335
+
336
+ If `currentParticipant` is you, approve the current stage by patching the issue to `done` with a required comment:
337
+
338
+ ```
339
+ PATCH /api/issues/issue-77
340
+ { "status": "done", "comment": "QA signoff complete. Verified the regression and test coverage." }
341
+ ```
342
+
343
+ Paperclip writes the execution decision automatically. If another stage remains, the issue stays in `in_review` and is reassigned to the next participant. If this was the final stage, the issue reaches actual `done`.
344
+
345
+ To request changes, use a non-`done` status with a required comment. Prefer `in_progress`:
346
+
347
+ ```
348
+ PATCH /api/issues/issue-77
349
+ { "status": "in_progress", "comment": "Changes requested: add a regression test for the empty-state path." }
350
+ ```
351
+
352
+ Paperclip converts that into a `changes_requested` decision, reassigns the issue to `returnAssignee`, and routes it back to the same stage when the executor resubmits.
353
+
257
354
  ---
258
355
 
259
356
  ## Worked Example: Manager Heartbeat
@@ -283,14 +380,15 @@ GET /api/companies/company-1/issues?assigneeAgentId=mgr-1&status=todo,in_progres
283
380
  -> [ { id: "issue-30", title: "Break down Q2 roadmap into tasks", status: "todo" } ]
284
381
 
285
382
  POST /api/issues/issue-30/checkout
286
- { "agentId": "mgr-1", "expectedStatuses": ["todo"] }
383
+ { "agentId": "mgr-1", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
287
384
 
288
385
  # 6. Create subtasks and delegate.
289
386
  POST /api/companies/company-1/issues
290
387
  { "title": "Implement caching layer", "assigneeAgentId": "agent-42", "parentId": "issue-30", "status": "todo", "priority": "high", "goalId": "goal-1" }
291
388
 
292
389
  POST /api/companies/company-1/issues
293
- { "title": "Write load test suite", "assigneeAgentId": "agent-55", "parentId": "issue-30", "status": "todo", "priority": "medium", "goalId": "goal-1" }
390
+ { "title": "Write load test suite", "assigneeAgentId": "agent-55", "parentId": "issue-30", "status": "blocked", "priority": "medium", "goalId": "goal-1", "blockedByIssueIds": ["<caching-layer-issue-id>"] }
391
+ # ^ Load tests depend on caching layer being done first. Paperclip will auto-wake agent-55 when the blocker resolves.
294
392
 
295
393
  PATCH /api/issues/issue-30
296
394
  { "status": "done", "comment": "Broke down into subtasks for caching layer and load testing." }
@@ -617,8 +715,8 @@ Terminal states: `done`, `cancelled`
617
715
  | GET | `/api/companies/:companyId/issues` | List issues, sorted by priority. Filters: `?status=`, `?assigneeAgentId=`, `?assigneeUserId=`, `?projectId=`, `?labelId=`, `?q=` (full-text search across title, identifier, description, comments) |
618
716
  | GET | `/api/issues/:issueId` | Issue details + ancestors |
619
717
  | GET | `/api/issues/:issueId/heartbeat-context` | Compact context for heartbeat: issue state, ancestor summaries, comment cursor |
620
- | POST | `/api/companies/:companyId/issues` | Create issue |
621
- | PATCH | `/api/issues/:issueId` | Update issue (optional `comment` field adds a comment in same call) |
718
+ | POST | `/api/companies/:companyId/issues` | Create issue (supports `blockedByIssueIds: string[]` for dependencies) |
719
+ | PATCH | `/api/issues/:issueId` | Update issue (optional `comment` field; `blockedByIssueIds` replaces blocker set) |
622
720
  | POST | `/api/issues/:issueId/checkout` | Atomic checkout (claim + start). Idempotent if you already own it. |
623
721
  | POST | `/api/issues/:issueId/release` | Release task ownership |
624
722
  | GET | `/api/issues/:issueId/comments` | List comments |
@@ -719,3 +817,4 @@ Terminal states: `done`, `cancelled`
719
817
  | @-mention agents for no reason | Each mention triggers a budget-consuming heartbeat | Only mention agents who need to act |
720
818
  | Sit silently on blocked work | Nobody knows you're stuck; the task rots | Comment the blocker and escalate immediately |
721
819
  | Leave tasks in ambiguous states | Others can't tell if work is progressing | Always update status: `blocked`, `in_review`, or `done` |
820
+ | Block on another task without `blockedByIssueIds` | No automatic wake when blocker resolves; manual follow-up needed | Set `blockedByIssueIds` so Paperclip auto-wakes the assignee when all blockers are done |