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

223 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.