Files
paperclip-harness/docs/PAPERCLIP-API-NOTES.md

10 KiB
Raw Blame History

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: paperclipai 2026.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, 2 blocked, 2 in_progress, 1 todo, 1 backlog.
  • 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}/blockers or /api/issues/{id}/diagnostics/blockers rather 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_local is the identity's own adapter, and Pi is listed as a supported runtime upstream.
  • H5 — cost and run history readable. PASS. /api/issues/{id}/cost-summary returns costCents, 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-157 Spike T1 — write a file (2ce09ea8-…)
  • AUD-158 Spike 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 hash b093301148b62ab0f2a34bec4f31d97a26a212e9 … 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.