10 KiB
Paperclip API — read-only probe, 2026-11-09
Probe only: no writes, no task created, nothing in the existing projects touched. All
requests were GET with Authorization: Bearer $PAPERCLIP_API_KEY read from the
environment; the key is never printed here.
Where it runs
- Local server:
http://127.0.0.1:3100, pid 1588, started 2026-11-05. From~/.paperclip/instances/default/runtime-info.json. - Public URL:
https://paperclip.lab.audasmedia.com.au. - Postgres:
postgres://paperclip@127.0.0.1:5433/paperclip(embedded, local). - CLI:
paperclipai2026.916.1, managed install under~/.paperclip/cli/installs.
Endpoints that answered
| Request | Result |
|---|---|
GET /api/health |
200, 899 bytes |
GET /api/openapi.json |
200, 1.1 MB — 685 paths |
GET /api/agents/me |
200 — identity, company, adapter |
GET /api/companies/{companyId}/issues?limit=200 |
200 — 145 issues |
GET /api/issues/{id} |
200 |
GET /api/issues/{id}/comments |
200 |
GET /api/issues/{id}/activity |
200 |
GET /api/issues/{id}/cost-summary |
200 |
GET /api/issues?limit=5 |
400 — "Missing companyId in path" |
The identity the API answered as
name: Hub, role: general, status: idle, adapterType: **pi_local**,
model omni/deepseek/deepseek-v4-flash. So Pi is the installed worker adapter, not
something we must write.
What already exists in this instance
- 10 projects, 145 issues: 139
done, 2blocked, 2in_progress, 1todo, 1backlog. - Every status the four-screen TUI needs is already a first-class value.
Endpoints the TUI and the executor will need (from the 685 in the spec)
- Read:
/api/companies/{companyId}/issues,/api/issues/{id},/api/issues/{id}/comments,/api/issues/{id}/activity,/api/issues/{id}/children,/api/issues/{id}/diagnostics/blockers,/api/issues/{id}/cost-summary,/api/issues/{id}/runs,/api/issues/{id}/heartbeat-context,/api/agents/me/inbox-lite. - Write (later, not in this probe):
/api/issues,/api/issues/{id}/comments,/api/issues/{id}/checkout,/api/issues/{id}/release,/api/approvals/{id}/approve|reject|request-revision,/api/cases/{caseId}/blockers,/api/issues/{id}/interactions/{interactionId}/respond. - Trees and holds:
/api/issues/{id}/tree-control/state|preview,/api/issues/{id}/tree-holds,/api/issues/{id}/diagnostics/subtree.
Hypotheses
- H1 — the control plane answers. PASS. Health, identity, 145 issues.
- H2 — task with a blocker via API. NOT TESTED (a write; out of scope for a
read-only probe). Note: the two blocked issues returned
blockedByIssueIds: None, so the blocker relation may live in/api/cases/{caseId}/blockersor/api/issues/{id}/diagnostics/blockersrather than on the issue row. This must be established before trusting screen 1. - H3 — a comment wakes an agent. NOT TESTED. The wake plumbing is visible in the
spec (
heartbeat-context,inbox-lite,queued-comments/steer,diagnostics/wakes), and agents run on heartbeats, so the shape is right. Unproven. - H4 — Pi can be a worker. Likely PASS:
adapterType: pi_localis the identity's own adapter, andPiis listed as a supported runtime upstream. - H5 — cost and run history readable. PASS.
/api/issues/{id}/cost-summaryreturnscostCents,inputTokens,cachedInputTokens,outputTokens,runCount.
What this changes
The premise holds: there is a live control plane, with the statuses, threads, activity, cost and blocker diagnostics a four-screen TUI needs, and Pi is already the adapter. The work that remains is small and specific: prove a blocker edge by API, prove a comment wakes an agent, and write the executor that puts a task into a git worktree, runs it and reports back.
Sandbox writes, 2026-11-09 (approved in advance)
Sandbox created: project spike-2026-11-09 (9e3f61f6-cce9-49b1-8083-3130345a04a5),
status backlog, workspace git_repo at /tmp/pc-sandbox (a throwaway repo with one
commit). It is safe to delete.
Two issues created by API, status todo:
AUD-157Spike T1 — write a file (2ce09ea8-…)AUD-158Spike T2 — read that file (abc5d318-…)
The permission rule, learned the hard way
With PAPERCLIP_API_KEY (the agent Hub), every write returned 403:
"Cross-issue writes need a run to attribute them to (Heartbeat run context). Every agent comment and task update is attributed to a heartbeat run so the cross-issue cap can be counted and the audit trail can name who acted for whom."
So an agent key is only usable inside a heartbeat run, with X-Paperclip-Run-Id.
A cockpit acting for the person must use the user credential
(PAPERCLIP_BOARD_KEY), not the agent key. That is a design fact, not an obstacle: the
TUI is the person's surface, and the audit trail correctly refuses anonymous agent
writes.
H2 — a task with a blocker: PASS
PATCH /api/issues/{id} with {"blockedByIssueIds":["<t1>"]} → 200, and the edge reads
back. GET /api/issues/{id}/diagnostics/blockers returns:
readiness: { allBlockersDone: false, isDependencyReady: false, unresolvedBlockerCount: 1 }
blockers: [ "Spike T1 — write a file" ]
Note: the blocker edge is not on the issue list rows (blockedByIssueIds: 0 there).
A tree view must read diagnostics/blockers (or the single-issue GET) per task, not the
list. That is the one place screen 1 could have lied to us.
H3 — a comment lands on a blocked task: PASS
POST /api/issues/{id}/comments with the user key → 201, authorType: user, and the
comment is on the thread of the blocked task. What this does not yet prove is that a
comment wakes an agent: that requires a real agent on the task and a heartbeat. The
wake plumbing is present in the spec (heartbeat-context, inbox-lite,
queued-comments/steer, diagnostics/wakes); the wake itself is unproven.
Updated verdict
H1 pass, H2 pass, H3 partly (comment lands; wake unproven), H4 likely (Pi is the
adapter), H5 pass. The remaining proof is one real agent doing one task, which is
Milestone 1 of plan/92-paperclip-spike.md, not a probe.
One more thing worth knowing
A project can be created with an executionWorkspacePolicy whose workspaceStrategy.type
is git_worktree, with baseRef, branchTemplate, worktreeParentDir, provision and
teardown commands. Paperclip may already do the worktrees we assumed we would build.
That deserves its own probe before any executor is written.
Milestone 1 measured, 2026-11-10 (writes, sandbox only)
Everything below was run with the user key (PAPERCLIP_BOARD_KEY). The key is never
printed. The sandbox is spike-2026-11-10 and its scratch repository
/tmp/pc-sandbox-2026-11-10. Both are safe to delete.
The order that works
The board guide's rule holds: never create a task and assign it in the same action. The four calls, in order, with the result each one returned:
| Call | Result |
|---|---|
POST /api/companies/{company}/projects |
201, project 17adbb47-f966-4a82-a8ef-e48c941443ea, workspace 4d4eb32a-08a8-4e3e-88c9-2a8cedfc288b |
POST /api/companies/{company}/issues ×2 |
201, AUD-159 34abf83f-…, AUD-160 21d1f49e-…, both todo, unassigned |
PATCH /api/issues/AUD-160 {"blockedByIssueIds":["34abf83f-…"]} |
200, echoed blockedBy with AUD-159; diagnostics then said unresolvedBlockerCount: 1 |
PATCH /api/issues/AUD-159 {"assigneeAgentId":"403de1f7-…"} |
200, todo |
The activity record of the assignment names the person, not an agent:
actorType: "user", authorizationReason: "allow_board_actor". That is the audit trail
the user key exists for.
The wake, timed
00:01:24.891Z PATCH assigneeAgentId -> 200
00:01:24.937Z wake requested source=assignment reason=issue_assigned status=claimed
00:01:25.036Z run aae77e89-7059-43b4-9154-972282770212 started, issue -> in_progress
00:01:51.351Z comment posted by agent 403de1f7-…
00:01:53.153Z issue -> done
00:01:59.377Z run -> succeeded (resultJson.stopReason = "completed")
GET /api/issues/AUD-159/diagnostics/wakes returned that single wake request and the
plain-English line "The most recent wake for AUD-159 is currently claimed for
issue_assigned." This endpoint answers the question we used to have to guess at: it names
each wake, the run it produced, and why a wake did not happen.
H3 is now PASS: a real agent woke on assignment, worked, commented and closed the task with no person in the loop and no timer.
What the agent reported
Comment 822f2924-dd2f-4f26-9046-c7801f2196a9, author agent, id 403de1f7-…:
Created
hello.txt… Commit hashb093301148b62ab0f2a34bec4f31d97a26a212e9… Working directory (absolute path):/tmp/pc-sandbox-2026-11-10… Branch:main.
Checked on disk: git log --oneline shows b093301 add hello.txt on main, the file
holds the expected line, and git status is clean.
Cost, read back
GET /api/issues/AUD-159/cost-summary →
costCents: 1, inputTokens: 101002, outputTokens: 1515, runCount: 1,
runtimeMs: 34341. The run's own usageJson says costUsd: 0.00749434.
No worktree was created, and why
The task's executionWorkspaceId and currentExecutionWorkspace are null. The run
worked in the project folder on main, so a second task would collide rather than work on
its own copy.
Cause, measured: GET /api/instance/settings/experimental returns
enableIsolatedWorkspaces: false and enableWorktreeRunExecution: false. With the first
flag off, Paperclip throws away the task's workspace settings before dispatch, so the mode
resolves to the shared project folder even though the project asked for git_worktree.
The supporting code is in the installed server:
dist/services/heartbeat.js sets issueExecutionWorkspaceSettings = null when the flag is
off, and dist/services/workspace-runtime.js is where git worktree add would run.
Default worktree location is <repo>/.paperclip/worktrees/<branch>; default branch
template is {{issue.identifier}}-{{slug}}.
Consequence for the plan. The "Paperclip makes the worktree for us" assumption is
true only with an experimental instance setting switched on. Until a person turns it on,
every task shares one folder and merges happen on main with no branch and no review
step. That is a decision to put to the person, not a bug to work around.
Second task left where it is
AUD-160 stayed todo, unassigned, blocked by a task that is now done. Nothing woke,
because no agent is assigned to it. Whether the issue_blockers_resolved wake fires for an
assigned dependent task is still unmeasured.