723 lines
36 KiB
Markdown
723 lines
36 KiB
Markdown
# The plan, and how the interface sits on Paperclip
|
|
|
|
Written 2026-11-10, after a wrong turn: I split every workspace into three panes without
|
|
being asked, and it broke the working layout. It has been reverted. This document says what
|
|
exists, what is proposed, and which Paperclip endpoint each thing reads or writes. Nothing
|
|
here is invented: every endpoint below was called on this machine and answered.
|
|
|
|
## What is on screen today
|
|
|
|
Seven workspaces, one pane each: `Shell>shell`, `Board>board`, `Work>work`, `Reply>reply`,
|
|
`New task>new`, `Project>project`, `Repair>repair`. Inside a pane the program draws several
|
|
panels (Board draws three), and you switch focus with Tab. That is the state that works.
|
|
|
|
## Paperclip's objects, and where each one is seen
|
|
|
|
| Paperclip object | Endpoint, measured | Where it appears now |
|
|
|---|---|---|
|
|
| company | `GET /api/companies/{id}` | not shown; the id is in the project file |
|
|
| project | `GET /api/companies/{id}/projects` | `Project>project` |
|
|
| task | `GET /api/companies/{id}/issues`, `GET /api/issues/{id}` | `Board>board`, `Reply>reply` task panel |
|
|
| blockers of a task | `GET /api/issues/{id}/diagnostics/blockers` | `Board>board`, per row |
|
|
| comment thread | `GET /api/issues/{id}/comments` | `Reply>reply` |
|
|
| runs of a task | `GET /api/issues/{id}/runs` | `Work>work` |
|
|
| activity | `GET /api/issues/{id}/activity` | `Board>board` needs-you tab, `Work>work` |
|
|
| cost per task | `GET /api/issues/{id}/cost-summary` | `Board>board` detail, `Work>work` facts, `Project>project` costs |
|
|
| worktree and branch | the task's `currentExecutionWorkspace` | `Work>work` change and facts |
|
|
| agents | `GET /api/companies/{id}/agents` | `Project>project` facts and limits, by name and budget |
|
|
| **an agent's runs, company-wide** | `GET /api/companies/{id}/heartbeat-runs` | **nowhere yet** |
|
|
| **a run's log** | `GET /api/heartbeat-runs/{runId}/log` — 300 KB of JSON lines for one finished run | **nowhere yet** |
|
|
| **a run's events** | `GET /api/heartbeat-runs/{runId}/events` | **nowhere yet** |
|
|
| agent pause, resume, wake | `POST /api/agents/{id}/pause`, `/resume`, `/wakeup` | **nowhere yet** |
|
|
| agent settings | `PATCH /api/agents/{id}` — accepts `role`, `adapterConfig` (the model), `runtimeConfig` (heartbeat), `budgetMonthlyCents`, `status` | **nowhere yet** |
|
|
|
|
So the honest answer to "where do I see the Pi agents working": **you cannot, yet.** The data
|
|
is all there and the runs are already recorded — 281 of them on this instance — but no screen
|
|
shows them. That is the largest missing piece, and it is the next workspace.
|
|
|
|
## Missing: the Agents workspace
|
|
|
|
This is the one the person keeps asking for and it does not exist. Proposed as one pane, the
|
|
same shape as the others, drawing three panels with tabs:
|
|
|
|
```
|
|
Agents>agents
|
|
╭─ agents roster runs ╮
|
|
│ Campfire idle
|
|
│ ● Builder running AUD-163 started 14:12 1m 20s 1 cent
|
|
│ ● Hub idle last run 13:58
|
|
│ ● Documenter idle
|
|
╰───────────────────────────╯
|
|
```
|
|
|
|
- **roster** — every agent: name, state (`idle`, `running`, `needs input`, and Paperclip's
|
|
own status), current task if any, spend this month, budget. From
|
|
`GET /api/companies/{id}/agents`.
|
|
- **runs** — the company's run list, newest first: agent, task, state, what woke it, start,
|
|
duration, cost. From `GET /api/companies/{id}/heartbeat-runs` (281 today).
|
|
- **log** — the tail of the chosen run, from `GET /api/heartbeat-runs/{runId}/log`. This is
|
|
the closest thing to "watching Pi work": the log grows while the run is live, and
|
|
`lastOutputAt`/`lastOutputBytes` on the run say how fresh it is.
|
|
|
|
Actions, all with the user key, all reversible:
|
|
- `enter` on a run → its log in the log tab.
|
|
- `p` pause, `P` resume an agent (`POST /api/agents/{id}/pause|resume`).
|
|
- `w` wake an agent (`POST /api/agents/{id}/wakeup`).
|
|
- `e` open its settings (see below).
|
|
|
|
## Missing: working on an agent — role, model, cost
|
|
|
|
Paperclip owns these, so the interface edits Paperclip, never a local file. One more panel,
|
|
`Agents>settings`, or a tab inside `Agents>agents`:
|
|
|
|
| Field | Paperclip field | Endpoint |
|
|
|---|---|---|
|
|
| name and job title | `name`, `title` | `PATCH /api/agents/{id}` |
|
|
| role | `role` | same |
|
|
| who it reports to | `reportsTo` | same |
|
|
| model (the LLM) | `adapterConfig.model` | same, with `replaceAdapterConfig` care |
|
|
| heartbeat on/off, concurrency | `runtimeConfig.heartbeat` | same |
|
|
| monthly budget | `budgetMonthlyCents` | same |
|
|
| pause / resume | `POST /api/agents/{id}/pause`, `/resume` | separate calls |
|
|
|
|
Two rules that fall out of this and are not negotiable: a change to a model applies to that
|
|
agent's **next** run, never to one in flight; and a budget change is recorded with who
|
|
changed it, because Paperclip's audit trail is the only record we have.
|
|
|
|
## Tool panels: Lazygit, Hunk, a shell, the review
|
|
|
|
Both are installed on this machine and I verified the paths:
|
|
`/run/current-system/sw/bin/lazygit` and `/etc/profiles/per-user/sam/bin/hunk`.
|
|
|
|
They are **not** to be reimplemented. The proposal is one key in `Work>work`:
|
|
|
|
- `t` opens a chooser: `shell · lazygit · hunk · git log · ocr`.
|
|
- The chosen tool opens as a pane **in the current workspace**, beside the panel you were
|
|
reading, and takes focus.
|
|
- `q` or `esc` closes it and focus returns exactly where it was.
|
|
- Nothing is created until you ask, so the layout stays as it is today.
|
|
|
|
This is the answer to "I thought we had Lazygit and Hunk": they are installed, they are not
|
|
wired in, and wiring them in is one pane-opener plus a key.
|
|
|
|
## The review: ocr is installed and has already run
|
|
|
|
`ocr` is at `/home/sam/.local/bin/ocr`, version `open-code-review v1.13.6`. It is in the
|
|
project's `bin/merge` and `bin/review`, and it has blocked a real merge: on `AUD-165` it
|
|
reported a critical finding (`rm -rf "$HOME/tmp"` deleting `/tmp` when `HOME` is empty) and
|
|
the merge was refused. The Repair screen once said "not on the path" because a pane started
|
|
by TUIOS gets a smaller PATH than a shell; that was fixed by looking in the usual places.
|
|
|
|
Where it belongs in the interface: a **review panel inside `Work>work`**, showing the last
|
|
verdict for the chosen task, its findings, and the blocking ones in red. The merge stays a
|
|
command (`bin/merge`), because a merge is a decision with a lock, not a keystroke.
|
|
|
|
## Mouse-1 and Switch
|
|
|
|
Measured just now: **not installed** — no `mouse`, `mouse1` or `switch` binary on this
|
|
machine, and nothing obviously serving them locally. So nothing can be built on them yet
|
|
without adding software first.
|
|
|
|
Where they would fit if added: Mouse-1 is a small probability model that answers typed
|
|
questions; Switch turns a state into those questions. The honest use in this factory is
|
|
**routing and judgement that must be cheap and repeatable** — "is this task ready", "does
|
|
this comment answer the question", "which agent should take this" — rather than prose
|
|
generation. That is a later milestone, after agents and the review panel, and it needs a
|
|
decision about installing both, plus a measurement of whether they are better than the
|
|
existing cheap model already configured for agents (`omni/deepseek/deepseek-v4-flash`).
|
|
|
|
## The order I propose
|
|
|
|
1. **Agents workspace** — roster, runs, log. This is the missing view of Pi working, and it
|
|
is read-only, so it cannot break anything.
|
|
2. **Agent settings** — role, model, budget, pause/resume. First write surface aimed at
|
|
agents rather than tasks.
|
|
3. **Tool panels** — `t` opens Lazygit, Hunk, a shell, `ocr` in the current workspace.
|
|
4. **Review panel** in `Work>work` — the last verdict and its findings, beside the diff.
|
|
5. **Mouse-1 / Switch** — only after the above, and only after they are installed and
|
|
measured.
|
|
|
|
Each of the first four is one pane, one program, no change to the working layout. Nothing in
|
|
this list requires splitting workspaces again.
|
|
|
|
---
|
|
|
|
# Answers to the open questions, 2026-11-10
|
|
|
|
**Where settings are set.** `Agents>agents` has a fourth tab, **settings**, which edits the
|
|
chosen agent: role, job title, model, monthly budget, heartbeat. `enter` edits the field in
|
|
place, `esc` cancels, and `p` / `P` / `w` pause, resume and wake. Each save is one
|
|
`PATCH /api/agents/{id}` merged into that agent's existing configuration, so a model change
|
|
never wipes its folder or instructions. `Project>project` shows spend and limits read-only,
|
|
because Paperclip owns them and a second editable copy is how two screens start disagreeing.
|
|
|
|
**Spend.** Read from `cost-summary` per task (`Project>costs`, `Board>board`,
|
|
`Work>work`). The monthly budget is per agent, set in `Agents>agents` settings. There is no
|
|
company-level control on any screen yet.
|
|
|
|
**Skills for sub-agents.** Not in the interface. Paperclip has the endpoints —
|
|
`GET /api/agents/{id}/skills`, `POST /api/agents/{id}/skills/sync` with `add`, `remove` or
|
|
`replace` — so a read-only skills row is cheap and a full editor is a later step. The rule
|
|
worth keeping: an agent gets the skills its job needs and no more, which the `add`/`replace`
|
|
model already expresses.
|
|
|
|
**Where work is added.** `New task>new`, which writes a Paperclip task. A second place to
|
|
write tasks (Tuxedo, a notes file, a card board) would be a second owner for the same fact
|
|
and is exactly the failure the old project had. Personal notes are different from tasks: if
|
|
they are wanted, they belong in the project's own `.harness/NOTES.md`, and Tuxedo can read
|
|
it per project without pretending to be a task board.
|
|
|
|
**`office --host` from another machine.** Not implemented, and the runbook says why:
|
|
session creation needs a real terminal, so a person creates the session
|
|
(`tuios new --host nixos-desktop "new test project"`) and then runs `bin/office <folder>`
|
|
inside it, on the machine that owns the folder. `--host` today only records the host in the
|
|
project file. Driving `bin/office` on `.27` from `.13` would need ssh plus a remote
|
|
`bin/board`, which is a deliberate later step, not an accident.
|
|
|
|
**Seeing and switching projects.** Not built. Paperclip has the list
|
|
(`GET /api/companies/{id}/projects`); what it cannot know is which folder on which machine
|
|
each one lives in. The proposal: a small local registry of adopted projects
|
|
(`~/.config/paperclip-harness/projects.toml`: project id, folder, host, session), filled by
|
|
`bin/office`, and a `Project>projects` tab that lists them and switches by writing the
|
|
project into the current folder's `.harness/harness.toml`. A project on another machine
|
|
can only be switched to if the pane can reach it, so it would be shown as `on .27`, not
|
|
offered.
|
|
|
|
**Artifacts.** Paperclip work products: an uploaded file, a pull request, a preview URL, a
|
|
commit or a branch, attached to a task by an agent. They are inspectable records of what a
|
|
task produced. Nothing shows them yet; the natural home is a `Work>artifacts` tab listing
|
|
them with their links, read from the task's work products.
|
|
|
|
**"Workspaces" in Paperclip.** Paperclip has no workspaces. It has companies, projects,
|
|
issues (tasks), agents and runs. A *workspace* is TUIOS's word for a page; a *pane* is a
|
|
terminal inside it. When Paperclip says workspace it means a project's code folder
|
|
(`currentExecutionWorkspace`), which is the git worktree a task runs in.
|
|
|
|
**Charts.** TUIOS ships `plot` (Go), `loom` and TML. Go is the better fit here because the
|
|
panels are already Go and the data is already in memory: a spend-by-day chart in
|
|
`Project>costs` is a few dozen lines against `tuios.dev/plot`, where a TML surface would be
|
|
a second language and a second round trip. Worth doing once the functional tests pass.
|
|
|
|
**Mouse-1 and Switch.** Still not installed on this machine. Unchanged from the earlier
|
|
note: they would serve cheap repeatable judgement, later, after they are installed and
|
|
measured.
|
|
|
|
---
|
|
|
|
# Findings for the next round of decisions, 2026-11-10
|
|
|
|
Everything below was measured on this machine. Nothing here is proposed without saying so.
|
|
|
|
## Skills per agent — the API already models it
|
|
|
|
`GET /api/agents/{id}/skills` returns, for each agent:
|
|
|
|
```
|
|
mode: "persistent"
|
|
desiredSkills: ["paperclipai/paperclip/paperclip"]
|
|
entries: [
|
|
{ key: "company/<id>/obsidian-cli", runtimeName: "obsidian-cli--b74ae42966",
|
|
desired: false, managed: true, state: "stale",
|
|
sourcePath: "…/instances/default/skills/<id>/obsidian-cli",
|
|
targetPath: "/home/sam/.pi/agent/skills/obsidian-cli--b74ae42966" }
|
|
]
|
|
```
|
|
|
|
So Paperclip already models exactly what is wanted: a desired set per agent, a managed set
|
|
actually installed, and a state (`stale` today for `obsidian-cli` on `Builder`, which is
|
|
worth fixing on its own). `POST /api/agents/{id}/skills/sync` with `add`, `remove` or
|
|
`replace` sets the desired set; `replace` is the operation that means "this agent gets these
|
|
and nothing else", which is how a sub-agent's context is kept small.
|
|
|
|
Company level, there is more: `GET/POST /api/companies/{id}/skills` (the catalog),
|
|
`/skills/{id}/versions`, `/skills/{id}/audit`, and `GET/PUT /api/companies/{id}/skill-policy`
|
|
with `/skill-policy/evaluate`, which is a company-wide rule about which skills may be used.
|
|
|
|
Proposed in the interface: `Agents>agents` settings tab gains two rows — **skills** (the
|
|
desired set, read-only at first, with `add`/`remove` later) and **skill source** (company
|
|
catalog or the agent's own). A full skill browser is a later screen, not a panel.
|
|
|
|
## Memory — three different things wearing one word
|
|
|
|
| What | State here | What it does |
|
|
|---|---|---|
|
|
| **engram** | installed, `engram v1.16.1`, MCP server mode, no pi MCP entry yet | persistent memory for agents, over HTTP or MCP |
|
|
| **headroom** | running, `headroom-proxy v0.24.0`, healthy at `192.168.20.13:8787`, memory backend **disabled**, compression enabled | compresses prompts on the way out; it is a proxy, not a store |
|
|
| **pi memctx** | in use in this session | the memory gateway that injects notes into a prompt |
|
|
|
|
They are not alternatives: engram stores, headroom compresses, memctx injects. For agents,
|
|
the only lever Paperclip gives us is per-agent configuration — `adapterConfig` (the folder,
|
|
the model, the instructions bundle) and `runtimeConfig` — so memory is set per agent by
|
|
pointing that agent at the proxy or the storage it should use, not by a global switch.
|
|
|
|
Proposed: a **memory** row in `Agents>agents` settings showing which of the three that agent
|
|
uses, read from its adapter configuration; and nothing more until one agent has been run
|
|
both ways and the difference measured.
|
|
|
|
## MOUSE-1
|
|
|
|
- `MOUSE_API_KEY` **is** set in `~/.config/environment.d/10-secrets.conf` (name checked, value
|
|
never printed). `JEV_API_KEY` is set as well.
|
|
- No `mouse`, `mouse1`, `jev` or `switch` binary is installed.
|
|
- The previous project already wrote the policy, in
|
|
`~/paseo/projects/tuios_coding_harness/plan/*`: MOUSE-1 is **"triage only: label, score,
|
|
route, escalate. Never a gate, never a permit"**, and explicitly ruled out for deciding a
|
|
merge or allowing a tool call, because a cheap classifier that fails open on a network
|
|
error turns a green run into a lie.
|
|
|
|
That policy is right and should be adopted unchanged. Its place in this interface, if it is
|
|
added: `Board>board` needs-you could be ordered by a MOUSE-1 score, and `Work>review` could
|
|
use it to pre-sort findings. Never the merge, never an approval.
|
|
|
|
## Lazygit
|
|
|
|
Already wired: `t` in `Board>board` or `Work>work`, then choose Lazygit. It opens beside the
|
|
pane, in the same workspace, and `q` closes it. Hunk, Yazi, Neovim, fzf, a shell and ocr are
|
|
in the same chooser.
|
|
|
|
## The old `office` command, and what ours is missing
|
|
|
|
The previous project's `office` does four things, in order:
|
|
|
|
1. makes the folder and remembers the host in `<folder>/.harness/host`;
|
|
2. runs `new-project` to write the project scaffold — `settings.toml`, one file per role,
|
|
`verify.sh`, the per-screen notes — **writing only what is missing, never overwriting**;
|
|
3. refuses to continue if the TUIOS session does not exist, and prints the exact
|
|
`tuios new <session>` line to run;
|
|
4. runs its `board` to build or repair the pages, then prints how to attach.
|
|
|
|
Ours does 1, 3 and 4 (with a different file: `.harness/harness.toml`). It does **not** do 2:
|
|
there is no scaffold, no per-role file, no `verify.sh`, no page notes. That is a real gap and
|
|
the cheapest useful one to close.
|
|
|
|
## Dashboard — the API already answers it
|
|
|
|
`GET /api/companies/{id}/dashboard` returns 1.8 KB:
|
|
|
|
```
|
|
agents: {active: 6, running: 0, paused: 0, error: 0}
|
|
tasks: {open: 8, inProgress: 0, blocked: 4, done: 147}
|
|
costs: {monthSpendCents: 11, monthBudgetCents: 0, monthUtilizationPercent: 0}
|
|
pendingApprovals: 0
|
|
budgets: {activeIncidents: 0, pendingApprovals: 0, pausedAgents: 0, pausedProjects: 0}
|
|
runActivity: [14 entries]
|
|
```
|
|
|
|
That is a whole dashboard in one call, and it is cheap enough to poll. Proposal: a
|
|
`Project>overview` panel showing those four blocks and a sparkline of `runActivity`, drawn in
|
|
Go with `tuios.dev/plot` rather than embedding Paperclip's web dashboard.
|
|
|
|
## Not doing
|
|
|
|
Artifacts, as instructed. Nothing about them will be added.
|
|
|
|
---
|
|
|
|
# Pi agents: how their configuration actually resolves
|
|
|
|
Advice only — nothing here was built. Every fact below was read from the installed code, the
|
|
config files, or the API on this machine.
|
|
|
|
## Your picker, and what it really writes
|
|
|
|
`pi-config` is an extension with five commands: `/config-add`, `/config-remove`,
|
|
`/config-list`, `/config-show`, `/config-setup`. `picker.ts` is the overlay — a filtered list
|
|
of `~/.agents/skills` and `~/.agents/extensions` showing which are active in the project and
|
|
which are active globally. The commands write **JSON**, at:
|
|
|
|
```
|
|
<project>/.pi/settings.json ← project settings (the picker's "-l" mode)
|
|
~/.pi/agent/settings.json ← global settings
|
|
```
|
|
|
|
and the shape they manipulate is a `packages` array of sources and filters:
|
|
|
|
```json
|
|
{"packages": [
|
|
"npm:pi-memctx",
|
|
{"source": "git:https://gitea.lab.audasmedia.com.au/sam/pi-config",
|
|
"extensions": ["extensions/pi-config/index.ts", "extensions/tavily-search/index.ts"]},
|
|
{"source": "/home/sam/.agents", "skills": ["nixos-workflow"], "extensions": []}
|
|
]}
|
|
```
|
|
|
|
So yes: this is a stable JSON contract, and anything that can write a file can formulate an
|
|
agent's context the way the picker does. The question is not *whether* it can be written, but
|
|
*which one Pi actually reads on a Paperclip run*.
|
|
|
|
## What Pi reads, in order
|
|
|
|
From Pi's own documentation:
|
|
|
|
- global: `~/.pi/agent/skills/`, `~/.agents/skills/`
|
|
- **project, only after the project is trusted**: `.pi/skills/`, `.agents/skills/` in the
|
|
working directory and its ancestors up to the git root
|
|
- packages and the settings `skills` array
|
|
- command line: `--skill <path>`, additive even with `--no-skills`
|
|
|
|
Settings merge the same way: user settings, then `<cwd>/.pi/settings.json`.
|
|
|
|
## What Paperclip actually runs
|
|
|
|
From `@paperclipai/adapter-pi-local`, a run is:
|
|
|
|
```
|
|
pi --mode json
|
|
--append-system-prompt <rendered system prompt>
|
|
--provider <p> --model <m> --thinking <t>
|
|
--tools read,bash,edit,write,grep,find,ls
|
|
--session /home/sam/.pi/paperclips/<timestamp>-<agentId>.jsonl
|
|
--skill <a directory Paperclip builds from that agent's desired skills>
|
|
```
|
|
|
|
and before the run it symlinks the agent's desired skills into `~/.pi/agent/skills/<runtimeName>`.
|
|
|
|
Four consequences, and they matter:
|
|
|
|
1. **Skills are already per agent** — `desiredSkills` plus `POST /api/agents/{id}/skills/sync`.
|
|
That is the supported lever, and it is what the `--skill` directory is built from.
|
|
2. **The isolation is not complete.** Injected skills are symlinked into the *shared* global
|
|
directory and are not removed when another agent runs, and Paperclip does not pass
|
|
`--no-skills`. So a skill an agent once had can remain visible to another agent.
|
|
3. **The tool list is fixed** at `read,bash,edit,write,grep,find,ls`. An agent cannot gain a
|
|
tool by being given an extension unless the adapter is changed.
|
|
4. **Project-local `.pi/settings.json` is unlikely to be honoured for Paperclip runs.** Pi only
|
|
loads project settings from a trusted path. `~/.pi/agent/trust.json` holds 14 entries and none
|
|
of them is a Paperclip worktree or `~/paperclip-agents`. Worse, a worktree path changes with
|
|
every task (`…/worktrees/AUD-165-…`), so there is nothing stable to trust.
|
|
|
|
## So what is the honest advice
|
|
|
|
- **Agent identity, instructions, model, budget, and skills: Paperclip.** Those are per-agent
|
|
already and they are what a run uses. The instructions bundle at
|
|
`~/.paperclip/instances/default/companies/<co>/agents/<id>/instructions/AGENTS.md` is the
|
|
real per-agent context control, and it is editable through the API.
|
|
- **Extensions: one global set, not per agent.** There is no per-agent extension mechanism in
|
|
this adapter. Pretending otherwise would produce agents whose behaviour depends on which one
|
|
ran last.
|
|
- **Folder-level cleverness: keep out of the critical path.** Writing `.pi/settings.json` into a
|
|
project is useful for *you* when you open Pi in that folder by hand; it is not reliable
|
|
inside a Paperclip run, because of trust and because worktrees are new paths each time.
|
|
- **If per-agent extensions are genuinely wanted**, the change is in the adapter: pass a
|
|
per-agent config directory or `--no-skills` plus an explicit set, and remove stale links.
|
|
That is a Paperclip feature, not something our terminal can arrange from outside.
|
|
|
|
## History: yes, and it is richer than the run log
|
|
|
|
- `~/.pi/paperclips/*.jsonl` — **178 files** on this machine, one per run, named
|
|
`<timestamp>-<agentId>.jsonl`, and each is the full conversation (session start, model
|
|
changes, messages).
|
|
- `GET /api/agents/{id}/task-sessions` — maps a task to its session file and working directory.
|
|
- `GET /api/heartbeat-runs/{runId}/log` — the run log already shown in `Agents>agents`.
|
|
- `GET /api/agents/{id}/runtime-state` — the agent's current runtime state.
|
|
|
|
A history panel is therefore feasible without new plumbing: list an agent's sessions from
|
|
`task-sessions`, open one, and render the JSONL transcript. That is the natural next panel
|
|
after the functional tests, and it is read-only.
|
|
|
|
---
|
|
|
|
# Correction: per-agent skills are the design, not an accident
|
|
|
|
The section above gives the wrong impression. Here is the accurate version.
|
|
|
|
## Each agent has its own skill set, and that is stored in Paperclip
|
|
|
|
`GET /api/agents/{id}/skills` returns, per agent, `desiredSkills` and the entries actually
|
|
managed for it. `Builder` had its own list; `Documenter` has another. Setting it is one call:
|
|
|
|
```
|
|
POST /api/agents/{id}/skills/sync
|
|
{ "mode": "replace" | "add" | "remove",
|
|
"desiredSkills": ["company/<id>/nixos-workflow", "paperclipai/paperclip/paperclip"] }
|
|
```
|
|
|
|
`replace` means "this agent gets exactly these". At run time the adapter builds a directory
|
|
from **that agent's** desired set and passes it as `--skill`, and symlinks those skills into
|
|
`~/.pi/agent/skills/<runtimeName>` so the run can find them. So: different agents, different
|
|
skills, no problem — this is already the supported path and it needs no picker.
|
|
|
|
The caveat I over-stated is narrow: the symlinks stay in the shared global directory after a
|
|
run, and Paperclip does not pass `--no-skills`, so a skill an agent used once can remain
|
|
discoverable by another agent afterwards. That is a leak to know about (and a good argument
|
|
for `replace` plus a periodic clean), not "one set for everybody".
|
|
|
|
Where skills come from: `GET /api/skills/available`, the company list at
|
|
`GET /api/companies/{id}/skills`, and the catalog at `GET /api/skills/catalog`. A skill there
|
|
can be assigned to any agent with the call above.
|
|
|
|
## Extensions per agent: yes, through the command
|
|
|
|
The adapter's configuration document says the agent's command defaults to `pi` and can be set
|
|
per agent:
|
|
|
|
```
|
|
adapterConfig.command defaults to "pi"
|
|
adapterConfig.env KEY=VALUE pairs
|
|
```
|
|
|
|
So an agent can be given its own wrapper:
|
|
|
|
```
|
|
~/paperclip-agents/builder/pi-wrapper.sh:
|
|
|
|
#!/bin/sh
|
|
exec pi --extension npm:pi-memctx --extension git:https://gitea…/pi-config/extensions/tavily-search/index.ts "$@"
|
|
```
|
|
|
|
and `PATCH /api/agents/{id}` with `{"adapterConfig": {…, "command": "…/pi-wrapper.sh"}}`. The
|
|
adapter appends its own arguments (`--mode json`, `--session`, `--skill`, `--tools`,
|
|
`--model`) to whatever the command is, so the wrapper is additive and cannot break the run.
|
|
`env` is the same lever for environment variables.
|
|
|
|
That is how per-agent extensions become real today: one wrapper per agent role (`builder`,
|
|
`documenter`, `reviewer`), each listing the extensions that role may use. It also gives a
|
|
place to set `--no-skills` if strict isolation is wanted, and a place to keep the tool list
|
|
deliberately small.
|
|
|
|
## History: three options, in order of fidelity
|
|
|
|
1. **Open the real Pi session in a pane.** Paperclip writes every run to
|
|
`~/.pi/paperclips/<timestamp>-<agentId>.jsonl` and starts Pi with `--session <file>`. A
|
|
pane running `pi --session <that file>` gives the actual conversation, with Pi's own
|
|
interface, and can be continued. Highest fidelity, zero new code — a tool-pane entry that
|
|
takes a file path. Two writers on one session file is the risk, so this is for sessions
|
|
that are finished, or for a deliberate hand-over.
|
|
2. **A read-only transcript panel.** Parse the JSONL and draw the messages in `Agents>agents`
|
|
as a fourth tab. Integrated, safe, no second process; less faithful than the real thing.
|
|
3. **`pi --resume`** in a tool pane, and pick the session from Pi's own list. Simplest of all,
|
|
but it makes you find the session by eye.
|
|
|
|
`GET /api/agents/{id}/task-sessions` maps task → session file and working directory, so option
|
|
1 and 2 both have a reliable index and neither needs guessing.
|
|
|
|
---
|
|
|
|
# Confirmed: what the picker emulation needs, and what it can actually do
|
|
|
|
Researched by doing it, on this machine, with the board key. Everything below returned 200 or
|
|
201 unless it says otherwise.
|
|
|
|
## The gap, measured
|
|
|
|
| Set | Where | Count |
|
|
|---|---|---|
|
|
| what the pi-config picker lists | `~/.agents/skills` | **21** (archify, caveman, nixos-workflow, tuios, …) |
|
|
| what Paperclip can assign | `GET /api/companies/{id}/skills` | **9** (agentmail, obsidian-cli, paperclip, project-docs, system-architect, …) |
|
|
|
|
So eleven skills a person can see in the picker cannot be given to a Paperclip agent today,
|
|
because Paperclip has never heard of them. That is the whole problem, and it is smaller than
|
|
it looked.
|
|
|
|
## What works, in order
|
|
|
|
1. **Copy the skill into the company's managed directory.** A local import is refused from
|
|
anywhere else — `POST …/skills/import` with a path under `~/.agents` returned **403
|
|
`skill_workspace_boundary_denied`**, "Import from a configured Paperclip workspace or the
|
|
company managed-skill directory". Copying to
|
|
`~/.paperclip/instances/default/skills/<companyId>/<name>` and importing that path returned
|
|
**201**:
|
|
|
|
```
|
|
{"key": "local/9a9eba3826/nixos-workflow", "slug": "nixos-workflow", …}
|
|
```
|
|
|
|
2. **Assign it to one agent, and only that agent**:
|
|
|
|
```
|
|
POST /api/agents/{agentId}/skills/sync
|
|
{"mode": "add", "desiredSkills": ["local/9a9eba3826/nixos-workflow"]} → 200
|
|
```
|
|
|
|
Verified on `Documenter`: its list went from three skills to four and back to three after a
|
|
`remove`. Different agents, different lists — as expected.
|
|
|
|
3. **Extensions have no Paperclip equivalent.** `~/.agents/extensions` holds them; Paperclip's
|
|
skill model does not. The per-agent lever is `adapterConfig.command`, which defaults to `pi`
|
|
and is appended to by the adapter. A wrapper per agent that passes `--extension …` and then
|
|
`exec pi "$@"` is the equivalent of `pi-config` for a Paperclip run.
|
|
|
|
## So the emulation is this, and nothing more
|
|
|
|
A picker that does what `pi-config`'s does, for Paperclip agents:
|
|
|
|
- **reads** `~/.agents/skills` and `~/.agents/extensions` (the same source the picker uses),
|
|
the company skill list, and the chosen agent's `desiredSkills`;
|
|
- **marks** each entry: available but not in Paperclip · in Paperclip · on this agent;
|
|
- **on choosing** an unavailable skill: copy it into the managed directory, import it, then
|
|
`skills/sync` it onto that agent;
|
|
- **on removing**: `skills/sync` with `remove`;
|
|
- **for an extension**: add it to that agent's wrapper file and point
|
|
`adapterConfig.command` at the wrapper.
|
|
|
|
That is a small command or one panel, not a new subsystem, and every step above is already
|
|
verified.
|
|
|
|
## A change made while checking this
|
|
|
|
`nixos-workflow` was copied into the company's managed directory and imported, so it is now in
|
|
Paperclip's list and can be assigned to any agent. It is assigned to nobody. Nothing else was
|
|
changed; `Documenter`'s skills were put back exactly as they were.
|
|
|
|
## Pi history — the cheap answer is probably enough
|
|
|
|
The instinct is right. Every run is already a file:
|
|
|
|
```
|
|
~/.pi/paperclips/<timestamp>-<agentId>.jsonl 178 of them here
|
|
```
|
|
|
|
`GET /api/agents/{id}/task-sessions` maps a task to its session file and folder. So the
|
|
cheapest useful thing is a tool-pane entry that opens `~/.pi/paperclips` in Yazi, or a pane
|
|
running `pi --resume` that lists sessions and opens one with Pi's own interface. Both are
|
|
minutes of work and neither invents anything.
|
|
|
|
A transcript tab inside `Agents>agents` is the integration option: read the JSONL, draw the
|
|
messages, no second process. Worth doing only if the file browser feels like a detour after a
|
|
week of use. Recommendation: do the cheap one now, decide later.
|
|
|
|
## The project's check: `.harness/check.sh`
|
|
|
|
This is a script in the project folder that exits 0 when the project still passes its own
|
|
tests. It answers "did this change break the project", not "is this task finished".
|
|
|
|
**What decides a task is finished**, in order of authority:
|
|
|
|
1. the agent, by setting the task's status — this is Paperclip's own model and nothing in our
|
|
software can overrule it;
|
|
2. a reviewer, if a review stage is configured on the task (Paperclip's execution policy);
|
|
3. `ocr`, at merge time, which can refuse the change.
|
|
|
|
The project's check is a **regression check** at the same point as a CI pipeline: `bin/merge`
|
|
runs it in the task's worktree before merging, and again on the base branch after merging, and
|
|
rolls the merge back if it fails. It never decides that a task is finished.
|
|
|
|
Why keep it at all: an agent says it is done, a review says the diff looks right, and neither
|
|
proves the build still works. One command does. If a project has no check, `bin/merge` says so
|
|
and the review decides alone — that is the current state of this sandbox.
|
|
|
|
Naming: `check.sh` is the current name. `.harness/verify.sh` was the earlier project's name and
|
|
is still honoured if present, so an adopted folder keeps working. `bin/office` writes
|
|
`check.sh` with a comment explaining what to replace.
|
|
|
|
## Headroom, concretely
|
|
|
|
Headroom is a proxy that accepts the same HTTP API as the model router and shortens long
|
|
prompts before forwarding. Measured here:
|
|
|
|
```
|
|
GET http://192.168.20.13:8787/v1/models → 200, 486 models (auto/best-coding, deepseek/…)
|
|
GET http://192.168.20.13:8787/health → 200, healthy, compression on, memory off
|
|
upstream (config `openai_api_url`) → http://192.168.20.13:20129/v1
|
|
```
|
|
|
|
The agents' provider `omni` already points at `http://192.168.20.13:20129/v1`, which is the
|
|
same upstream headroom forwards to. So headroom can sit in front of it.
|
|
|
|
**How to use it for one agent.** Two steps, and only the second is per agent:
|
|
|
|
1. Add a provider to `~/.pi/agent/settings.json` (global, once):
|
|
|
|
```json
|
|
"headroom": {
|
|
"baseUrl": "http://192.168.20.13:8787/v1",
|
|
"apiKey": "<the same key the omni provider uses>",
|
|
"api": "openai-completions"
|
|
}
|
|
```
|
|
|
|
2. Set that agent's model in `Agents>settings` to `headroom/auto/best-coding` (or any model id
|
|
from the list above). Paperclip stores the model per agent, so exactly one agent changes.
|
|
|
|
**Would it work.** The endpoint answers the OpenAI-style API that Pi's `openai-completions`
|
|
provider speaks, and lists the models Pi would ask for, so the pieces line up. It is still a
|
|
hypothesis until one run is made through it: the unknowns are whether the proxy passes Pi's
|
|
tool-calling requests unchanged, and what compression does to a prompt with large tool results.
|
|
The measurement is one run: compare cost, duration and result quality for the same task with and
|
|
without the provider.
|
|
|
|
**Per project is not possible.** The provider is chosen by the agent's model, and agents belong
|
|
to the company, not to a project. The finest granularity is one agent, which is what the
|
|
`Agents>settings` model field already gives.
|
|
|
|
### Measured: what actually fails, and why
|
|
|
|
The earlier claim that "headroom needs an Anthropic key" was wrong. Here is what the tests show.
|
|
|
|
Headroom runs as a container from `ghcr.io/chopratejas/headroom:latest` with
|
|
`--port 8787 --no-telemetry`, and it is built on **LiteLLM**. Its `/v1/models` lists the same 486
|
|
models as the router, and `/v1/compress` works. What fails is a completion through it:
|
|
|
|
```
|
|
router POST /v1/chat/completions deepseek/deepseek-v4-flash → 200, SSE stream, "ok" in 1.1 s
|
|
headroom POST /v1/chat/completions deepseek/deepseek-v4-flash → 502 {"code":"proxy_error"}
|
|
```
|
|
|
|
`docker logs headroom` shows LiteLLM repeating *"Provider List: https://docs.litellm.ai/docs/providers"*
|
|
— LiteLLM does not recognise our router's model ids (`deepseek/deepseek-v4-flash`,
|
|
`auto/best-fast`), so it never makes the call. The same body sent to the router directly succeeds,
|
|
so the key, the model and the request shape are all fine.
|
|
|
|
**So headroom is not blocked by Anthropic, and not by a key.** It is blocked by LiteLLM not
|
|
knowing the router's models. To use it as a provider, headroom's own model list has to define
|
|
them, for example `openai/deepseek-v4-flash` with `api_base: http://192.168.20.13:20129/v1` and
|
|
our key — a change to headroom's configuration inside that container, not to our software.
|
|
|
|
### The path that does work: headroom as a tool
|
|
|
|
The `headroom-bridge` extension in `~/.agents/extensions` does something different from a
|
|
provider: it gives a Pi agent two tools, `compress_for_agent` and `check_headroom`. The first
|
|
calls headroom's compression endpoint; the second reports whether the service is up. That is the
|
|
integration that fits: headroom shortens text an agent is about to pass on, rather than standing
|
|
between the agent and its model.
|
|
|
|
Measured directly, without the extension:
|
|
|
|
```
|
|
POST http://192.168.20.13:8787/v1/compress {"model": …, "messages":[…]} → 200
|
|
{tokens_before: 4009, tokens_after: 4009, tokens_saved: 0, compression_ratio: 1.0,
|
|
transforms_applied: ["router:protected:user_message"]}
|
|
```
|
|
|
|
So the endpoint works and reports its effect honestly; a single long user message is protected
|
|
from compression, which is why it saved nothing in that test. To give one agent the tool:
|
|
|
|
1. `~/.agents/extensions/headroom-bridge` needs the `headroom-ai` npm package resolvable by Pi;
|
|
it is **not** installed in `~/.pi/agent/npm`, so the extension will fail to import until it is.
|
|
2. Give the extension to one agent with the extensions picker in `Agents>agents` — `space` writes a
|
|
wrapper and points that agent's command at it.
|
|
3. Then that agent, and no other, has the compression tool.
|
|
|
|
That is the honest position: compression is available as a tool, blocked on one npm package, and
|
|
it is per agent because the wrapper is per agent.
|
|
|
|
## Mouse-1, measured
|
|
|
|
The person supplied `docs/MOUSE-1-NOTES.md`, written from measurements on 2026-11-08/09. The
|
|
facts that shape the design:
|
|
|
|
- `POST https://api.mouse.dev/v1/decisions`, `Authorization: Bearer $MOUSE_API_KEY`, body
|
|
`{model, state, questions:{id:{type:"choice", instructions, criteria}}}`; the answer returns
|
|
`choice`, `confidence`, `probabilities`.
|
|
- **Cold start is minutes.** The first call after idle returned `503 engine_timeout` after 30 s;
|
|
the retry returned 200 in 13 s; warm calls were 1044 ms then ~489 ms.
|
|
- The same state and options gave the same choice at ~0.98 confidence. It behaves as a
|
|
classifier, not a writer.
|
|
- Proven useful: **failure routing** (redo / stronger model / environment / person), unblock
|
|
triage, intake clarity, task sizing. Measured in the old project as `bin/route-failure`.
|
|
- Ruled out: the merge decision, judging test results, anything on a keystroke path, vague
|
|
options, and being the only record of a decision.
|
|
|
|
Where it would go in this interface, if added: `Agents>agents` → a failure on a run offers
|
|
`f`, which asks Mouse-1 one question with four options and writes the recommendation, the
|
|
confidence and the options as a comment on the task. A person still decides. Expected cost of
|
|
the first call after idle is 30 seconds, so `f` must show "asking, this can take half a minute"
|
|
rather than appear to hang.
|
|
|
|
## Mouse-1, corrected
|
|
|
|
Earlier notes in this file said "software is not installed". That was wrong: Mouse-1 is a
|
|
**hosted API**, not a program. What exists here is `MOUSE_API_KEY`; what is missing is a client
|
|
integration — an HTTP call like any other. No install is needed, and nothing has to run locally.
|
|
The old project's policy still applies: triage only, never a gate, never a permit.
|