477 lines
21 KiB
Markdown
477 lines
21 KiB
Markdown
# 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.
|
||
|
||
---
|
||
|
||
# Worktrees switched on, measured, 2026-11-10
|
||
|
||
The first worktree attempt proved the flags, not the strategy: with
|
||
`enableIsolatedWorkspaces: false` the project's `git_worktree` policy was discarded before
|
||
dispatch. That is recorded above and is worth keeping, because it is what an operator will
|
||
see on any fresh instance.
|
||
|
||
After switching on `enableIsolatedWorkspaces` and `enableWorktreeRunExecution` (and leaving
|
||
`enableIsolatedWorkspacesByDefault` off, so the other projects are untouched), the same
|
||
project produced a real worktree on the next task:
|
||
|
||
| Fact | Value |
|
||
|---|---|
|
||
| Task | `AUD-160`, run `11a94551-dd12-4b13-92fe-835ea63d7753` |
|
||
| Workspace id | `7900dbf1-692c-4217-aebb-5d539c30aa98` |
|
||
| Mode / strategy | `isolated_workspace` / `git_worktree` |
|
||
| Branch | `AUD-160-milestone-1-t2-read-hello-txt` |
|
||
| Path | `/tmp/pc-sandbox-2026-11-10/.paperclip/worktrees/AUD-160-milestone-1-t2-read-hello-txt` |
|
||
| Base | `main` at `b093301` |
|
||
| Delivery state | `merged_by_ancestry` (the task made no commit) |
|
||
|
||
Paperclip wrote its own comment on the task, titled **Workspace Ready**, naming the
|
||
strategy, the branch and the working directory. `git worktree list` shows the new path, and
|
||
`git branch` shows the new branch. The file `hello.txt` from the first task was present in
|
||
the worktree, so the branch really was cut from the finished work.
|
||
|
||
Two consequences worth remembering:
|
||
|
||
- **Name the branch with a template.** The default is `{{issue.identifier}}-{{slug}}`;
|
||
usable placeholders include `{{issue.identifier}}`, `{{slug}}`, and the project and agent.
|
||
Without a template every worktree of the same task would land on the same name.
|
||
- **`merged_by_ancestry` is not a merge.** A task that changes nothing leaves its branch at
|
||
the base commit, and Paperclip marks it delivered because the branch adds nothing. A task
|
||
that does commit will stay `unmerged` until something merges it. That is the state the
|
||
review gate will read.
|
||
|
||
## Budgets as they stand
|
||
|
||
Read from `GET /api/companies/{companyId}/agents`:
|
||
|
||
| Agent | Monthly budget (cents) | Spent (cents) |
|
||
|---|---|---|
|
||
| Builder | 5000 | 2 |
|
||
| Chief of Staff | 5000 | 0 |
|
||
| System | 3000 | 0 |
|
||
| researcher | 2000 | 0 |
|
||
| Documenter | 2000 | 0 |
|
||
| Hub | 0 | 1 |
|
||
|
||
The company's `budgetMonthlyCents` is 0. There is no currency field anywhere in the payload:
|
||
Paperclip counts cents and does not name a currency, so "5000" is a number, not "$50 AUD".
|
||
|
||
---
|
||
|
||
# Milestone 2 passed from the terminal, 2026-11-10
|
||
|
||
The reply screen sent one call that both comments and changes status:
|
||
|
||
```
|
||
PATCH /api/issues/AUD-162 {"status":"todo","comment":"Unblocked: AUD-161 is done, …"}
|
||
→ 200, status todo
|
||
```
|
||
|
||
Read back afterwards:
|
||
|
||
```
|
||
GET /api/issues/AUD-162/comments → 1 comment, authorType: user, authorUserId: 2204TMD0…
|
||
GET /api/issues/AUD-162/activity → actorType: user, issue.comment_added, then
|
||
issue.updated status {from blocked, to todo}
|
||
```
|
||
|
||
Before the move, `AUD-161` was set to `done` and `diagnostics/blockers` for `AUD-162`
|
||
returned `allBlockersDone: true, unresolvedBlockerCount: 0` while the task itself stayed
|
||
`blocked`. So Paperclip does not walk a task forward on its own when its blocker clears; it
|
||
makes the task ready and leaves the decision to a person or a program. That is the job the
|
||
reply screen does, and the reason this milestone exists.
|
||
|
||
---
|
||
|
||
# Milestone 3, from the terminal, 2026-11-10
|
||
|
||
Four screens now run as panes in the session `paperclip-harness`, on workspaces named
|
||
`shell`, `tree`, `reply`, `new` and `log`.
|
||
|
||
## Screen 2 made a two-task plan
|
||
|
||
Typed in the `new` pane, twice, and read back from the API:
|
||
|
||
| Task | Created as | Blocker | Assignee |
|
||
|---|---|---|---|
|
||
| `AUD-163` | `todo` | none | none, until assigned from the tree |
|
||
| `AUD-164` | `todo` | `AUD-163` via a separate `PATCH` | none |
|
||
|
||
`GET /api/issues/AUD-164/diagnostics/blockers` then said
|
||
`unresolvedBlockerCount: 1`, and the tree drew `[AUD-163 todo]` against it. The verify
|
||
command typed in the form is stored in the task's own description:
|
||
|
||
```
|
||
Create NOTES.md at the repository root. …
|
||
|
||
Done when this command succeeds:
|
||
|
||
test -s NOTES.md
|
||
```
|
||
|
||
Assigning is a separate key (`a` in the tree), never part of creation. That is what woke
|
||
`Builder` both times: `runs[0].invocationSource` is `assignment`.
|
||
|
||
## Screen 3 showed the work happening
|
||
|
||
Read from the `log` pane while the run was live, not afterwards:
|
||
|
||
```
|
||
runs
|
||
running assignment 2026-11-10T01:12:28.391Z
|
||
diff and worktree
|
||
worktree: …/worktrees/AUD-163-write-notes-md-with…
|
||
branch: AUD-163-write-notes-md-with-the-three-milestone-names base: main
|
||
no commits on this branch yet
|
||
uncommitted
|
||
?? NOTES.md
|
||
```
|
||
|
||
and after it finished:
|
||
|
||
```
|
||
succeeded assignment 2026-11-10T01:12:28.391Z → 2026-11-10T01:13:01.683Z
|
||
commits
|
||
754cdb5 AUD-163 add NOTES.md with the three milestone names
|
||
changed
|
||
NOTES.md | 3 +++
|
||
diff
|
||
diff --git a/NOTES.md b/NOTES.md
|
||
```
|
||
|
||
## The gap this exposed: nothing merges
|
||
|
||
`main` never received `NOTES.md`:
|
||
|
||
```
|
||
$ git -C /tmp/pc-sandbox-2026-11-10 cat-file -e main:NOTES.md
|
||
fatal: path 'NOTES.md' does not exist in 'main'
|
||
```
|
||
|
||
So `AUD-164`, whose branch was cut from `main`, had no file to read. The agent worked
|
||
around it by reading the other task's branch directly —
|
||
`git show AUD-163-write-notes-md-with-the-three-milestone-names:NOTES.md` — and reported
|
||
that in its comment. Two tasks both show `done`, and the handoff never passed through
|
||
`main`.
|
||
|
||
A dependent task that finds its predecessor's work by reaching across branches is not a
|
||
merge and not a review. Milestone 5 (merge one at a time, re-checked) is what closes this,
|
||
and until it exists the second task in any chain is working from a guess.
|
||
|
||
## Two behaviours worth knowing
|
||
|
||
**A corrective wake is real.** `AUD-164`'s first run ended `succeeded` without setting a
|
||
status, and Paperclip immediately started a second run with `invocationSource:
|
||
automation` to ask the agent to finish what it started. The docs call this
|
||
`finish_successful_run_handoff`. It doubles the cost of an agent that forgets to set a
|
||
status, and our screens should show that second run rather than hide it.
|
||
|
||
**A comment can be attributed to the person when an agent wrote it.** The first comment on
|
||
`AUD-164` is `authorType: user`, and the agent's own next comment says the text "landed
|
||
under the board identity by mistake". So `authorType: user` is not proof that the person
|
||
typed it. Screen 4 currently prints `user` as "you"; that label is wrong often enough to
|
||
matter, and it needs a better rule than the author type alone.
|
||
|
||
---
|
||
|
||
# Milestones 4 and 5, measured, 2026-11-10
|
||
|
||
## The project layer
|
||
|
||
`bin/office <folder>` writes exactly one file, `<folder>/.harness/harness.toml`:
|
||
|
||
```toml
|
||
session = "paperclip-harness"
|
||
watching = "AUD-163"
|
||
|
||
[paperclip]
|
||
company = "d4921013-e5da-4d88-8016-57d5746e2d03"
|
||
project = "17adbb47-f966-4a82-a8ef-e48c941443ea"
|
||
|
||
[machine]
|
||
host = "nixos-desktop"
|
||
folder = "/tmp/pc-sandbox-2026-11-10"
|
||
```
|
||
|
||
That is the whole of what we store: which folder, on which machine, in which session,
|
||
showing which task. Everything else is read from Paperclip.
|
||
|
||
Measured, in this order, on one project:
|
||
|
||
| Step | Result |
|
||
|---|---|
|
||
| `bin/office` | wrote the file, built five panes on workspaces 1–5 |
|
||
| run again | five panes adopted, none created |
|
||
| `bin/board --replace` | five closed, five created, then placed 1–5 in order |
|
||
| `bin/project-reset --dry-run` | listed the panes, three worktrees and three branches |
|
||
| `bin/project-reset --yes` | removed them; `git worktree list` left only the main checkout |
|
||
| `bin/office` afterwards | five panes built again from nothing |
|
||
|
||
Two failures found by doing it:
|
||
|
||
- **A rebuild scatters the workspace order.** Closing panes renumbers the rest, so the
|
||
pages landed as shell 2, tree 3, reply 4, log 5, new 1. The fix is a final pass that
|
||
moves each pane once to the workspace it belongs on (`tuios move-window`) and names it
|
||
again. Without that pass the rail order is meaningless.
|
||
- **Adopting a project must not invent a session.** The first version defaulted the session
|
||
to the folder name, wrote that into the file, then failed because no such session
|
||
existed — and the good session name was already overwritten. Sessions are made by a
|
||
person, so `bin/office` now reads the recorded session first, checks it exists, and
|
||
refuses with the exact `tuios new` command when it does not.
|
||
|
||
## The merge gate
|
||
|
||
`bin/merge` takes a lock (`flock` on `/tmp/paperclip-harness-merge.lock`), rebases the
|
||
task's branch onto the base, runs the task's own verify command, runs `ocr` over the
|
||
rebased range, and only then merges — then runs the verify command again on the base
|
||
branch and rolls the merge back if it fails.
|
||
|
||
Every branch of that path was exercised, not reasoned about:
|
||
|
||
| Case | What happened |
|
||
|---|---|
|
||
| clean task (`AUD-163`, docs only) | merged at `2899981`; `ocr` reported `skipped`, 0 files, and said so instead of pretending to have reviewed |
|
||
| verify command fails | exit 4, task left alone, comment written |
|
||
| review finds a critical | `AUD-165` added `cleanup.sh` with `rm -rf "$HOME/tmp"`; exit 5, finding quoted, `cleanup.sh` never reached `main` |
|
||
| review cannot run | exit 5, refused |
|
||
| another merge holds the lock | exit 6, refused immediately |
|
||
|
||
`ocr`'s JSON has `status`, `summary.files_reviewed` and `comments[]` with `severity`
|
||
(`critical`, `high`, …), `path`, `start_line` and `content`. Blocking severities are
|
||
`critical` and `high`; `skipped` with zero files is "nothing to review", recorded
|
||
plainly, while any other non-`complete` status refuses.
|
||
|
||
## Two API behaviours that shape the code
|
||
|
||
**A comment on an assigned task wakes its agent.** The merge tool's first refusal comment
|
||
went onto a task still assigned to `Builder`, and `Builder` immediately woke and took the
|
||
task back to `in_progress` — paying for a run to read a message meant for a person. So a
|
||
tool's verdict is written only after the task is handed back:
|
||
`PATCH {"assigneeAgentId": null, "assigneeUserId": "<the board user>"}`. A person's reply
|
||
from screen 4 is the opposite case: there the wake is the point.
|
||
|
||
**The issue list is a summary.** `GET /api/companies/{id}/issues` carries no
|
||
`currentExecutionWorkspace` and no blockers; the single-task read has both. Any code that
|
||
needs a worktree path, a branch or a blocker must do a second `GET /api/issues/{id}`.
|
||
That is the third time this list-versus-record difference has bitten, after blockers and
|
||
after the earlier probe.
|