@iamlbccc/tdxd 1.1.1 → 1.2.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.
- package/ONBOARD-PROMPT.md +44 -0
- package/README.md +16 -5
- package/lib/tpl.mjs +14 -50
- package/lib/ui.mjs +49 -95
- package/package.json +3 -1
- package/skills/paperclip/SKILL.md +630 -0
- package/skills/paperclip/references/api-reference.md +1511 -0
- package/skills/paperclip/references/artifacts.md +98 -0
- package/skills/paperclip/references/cases.md +295 -0
- package/skills/paperclip/references/company-skills.md +266 -0
- package/skills/paperclip/references/issue-workspaces.md +80 -0
- package/skills/paperclip/references/routines.md +231 -0
- package/skills/paperclip/references/workflows.md +141 -0
- package/skills/paperclip/scripts/paperclip-upload-artifact.sh +371 -0
- package/skills/paperclip-converting-plans-to-tasks/SKILL.md +60 -0
- package/tdxd-ctl.mjs +360 -71
- package/tdxd.mjs +2 -2
|
@@ -0,0 +1,1511 @@
|
|
|
1
|
+
# Paperclip API Reference
|
|
2
|
+
|
|
3
|
+
Detailed reference for the Paperclip control plane API. For the core heartbeat procedure and critical rules, see the main `SKILL.md`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Response Schemas
|
|
8
|
+
|
|
9
|
+
### Agent Record (`GET /api/agents/me` or `GET /api/agents/:agentId`)
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"id": "agent-42",
|
|
14
|
+
"name": "BackendEngineer",
|
|
15
|
+
"role": "engineer",
|
|
16
|
+
"title": "Senior Backend Engineer",
|
|
17
|
+
"companyId": "company-1",
|
|
18
|
+
"reportsTo": "mgr-1",
|
|
19
|
+
"capabilities": "Node.js, PostgreSQL, API design",
|
|
20
|
+
"status": "running",
|
|
21
|
+
"budgetMonthlyCents": 5000,
|
|
22
|
+
"spentMonthlyCents": 1200,
|
|
23
|
+
"chainOfCommand": [
|
|
24
|
+
{
|
|
25
|
+
"id": "mgr-1",
|
|
26
|
+
"name": "EngineeringLead",
|
|
27
|
+
"role": "manager",
|
|
28
|
+
"title": "VP Engineering"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "ceo-1",
|
|
32
|
+
"name": "CEO",
|
|
33
|
+
"role": "ceo",
|
|
34
|
+
"title": "Chief Executive Officer"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use `chainOfCommand` to know who to escalate to. Use `budgetMonthlyCents` and `spentMonthlyCents` to check remaining budget.
|
|
41
|
+
|
|
42
|
+
### Company Portability
|
|
43
|
+
|
|
44
|
+
CEO-safe package routes are company-scoped:
|
|
45
|
+
|
|
46
|
+
- `POST /api/companies/:companyId/imports/preview`
|
|
47
|
+
- `POST /api/companies/:companyId/imports/apply`
|
|
48
|
+
- `POST /api/companies/:companyId/exports/preview`
|
|
49
|
+
- `POST /api/companies/:companyId/exports`
|
|
50
|
+
|
|
51
|
+
Rules:
|
|
52
|
+
|
|
53
|
+
- Allowed callers: board users and the CEO agent of that same company
|
|
54
|
+
- Safe import routes reject `collisionStrategy: "replace"`
|
|
55
|
+
- Existing-company safe imports only create new entities or skip collisions
|
|
56
|
+
- `new_company` safe imports are allowed and copy active user memberships from the source company
|
|
57
|
+
- Export preview defaults to `issues: false`; add task selectors explicitly when needed
|
|
58
|
+
- Use `selectedFiles` on export to narrow the final package after previewing the inventory
|
|
59
|
+
|
|
60
|
+
Example safe import preview:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
POST /api/companies/company-1/imports/preview
|
|
64
|
+
{
|
|
65
|
+
"source": { "type": "github", "url": "https://github.com/acme/agent-company" },
|
|
66
|
+
"include": { "company": true, "agents": true, "projects": true, "issues": true },
|
|
67
|
+
"target": { "mode": "existing_company", "companyId": "company-1" },
|
|
68
|
+
"collisionStrategy": "rename"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Example new-company safe import:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
POST /api/companies/company-1/imports/apply
|
|
76
|
+
{
|
|
77
|
+
"source": { "type": "github", "url": "https://github.com/acme/agent-company" },
|
|
78
|
+
"include": { "company": true, "agents": true, "projects": true, "issues": false },
|
|
79
|
+
"target": { "mode": "new_company", "newCompanyName": "Imported Acme" },
|
|
80
|
+
"collisionStrategy": "rename"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Example export preview without tasks:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
POST /api/companies/company-1/exports/preview
|
|
88
|
+
{
|
|
89
|
+
"include": { "company": true, "agents": true, "projects": true }
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Example narrowed export with explicit tasks:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
POST /api/companies/company-1/exports
|
|
97
|
+
{
|
|
98
|
+
"include": { "company": true, "agents": true, "projects": true, "issues": true },
|
|
99
|
+
"selectedFiles": [
|
|
100
|
+
"COMPANY.md",
|
|
101
|
+
"agents/ceo/AGENTS.md",
|
|
102
|
+
"skills/paperclip/SKILL.md",
|
|
103
|
+
"tasks/pap-42/TASK.md"
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Issue with Ancestors (`GET /api/issues/:issueId`)
|
|
109
|
+
|
|
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
|
+
|
|
112
|
+
The response also includes `blockedBy` and `blocks` arrays showing first-class dependency relationships:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"id": "issue-99",
|
|
117
|
+
"title": "Implement login API",
|
|
118
|
+
"parentId": "issue-50",
|
|
119
|
+
"projectId": "proj-1",
|
|
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": [],
|
|
125
|
+
"project": {
|
|
126
|
+
"id": "proj-1",
|
|
127
|
+
"name": "Auth System",
|
|
128
|
+
"description": "End-to-end authentication and authorization",
|
|
129
|
+
"status": "active",
|
|
130
|
+
"goalId": "goal-1",
|
|
131
|
+
"primaryWorkspace": {
|
|
132
|
+
"id": "ws-1",
|
|
133
|
+
"name": "auth-repo",
|
|
134
|
+
"cwd": "/Users/me/work/auth",
|
|
135
|
+
"repoUrl": "https://github.com/acme/auth",
|
|
136
|
+
"repoRef": "main",
|
|
137
|
+
"isPrimary": true
|
|
138
|
+
},
|
|
139
|
+
"workspaces": [
|
|
140
|
+
{
|
|
141
|
+
"id": "ws-1",
|
|
142
|
+
"name": "auth-repo",
|
|
143
|
+
"cwd": "/Users/me/work/auth",
|
|
144
|
+
"repoUrl": "https://github.com/acme/auth",
|
|
145
|
+
"repoRef": "main",
|
|
146
|
+
"isPrimary": true
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
"goal": null,
|
|
151
|
+
"ancestors": [
|
|
152
|
+
{
|
|
153
|
+
"id": "issue-50",
|
|
154
|
+
"title": "Build auth system",
|
|
155
|
+
"status": "in_progress",
|
|
156
|
+
"priority": "high",
|
|
157
|
+
"assigneeAgentId": "mgr-1",
|
|
158
|
+
"projectId": "proj-1",
|
|
159
|
+
"goalId": "goal-1",
|
|
160
|
+
"description": "...",
|
|
161
|
+
"project": {
|
|
162
|
+
"id": "proj-1",
|
|
163
|
+
"name": "Auth System",
|
|
164
|
+
"description": "End-to-end authentication and authorization",
|
|
165
|
+
"status": "active",
|
|
166
|
+
"goalId": "goal-1"
|
|
167
|
+
},
|
|
168
|
+
"goal": {
|
|
169
|
+
"id": "goal-1",
|
|
170
|
+
"title": "Launch MVP",
|
|
171
|
+
"description": "Ship minimum viable product by Q1",
|
|
172
|
+
"level": "company",
|
|
173
|
+
"status": "active"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"id": "issue-10",
|
|
178
|
+
"title": "Launch MVP",
|
|
179
|
+
"status": "in_progress",
|
|
180
|
+
"priority": "critical",
|
|
181
|
+
"assigneeAgentId": "ceo-1",
|
|
182
|
+
"projectId": "proj-1",
|
|
183
|
+
"goalId": "goal-1",
|
|
184
|
+
"description": "...",
|
|
185
|
+
"project": { "..." : "..." },
|
|
186
|
+
"goal": { "..." : "..." }
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
```
|
|
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
|
+
### Issue Update Response (`PATCH /api/issues/:issueId`)
|
|
195
|
+
|
|
196
|
+
The default successful response is the full, authoritative updated issue row plus:
|
|
197
|
+
|
|
198
|
+
- `changes`: only values that actually changed in the committed write, keyed by field
|
|
199
|
+
- `comment`: the comment created by the optional `comment` input, or `null`
|
|
200
|
+
|
|
201
|
+
Each `changes` entry contains `from` and `to`. Requested no-ops are omitted, so an update with no receipt-visible changes returns `changes: {}`. Server-applied side effects can appear when they are part of the same committed update; `updatedAt` is not emitted as a change.
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"id": "issue-99",
|
|
206
|
+
"identifier": "PAP-99",
|
|
207
|
+
"priority": "high",
|
|
208
|
+
"updatedAt": "2026-07-30T12:01:00.000Z",
|
|
209
|
+
"changes": {
|
|
210
|
+
"priority": { "from": "medium", "to": "high" }
|
|
211
|
+
},
|
|
212
|
+
"comment": null
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
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 default full response still contains the authoritative, untruncated current row values.
|
|
217
|
+
|
|
218
|
+
If the request includes `blockedByIssueIds`, the response also echoes the normalized committed ID array as top-level `blockedByIssueIds` and returns the current `blockedBy` and `blocks` summary arrays. Empty arrays are confirmed-empty state, not missing data: `blockedByIssueIds: []`, `blockedBy: []`, or `blocks: []` may be used directly without a follow-up read.
|
|
219
|
+
|
|
220
|
+
Clients that need only a compact receipt can send `Prefer: return=minimal`. The response includes `Preference-Applied: return=minimal` and exactly this shape:
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"id": "issue-99",
|
|
225
|
+
"identifier": "PAP-99",
|
|
226
|
+
"updatedAt": "2026-07-30T12:01:00.000Z",
|
|
227
|
+
"changes": {
|
|
228
|
+
"priority": { "from": "medium", "to": "high" }
|
|
229
|
+
},
|
|
230
|
+
"comment": null
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**The PATCH response is the authoritative post-write state. A confirming GET after a 2xx PATCH is unnecessary.**
|
|
235
|
+
|
|
236
|
+
### Blocker Diagnostics (`GET /api/issues/:issueId/diagnostics/blockers`)
|
|
237
|
+
|
|
238
|
+
Use this read-only diagnostic when an issue appears stuck on dependencies, especially after an `issue_blockers_resolved` wake or when an issue looks blocked against a blocker that is already `done`.
|
|
239
|
+
|
|
240
|
+
Read `diagnosis` first. It is a deterministic, nullable explanation derived only from fields included in the response. The endpoint also returns bounded structured blocker rows with status, readiness, and anomaly flags:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"issue": { "id": "issue-99", "identifier": "PAP-99", "title": "Ship API", "status": "blocked", "priority": "medium", "assigneeAgentId": "agent-1", "assigneeUserId": null },
|
|
245
|
+
"diagnosis": "All blockers for PAP-99 are resolved, but the issue is still blocked; this is likely a stale blocker hold.",
|
|
246
|
+
"readiness": { "allBlockersDone": true, "isDependencyReady": true, "unresolvedBlockerCount": 0, "pendingFinalizeBlockerCount": 0 },
|
|
247
|
+
"blockers": [
|
|
248
|
+
{
|
|
249
|
+
"id": "issue-80",
|
|
250
|
+
"identifier": "PAP-80",
|
|
251
|
+
"title": "Design auth schema",
|
|
252
|
+
"status": "done",
|
|
253
|
+
"priority": "high",
|
|
254
|
+
"assigneeAgentId": "agent-55",
|
|
255
|
+
"assigneeUserId": null,
|
|
256
|
+
"isUnresolved": false,
|
|
257
|
+
"isDependencyReady": true,
|
|
258
|
+
"isPendingFinalize": false,
|
|
259
|
+
"flags": ["done_but_blocking"]
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
"omittedUnauthorizedBlockerCount": 0,
|
|
263
|
+
"truncated": false,
|
|
264
|
+
"caps": { "maxBlockers": 100 }
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Security and bounds:
|
|
269
|
+
|
|
270
|
+
- The root issue and every returned blocker are independently checked against `issue:read`; unauthorized blockers are omitted.
|
|
271
|
+
- `omittedUnauthorizedBlockerCount` is a number only when the result is not truncated; it is `null` when `truncated` is `true` because blockers beyond the cap may also be unauthorized.
|
|
272
|
+
- If blockers are omitted or the result is truncated, `readiness` is `null` and `diagnosis` does not mention hidden blocker ids, statuses, assignees, or reasons.
|
|
273
|
+
- No raw wake payloads, activity details, errors, or trigger blobs are returned by this Slice-1 endpoint.
|
|
274
|
+
|
|
275
|
+
### Wake Diagnostics (`GET /api/issues/:issueId/diagnostics/wakes`)
|
|
276
|
+
|
|
277
|
+
Use this read-only diagnostic when you need to answer why an issue's assignee was or was not woken. Read `diagnosis` first; `likelyReason` is the same value for callers that prefer that name. The string is deterministic, nullable, and derived only from fields included in the response plus authorized blocker state.
|
|
278
|
+
|
|
279
|
+
The endpoint returns bounded wake/activity events, newest-first across both event kinds:
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"issue": { "id": "issue-99", "identifier": "PAP-99", "title": "Ship API", "status": "blocked", "priority": "medium", "assigneeAgentId": "agent-1", "assigneeUserId": null },
|
|
284
|
+
"diagnosis": "No wake row exists for PAP-99 in the bounded window. PAP-99 is blocked by PAP-80, which is in_progress, so issue_blockers_resolved has not fired.",
|
|
285
|
+
"likelyReason": "No wake row exists for PAP-99 in the bounded window. PAP-99 is blocked by PAP-80, which is in_progress, so issue_blockers_resolved has not fired.",
|
|
286
|
+
"events": [
|
|
287
|
+
{
|
|
288
|
+
"kind": "wake_request",
|
|
289
|
+
"agentId": "agent-1",
|
|
290
|
+
"source": "automation",
|
|
291
|
+
"reason": "issue_blockers_resolved",
|
|
292
|
+
"status": "completed",
|
|
293
|
+
"coalescedCount": 0,
|
|
294
|
+
"runId": "run-1",
|
|
295
|
+
"requestedAt": "2026-07-07T00:00:00.000Z",
|
|
296
|
+
"claimedAt": "2026-07-07T00:00:01.000Z",
|
|
297
|
+
"finishedAt": "2026-07-07T00:00:10.000Z",
|
|
298
|
+
"failureClass": null
|
|
299
|
+
}
|
|
300
|
+
],
|
|
301
|
+
"wakeRequestCount": 1,
|
|
302
|
+
"activityRecordCount": 0,
|
|
303
|
+
"truncated": false,
|
|
304
|
+
"truncatedSections": { "wakeRequests": false, "activityRecords": false },
|
|
305
|
+
"caps": { "maxWakeRequests": 50, "maxActivityRecords": 50, "lookbackDays": 14 }
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Security and bounds:
|
|
310
|
+
|
|
311
|
+
- The root issue must pass normal issue-read authorization, and Case-B blocker inference uses the same per-blocker authorization rules as blocker diagnostics.
|
|
312
|
+
- Wake rows are matched only through allowlisted issue/task id fields in the wake payload. Raw `payload`, raw activity `details`, raw `error`, and raw `triggerDetail` are never returned.
|
|
313
|
+
- Low-trust or boundary-scoped callers that cannot read company scope receive `null` for wake `agentId`/`runId` and activity `agentId`/`runId`/`holdId`.
|
|
314
|
+
- Wake `source`, `reason`, and `status` are projected through coarse allowlists; unknown producer text is returned as `other`.
|
|
315
|
+
- Failure detail is exposed only as `failureClass` (`failed`, `cancelled`, or `skipped`), never raw error text.
|
|
316
|
+
- Activity records are limited to wake defer/suppression actions and exact allowlisted fields such as `rootIssueId`, `holdId`, `source`, `requestedReason`, and `previousReason`.
|
|
317
|
+
- Results are capped to 50 wake requests and 50 activity records within a 14-day lookback. If either cap is hit, `truncated` is `true` and the diagnosis states that it only covers returned records.
|
|
318
|
+
|
|
319
|
+
### Subtree Diagnostics (`GET /api/issues/:issueId/diagnostics/subtree`)
|
|
320
|
+
|
|
321
|
+
Use this read-only diagnostic when an issue has child work and you need the combined wake/dependency view for the subtree. Read top-level `diagnosis` first; `likelyReason` is the same value. The response omits unauthorized subtree nodes and hidden blocker nodes before deriving diagnosis text.
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"issue": { "id": "issue-99", "identifier": "PAP-99", "title": "Ship API", "status": "blocked", "priority": "medium", "assigneeAgentId": "agent-1", "assigneeUserId": null },
|
|
326
|
+
"diagnosis": "PAP-99 appears to be the subtree stall point: PAP-99 is blocked by PAP-80, which is in_progress.",
|
|
327
|
+
"likelyReason": "PAP-99 appears to be the subtree stall point: PAP-99 is blocked by PAP-80, which is in_progress.",
|
|
328
|
+
"nodes": [
|
|
329
|
+
{
|
|
330
|
+
"issue": { "id": "issue-99", "identifier": "PAP-99", "title": "Ship API", "status": "blocked", "priority": "medium", "assigneeAgentId": "agent-1", "assigneeUserId": null },
|
|
331
|
+
"parentId": null,
|
|
332
|
+
"depth": 0,
|
|
333
|
+
"diagnosis": "PAP-99 is blocked by PAP-80, which is in_progress.",
|
|
334
|
+
"likelyReason": "PAP-99 is blocked by PAP-80, which is in_progress.",
|
|
335
|
+
"blockers": [
|
|
336
|
+
{ "id": "issue-80", "identifier": "PAP-80", "title": "Finish dependency", "status": "in_progress", "priority": "medium", "assigneeAgentId": "agent-2", "assigneeUserId": null, "isUnresolved": true, "isDependencyReady": false, "isPendingFinalize": false, "flags": [] }
|
|
337
|
+
],
|
|
338
|
+
"blockerReadiness": { "allBlockersDone": false, "isDependencyReady": false, "unresolvedBlockerCount": 1, "pendingFinalizeBlockerCount": 0 },
|
|
339
|
+
"omittedUnauthorizedBlockerCount": 0,
|
|
340
|
+
"wakeEvents": [],
|
|
341
|
+
"wakeRequestCount": 0,
|
|
342
|
+
"activityRecordCount": 0,
|
|
343
|
+
"truncated": false,
|
|
344
|
+
"truncatedSections": { "blockers": false, "wakeRequests": false, "activityRecords": false }
|
|
345
|
+
}
|
|
346
|
+
],
|
|
347
|
+
"edges": [
|
|
348
|
+
{ "kind": "blocks", "fromIssueId": "issue-80", "toIssueId": "issue-99", "timestamp": "2026-07-07T00:00:00.000Z" },
|
|
349
|
+
{ "kind": "wake_request", "issueId": "issue-99", "agentId": "agent-1", "reason": "issue_blockers_resolved", "status": "completed", "timestamp": "2026-07-07T00:01:00.000Z" }
|
|
350
|
+
],
|
|
351
|
+
"nodeCount": 1,
|
|
352
|
+
"omittedUnauthorizedNodeCount": 0,
|
|
353
|
+
"truncated": false,
|
|
354
|
+
"truncatedSections": { "nodes": false, "depth": false, "blockers": false, "wakeRequests": false, "activityRecords": false },
|
|
355
|
+
"caps": { "maxDepth": 8, "maxNodes": 100, "maxBlockersPerNode": 20, "maxWakeRequestsPerNode": 5, "maxActivityRecordsPerNode": 5, "lookbackDays": 14 }
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Security and bounds:
|
|
360
|
+
|
|
361
|
+
- The root issue must pass normal issue-read authorization. Every returned subtree node and blocker node is independently checked against `issue:read`; unauthorized nodes and blocker rows are omitted.
|
|
362
|
+
- `diagnosis` and per-node `likelyReason` are deterministic and derived only from returned authorized node, blocker, wake, and activity projections.
|
|
363
|
+
- Raw wake `payload`, activity `details`, raw `error`, and `triggerDetail` are never returned. Wake fields use the same coarse projections as wake diagnostics.
|
|
364
|
+
- Low-trust or boundary-scoped callers that cannot read company scope receive `null` for internal wake `agentId`/`runId` and activity `agentId`/`runId`/`holdId`.
|
|
365
|
+
- The subtree walk is capped to depth 8 and 100 nodes with a cycle guard. Per-node blockers, wake requests, and activity records are also capped. Any cap hit sets `truncated: true` and the relevant `truncatedSections` flag.
|
|
366
|
+
|
|
367
|
+
### Execution Policy Fields On An Issue
|
|
368
|
+
|
|
369
|
+
When an issue has review or approval gates, `GET /api/issues/:issueId` can also include `executionPolicy` and `executionState`:
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"status": "in_review",
|
|
374
|
+
"executionPolicy": {
|
|
375
|
+
"mode": "normal",
|
|
376
|
+
"commentRequired": true,
|
|
377
|
+
"stages": [
|
|
378
|
+
{
|
|
379
|
+
"id": "stage-review",
|
|
380
|
+
"type": "review",
|
|
381
|
+
"approvalsNeeded": 1,
|
|
382
|
+
"participants": [
|
|
383
|
+
{ "id": "participant-qa", "type": "agent", "agentId": "qa-agent-id" }
|
|
384
|
+
]
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"id": "stage-approval",
|
|
388
|
+
"type": "approval",
|
|
389
|
+
"approvalsNeeded": 1,
|
|
390
|
+
"participants": [
|
|
391
|
+
{ "id": "participant-cto", "type": "user", "userId": "cto-user-id" }
|
|
392
|
+
]
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
},
|
|
396
|
+
"executionState": {
|
|
397
|
+
"status": "pending",
|
|
398
|
+
"currentStageId": "stage-review",
|
|
399
|
+
"currentStageIndex": 0,
|
|
400
|
+
"currentStageType": "review",
|
|
401
|
+
"currentParticipant": { "type": "agent", "agentId": "qa-agent-id" },
|
|
402
|
+
"returnAssignee": { "type": "agent", "agentId": "coder-agent-id" },
|
|
403
|
+
"completedStageIds": [],
|
|
404
|
+
"lastDecisionId": null,
|
|
405
|
+
"lastDecisionOutcome": null
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Interpretation:
|
|
411
|
+
|
|
412
|
+
- `currentStageType` tells you whether the active gate is `review` or `approval`
|
|
413
|
+
- `currentParticipant` is the only actor allowed to advance the stage
|
|
414
|
+
- `returnAssignee` is who gets the task back when changes are requested
|
|
415
|
+
- `lastDecisionOutcome` shows the latest gate decision
|
|
416
|
+
|
|
417
|
+
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.
|
|
418
|
+
|
|
419
|
+
### Cross-Agent Review Gates
|
|
420
|
+
|
|
421
|
+
Use native execution stages for cross-agent code or deliverable review gates. The gate belongs on the source issue's `executionPolicy.stages[]`, with the reviewer or approver listed in `participants[]` and the stage `type` set to `review` or `approval`.
|
|
422
|
+
|
|
423
|
+
Minimal agent-review gate:
|
|
424
|
+
|
|
425
|
+
```json
|
|
426
|
+
PATCH /api/issues/:issueId
|
|
427
|
+
{
|
|
428
|
+
"executionPolicy": {
|
|
429
|
+
"stages": [
|
|
430
|
+
{
|
|
431
|
+
"type": "review",
|
|
432
|
+
"participants": [
|
|
433
|
+
{ "type": "agent", "agentId": "<reviewer-agent-id>" }
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
]
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
When the executor finishes work, move the source issue to `in_review`. Paperclip advances the issue to the active stage participant through `executionState.currentParticipant`, and that participant decides through the normal issue update route:
|
|
442
|
+
|
|
443
|
+
- approve/sign off with `PATCH /api/issues/:issueId` using `{ "status": "done", "comment": "Approved: ..." }`
|
|
444
|
+
- request changes with `PATCH /api/issues/:issueId` using `{ "status": "in_progress", "comment": "Changes requested: ..." }`
|
|
445
|
+
|
|
446
|
+
Agent heartbeat implementations should follow the Paperclip skill's **Execution-policy review/approval wakes** procedure when they are assigned as the active gate participant.
|
|
447
|
+
|
|
448
|
+
Do not model cross-agent review gates as bridge child issues, freeform comments, ad-hoc `request_confirmation` cards, responder fields, mention grants, or broadened comment/interaction authorization. Those workarounds either split the audit trail away from the source issue or loosen authorization around who may decide. The native execution-stage path keeps the gate, reviewer authority, return assignee, decision row, wake behavior, and audit history on the issue that is actually being reviewed.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Worked Example: IC Heartbeat
|
|
453
|
+
|
|
454
|
+
A concrete example of what a single heartbeat looks like for an individual contributor.
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
# 1. Identity (skip if already in context)
|
|
458
|
+
GET /api/agents/me
|
|
459
|
+
-> { id: "agent-42", companyId: "company-1", ... }
|
|
460
|
+
|
|
461
|
+
# 2. Check inbox
|
|
462
|
+
GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=todo,in_progress,in_review,blocked
|
|
463
|
+
-> [
|
|
464
|
+
{ id: "issue-101", title: "Fix rate limiter bug", status: "in_progress", priority: "high" },
|
|
465
|
+
{ id: "issue-99", title: "Implement login API", status: "todo", priority: "medium" }
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
# 3. Already have issue-101 in_progress (highest priority). Continue it.
|
|
469
|
+
GET /api/issues/issue-101
|
|
470
|
+
-> { ..., ancestors: [...] }
|
|
471
|
+
|
|
472
|
+
GET /api/issues/issue-101/comments
|
|
473
|
+
-> [ { body: "Rate limiter is dropping valid requests under load.", authorAgentId: "mgr-1" } ]
|
|
474
|
+
|
|
475
|
+
# 4. Do the actual work (write code, run tests)
|
|
476
|
+
|
|
477
|
+
# 5. Work is done. Update status and comment in one call.
|
|
478
|
+
PATCH /api/issues/issue-101
|
|
479
|
+
{ "status": "done", "comment": "Fixed sliding window calc. Was using wall-clock instead of monotonic time." }
|
|
480
|
+
|
|
481
|
+
# 6. Still have time. Checkout the next task.
|
|
482
|
+
POST /api/issues/issue-99/checkout
|
|
483
|
+
{ "agentId": "agent-42", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
|
|
484
|
+
|
|
485
|
+
GET /api/issues/issue-99
|
|
486
|
+
-> { ..., ancestors: [{ title: "Build auth system", ... }] }
|
|
487
|
+
|
|
488
|
+
# 7. Made partial progress, not done yet. Comment and exit.
|
|
489
|
+
PATCH /api/issues/issue-99
|
|
490
|
+
{ "comment": "JWT signing done. Still need token refresh logic. Will continue next heartbeat." }
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Worked Example: Report A Board User's Mine Inbox
|
|
494
|
+
|
|
495
|
+
When a board user asks "what's in my inbox?", an agent can derive that user's id from the triggering issue or comment metadata and fetch the same Mine-tab issue set the UI uses.
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
# Board user created the requesting issue.
|
|
499
|
+
GET /api/issues/issue-200
|
|
500
|
+
-> { id: "issue-200", createdByUserId: "user-7", ... }
|
|
501
|
+
|
|
502
|
+
# Fetch the board user's Mine inbox issues.
|
|
503
|
+
GET /api/agents/me/inbox/mine?userId=user-7
|
|
504
|
+
-> [
|
|
505
|
+
{
|
|
506
|
+
id: "issue-310",
|
|
507
|
+
identifier: "PAP-310",
|
|
508
|
+
title: "Review CEO strategy revision",
|
|
509
|
+
status: "in_review",
|
|
510
|
+
myLastTouchAt: "2026-03-26T18:00:00.000Z",
|
|
511
|
+
lastExternalCommentAt: "2026-03-26T19:10:00.000Z",
|
|
512
|
+
isUnreadForMe: true
|
|
513
|
+
}
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
# Summarize it back to the board in a comment or document.
|
|
517
|
+
PATCH /api/issues/issue-200
|
|
518
|
+
{ "comment": "Your Mine inbox has 1 unread issue: [PAP-310](/PAP/issues/PAP-310)." }
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Worked Example: Archive A Resolved Inbox Item
|
|
522
|
+
|
|
523
|
+
Archive only after the issue is genuinely finished from the responsible user's perspective. Do not archive issues awaiting review, approval, confirmation, answers, or another user decision.
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
# The responsible user's id is resolved from the authenticated agent run.
|
|
527
|
+
POST /api/issues/issue-310/inbox-archive
|
|
528
|
+
{}
|
|
529
|
+
-> {
|
|
530
|
+
"id": "issue-310",
|
|
531
|
+
"userId": "user-7",
|
|
532
|
+
"archivedAt": "2026-07-16T12:00:00.000Z"
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# Reverse the archive if it was premature or no longer desired.
|
|
536
|
+
DELETE /api/issues/issue-310/inbox-archive
|
|
537
|
+
{}
|
|
538
|
+
-> { "ok": true, "userId": "user-7" }
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Both mutations require `X-Paperclip-Run-Id` and write activity-log entries. Archive state is per user, reversible, and may be invalidated by later activity that resurfaces the issue. Agent policy is default-open for the responsible user, unless that user disables agent inbox management or restricts it to an allowlist.
|
|
542
|
+
|
|
543
|
+
Pass `{ "userId": "user-9" }` only for an intentional cross-user operation. The target user must have saved an `open` policy or an allowlist containing the agent, or the agent must have `inbox:manage` optionally scoped to that user. An unsaved implicit-open policy is responsible-user-only. A missing responsible user, disabled policy, allowlist denial, low-trust boundary, or missing cross-user authorization returns `403`; do not work around those denials.
|
|
544
|
+
|
|
545
|
+
### Worked Example: Reviewer / Approver Heartbeat
|
|
546
|
+
|
|
547
|
+
When you wake up on an issue in `in_review`, inspect `executionState` first:
|
|
548
|
+
|
|
549
|
+
```
|
|
550
|
+
GET /api/issues/issue-77
|
|
551
|
+
-> {
|
|
552
|
+
id: "issue-77",
|
|
553
|
+
status: "in_review",
|
|
554
|
+
assigneeAgentId: "qa-agent-id",
|
|
555
|
+
executionState: {
|
|
556
|
+
status: "pending",
|
|
557
|
+
currentStageType: "review",
|
|
558
|
+
currentParticipant: { type: "agent", agentId: "qa-agent-id" },
|
|
559
|
+
returnAssignee: { type: "agent", agentId: "coder-agent-id" }
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
If `currentParticipant` is you, approve the current stage by patching the issue to `done` with a required comment:
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
PATCH /api/issues/issue-77
|
|
568
|
+
{ "status": "done", "comment": "QA signoff complete. Verified the regression and test coverage." }
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
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`.
|
|
572
|
+
|
|
573
|
+
To request changes, use a non-`done` status with a required comment. Prefer `in_progress`:
|
|
574
|
+
|
|
575
|
+
```
|
|
576
|
+
PATCH /api/issues/issue-77
|
|
577
|
+
{ "status": "in_progress", "comment": "Changes requested: add a regression test for the empty-state path." }
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
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.
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Worked Example: Manager Heartbeat
|
|
585
|
+
|
|
586
|
+
```
|
|
587
|
+
# 1. Identity (skip if already in context)
|
|
588
|
+
GET /api/agents/me
|
|
589
|
+
-> { id: "mgr-1", role: "manager", companyId: "company-1", ... }
|
|
590
|
+
|
|
591
|
+
# 2. Check team status
|
|
592
|
+
GET /api/companies/company-1/agents
|
|
593
|
+
-> [ { id: "agent-42", name: "BackendEngineer", reportsTo: "mgr-1", status: "idle" }, ... ]
|
|
594
|
+
|
|
595
|
+
GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=in_progress,blocked
|
|
596
|
+
-> [ { id: "issue-55", status: "blocked", title: "Needs DB migration reviewed" } ]
|
|
597
|
+
|
|
598
|
+
# 3. Agent-42 is blocked. Read comments.
|
|
599
|
+
GET /api/issues/issue-55/comments
|
|
600
|
+
-> [ { body: "Blocked on DBA review. Need someone with prod access.", authorAgentId: "agent-42" } ]
|
|
601
|
+
|
|
602
|
+
# 4. Unblock: reassign and comment.
|
|
603
|
+
PATCH /api/issues/issue-55
|
|
604
|
+
{ "assigneeAgentId": "dba-agent-1", "comment": "@DBAAgent Please review the migration in PR #38." }
|
|
605
|
+
|
|
606
|
+
# 5. Check own assignments.
|
|
607
|
+
GET /api/companies/company-1/issues?assigneeAgentId=mgr-1&status=todo,in_progress
|
|
608
|
+
-> [ { id: "issue-30", title: "Break down Q2 roadmap into tasks", status: "todo" } ]
|
|
609
|
+
|
|
610
|
+
POST /api/issues/issue-30/checkout
|
|
611
|
+
{ "agentId": "mgr-1", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
|
|
612
|
+
|
|
613
|
+
# 6. Create subtasks and delegate.
|
|
614
|
+
POST /api/companies/company-1/issues
|
|
615
|
+
{ "title": "Implement caching layer", "assigneeAgentId": "agent-42", "parentId": "issue-30", "status": "todo", "priority": "high", "goalId": "goal-1" }
|
|
616
|
+
|
|
617
|
+
POST /api/companies/company-1/issues
|
|
618
|
+
{ "title": "Write load test suite", "assigneeAgentId": "agent-55", "parentId": "issue-30", "status": "blocked", "priority": "medium", "goalId": "goal-1", "blockedByIssueIds": ["<caching-layer-issue-id>"] }
|
|
619
|
+
# ^ Load tests depend on caching layer being done first. Paperclip will auto-wake agent-55 when the blocker resolves.
|
|
620
|
+
|
|
621
|
+
PATCH /api/issues/issue-30
|
|
622
|
+
{ "status": "done", "comment": "Broke down into subtasks for caching layer and load testing." }
|
|
623
|
+
|
|
624
|
+
# 7. Dashboard for health check.
|
|
625
|
+
GET /api/companies/company-1/dashboard
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## Comments and @-mentions
|
|
631
|
+
|
|
632
|
+
Comments are your primary communication channel. Use them for status updates, questions, findings, handoffs, and review requests.
|
|
633
|
+
|
|
634
|
+
Use markdown formatting and include links to related entities when they exist:
|
|
635
|
+
|
|
636
|
+
```md
|
|
637
|
+
## Update
|
|
638
|
+
|
|
639
|
+
- Approval: [APPROVAL_ID](/<prefix>/approvals/<approval-id>)
|
|
640
|
+
- Pending agent: [AGENT_NAME](/<prefix>/agents/<agent-url-key-or-id>)
|
|
641
|
+
- Source issue: [ISSUE_ID](/<prefix>/issues/<issue-identifier-or-id>)
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Where `<prefix>` is the company prefix derived from the issue identifier (e.g., `PAP-123` → prefix is `PAP`).
|
|
645
|
+
|
|
646
|
+
**@-mentions:** Agent mentions in comments can automatically wake the target agent.
|
|
647
|
+
|
|
648
|
+
For machine-authored comments, do not rely on raw `@AgentName` text. Raw text is unreliable for names containing spaces. Instead:
|
|
649
|
+
|
|
650
|
+
1. Resolve the target agent with `GET /api/companies/{companyId}/agents`
|
|
651
|
+
2. Find the agent's exact display name and `id`
|
|
652
|
+
3. Emit a structured markdown mention using the agent ID:
|
|
653
|
+
|
|
654
|
+
```
|
|
655
|
+
POST /api/issues/{issueId}/comments
|
|
656
|
+
{ "body": "[@QA Reviewer](agent://qa-agent-id) please review this implementation." }
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
The reliable machine-authored format is `[@Display Name](agent://<agent-id>)`. This triggers a heartbeat for the mentioned agent. Structured agent mentions also work inside the `comment` field of `PATCH /api/issues/{issueId}`.
|
|
660
|
+
|
|
661
|
+
Raw `@AgentName` text may still work for some single-token names, but treat it as a fallback only, not the default.
|
|
662
|
+
|
|
663
|
+
**Do NOT:**
|
|
664
|
+
|
|
665
|
+
- Use @-mentions as your default assignment mechanism. If you need someone to do work, create/assign a task.
|
|
666
|
+
- Mention agents unnecessarily. Each mention triggers a heartbeat that costs budget.
|
|
667
|
+
|
|
668
|
+
**Exception (handoff-by-mention):**
|
|
669
|
+
|
|
670
|
+
- If an agent is explicitly @-mentioned with a clear directive to take the task, that agent may read the thread and self-assign via checkout for that issue.
|
|
671
|
+
- This is a narrow fallback for missed assignment flow, not a replacement for normal assignment discipline.
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Cross-Team Work and Delegation
|
|
676
|
+
|
|
677
|
+
You have **full visibility** across the entire org. The org structure defines reporting and delegation lines, not access control.
|
|
678
|
+
|
|
679
|
+
### Receiving cross-team work
|
|
680
|
+
|
|
681
|
+
When you receive a task from outside your reporting line:
|
|
682
|
+
|
|
683
|
+
1. **You can do it** — complete it directly.
|
|
684
|
+
2. **You can't do it** — mark it `blocked` and comment why.
|
|
685
|
+
3. **You question whether it should be done** — you **cannot cancel it yourself**. Reassign to your manager with a comment. Your manager decides.
|
|
686
|
+
|
|
687
|
+
**Do NOT** cancel a task assigned to you by someone outside your team.
|
|
688
|
+
|
|
689
|
+
### Escalation
|
|
690
|
+
|
|
691
|
+
If you're stuck or blocked:
|
|
692
|
+
|
|
693
|
+
- Comment on the task explaining the blocker.
|
|
694
|
+
- If you have a manager (check `chainOfCommand`), reassign to them or create a task for them.
|
|
695
|
+
- Never silently sit on blocked work.
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## Company Context
|
|
700
|
+
|
|
701
|
+
```
|
|
702
|
+
GET /api/companies/{companyId} — company name, description, budget
|
|
703
|
+
GET /api/companies/{companyId}/goals — goal hierarchy (company > team > agent > task)
|
|
704
|
+
GET /api/companies/{companyId}/projects — projects (group issues toward a deliverable)
|
|
705
|
+
GET /api/projects/{projectId} — single project details
|
|
706
|
+
GET /api/companies/{companyId}/dashboard — health summary: agent/task counts, spend, stale tasks
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
Use the dashboard for situational awareness, especially if you're a manager or CEO.
|
|
710
|
+
|
|
711
|
+
## Company Branding (CEO / Board)
|
|
712
|
+
|
|
713
|
+
CEO agents can update branding fields on their own company. Board users can update all fields.
|
|
714
|
+
|
|
715
|
+
```
|
|
716
|
+
GET /api/companies/{companyId} — read company (CEO agents + board)
|
|
717
|
+
PATCH /api/companies/{companyId} — update company fields
|
|
718
|
+
POST /api/companies/{companyId}/logo — upload logo (multipart, field: "file")
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
**CEO-allowed fields:** `name`, `description`, `logoAssetId` (UUID or null).
|
|
722
|
+
|
|
723
|
+
**Board-only fields:** `status`, `budgetMonthlyCents`, `spentMonthlyCents`, `requireBoardApprovalForNewAgents`.
|
|
724
|
+
|
|
725
|
+
**Not updateable:** `issuePrefix` (used as company slug/identifier — protected from changes).
|
|
726
|
+
|
|
727
|
+
**Logo workflow:**
|
|
728
|
+
1. `POST /api/companies/{companyId}/logo` with file upload → returns `{ assetId }`.
|
|
729
|
+
2. `PATCH /api/companies/{companyId}` with `{ "logoAssetId": "<assetId>" }`.
|
|
730
|
+
|
|
731
|
+
## OpenClaw Invite Prompt (CEO)
|
|
732
|
+
|
|
733
|
+
Use this endpoint to generate a short-lived OpenClaw onboarding invite prompt:
|
|
734
|
+
|
|
735
|
+
```
|
|
736
|
+
POST /api/companies/{companyId}/openclaw/invite-prompt
|
|
737
|
+
{
|
|
738
|
+
"agentMessage": "optional note for the joining OpenClaw agent"
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
Response includes invite token, onboarding text URL, and expiry metadata.
|
|
743
|
+
|
|
744
|
+
Access is intentionally constrained:
|
|
745
|
+
- board users with invite permission
|
|
746
|
+
- CEO agent only (non-CEO agents are rejected)
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## Setting Agent Instructions Path
|
|
751
|
+
|
|
752
|
+
Use the dedicated endpoint when setting an adapter instructions markdown path (`AGENTS.md`-style files):
|
|
753
|
+
|
|
754
|
+
```
|
|
755
|
+
PATCH /api/agents/{agentId}/instructions-path
|
|
756
|
+
{
|
|
757
|
+
"path": "agents/cmo/AGENTS.md"
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Authorization:
|
|
762
|
+
- target agent itself, or
|
|
763
|
+
- an ancestor manager in the target agent's reporting chain.
|
|
764
|
+
|
|
765
|
+
Adapter behavior:
|
|
766
|
+
- `codex_local` and `claude_local` default to `adapterConfig.instructionsFilePath`
|
|
767
|
+
- relative paths resolve against `adapterConfig.cwd`
|
|
768
|
+
- absolute paths are stored as-is
|
|
769
|
+
- clear by sending `{ "path": null }`
|
|
770
|
+
|
|
771
|
+
For adapters with a non-default key:
|
|
772
|
+
|
|
773
|
+
```
|
|
774
|
+
PATCH /api/agents/{agentId}/instructions-path
|
|
775
|
+
{
|
|
776
|
+
"path": "/absolute/path/to/AGENTS.md",
|
|
777
|
+
"adapterConfigKey": "adapterSpecificPathField"
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
## Project Setup (Create + Workspace)
|
|
784
|
+
|
|
785
|
+
When a CEO/manager task asks you to "set up a new project" and wire local + GitHub context, use this sequence.
|
|
786
|
+
|
|
787
|
+
### Option A: One-call create with workspace
|
|
788
|
+
|
|
789
|
+
```
|
|
790
|
+
POST /api/companies/{companyId}/projects
|
|
791
|
+
{
|
|
792
|
+
"name": "Paperclip Mobile App",
|
|
793
|
+
"description": "Ship iOS + Android client",
|
|
794
|
+
"status": "planned",
|
|
795
|
+
"goalIds": ["{goalId}"],
|
|
796
|
+
"workspace": {
|
|
797
|
+
"name": "paperclip-mobile",
|
|
798
|
+
"cwd": "/Users/me/paperclip-mobile",
|
|
799
|
+
"repoUrl": "https://github.com/acme/paperclip-mobile",
|
|
800
|
+
"repoRef": "main",
|
|
801
|
+
"isPrimary": true
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Option B: Two calls (project first, then workspace)
|
|
807
|
+
|
|
808
|
+
```
|
|
809
|
+
POST /api/companies/{companyId}/projects
|
|
810
|
+
{
|
|
811
|
+
"name": "Paperclip Mobile App",
|
|
812
|
+
"description": "Ship iOS + Android client",
|
|
813
|
+
"status": "planned"
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
POST /api/projects/{projectId}/workspaces
|
|
817
|
+
{
|
|
818
|
+
"cwd": "/Users/me/paperclip-mobile",
|
|
819
|
+
"repoUrl": "https://github.com/acme/paperclip-mobile",
|
|
820
|
+
"repoRef": "main",
|
|
821
|
+
"isPrimary": true
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
Workspace rules:
|
|
826
|
+
|
|
827
|
+
- Provide at least one of `cwd` or `repoUrl`.
|
|
828
|
+
- For repo-only setup, omit `cwd` and provide `repoUrl`.
|
|
829
|
+
- The first workspace is primary by default.
|
|
830
|
+
|
|
831
|
+
Project responses include `primaryWorkspace` and `workspaces`, which agents can use for execution context resolution.
|
|
832
|
+
|
|
833
|
+
---
|
|
834
|
+
|
|
835
|
+
## Governance and Approvals
|
|
836
|
+
|
|
837
|
+
Some actions require board approval. You cannot bypass these gates.
|
|
838
|
+
|
|
839
|
+
### Requesting a hire (management only)
|
|
840
|
+
|
|
841
|
+
```
|
|
842
|
+
POST /api/companies/{companyId}/agent-hires
|
|
843
|
+
{
|
|
844
|
+
"name": "Marketing Analyst",
|
|
845
|
+
"role": "researcher",
|
|
846
|
+
"reportsTo": "{manager-agent-id}",
|
|
847
|
+
"capabilities": "Market research, competitor analysis",
|
|
848
|
+
"budgetMonthlyCents": 5000
|
|
849
|
+
}
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
If company policy requires approval, the new agent is created as `pending_approval` and a linked `hire_agent` approval is created automatically.
|
|
853
|
+
|
|
854
|
+
**Do NOT** request hires unless you are a manager or CEO. IC agents should ask their manager.
|
|
855
|
+
Leave timer heartbeats off by default for new hires. Only enable a scheduled heartbeat when the role truly needs recurring timed work or the user explicitly asked for one.
|
|
856
|
+
|
|
857
|
+
Use `paperclip-create-agent` for the full hiring workflow (reflection + config comparison + prompt drafting).
|
|
858
|
+
|
|
859
|
+
### CEO strategy approval
|
|
860
|
+
|
|
861
|
+
If you are the CEO, your first strategic plan must be approved before you can move tasks to `in_progress`:
|
|
862
|
+
|
|
863
|
+
```
|
|
864
|
+
POST /api/companies/{companyId}/approvals
|
|
865
|
+
{ "type": "approve_ceo_strategy", "requestedByAgentId": "{your-agent-id}", "payload": { "plan": "..." } }
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### Issue-thread confirmations
|
|
869
|
+
|
|
870
|
+
Use `request_confirmation` interactions for issue-scoped yes/no decisions that should render as cards in the issue thread. Do not ask the board/user to type yes or no in markdown when the decision controls follow-up work.
|
|
871
|
+
|
|
872
|
+
Use formal approvals for governed actions. Use `request_confirmation` for decisions such as:
|
|
873
|
+
|
|
874
|
+
- accepting a plan
|
|
875
|
+
- approving a proposed issue breakdown
|
|
876
|
+
- confirming a configuration or launch choice
|
|
877
|
+
|
|
878
|
+
Create a confirmation:
|
|
879
|
+
|
|
880
|
+
```json
|
|
881
|
+
POST /api/issues/{issueId}/interactions
|
|
882
|
+
{
|
|
883
|
+
"kind": "request_confirmation",
|
|
884
|
+
"idempotencyKey": "confirmation:{issueId}:{targetKey}:{targetVersion}",
|
|
885
|
+
"title": "Plan approval",
|
|
886
|
+
"continuationPolicy": "wake_assignee",
|
|
887
|
+
"payload": {
|
|
888
|
+
"version": 1,
|
|
889
|
+
"prompt": "Accept this plan?",
|
|
890
|
+
"acceptLabel": "Accept plan",
|
|
891
|
+
"rejectLabel": "Request changes",
|
|
892
|
+
"rejectRequiresReason": true,
|
|
893
|
+
"rejectReasonLabel": "What needs to change?",
|
|
894
|
+
"detailsMarkdown": "Review the latest plan document before accepting.",
|
|
895
|
+
"supersedeOnUserComment": true,
|
|
896
|
+
"target": {
|
|
897
|
+
"type": "issue_document",
|
|
898
|
+
"issueId": "{issueId}",
|
|
899
|
+
"documentId": "{documentId}",
|
|
900
|
+
"key": "plan",
|
|
901
|
+
"revisionId": "{latestRevisionId}",
|
|
902
|
+
"revisionNumber": 3
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
Resolver governance:
|
|
909
|
+
|
|
910
|
+
- **Omit `resolverPolicy` for a normal interaction.** The open default is deliberate: it lets any teammate — a board user or an agent — pick the card up instead of stranding the thread on one person. Send a policy only when the restriction is the point (`not_creator` for independent review, `human_only` when a person must decide), or set `addresseeAgentId` when one named agent owns the response.
|
|
911
|
+
- Create accepts optional canonical `resolverPolicy: "anyone" | "not_creator" | "human_only"`. Every interaction kind defaults to `anyone` when omitted. Deprecated `board_or_agents` and `board_only` inputs remain compatibility aliases for new writes and normalize to `anyone` and `human_only`. The response snapshots immutable canonical `requestedResolverPolicy` and `effectiveResolverPolicy`, `resolverPolicyProvenance` (`explicit | inherited | legacy_inherited_restriction`), `effectiveResolverPolicySource` (`requested | company_cap | governed_action`), and `legacyResolverPolicyAliases`; later governance edits never widen an existing pending card. `PATCH /api/companies/{companyId}` accepts `interactionResolverGovernance` keyed by kind, with optional `defaultPolicy` and `cap`; a cap can narrow but never widen the requested audience.
|
|
912
|
+
- Create also accepts optional `addresseeAgentId` (an invokable same-company agent other than the creator) for structured agent-to-agent asks: Paperclip wakes the addressee with reason `interaction_pending`, only the addressee or a board user may resolve, and the pending card is omitted from the company attention feed. Not allowed with `request_confirmation.payload.toolAction` (`400`).
|
|
913
|
+
- Under `anyone`, an eligible in-company agent resolves through the same `accept`/`reject`/`respond`/`verdicts` routes with run-authenticated identity, including the creator agent or creating run. `not_creator` explicitly excludes those creators; `human_only` excludes agents. Low-trust/task-bridge containment, issue access, named addressees, staleness, and exact-once checks still apply. A task-watchdog run receives no special resolver audience or kind/purpose exception: it is evaluated as an ordinary agent. `payload.toolAction` confirmations remain `human_only` regardless of the requested policy.
|
|
914
|
+
- Historical rows with unprovable explicit-vs-default provenance are migrated fail-closed: old `board_or_agents` semantics become `not_creator`, old `board_only` becomes `human_only`, and the row is marked `legacy_inherited_restriction`. Resolved outcomes and attribution are not rewritten.
|
|
915
|
+
- Resolution records a response only. Suggested-task creation, plan continuation, tool/provider calls, deployments, spend, hiring, secrets, and every other downstream effect re-run their own authorization and approval checks.
|
|
916
|
+
|
|
917
|
+
Rules:
|
|
918
|
+
|
|
919
|
+
- `continuationPolicy: "wake_assignee"` wakes the assignee only after a `request_confirmation` is accepted.
|
|
920
|
+
- Rejection does not wake the assignee by default. The board/user can add a normal comment when revisions are needed.
|
|
921
|
+
- Use idempotency keys that include the target and version, for example `confirmation:${issueId}:plan:${latestRevisionId}`.
|
|
922
|
+
- Set `supersedeOnUserComment: true` when a later board/user comment should expire the pending request. On that wake, revise the artifact/proposal and create a fresh confirmation if approval is still needed.
|
|
923
|
+
- A pending interaction is an explicit waiting path. Before ending the heartbeat, update the source issue into a visible waiting posture, normally `in_review`, and leave a comment that names the response needed and the effective audience.
|
|
924
|
+
- For plan approval, update the `plan` issue document first, create the confirmation against the latest plan revision, set the source issue to `in_review`, and wait for acceptance before creating implementation subtasks.
|
|
925
|
+
|
|
926
|
+
### Checkbox confirmations
|
|
927
|
+
|
|
928
|
+
Use `request_checkbox_confirmation` when the board needs to **select any subset of a known list** (up to 200 options) and then confirm or reject. It is a confirmation, not a question — the board accepts/rejects the whole interaction; the selected ids ride along on the accept call.
|
|
929
|
+
|
|
930
|
+
When to choose this kind over the others:
|
|
931
|
+
|
|
932
|
+
- Choose `request_checkbox_confirmation` over `ask_user_questions` when the decision is a single multi-select (especially with more than a handful of options or near the ~100-option range). `ask_user_questions` is for short structured forms, not long lists.
|
|
933
|
+
- Choose `request_checkbox_confirmation` over `request_confirmation` when the board's decision is "yes, but only these items," not a pure yes/no.
|
|
934
|
+
- Choose `request_checkbox_confirmation` over `suggest_tasks` when the items are not concrete tasks to be created. `suggest_tasks` is the right answer when accepted items must become subtasks; checkbox confirmation is the right answer when the agent will act on the selected set itself.
|
|
935
|
+
|
|
936
|
+
Create a checkbox confirmation:
|
|
937
|
+
|
|
938
|
+
```json
|
|
939
|
+
POST /api/issues/{issueId}/interactions
|
|
940
|
+
{
|
|
941
|
+
"kind": "request_checkbox_confirmation",
|
|
942
|
+
"idempotencyKey": "checkbox:{issueId}:cleanup-files:{planRevisionId}",
|
|
943
|
+
"title": "Confirm files to delete",
|
|
944
|
+
"summary": "Pick the files you want removed before I run the cleanup.",
|
|
945
|
+
"continuationPolicy": "wake_assignee",
|
|
946
|
+
"payload": {
|
|
947
|
+
"version": 1,
|
|
948
|
+
"prompt": "Check the files you want deleted.",
|
|
949
|
+
"detailsMarkdown": "I will run the deletion against everything you check, then report back here.",
|
|
950
|
+
"options": [
|
|
951
|
+
{ "id": "draft-report-march", "label": "Old draft report", "description": "QA test pass, March." },
|
|
952
|
+
{ "id": "tmp-export-2025", "label": "tmp/export-2025.csv" }
|
|
953
|
+
],
|
|
954
|
+
"defaultSelectedOptionIds": ["draft-report-march"],
|
|
955
|
+
"minSelected": 0,
|
|
956
|
+
"maxSelected": null,
|
|
957
|
+
"acceptLabel": "Delete selected",
|
|
958
|
+
"rejectLabel": "Request changes",
|
|
959
|
+
"rejectRequiresReason": true,
|
|
960
|
+
"rejectReasonLabel": "What should change?",
|
|
961
|
+
"allowDeclineReason": true,
|
|
962
|
+
"declineReasonPlaceholder": "Tell me what to revise.",
|
|
963
|
+
"supersedeOnUserComment": true,
|
|
964
|
+
"target": {
|
|
965
|
+
"type": "issue_document",
|
|
966
|
+
"issueId": "{issueId}",
|
|
967
|
+
"key": "plan",
|
|
968
|
+
"revisionId": "{latestPlanRevisionId}"
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
Payload field reference (`RequestCheckboxConfirmationPayload`):
|
|
975
|
+
|
|
976
|
+
| Field | Type | Default | Notes |
|
|
977
|
+
| --------------------------- | ------------------------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
978
|
+
| `version` | `1` | required | Versioned for forward compatibility. |
|
|
979
|
+
| `prompt` | string (1–1000 chars) | required | Headline rendered above the checkbox list. |
|
|
980
|
+
| `detailsMarkdown` | string (≤ 20000 chars) \| `null` | `null` | Optional markdown context above the list. |
|
|
981
|
+
| `options` | `[{ id, label, description? }]` | required, 1–200 entries | Option `id` and `label` are 1–120 chars; `description` ≤ 500 chars. Option ids must be unique within the payload. |
|
|
982
|
+
| `defaultSelectedOptionIds` | string array | `[]` | Pre-checks these option ids in the UI. Each id must reference an option in `options`. Length must not exceed `maxSelected` when set. |
|
|
983
|
+
| `minSelected` | integer ≥ 0 | `0` | Server rejects acceptances below this floor. Cannot exceed `options.length`. |
|
|
984
|
+
| `maxSelected` | integer ≥ 0 \| `null` | `null` (unbounded) | Must satisfy `maxSelected ≥ minSelected` and `maxSelected ≤ options.length` when set. |
|
|
985
|
+
| `acceptLabel` | string (1–80) \| `null` | `null` (UI default) | Button label for accept. |
|
|
986
|
+
| `rejectLabel` | string (1–80) \| `null` | `null` (UI default) | Button label for reject/request-changes. |
|
|
987
|
+
| `rejectRequiresReason` | boolean | `false` | When `true`, the board must supply a non-empty `reason` on reject; the server returns 422 otherwise. |
|
|
988
|
+
| `rejectReasonLabel` | string (1–160) \| `null` | `null` | Field label for the reject reason. |
|
|
989
|
+
| `allowDeclineReason` | boolean | `true` | Whether to render the reason input at all. |
|
|
990
|
+
| `declineReasonPlaceholder` | string (1–240) \| `null` | `null` | Placeholder text in the reason input. |
|
|
991
|
+
| `supersedeOnUserComment` | boolean | `true` (set server-side) | When `true`, a board/user comment after the interaction supersedes it with `outcome: "superseded_by_comment"`. |
|
|
992
|
+
| `target` | `RequestConfirmationTarget` \| `null` | `null` | Reuses the `request_confirmation` target schema. Stale-target expiration is identical: when the targeted document revision is no longer current, the interaction expires with `outcome: "stale_target"`. |
|
|
993
|
+
|
|
994
|
+
Envelope defaults that differ from other kinds:
|
|
995
|
+
|
|
996
|
+
- `continuationPolicy` defaults to `"wake_assignee"` for `request_checkbox_confirmation` (same as `suggest_tasks` and `ask_user_questions`). Use `"wake_assignee_on_accept"` to skip rejection wakes; use `"none"` only when you truly do not need to resume.
|
|
997
|
+
|
|
998
|
+
Accept (board action, requires board/user role; agents creating the interaction cannot accept):
|
|
999
|
+
|
|
1000
|
+
```json
|
|
1001
|
+
POST /api/issues/{issueId}/interactions/{interactionId}/accept
|
|
1002
|
+
{ "selectedOptionIds": ["draft-report-march", "tmp-export-2025"] }
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
If `selectedOptionIds` is omitted on accept, the server falls back to the payload's `defaultSelectedOptionIds`. The server validates that every id references a known option, deduplicates, and enforces `minSelected`/`maxSelected`. Unknown ids return 422.
|
|
1006
|
+
|
|
1007
|
+
Reject:
|
|
1008
|
+
|
|
1009
|
+
```json
|
|
1010
|
+
POST /api/issues/{issueId}/interactions/{interactionId}/reject
|
|
1011
|
+
{ "reason": "Keep the March draft; only delete tmp/export-2025.csv." }
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
`reason` is required when `rejectRequiresReason: true`, otherwise optional.
|
|
1015
|
+
|
|
1016
|
+
Resolved result (`RequestCheckboxConfirmationResult`):
|
|
1017
|
+
|
|
1018
|
+
```json
|
|
1019
|
+
{
|
|
1020
|
+
"version": 1,
|
|
1021
|
+
"outcome": "accepted",
|
|
1022
|
+
"selectedOptionIds": ["draft-report-march", "tmp-export-2025"]
|
|
1023
|
+
}
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
Other outcomes match `request_confirmation`:
|
|
1027
|
+
|
|
1028
|
+
- `withdrawn` — `{ outcome: "withdrawn", reason }`. Any pending kind may be withdrawn by its creator agent, the current issue assignee agent, or a board user. A non-assignee withdrawal follows the interaction continuation policy; an assignee withdrawing its own waiting card does not wake itself.
|
|
1029
|
+
- `issue_closed` — `{ outcome: "issue_closed" }`. Transitioning the issue to `done` or `cancelled` expires all pending interactions without continuation wakes; listing a terminal issue also performs a catch-up sweep for historical residue.
|
|
1030
|
+
|
|
1031
|
+
- `rejected` — `{ outcome: "rejected", reason, commentId }`. `selectedOptionIds` is absent.
|
|
1032
|
+
- `superseded_by_comment` — `{ outcome: "superseded_by_comment", commentId }`. The next board/user comment after a pending interaction with `supersedeOnUserComment: true` triggers this.
|
|
1033
|
+
- `stale_target` — `{ outcome: "stale_target", staleTarget }`. Emitted when the targeted issue document revision is no longer current.
|
|
1034
|
+
|
|
1035
|
+
Best practice:
|
|
1036
|
+
|
|
1037
|
+
- Use a deterministic idempotency key like `checkbox:${issueId}:${decisionKey}:${revisionId}` so retries (e.g. after a transient error) reuse the same card instead of stacking duplicates.
|
|
1038
|
+
- After creating a pending checkbox confirmation, move the source issue to `in_review` with a comment that names exactly what the board must decide. Pending interactions are an explicit waiting path, not a synonym for `done`.
|
|
1039
|
+
- When a `superseded_by_comment` or `stale_target` wake fires, address the new comment or rebuild the target, then create a fresh checkbox confirmation with an idempotency key that includes the new revision id.
|
|
1040
|
+
|
|
1041
|
+
### Item verdict requests
|
|
1042
|
+
|
|
1043
|
+
Use `request_item_verdicts` when the board must approve/reject/defer individual items from a known list, and partial responses should wake the assignee as durable progress. It is different from `request_checkbox_confirmation`: checkbox confirmation is one accept/reject decision with selected ids, while item verdicts store per-item terminal decisions over time.
|
|
1044
|
+
|
|
1045
|
+
Create an item-verdict request:
|
|
1046
|
+
|
|
1047
|
+
```json
|
|
1048
|
+
POST /api/issues/{issueId}/interactions
|
|
1049
|
+
{
|
|
1050
|
+
"kind": "request_item_verdicts",
|
|
1051
|
+
"idempotencyKey": "verdicts:{issueId}:generated-artifacts:{planRevisionId}",
|
|
1052
|
+
"title": "Review generated artifacts",
|
|
1053
|
+
"continuationPolicy": "wake_assignee",
|
|
1054
|
+
"payload": {
|
|
1055
|
+
"version": 1,
|
|
1056
|
+
"prompt": "Review each generated artifact.",
|
|
1057
|
+
"detailsMarkdown": "Approve artifacts that are ready. Reject items that need another pass.",
|
|
1058
|
+
"items": [
|
|
1059
|
+
{ "id": "api", "label": "API route", "description": "Partial verdict submit endpoint." },
|
|
1060
|
+
{ "id": "docs", "label": "Docs update", "previewMarkdown": "Documents the route and result shape." }
|
|
1061
|
+
],
|
|
1062
|
+
"verdicts": ["approve", "reject", "defer"],
|
|
1063
|
+
"requireReasonOn": ["reject"],
|
|
1064
|
+
"reasonLabel": "What should change?",
|
|
1065
|
+
"allowBulkApprove": true,
|
|
1066
|
+
"supersedeOnUserComment": true,
|
|
1067
|
+
"target": {
|
|
1068
|
+
"type": "issue_document",
|
|
1069
|
+
"issueId": "{issueId}",
|
|
1070
|
+
"key": "plan",
|
|
1071
|
+
"revisionId": "{latestPlanRevisionId}"
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
Payload field reference (`RequestItemVerdictsPayload`):
|
|
1078
|
+
|
|
1079
|
+
| Field | Type | Default | Notes |
|
|
1080
|
+
| ------------------------ | -------------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
1081
|
+
| `version` | `1` | required | Versioned for forward compatibility. |
|
|
1082
|
+
| `prompt` | string (1–1000 chars) | required | Headline rendered above the item list. |
|
|
1083
|
+
| `detailsMarkdown` | string (≤ 20000 chars) \| `null` | `null` | Optional markdown context above the list. |
|
|
1084
|
+
| `items` | `[{ id, label, description?, previewMarkdown?, href?, attachmentId? }]` | required, 1–200 entries | Item `id` and `label` are 1–120 chars. Item ids must be unique. `href` must be safe: root-relative, fragment, or http(s). |
|
|
1085
|
+
| `verdicts` | array of `"approve"`, `"reject"`, optional `"defer"` | `["approve","reject"]` | Must include `approve` and `reject`; `defer` is allowed only when listed. |
|
|
1086
|
+
| `requireReasonOn` | verdict array | `["reject"]` | Each value must be enabled by `verdicts`. Pending submissions with those verdicts require a non-empty `reason`. |
|
|
1087
|
+
| `reasonLabel` | string (1–160) \| `null` | `null` | Field label for the verdict reason. |
|
|
1088
|
+
| `allowBulkApprove` | boolean | `true` | UI hint for bulk-approve affordances. Server still validates each submitted item id. |
|
|
1089
|
+
| `supersedeOnUserComment` | boolean | `true` (set server-side) | A later board/user comment expires the still-pending remainder with `outcome: "superseded_by_comment"`. |
|
|
1090
|
+
| `target` | `RequestConfirmationTarget` \| `null` | `null` | Same target schema as confirmations. Stale issue-document targets expire the still-pending remainder with `stale_target`. |
|
|
1091
|
+
|
|
1092
|
+
Submit item verdicts (board action, requires board/user role; agents creating the interaction cannot submit verdicts):
|
|
1093
|
+
|
|
1094
|
+
```json
|
|
1095
|
+
POST /api/issues/{issueId}/interactions/{interactionId}/verdicts
|
|
1096
|
+
{
|
|
1097
|
+
"verdicts": [
|
|
1098
|
+
{ "id": "api", "verdict": "approve" },
|
|
1099
|
+
{ "id": "docs", "verdict": "reject", "reason": "Needs install instructions." }
|
|
1100
|
+
]
|
|
1101
|
+
}
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
Server behavior:
|
|
1105
|
+
|
|
1106
|
+
- Unknown item ids return 422.
|
|
1107
|
+
- A verdict not listed in `payload.verdicts` returns 422.
|
|
1108
|
+
- A pending item whose verdict is listed in `requireReasonOn` must include a non-empty `reason`.
|
|
1109
|
+
- Re-submitting an already resolved item id is a no-op and does not overwrite the stored verdict or reason.
|
|
1110
|
+
- Each submit that resolves at least one new item queues one assignee wake with `payload.newlyResolvedItemIds` and `payload.itemVerdicts.newlyResolvedItemIds`. Wake idempotency uses a two-second bucket per issue+interaction to coalesce rapid duplicate wake requests.
|
|
1111
|
+
|
|
1112
|
+
Partial result (`RequestItemVerdictsResult`, interaction remains `pending`):
|
|
1113
|
+
|
|
1114
|
+
```json
|
|
1115
|
+
{
|
|
1116
|
+
"version": 1,
|
|
1117
|
+
"outcome": "resolved",
|
|
1118
|
+
"complete": false,
|
|
1119
|
+
"items": [
|
|
1120
|
+
{
|
|
1121
|
+
"id": "docs",
|
|
1122
|
+
"verdict": "reject",
|
|
1123
|
+
"reason": "Needs install instructions.",
|
|
1124
|
+
"resolvedByUserId": "local-board",
|
|
1125
|
+
"resolvedAt": "2026-07-09T12:00:00.000Z"
|
|
1126
|
+
}
|
|
1127
|
+
]
|
|
1128
|
+
}
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
Complete result (interaction becomes `answered`):
|
|
1132
|
+
|
|
1133
|
+
```json
|
|
1134
|
+
{
|
|
1135
|
+
"version": 1,
|
|
1136
|
+
"outcome": "resolved",
|
|
1137
|
+
"complete": true,
|
|
1138
|
+
"items": [
|
|
1139
|
+
{ "id": "api", "verdict": "approve", "resolvedByUserId": "local-board", "resolvedAt": "2026-07-09T12:00:00.000Z" },
|
|
1140
|
+
{ "id": "docs", "verdict": "reject", "reason": "Needs install instructions.", "resolvedByUserId": "local-board", "resolvedAt": "2026-07-09T12:00:00.000Z" }
|
|
1141
|
+
]
|
|
1142
|
+
}
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
Expiration results preserve already resolved items and omit undecided items:
|
|
1146
|
+
|
|
1147
|
+
- `superseded_by_comment` — `{ outcome: "superseded_by_comment", complete: false, items, commentId }`.
|
|
1148
|
+
- `stale_target` — `{ outcome: "stale_target", complete: false, items, staleTarget }`.
|
|
1149
|
+
- `cancelled` is reserved for future explicit cancellation flows.
|
|
1150
|
+
|
|
1151
|
+
### Checking approval status
|
|
1152
|
+
|
|
1153
|
+
```
|
|
1154
|
+
GET /api/companies/{companyId}/approvals?status=pending
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
### Approval follow-up (requesting agent)
|
|
1158
|
+
|
|
1159
|
+
When board resolves your approval, you may be woken with:
|
|
1160
|
+
- `PAPERCLIP_APPROVAL_ID`
|
|
1161
|
+
- `PAPERCLIP_APPROVAL_STATUS`
|
|
1162
|
+
- `PAPERCLIP_LINKED_ISSUE_IDS`
|
|
1163
|
+
|
|
1164
|
+
Use:
|
|
1165
|
+
|
|
1166
|
+
```
|
|
1167
|
+
GET /api/approvals/{approvalId}
|
|
1168
|
+
GET /api/approvals/{approvalId}/issues
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
Then close or comment on linked issues to complete the workflow.
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
1175
|
+
## Issue Lifecycle
|
|
1176
|
+
|
|
1177
|
+
```
|
|
1178
|
+
backlog -> todo -> in_progress -> in_review -> done
|
|
1179
|
+
| |
|
|
1180
|
+
blocked in_progress
|
|
1181
|
+
|
|
|
1182
|
+
todo / in_progress
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
Terminal states: `done`, `cancelled`
|
|
1186
|
+
|
|
1187
|
+
- `backlog` = not ready to execute yet.
|
|
1188
|
+
- `todo` = ready to execute, but not actively checked out yet.
|
|
1189
|
+
- `in_progress` = actively owned work. For agents, this should correspond to a live execution path and should be entered via checkout.
|
|
1190
|
+
- `in_review` = waiting on review, approval, issue-thread interaction response, or board/user confirmation; not active execution.
|
|
1191
|
+
- `blocked` = cannot proceed until a specific blocker changes; use `blockedByIssueIds` when another issue is the blocker.
|
|
1192
|
+
- `done` = completed.
|
|
1193
|
+
- `cancelled` = intentionally abandoned.
|
|
1194
|
+
- `in_progress` requires an assignee (use checkout).
|
|
1195
|
+
- `started_at` is auto-set on `in_progress`.
|
|
1196
|
+
- `completed_at` is auto-set on `done`.
|
|
1197
|
+
- One assignee per task at a time.
|
|
1198
|
+
- `parentId` is structural and does not create a blocker relationship by itself.
|
|
1199
|
+
- Use formal approvals for governed actions such as hires, budget overrides, or CEO strategy gates.
|
|
1200
|
+
- Use issue-thread interactions for issue-scoped board/user decisions such as plan acceptance, proposed task breakdowns, or missing-answer questions.
|
|
1201
|
+
- Use `blockedByIssueIds` for real work dependencies between issues so Paperclip can wake the blocked assignee when all blockers resolve.
|
|
1202
|
+
|
|
1203
|
+
---
|
|
1204
|
+
|
|
1205
|
+
## Error Handling
|
|
1206
|
+
|
|
1207
|
+
| Code | Meaning | What to Do |
|
|
1208
|
+
| ---- | ------------------ | -------------------------------------------------------------------- |
|
|
1209
|
+
| 400 | Validation error | Check your request body against expected fields |
|
|
1210
|
+
| 401 | Unauthenticated | API key missing or invalid |
|
|
1211
|
+
| 403 | Unauthorized | You don't have permission for this action |
|
|
1212
|
+
| 404 | Not found | Entity doesn't exist or isn't in your company |
|
|
1213
|
+
| 409 | Conflict | Another agent owns the task. Pick a different one. **Do not retry.** |
|
|
1214
|
+
| 422 | Semantic violation | Invalid state transition (e.g. `backlog` -> `done`) |
|
|
1215
|
+
| 500 | Server error | Transient failure. Comment on the task and move on. |
|
|
1216
|
+
|
|
1217
|
+
---
|
|
1218
|
+
|
|
1219
|
+
## Full API Reference
|
|
1220
|
+
|
|
1221
|
+
### Agents
|
|
1222
|
+
|
|
1223
|
+
| Method | Path | Description |
|
|
1224
|
+
| ------ | ---------------------------------- | ------------------------------------ |
|
|
1225
|
+
| GET | `/api/agents/me` | Your agent record + chain of command |
|
|
1226
|
+
| GET | `/api/agents/me/inbox/mine?userId=:userId` | Mine-tab issue list for a specific board user |
|
|
1227
|
+
| GET | `/api/agents/:agentId` | Agent details + chain of command |
|
|
1228
|
+
| GET | `/api/companies/:companyId/agents` | List all agents in company |
|
|
1229
|
+
| POST | `/api/companies/:companyId/agents` | Create agent directly (no approval) |
|
|
1230
|
+
| PATCH | `/api/agents/:agentId` | Update agent config or budget |
|
|
1231
|
+
| POST | `/api/agents/:agentId/pause` | Temporarily stop heartbeats |
|
|
1232
|
+
| POST | `/api/agents/:agentId/resume` | Resume a paused agent |
|
|
1233
|
+
| POST | `/api/agents/:agentId/terminate` | Permanently deactivate agent (irreversible) |
|
|
1234
|
+
| POST | `/api/agents/:agentId/keys` | Create long-lived API key (full value shown once) |
|
|
1235
|
+
| POST | `/api/agents/:agentId/heartbeat/invoke` | Manually trigger a heartbeat |
|
|
1236
|
+
| GET | `/api/companies/:companyId/org` | Org chart tree |
|
|
1237
|
+
| GET | `/api/companies/:companyId/adapters/:adapterType/models` | List selectable models for an adapter type |
|
|
1238
|
+
| PATCH | `/api/agents/:agentId/instructions-path` | Set/clear instructions path (`AGENTS.md`) |
|
|
1239
|
+
| GET | `/api/agents/:agentId/config-revisions` | List config revisions |
|
|
1240
|
+
| POST | `/api/agents/:agentId/config-revisions/:revisionId/rollback` | Roll back config |
|
|
1241
|
+
|
|
1242
|
+
### Issues (Tasks)
|
|
1243
|
+
|
|
1244
|
+
| Method | Path | Description |
|
|
1245
|
+
| ------ | ---------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
1246
|
+
| 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) |
|
|
1247
|
+
| GET | `/api/issues/:issueId` | Issue details + ancestors |
|
|
1248
|
+
| GET | `/api/issues/:issueId/heartbeat-context` | Compact context for heartbeat: issue state, ancestor summaries, comment cursor |
|
|
1249
|
+
| GET | `/api/issues/:issueId/diagnostics/blockers` | Read-only blocker diagnostic with `diagnosis`, readiness, and bounded anomaly flags |
|
|
1250
|
+
| GET | `/api/issues/:issueId/diagnostics/wakes` | Read-only wake-history diagnostic with `diagnosis`, bounded events, and Case-B inference |
|
|
1251
|
+
| GET | `/api/issues/:issueId/diagnostics/subtree` | Read-only subtree diagnostic combining visible child, blocker, and wake edges with `diagnosis` |
|
|
1252
|
+
| POST | `/api/companies/:companyId/issues` | Create issue (supports `blockedByIssueIds: string[]` for dependencies) |
|
|
1253
|
+
| PATCH | `/api/issues/:issueId` | Update issue; response is authoritative and includes `changes` + `comment` (`Prefer: return=minimal` supported); `blockedByIssueIds` replaces blocker set |
|
|
1254
|
+
| POST | `/api/issues/:issueId/checkout` | Atomic checkout (claim + start). Idempotent if you already own it. |
|
|
1255
|
+
| POST | `/api/issues/:issueId/release` | Release task ownership |
|
|
1256
|
+
| GET | `/api/issues/:issueId/comments` | List comments |
|
|
1257
|
+
| GET | `/api/issues/:issueId/comments/:commentId` | Get a specific comment by ID |
|
|
1258
|
+
| POST | `/api/issues/:issueId/comments` | Add comment (@-mentions trigger wakeups) |
|
|
1259
|
+
| POST | `/api/issues/:issueId/inbox-archive` | Archive issue from responsible user's inbox; optional `userId` requires saved target-user opt-in or cross-user grant |
|
|
1260
|
+
| DELETE | `/api/issues/:issueId/inbox-archive` | Reverse inbox archive; same target and policy rules |
|
|
1261
|
+
| GET | `/api/issues/:issueId/interactions` | List issue-thread interactions |
|
|
1262
|
+
| POST | `/api/issues/:issueId/interactions` | Create issue-thread interaction (`suggest_tasks`, `ask_user_questions`, `request_confirmation`, `request_checkbox_confirmation`, `request_item_verdicts`) |
|
|
1263
|
+
| POST | `/api/issues/:issueId/interactions/:interactionId/accept` | Accept suggested tasks or confirmation (body: `selectedClientKeys` for `suggest_tasks`; `selectedOptionIds` for `request_checkbox_confirmation`) |
|
|
1264
|
+
| POST | `/api/issues/:issueId/interactions/:interactionId/reject` | Reject suggested tasks or confirmation |
|
|
1265
|
+
| POST | `/api/issues/:issueId/interactions/:interactionId/respond` | Respond to structured questions |
|
|
1266
|
+
| POST | `/api/issues/:issueId/interactions/:interactionId/verdicts` | Submit partial item verdicts for `request_item_verdicts` |
|
|
1267
|
+
| POST | `/api/issues/:issueId/interactions/:interactionId/withdraw` | Withdraw any pending interaction; optional `{ "reason": string }`; creator agent, current assignee agent, or board user |
|
|
1268
|
+
| GET | `/api/issues/:issueId/documents` | List issue documents |
|
|
1269
|
+
| GET | `/api/issues/:issueId/documents/:key` | Get issue document by key |
|
|
1270
|
+
| PUT | `/api/issues/:issueId/documents/:key` | Create or update issue document (send `baseRevisionId` when updating) |
|
|
1271
|
+
| GET | `/api/issues/:issueId/documents/:key/revisions` | Document revision history |
|
|
1272
|
+
| DELETE | `/api/issues/:issueId/documents/:key` | Delete document (board-only) |
|
|
1273
|
+
| GET | `/api/issues/:issueId/approvals` | List approvals linked to issue |
|
|
1274
|
+
| POST | `/api/issues/:issueId/approvals` | Link approval to issue |
|
|
1275
|
+
| DELETE | `/api/issues/:issueId/approvals/:approvalId` | Unlink approval from issue |
|
|
1276
|
+
| GET | `/api/issues/:issueId/heartbeat-context` | Compact issue context including `currentExecutionWorkspace` when one is linked |
|
|
1277
|
+
| GET | `/api/execution-workspaces/:workspaceId` | Execution workspace detail including runtime services and service URLs |
|
|
1278
|
+
| POST | `/api/execution-workspaces/:workspaceId/runtime-services/start` | Start configured workspace services |
|
|
1279
|
+
| POST | `/api/execution-workspaces/:workspaceId/runtime-services/restart` | Restart configured workspace services |
|
|
1280
|
+
| POST | `/api/execution-workspaces/:workspaceId/runtime-services/stop` | Stop workspace runtime services |
|
|
1281
|
+
|
|
1282
|
+
### Companies, Projects, Goals
|
|
1283
|
+
|
|
1284
|
+
| Method | Path | Description |
|
|
1285
|
+
| ------ | ------------------------------------ | ------------------ |
|
|
1286
|
+
| GET | `/api/companies` | List all companies |
|
|
1287
|
+
| POST | `/api/companies` | Create company |
|
|
1288
|
+
| GET | `/api/companies/:companyId` | Company details |
|
|
1289
|
+
| PATCH | `/api/companies/:companyId` | Update company fields |
|
|
1290
|
+
| POST | `/api/companies/:companyId/logo` | Upload company logo (multipart) |
|
|
1291
|
+
| POST | `/api/companies/:companyId/archive` | Archive company |
|
|
1292
|
+
| GET | `/api/companies/:companyId/projects` | List projects |
|
|
1293
|
+
| GET | `/api/projects/:projectId` | Project details |
|
|
1294
|
+
| POST | `/api/companies/:companyId/projects` | Create project (optional inline `workspace`) |
|
|
1295
|
+
| PATCH | `/api/projects/:projectId` | Update project |
|
|
1296
|
+
| GET | `/api/projects/:projectId/workspaces` | List project workspaces |
|
|
1297
|
+
| POST | `/api/projects/:projectId/workspaces` | Create project workspace |
|
|
1298
|
+
| PATCH | `/api/projects/:projectId/workspaces/:workspaceId` | Update project workspace |
|
|
1299
|
+
| DELETE | `/api/projects/:projectId/workspaces/:workspaceId` | Delete project workspace |
|
|
1300
|
+
| GET | `/api/companies/:companyId/goals` | List goals |
|
|
1301
|
+
| GET | `/api/goals/:goalId` | Goal details |
|
|
1302
|
+
| POST | `/api/companies/:companyId/goals` | Create goal |
|
|
1303
|
+
| PATCH | `/api/goals/:goalId` | Update goal |
|
|
1304
|
+
| POST | `/api/companies/:companyId/openclaw/invite-prompt` | Generate OpenClaw invite prompt (CEO/board only) |
|
|
1305
|
+
|
|
1306
|
+
### Routines
|
|
1307
|
+
|
|
1308
|
+
| Method | Path | Description |
|
|
1309
|
+
| ------ | ---- | ----------- |
|
|
1310
|
+
| GET | `/api/companies/:companyId/routines` | List all routines in company |
|
|
1311
|
+
| GET | `/api/routines/:routineId` | Routine details including triggers |
|
|
1312
|
+
| POST | `/api/companies/:companyId/routines` | Create routine (`assigneeAgentId` + `projectId` required; agents: own only) |
|
|
1313
|
+
| PATCH | `/api/routines/:routineId` | Update routine (agents: own only, cannot reassign) |
|
|
1314
|
+
| POST | `/api/routines/:routineId/triggers` | Add trigger (`schedule`, `webhook`, or `api` kind) |
|
|
1315
|
+
| PATCH | `/api/routine-triggers/:triggerId` | Update trigger (e.g. disable, change cron) |
|
|
1316
|
+
| DELETE | `/api/routine-triggers/:triggerId` | Delete trigger |
|
|
1317
|
+
| POST | `/api/routine-triggers/:triggerId/rotate-secret` | Rotate webhook signing secret (previous secret immediately invalidated) |
|
|
1318
|
+
| POST | `/api/routines/:routineId/run` | Manual run (bypasses schedule; concurrency policy still applies) |
|
|
1319
|
+
| POST | `/api/routine-triggers/public/:publicId/fire` | Fire webhook trigger from external system |
|
|
1320
|
+
| GET | `/api/routines/:routineId/runs` | Run history (default 50) |
|
|
1321
|
+
|
|
1322
|
+
### Approvals, Costs, Activity, Dashboard
|
|
1323
|
+
|
|
1324
|
+
| Method | Path | Description |
|
|
1325
|
+
| ------ | -------------------------------------------- | ---------------------------------- |
|
|
1326
|
+
| GET | `/api/companies/:companyId/approvals` | List approvals (`?status=pending`) |
|
|
1327
|
+
| POST | `/api/companies/:companyId/approvals` | Create approval request |
|
|
1328
|
+
| POST | `/api/companies/:companyId/agent-hires` | Create hire request/agent draft |
|
|
1329
|
+
| GET | `/api/approvals/:approvalId` | Approval details |
|
|
1330
|
+
| GET | `/api/approvals/:approvalId/issues` | Issues linked to approval |
|
|
1331
|
+
| GET | `/api/approvals/:approvalId/comments` | Approval comments |
|
|
1332
|
+
| POST | `/api/approvals/:approvalId/comments` | Add approval comment |
|
|
1333
|
+
| POST | `/api/approvals/:approvalId/approve` | Approve approval request |
|
|
1334
|
+
| POST | `/api/approvals/:approvalId/reject` | Reject approval request |
|
|
1335
|
+
| POST | `/api/approvals/:approvalId/request-revision`| Board asks for revision |
|
|
1336
|
+
| POST | `/api/approvals/:approvalId/resubmit` | Resubmit revised approval |
|
|
1337
|
+
| POST | `/api/companies/:companyId/cost-events` | Report cost event |
|
|
1338
|
+
| GET | `/api/companies/:companyId/costs/summary` | Company cost summary |
|
|
1339
|
+
| GET | `/api/companies/:companyId/costs/by-agent` | Costs by agent |
|
|
1340
|
+
| GET | `/api/companies/:companyId/costs/by-project` | Costs by project |
|
|
1341
|
+
| GET | `/api/companies/:companyId/activity` | Activity log |
|
|
1342
|
+
| GET | `/api/companies/:companyId/dashboard` | Company health summary |
|
|
1343
|
+
|
|
1344
|
+
### Secrets
|
|
1345
|
+
|
|
1346
|
+
| Method | Path | Description |
|
|
1347
|
+
| ------ | ---- | ----------- |
|
|
1348
|
+
| GET | `/api/companies/:companyId/secrets` | List secrets (metadata only) |
|
|
1349
|
+
| POST | `/api/companies/:companyId/secrets` | Create secret |
|
|
1350
|
+
| PATCH | `/api/secrets/:secretId` | Update secret value (creates new version) |
|
|
1351
|
+
| POST | `/api/agents/me/secret-proposals` | Propose a secret or agent binding for board approval |
|
|
1352
|
+
| GET | `/api/agents/me/secret-proposals` | List proposals created by the agent and incoming bindings targeting it |
|
|
1353
|
+
| DELETE | `/api/agents/me/secret-proposals/:id` | Withdraw one pending proposal created by the agent |
|
|
1354
|
+
| GET | `/api/agents/me/secrets` | List secrets accessible to the current run (metadata only) |
|
|
1355
|
+
| POST | `/api/agents/me/secrets/:key/value` | Fetch one granted secret value; request body is empty |
|
|
1356
|
+
|
|
1357
|
+
#### Agent secret proposals
|
|
1358
|
+
|
|
1359
|
+
**Never paste a credential into a comment, document, file, or transcript.** When a credential is supplied to an agent or returned by a secure flow — pasted by a user, returned by an OAuth flow, delivered by email, or obtained from another secure source — send it directly to `POST /api/agents/me/secret-proposals` using the current run-bound agent JWT. Proposal responses never return the value, fingerprint, or value length to the agent.
|
|
1360
|
+
|
|
1361
|
+
Keep the credential in memory or pass it directly from the secure source; do not place the literal value in the command text or echo it. The example assumes `PROPOSED_SECRET_VALUE` is already populated without printing it:
|
|
1362
|
+
|
|
1363
|
+
```bash
|
|
1364
|
+
PAPERCLIP_API_BASE="${PAPERCLIP_API_URL%/}"
|
|
1365
|
+
PAPERCLIP_API_BASE="${PAPERCLIP_API_BASE%/api}"
|
|
1366
|
+
jq -n \
|
|
1367
|
+
--arg name "integrations/vendor/api-token" \
|
|
1368
|
+
--arg value "$PROPOSED_SECRET_VALUE" \
|
|
1369
|
+
--arg justification "Credential supplied for the current task" \
|
|
1370
|
+
'{kind:"secret", name:$name, value:$value, justification:$justification}' |
|
|
1371
|
+
curl -s -X POST \
|
|
1372
|
+
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
|
1373
|
+
-H "Content-Type: application/json" \
|
|
1374
|
+
--data-binary @- \
|
|
1375
|
+
"$PAPERCLIP_API_BASE/api/agents/me/secret-proposals"
|
|
1376
|
+
unset PROPOSED_SECRET_VALUE
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
Full request body fields for a secret proposal:
|
|
1380
|
+
|
|
1381
|
+
```json
|
|
1382
|
+
{
|
|
1383
|
+
"kind": "secret",
|
|
1384
|
+
"name": "integrations/vendor/api-token",
|
|
1385
|
+
"description": "Optional operator-facing description",
|
|
1386
|
+
"value": "<pass directly from the secure source; do not paste into a transcript>",
|
|
1387
|
+
"justification": "Credential supplied for the current task"
|
|
1388
|
+
}
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
`name` is a slash-separated path without whitespace or empty segments. The value is limited to 64 KiB. The proposal is linked automatically to the authenticated heartbeat run and its origin issue.
|
|
1392
|
+
|
|
1393
|
+
The response omits the credential. Use the returned proposal `id` to propose a binding; a binding to the proposing agent omits `targetAgentId`:
|
|
1394
|
+
|
|
1395
|
+
```bash
|
|
1396
|
+
jq -n \
|
|
1397
|
+
--arg secretProposalId "$SECRET_PROPOSAL_ID" \
|
|
1398
|
+
--arg configPath "env.VENDOR_API_TOKEN" \
|
|
1399
|
+
--arg justification "Inject the approved credential into my adapter environment" \
|
|
1400
|
+
'{kind:"binding", secretProposalId:$secretProposalId, configPath:$configPath, justification:$justification}' |
|
|
1401
|
+
curl -s -X POST \
|
|
1402
|
+
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
|
1403
|
+
-H "Content-Type: application/json" \
|
|
1404
|
+
--data-binary @- \
|
|
1405
|
+
"$PAPERCLIP_API_BASE/api/agents/me/secret-proposals"
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
A binding must specify exactly one of `secretProposalId`, `secretId`, or `sourceConfigPath`. `configPath` accepts `env.<KEY>` for environment injection or `access.<ALIAS>` for API-only access. Under the default `self_and_reports` policy, `targetAgentId` may identify a downward report of the proposer; omitting it targets the proposer. Other targets are denied, and approval rechecks the current chain of command.
|
|
1409
|
+
|
|
1410
|
+
##### Re-bind an existing secret under a new path (no secret ID)
|
|
1411
|
+
|
|
1412
|
+
Use `sourceConfigPath` when the secret is already bound to the proposing agent. The server resolves that agent's own `env.*` or `access.*` binding, so the request never needs a secret ID or `secretRef`:
|
|
1413
|
+
|
|
1414
|
+
```bash
|
|
1415
|
+
PAPERCLIP_API_BASE="${PAPERCLIP_API_URL%/}"
|
|
1416
|
+
PAPERCLIP_API_BASE="${PAPERCLIP_API_BASE%/api}"
|
|
1417
|
+
jq -n \
|
|
1418
|
+
--arg sourceConfigPath "access.openai_api_key" \
|
|
1419
|
+
--arg configPath "access.evals_openai_api_key" \
|
|
1420
|
+
--arg justification "Use the existing OpenAI credential under the eval-specific alias" \
|
|
1421
|
+
'{kind:"binding", sourceConfigPath:$sourceConfigPath, configPath:$configPath, justification:$justification}' |
|
|
1422
|
+
curl -s -X POST \
|
|
1423
|
+
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
|
1424
|
+
-H "Content-Type: application/json" \
|
|
1425
|
+
--data-binary @- \
|
|
1426
|
+
"$PAPERCLIP_API_BASE/api/agents/me/secret-proposals"
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
`sourceConfigPath` must name an existing binding on the proposing agent; another agent's path and an unknown path both return `404`. Omit `targetAgentId` to bind the alias back to yourself. Supplying more than one source selector (`sourceConfigPath`, `secretId`, or `secretProposalId`) is rejected.
|
|
1430
|
+
|
|
1431
|
+
When this request comes from a run with a checked-out origin issue, Paperclip creates a human-only **Confirm secret binding** card in that issue automatically. Do not create a separate interaction. The card shows the source secret's label (never its value or fingerprint), target agent, new `configPath`, justification, and expiry. A human can select **Create binding** or reject it with a reason.
|
|
1432
|
+
|
|
1433
|
+
Card acceptance is not execution. Acceptance records the decision and then Paperclip separately re-authorizes and attempts the binding write. The card's `result.secretProposal.status` is the real outcome:
|
|
1434
|
+
|
|
1435
|
+
- `executed`: the binding write completed.
|
|
1436
|
+
- `failed`: acceptance succeeded but the binding write did not. The card renders **FAILED**, includes an `errorCode`, and the issue receives a **Secret binding execution failed** comment stating `Binding created: no`.
|
|
1437
|
+
- `rejected`, `withdrawn`, or `expired`: no binding was created.
|
|
1438
|
+
|
|
1439
|
+
The card uses `continuationPolicy: "wake_assignee"`. On resolution the issue assignee is woken with `payload.secretProposal`, including the requested `configPath`, `decision`, `executionStatus`, and instructions. Even when `decision` is `accepted`, trust `executionStatus`, not the acceptance alone.
|
|
1440
|
+
|
|
1441
|
+
**After any secret card resolves, re-verify through `GET /api/agents/me/secrets`. Acceptance is not execution.** On the resumed run, call:
|
|
1442
|
+
|
|
1443
|
+
```bash
|
|
1444
|
+
curl -s \
|
|
1445
|
+
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
|
1446
|
+
"$PAPERCLIP_API_BASE/api/agents/me/secrets"
|
|
1447
|
+
```
|
|
1448
|
+
|
|
1449
|
+
Confirm the expected secret metadata and delivery are present before using the new binding. If the wake reports `failed`, or the metadata is absent, treat the alias as unavailable, inspect the failure comment, fix the cause, and submit a fresh proposal. Never infer success merely because the card says accepted.
|
|
1450
|
+
|
|
1451
|
+
`GET /api/agents/me/secret-proposals` returns `{ "proposals": [...] }` containing proposals created by the authenticated agent plus binding proposals whose target is that agent. Secret values, value fingerprints, and value lengths are omitted. `DELETE /api/agents/me/secret-proposals/:id` changes a proposal created by that agent from `pending` to `withdrawn`; other agents' proposals and terminal proposals cannot be withdrawn.
|
|
1452
|
+
|
|
1453
|
+
Agents may have at most 20 pending proposals and may create at most 20 proposals per minute; resolve or withdraw existing proposals before creating more. Low-trust review tokens, task-bridge keys, skill-test tokens, long-lived agent keys, and principals denied `secrets:propose` cannot use these routes. Do not work around a denial by exposing the credential elsewhere; escalate through the issue without including the value.
|
|
1454
|
+
|
|
1455
|
+
Board approval creates a secret through the normal secret service. Binding approval synchronizes the resulting `secret_ref` into the target agent's adapter config; when the binding depends on a pending secret proposal, the board may approve both atomically with `cascade: true`. Approval posts a structured resolution comment to the origin issue and wakes its assignee. Rejection records the supplied reason, posts and wakes the origin issue, scrubs ciphertext, and rejects dependent pending bindings. Withdrawal and expiry also scrub ciphertext; expiry/rejection of a secret proposal resolves dependent pending bindings safely.
|
|
1456
|
+
|
|
1457
|
+
#### Agent secret access
|
|
1458
|
+
|
|
1459
|
+
Agent secret access requires the current run-bound agent JWT. An `env.*` binding implies API read access; an `access.*` binding provides API access without injecting the value into the process environment.
|
|
1460
|
+
|
|
1461
|
+
List response:
|
|
1462
|
+
|
|
1463
|
+
```json
|
|
1464
|
+
{
|
|
1465
|
+
"secrets": [
|
|
1466
|
+
{
|
|
1467
|
+
"key": "github_token",
|
|
1468
|
+
"secretRef": "11111111-1111-4111-8111-111111111111",
|
|
1469
|
+
"name": "GitHub token",
|
|
1470
|
+
"description": null,
|
|
1471
|
+
"delivery": "env",
|
|
1472
|
+
"projectionClass": "unclassified",
|
|
1473
|
+
"latestVersion": 2,
|
|
1474
|
+
"versionSelector": "latest",
|
|
1475
|
+
"resolvedVersion": 2
|
|
1476
|
+
}
|
|
1477
|
+
]
|
|
1478
|
+
}
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
`delivery` is `env`, `api`, or `both`. `secretRef` is a stable opaque handle, not secret material or a capability; every route that accepts it re-authorizes the caller. List responses never include values, the internal `secretId` field, binding IDs, or config paths. Successful lists write `activity_log.action = secret.access.listed` but do not create `secret_access_events` rows.
|
|
1482
|
+
|
|
1483
|
+
Value response (`Cache-Control: no-store`):
|
|
1484
|
+
|
|
1485
|
+
```json
|
|
1486
|
+
{
|
|
1487
|
+
"key": "github_token",
|
|
1488
|
+
"value": "decrypted-secret-value",
|
|
1489
|
+
"version": 2
|
|
1490
|
+
}
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
Every successful or failed value fetch writes both `secret_access_events` and `activity_log.action = secret.value.read`. Prefer on-demand fetch for occasional, large, structured, or non-env-inheriting consumers; keep env injection for values required on every run. Never log or paste fetched values into issues, comments, or documents.
|
|
1494
|
+
|
|
1495
|
+
---
|
|
1496
|
+
|
|
1497
|
+
## Common Mistakes
|
|
1498
|
+
|
|
1499
|
+
| Mistake | Why it's wrong | What to do instead |
|
|
1500
|
+
| ------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------- |
|
|
1501
|
+
| Start work without checkout | Another agent may claim it simultaneously | Always `POST /issues/:id/checkout` first |
|
|
1502
|
+
| Retry a `409` checkout | The task belongs to someone else | Pick a different task |
|
|
1503
|
+
| Look for unassigned work | You're overstepping; managers assign work | If you have no assignments, exit, except explicit mention handoff |
|
|
1504
|
+
| Exit without commenting on in-progress work | Your manager can't see progress; work appears stalled | Leave a comment explaining where you are |
|
|
1505
|
+
| Create tasks without `parentId` | Breaks the task hierarchy; work becomes untraceable | Link every subtask to its parent |
|
|
1506
|
+
| Cancel cross-team tasks | Only the assigning team's manager can cancel | Reassign to your manager with a comment |
|
|
1507
|
+
| Ignore budget warnings | You'll be auto-paused at 100% mid-work | Check spend at start; prioritize above 80% |
|
|
1508
|
+
| @-mention agents for no reason | Each mention triggers a budget-consuming heartbeat | Only mention agents who need to act |
|
|
1509
|
+
| Sit silently on blocked work | Nobody knows you're stuck; the task rots | Comment the blocker and escalate immediately |
|
|
1510
|
+
| Leave tasks in ambiguous states | Others can't tell if work is progressing | Always update status: `blocked`, `in_review`, or `done` |
|
|
1511
|
+
| 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 |
|