Files
paperclip-harness/.pi/memory-vault/packs/paperclip-harness/70-runbooks/tmp-deployment-operations.md

102 KiB
Raw Blame History

type, id, title, status, source_of_truth, freshness, last_reviewed, tags
type id title status source_of_truth freshness last_reviewed tags
runbook runbook.paperclip-harness.tmp-deployment-operations tmp deployment operations active false current 2026-11-10
pack/paperclip-harness
agent-memory/runbook
autolearn
rich-discovery
deployment-runbook

tmp deployment operations

Deployment/runbook details discovered (2026-11-10 07:35:38)

Both bugs fixed, and here are direct answers.

Fixed

Repair>repair — the icon sample row is gone.

ocr closed instantly. Two causes, both real: a tool pane is started with a smaller PATH than your shell, so ocr was "command not found", and even when it ran, a review with nothing to look at exits in a second. Tool panes now get ~/.local/bin, ~/.nix-profile/bin, the system profile and your per-user profile on PATH, and the ocr entry ends with a "press enter to close" line. It now runs and stays:

[ocr] 1 file(s) changed, reviewing 1 in /tmp/pc-sandbox-2026-11-10
[ocr] ▶ code_search search_text=harness …

The chooser also gained g and G, which is what led me to open a second shell pane while testing.

Where settings are

Agents>agents, fourth tab, settings. Role, job title, model, monthly budget, heartbeat. enter edits a field in place, esc cancels, p/P/w pause, resume, wake. Each save is one PATCH merged into that agent's existing config, so changing the model does not wipe its folder or instructions — I verified that by reading the record back.

Spend is shown, not set, in Project>costs, Board>board and Work>work. The budget lives on the agent, so it is edited per agent on that settings tab. There is no company-level spend control on any screen yet.

Your questions, answered plainly

Skills. Not in the interface. Paperclip has GET /api/agents/{id}/skills and POST …/skills/sync with add, remove, replace — so a read-only skills row is cheap, a full editor is a later step. Your rule (sub-agents get only what their job needs) maps exactly onto replace.

Where to add work. New task>new. And my answer on Tuxedo is no for tasks: a second place that writes tasks is a second owner for the same fact, which is the fault that sank the last attempt. If you want notes, put them in the project's own .harness/NOTES.md and let Tuxedo read it per project — notes are not tasks, and nothing else should claim they are.

office --host from another machine. Not implemented, and it should not be faked. Sessions are created by you: tuios new --host nixos-desktop "new test project", then bin/office <folder> inside it on the machine that owns the folder. Driving .27 from .13 needs ssh plus a remote bin/board — a deliberate step, not an accident.

Switching projects. Not built. Proposal: a small local registry of adopted projects (id, folder, host, session) filled by bin/office, and a Project>projects tab that lists them and switches by rewriting the current folder's .harness/harness.toml. A project on another machine shows as on .27 rather than pretending it can be opened from here.

Artifacts are Paperclip work products attached to a task — an uploaded file, a pull request, a preview URL, a commit, a branch. Nothing shows them yet; their home is a Work>artifacts tab.

"Workspaces" in Paperclip do not exist. Paperclip has companies → projects → tasks → agents → runs. What Paperclip calls a workspace is a project's code folder, the git worktree a task runs in. Our workspace is TUIOS's word for a page.

Charts. Use Go, not TML: the panels are already Go and the data is in memory. tuios.dev/plot gives a spend-by-day chart in Project>costs for a few dozen lines.

Mouse-1/Switch are still not installed on this machine.

Functional tests — the list I propose

  1. Create a task in New task>new with a blocker and a verify command; see it in Board>board.
  2. Assign it in Board>board, then watch it in Agents>agents — roster, then runs, then its log.
  3. Let it finish; read the diff in Work>work, the verdict in Work>review.
  4. Merge with bin/merge, confirm the verify command passes on main.
  5. Open the blocker's dependent task and confirm it can now proceed.
  6. R from Repair>repair and confirm the eight workspaces come back.
  7. Set an agent's model in Agents>agents and confirm it applies to the next run only.

Committed as 7b12b45. Say go and I run that list end to end and report each result.

Source paths observed

  • paseo/projects/new_test_projects
  • tuios.dev/docs/libraries/plot
  • home/sam/paseo/projects/paperclip-harness/bin/harness
  • run/current-system/sw/bin/git
  • home/sam/.nix-profile/bin/go
  • home/sam/.local/bin/ocr
  • etc/profiles/per-user/sam/bin/tuios
  • run/current-system/sw/bin/lazygit
  • etc/profiles/per-user/sam/bin/hunk
  • tmp/pc-sandbox-2026-11-10/.gitignore

Evidence

  • Confidence: 83%
  • Reason: Extracted repeatable deploy/configuration procedure from rich discovery

Update (2026-11-10 10:01:16)

Trigger: bringing an existing git repo or folder into the TUIOS harness.

Procedure (office <dir> [--session <name>] [--dry]):

  1. Resolve host: read/write <dir>/.harness/host; mkdir <dir>/.harness if missing (dry-run prints planned actions and exits).
  2. Detect whether project is 'adopting (writing what is missing)' vs 'already a harness project'.
  3. Ensure TUIOS session exists: timeout 20 tuios list-sessions (strip ANSI, parse column 2) must contain the session. If absent, office prints tuios new <session> and exits 2 — TUIOS cannot create sessions non-interactively in this build ('Standard output is not a terminal').
  4. Run board with HARNESS_REPO and HARNESS_SESSION set to build/repair pages.
  5. Print attach commands: tuios-gpui attach <session>, tuios attach <session>, remote tuios-gpui attach --host <hostname> <session>.

Known gap: old office also ran new-project to write scaffold (settings.toml, one file per role, verify.sh, per-screen notes) only when missing; current office skips this. Safe: office/new-project never overwrite existing files. Troubleshooting: 'office: new-project failed' or 'office: board failed' indicate the respective step; re-run is idempotent.

Detailed procedure from completed turn

Explored all of it. Recorded in docs/PLAN-INTERFACE.md; the short version:

Skills per agent — Paperclip already models it

GET /api/agents/{id}/skills returns a desired set, a managed set and a state per skill. On Builder today:

desired: ["paperclipai/paperclip/paperclip"]
company/<id>/obsidian-cli   desired: false   managed: true   state: "stale"

POST /api/agents/{id}/skills/sync takes add, remove or replace — replace is exactly "this agent gets these and nothing else", which is how a sub-agent's context stays small. There is also a company skill catalog, versions, audit, and GET/PUT /api/companies/{id}/skill-policy with an evaluate call.

Proposal: two rows in Agents>agents settings — skills (the desired set, read-only first, add/remove later) and skill source. A full browser is a later screen, not a panel. Also: obsidian-cli sitting stale on Builder is worth fixing.

Memory — three different things

state here what it is
engram installed, v1.16.1, MCP mode, not registered in ~/.pi/agent/mcp.json (only codebase-memory, graphify-vault) persistent memory
headroom running, v0.24.0, healthy at 192.168.20.13:8787, memory backend disabled, compression on a prompt-compression proxy, not a store
pi memctx in use now injects notes into a prompt

They are complementary. Paperclip's only per-agent lever is adapterConfig and runtimeConfig, so memory is set per agent by pointing that agent at the proxy or store it should use. Proposal: a memory row in Agents>agents settings showing which of the three that agent uses, and nothing more until one agent has run both ways and been measured.

MOUSE-1

MOUSE_API_KEY is set (checked the name only, never the value). JEV_API_KEY too. No mouse, mouse1, jev or switch binary is installed.

There are notes, and they are good ones — in the old project's plan documents:

MOUSE-1 is "triage only: label, score, route, escalate. Never a gate, never a permit" — explicitly ruled out for deciding a merge or allowing a tool call, because a cheap classifier that fails open on a network error makes a green run a lie.

I would adopt that policy unchanged. Its place, if added: ordering Board>board's needs-you list, or pre-sorting findings in Work>review. Never the merge, never an approval.

Lazygit

Already wired: t in Board>board or Work>work, choose Lazygit. Verified opening in the right workspace and closing with q.

The old office — and what ours lacks

The previous office did four things: remember the host; run new-project to write the scaffold (settings.toml, a file per role, verify.sh, the per-screen notes) writing only what is missing; refuse if the session does not exist and print the exact tuios new line; then build the pages.

Ours does the host, the refusal and the pages — but no scaffold. No per-role files, no verify.sh, no page notes. That is the cheapest real gap in the whole plan.

Dashboard — the API answers it in one call

GET /api/companies/{id}/dashboard, 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, utilization 0}
runActivity [14 entries]

Proposal: a Project>overview panel with those blocks and a sparkline drawn in Go with tuios.dev/plot, rather than embedding the web dashboard in a pane.

Not doing

Artifacts — dropped as instructed.

I am ready for the task you are about to deliver; nothing in the tree is half-changed, the eight workspaces are running, and the last commit is 3d31243.

Evidence

  • Confidence: 85%
  • Reason: Repeatable procedure with prerequisites, exit codes, safe behavior, and the known-scaffold caveat.; enriched with final-answer detail because persistence should be reusable

Update (2026-11-10 10:01:16)

Trigger: desired skill set for an agent changed, or a skill shows stale.

  1. Inspect: GET /api/agents/{id}/skills — read desiredSkills, desiredSkillEntries, and entries[] (desired, managed, state, runtimeName, sourcePath, targetPath).
  2. Decide mode: POST /api/agents/{id}/skills/sync with add / remove, or replace for an exclusive set (recommended for sub-agents to keep context small).
  3. For company-level operations use /api/companies/{companyId}/skills/... (import, install-catalog, scan-projects, versions, audit, fork, install-update). Policy checks: GET/PUT /api/companies/{companyId}/skill-policy and POST .../skill-policy/evaluate.
  4. Verify after sync that entries show desired=true and state not stale.

Caveats: managed skills materialize from /home/sam/.paperclip/instances/default/skills// into /home/sam/.pi/agent/skills/; a stale managed skill (e.g. obsidian-cli on Builder) needs reset/install-update, not just add. Avoid replace on the top-level orchestrator agent, which intentionally carries the paperclipai/paperclip/paperclip skill.

Detailed procedure from completed turn

Explored all of it. Recorded in docs/PLAN-INTERFACE.md; the short version:

Skills per agent — Paperclip already models it

GET /api/agents/{id}/skills returns a desired set, a managed set and a state per skill. On Builder today:

desired: ["paperclipai/paperclip/paperclip"]
company/<id>/obsidian-cli   desired: false   managed: true   state: "stale"

POST /api/agents/{id}/skills/sync takes add, remove or replace — replace is exactly "this agent gets these and nothing else", which is how a sub-agent's context stays small. There is also a company skill catalog, versions, audit, and GET/PUT /api/companies/{id}/skill-policy with an evaluate call.

Proposal: two rows in Agents>agents settings — skills (the desired set, read-only first, add/remove later) and skill source. A full browser is a later screen, not a panel. Also: obsidian-cli sitting stale on Builder is worth fixing.

Memory — three different things

state here what it is
engram installed, v1.16.1, MCP mode, not registered in ~/.pi/agent/mcp.json (only codebase-memory, graphify-vault) persistent memory
headroom running, v0.24.0, healthy at 192.168.20.13:8787, memory backend disabled, compression on a prompt-compression proxy, not a store
pi memctx in use now injects notes into a prompt

They are complementary. Paperclip's only per-agent lever is adapterConfig and runtimeConfig, so memory is set per agent by pointing that agent at the proxy or store it should use. Proposal: a memory row in Agents>agents settings showing which of the three that agent uses, and nothing more until one agent has run both ways and been measured.

MOUSE-1

MOUSE_API_KEY is set (checked the name only, never the value). JEV_API_KEY too. No mouse, mouse1, jev or switch binary is installed.

There are notes, and they are good ones — in the old project's plan documents:

MOUSE-1 is "triage only: label, score, route, escalate. Never a gate, never a permit" — explicitly ruled out for deciding a merge or allowing a tool call, because a cheap classifier that fails open on a network error makes a green run a lie.

I would adopt that policy unchanged. Its place, if added: ordering Board>board's needs-you list, or pre-sorting findings in Work>review. Never the merge, never an approval.

Lazygit

Already wired: t in Board>board or Work>work, choose Lazygit. Verified opening in the right workspace and closing with q.

The old office — and what ours lacks

The previous office did four things: remember the host; run new-project to write the scaffold (settings.toml, a file per role, verify.sh, the per-screen notes) writing only what is missing; refuse if the session does not exist and print the exact tuios new line; then build the pages.

Ours does the host, the refusal and the pages — but no scaffold. No per-role files, no verify.sh, no page notes. That is the cheapest real gap in the whole plan.

Dashboard — the API answers it in one call

GET /api/companies/{id}/dashboard, 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, utilization 0}
runActivity [14 entries]

Proposal: a Project>overview panel with those blocks and a sparkline drawn in Go with tuios.dev/plot, rather than embedding the web dashboard in a pane.

Not doing

Artifacts — dropped as instructed.

I am ready for the task you are about to deliver; nothing in the tree is half-changed, the eight workspaces are running, and the last commit is 3d31243.

Evidence

  • Confidence: 80%
  • Reason: Actionable procedure derived from concrete API behavior and observed stale state.; enriched with final-answer detail because persistence should be reusable

Update (2026-11-10 10:01:16)

Deployment/runbook details discovered (2026-11-10 10:01:16)

Explored all of it. Recorded in docs/PLAN-INTERFACE.md; the short version:

Skills per agent — Paperclip already models it

GET /api/agents/{id}/skills returns a desired set, a managed set and a state per skill. On Builder today:

desired: ["paperclipai/paperclip/paperclip"]
company/<id>/obsidian-cli   desired: false   managed: true   state: "stale"

POST /api/agents/{id}/skills/sync takes add, remove or replace — replace is exactly "this agent gets these and nothing else", which is how a sub-agent's context stays small. There is also a company skill catalog, versions, audit, and GET/PUT /api/companies/{id}/skill-policy with an evaluate call.

Proposal: two rows in Agents>agents settings — skills (the desired set, read-only first, add/remove later) and skill source. A full browser is a later screen, not a panel. Also: obsidian-cli sitting stale on Builder is worth fixing.

Memory — three different things

state here what it is
engram installed, v1.16.1, MCP mode, not registered in ~/.pi/agent/mcp.json (only codebase-memory, graphify-vault) persistent memory
headroom running, v0.24.0, healthy at 192.168.20.13:8787, memory backend disabled, compression on a prompt-compression proxy, not a store
pi memctx in use now injects notes into a prompt

They are complementary. Paperclip's only per-agent lever is adapterConfig and runtimeConfig, so memory is set per agent by pointing that agent at the proxy or store it should use. Proposal: a memory row in Agents>agents settings showing which of the three that agent uses, and nothing more until one agent has run both ways and been measured.

MOUSE-1

MOUSE_API_KEY is set (checked the name only, never the value). JEV_API_KEY too. No mouse, mouse1, jev or switch binary is installed.

There are notes, and they are good ones — in the old project's plan documents:

MOUSE-1 is "triage only: label, score, route, escalate. Never a gate, never a permit" — explicitly ruled out for deciding a merge or allowing a tool call, because a cheap classifier that fails open on a network error makes a green run a lie.

I would adopt that policy unchanged. Its place, if added: ordering Board>board's needs-you list, or pre-sorting findings in Work>review. Never the merge, never an approval.

Lazygit

Already wired: t in Board>board or Work>work, choose Lazygit. Verified opening in the right workspace and closing with q.

The old office — and what ours lacks

The previous office did four things: remember the host; run new-project to write the scaffold (settings.toml, a file per role, verify.sh, the per-screen notes) writing only what is missing; refuse if the session does not exist and print the exact tuios new line; then build the pages.

Ours does the host, the refusal and the pages — but no scaffold. No per-role files, no verify.sh, no page notes. That is the cheapest real gap in the whole plan.

Dashboard — the API answers it in one call

GET /api/companies/{id}/dashboard, 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, utilization 0}
runActivity [14 entries]

Proposal: a Project>overview panel with those blocks and a sparkline drawn in Go with tuios.dev/plot, rather than embedding the web dashboard in a pane.

Not doing

Artifacts — dropped as instructed.

I am ready for the task you are about to deliver; nothing in the tree is half-changed, the eight workspaces are running, and the last commit is 3d31243.

Source paths observed

  • home/sam/.local/bin/engram
  • home/sam/.config/environment.d/10-secrets.conf
  • home/sam/paseo/projects/paperclip-harness/.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover.md
  • home/sam/paseo/projects/paperclip-harness/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-10-07-35-38.md
  • home/sam/paseo/projects/paperclip-harness/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48.md
  • home/sam/paseo/projects/paperclip-harness/.pi/memory-vault/packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups.md
  • home/sam/paseo/projects/paperclip-harness/.pi/memory-vault/packs/paperclip-harness/20-context/home.md
  • github.com/Gentleman-Programming/engram/cmd/engram
  • api/skills/available
  • api/skills/catalog
  • api/skills/index
  • paperclipai/paperclip/paperclip
  • company/d4921013-e5da-4d88-8016-57d5746e2d03/obsidian-cli
  • home/sam/.paperclip/instances/default/skills/d4921013-e5da-4d88-8016-57d5746e2d03/obsidian-cli
  • home/sam/.pi/agent/skills/obsidian-
  • dir/.harness/host
  • home/sam/paseo/projects/tuios_coding_harness/plan/82-control-room.md
  • home/sam/paseo/projects/tuios_coding_harness/plan/83-interface.md
  • home/sam/paseo/projects/tuios_coding_harness/plan/80-forked-plan.md
  • home/sam/paseo/projects/tuios_coding_harness/plan/91-paperclip-or-oma.md
  • home/sam/paseo/projects/tuios_coding_harness/plan/50-open-work.md
  • home/sam/.pi/agent/git/gitea.lab.audasmedia.com.au/sam/pi-config/skills/graphify/SKILL.md
  • home/sam/.pi/agent/git/gitea.lab.audasmedia.com.au/sam/pi-config/skills/system-architect/SKILL.md
  • home/sam/.pi/agent/git/gitea.lab.audasmedia.com.au/sam/pi-config/extensions/pi-graphify/skills/graphify/SKILL.md
  • home/sam/.pi/agent/git/gitea.lab.audasmedia.com.au/sam/pi-config/node_modules
  • google/genai/dist/node/index.cjs
  • google/genai/dist/node/node.d.ts
  • home/sam/.config/systemd/user/lan-mouse.service
  • home/sam/.config/lan-mouse
  • home/sam/.config/lan-mouse/lan-mouse.pem
  • home/sam/.cache/lan-mouse
  • home/sam/.local/bin/lan-mouse-011
  • home/sam/.pi/agent/auth.json
  • home/sam/.pi/agent/mcp-cache.json
  • home/sam/.pi/agent/mcp.json
  • home/sam/.pi/agent/models.json
  • home/sam/.pi/agent/models-store.json
  • home/sam/.pi/agent/settings.json
  • home/sam/.pi/agent/trust.json
  • home/sam/.local/bin/codebase-memory-mcp

Evidence

  • Confidence: 83%
  • Reason: Extracted repeatable deploy/configuration procedure from rich discovery

Update (2026-11-10 21:03:21)

Deployment/runbook details discovered (2026-11-10 21:03:21)

Advice only — nothing built. Written into docs/PLAN-INTERFACE.md as well.

Your picker, and what it actually writes

pi-config registers five commands — /config-add, /config-remove, /config-list, /config-show, /config-setup — and 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.

What the commands change is plain JSON:

<project>/.pi/settings.json      project settings ("-l" mode)
~/.pi/agent/settings.json        global settings

in the shape:

{"packages": [
  "npm:pi-memctx",
  {"source": "git:https://gitea…/pi-config", "extensions": ["extensions/pi-config/index.ts", "extensions/tavily-search/index.ts"]},
  {"source": "/home/sam/.agents", "skills": ["nixos-workflow"], "extensions": []}
]}

So yes: that JSON is the interface, and anything that can write a file can compose an agent's context exactly as the picker does. The real question is which of those files Pi actually reads on a Paperclip run.

What Paperclip actually runs

From the installed @paperclipai/adapter-pi-local:

pi --mode json --append-system-prompt <rendered>
   --provider <p> --model <m> --thinking <t>
   --tools read,bash,edit,write,grep,find,ls
   --session ~/.pi/paperclips/<timestamp>-<agentId>.jsonl
   --skill <a directory built from that agent's desired skills>

Four consequences:

  1. Skills are already per agent — desiredSkills and POST /api/agents/{id}/skills/sync. That is the supported lever, and the --skill directory is built from it.
  2. The isolation leaks. Injected skills are symlinked into the shared ~/.pi/agent/skills and are not removed when another agent runs, and --no-skills is not passed. A skill one agent had can remain visible to another.
  3. The tool list is fixed at read,bash,edit,write,grep,find,ls. An agent cannot gain a tool by being handed an extension.
  4. Project-local .pi/settings.json is unlikely to be honoured for Paperclip runs. Pi loads project settings only from a trusted path, and ~/.pi/agent/trust.json has 14 entries — none is a Paperclip worktree or ~/paperclip-agents. And a worktree path changes every task (…/worktrees/AUD-165-…), so there is nothing stable to trust.

That is the direct answer to your "if it uses /.pi in each folder it might be possible" — it is possible for you at a terminal, and not reliable for a Paperclip-spawned Pi, for two independent reasons.

So, honestly

  • Identity, instructions, model, budget, skills: Paperclip. They are already per agent, and the instructions bundle at ~/.paperclip/instances/default/companies/<co>/agents/<id>/instructions/AGENTS.md is your real per-agent context control.
  • Extensions: one global set. There is no per-agent extension mechanism in this adapter. Pretending otherwise gives you agents whose behaviour depends on which one ran last.
  • Folder-level .pi/settings.json: useful for you, not for the factory. Keep it for when you open Pi in a folder by hand.
  • If per-agent extensions are genuinely wanted, the change belongs in the adapter — a per-agent config directory, plus --no-skills with an explicit set and stale links removed. That is a Paperclip feature request, not something a terminal can arrange from outside.

Seeing the agents does give you their history

  • ~/.pi/paperclips/*.jsonl — 178 session files here, one per run, named <timestamp>-<agentId>.jsonl, each the full conversation.
  • GET /api/agents/{id}/task-sessions maps a task to its session file and working directory.
  • The run log is already visible in Agents>agents; runtime-state gives current state.

So an Agents>history panel is feasible with no new plumbing: list an agent's sessions, open one, render the transcript, link back to the task. Read-only, and the natural next panel after your tests.

Committed as 77014d0 — documentation only, nothing in the running layout changed.

Source paths observed

  • etc/profiles/per-user/sam/bin/pi
  • nix/store/rg248h9sz8dylm8p6a9w9fj4zrv4sgm5-pi-coding-agent-0.82.1/bin/pi
  • nix/store/rg248h9sz8dylm8p6a9w9fj4zrv4sgm5-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/compaction.md
  • .pi/agent/settings.json
  • nix/store/rg248h9sz8dylm8p6a9w9fj4zrv4sgm5-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/packages.md
  • nix/store/rg248h9sz8dylm8p6a9w9fj4zrv4sgm5-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/sdk.md
  • github.com/user/repo
  • .pi/agent/skills
  • home/sam/.local/share/tuios/worktrees/office-test-1/card-t1-build-skeleton-and-13ob
  • home/sam/.local/share/tuios/worktrees/office-test-1/card-t2-recipe-content-3yto
  • home/sam/ai_chat/general_chat/chat_chat
  • home/sam/ai_chat/sys_config/devops
  • home/sam/ai_chat/sys_config/neovim_config
  • home/sam/ai_chat/sys_config/nixos_config
  • home/sam/paseo/projects/central_ai_llm_master
  • home/sam/paseo/projects/drone_fire_fighting
  • home/sam/.pi/paperclips/2026-10-16T01-58-49-725Z-403de1f7-645c-4d35-bc7e-19b01d77bacc.jsonl
  • home/sam/paseo/projects/artemis_control
  • tmp/pc-sandbox-2026-11-10/.paperclip/worktrees/AUD-166-add-a-changelog-entry-for-the-harness
  • home/sam/.pi/paperclips/2026-11-10T04-55-52
  • extensions/pi-config/index.ts
  • extensions/tavily-search/index.ts
  • home/sam/.agents
  • .pi/agent/trust.json
  • .paperclip/instances/default/companies

Evidence

  • Confidence: 83%
  • Reason: Extracted repeatable deploy/configuration procedure from rich discovery

Update (2026-11-10 21:14:03)

Deployment/runbook details discovered (2026-11-10 21:14:03)

You are right, and my explanation was wrong in a way that matters. Correcting it.

Per-agent skills already exist — that is the design

GET /api/agents/{id}/skills returns a separate desiredSkills list for every agent. Setting it is one call:

POST /api/agents/{id}/skills/sync
{ "mode": "replace",
  "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 list and passes it as --skill. Builder had its own list; Documenter has another. Different agents, different skills, no picker needed.

What I got wrong: I let a narrow caveat ("links persist in the shared directory, so a skill can linger") stand in for the whole model, which read as "one set for everybody". It is not. The list is per agent and it is stored in Paperclip.

Skills are drawn from GET /api/skills/available, the company list, and the catalog — any of which can be assigned to any agent with the call above.

Per-agent extensions: yes, through the command

The adapter's own configuration document says the command defaults to pi and is settable per agent, and so is env:

adapterConfig.command   defaults to "pi"
adapterConfig.env       KEY=VALUE pairs

So an agent gets its own wrapper:

#!/bin/sh
exec pi --extension npm:pi-memctx \
        --extension git:https://gitea…/pi-config/extensions/tavily-search/index.ts \
        "$@"

and PATCH /api/agents/{id} sets adapterConfig.command to that file. 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.

That gives you, today and without touching Paperclip's source:

  • one wrapper per role — builder, documenter, reviewer — each with its own extension list;
  • a place to pass --no-skills if you ever want strict isolation;
  • a place to keep the tool list deliberately small.

So the honest summary is: skills are per agent in Paperclip already; extensions are per agent through the agent's command. My "one global set" sentence was simply false.

History — three options, best first

  1. Open the real Pi session in a pane. Every run is written to ~/.pi/paperclips/<timestamp>-<agentId>.jsonl and Pi is started with --session <file>. A pane running pi --session <that file> shows the actual conversation with Pi's own interface, and can be continued. No new code — a tool-pane entry that takes a file path. The only risk is two writers on one session file, so use it on finished runs or as a deliberate hand-over.
  2. A read-only transcript tab in Agents>agents, drawn from the same JSONL. Integrated and safe, less faithful.
  3. pi --resume in a tool pane and pick the session from Pi's own list. Simplest, but you find it by eye.

GET /api/agents/{id}/task-sessions maps task → session file and working directory, so options 1 and 2 have a reliable index.

Written into docs/PLAN-INTERFACE.md as a correction; committed as 47aacfd. Nothing in the running layout changed.

If you want, the next build can be small and specific: an Agents>skills tab that lists an agent's skills and adds or removes them with those two calls, and a t tool-pane entry that opens a chosen Pi session. Say which.

Source paths observed

  • nix/store/rg248h9sz8dylm8p6a9w9fj4zrv4sgm5-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/usage.md
  • paperclipai/paperclip/paperclip
  • api/skills/available
  • pi-config/extensions/tavily-search/index.ts

Evidence

  • Confidence: 83%
  • Reason: Extracted repeatable deploy/configuration procedure from rich discovery

Update (2026-11-10 21:58:38)

Trigger: user wants a skill listed in the local pi-config picker (~/.agents/skills) to be usable by/assigned to a Paperclip agent.

Prerequisites: companyId known; skill present under ~/.agents/skills/<skill>; API auth as used by the harness bash tool.

Steps:

  1. Copy the skill into the company managed-skill directory: ~/.paperclip/instances/default/skills/<companyId>/<skill>. Direct import of the ~/.agents path is blocked.
  2. Import: POST /api/companies/{companyId}/skills/import body {"source":"<path under managed dir>"} → expect 201 with a key like local/9a9eba3826/<skill>.
  3. Assign per agent: POST /api/agents/{agentId}/skills/sync body {"mode":"add","desiredSkills":["<key>"]} → 200. Use "mode":"remove" to unassign.
  4. Verify by re-reading the agent's skills; count should change by exactly the delta.

Dangerous/pitfalls:

  • Do NOT attempt POST /skills/import with source under ~/.agents; it is rejected (403 skill_workspace_boundary_denied).
  • POST /api/companies/{companyId}/skills create accepts only minimal name required; prefer import for existing skills.

Rollback: sync {"mode":"remove","desiredSkills":["<key>"]} on the agent; the imported company skill remains (import only, harmless if unassigned).

Extension equivalent: no assignments exist for extensions; create a per-agent wrapper that passes --extension <path> then exec pi "$@" and point adapterConfig.command at it.

Evidence

  • Confidence: 90%
  • Reason: Repeatable procedure with a proven workaround and rollback, useful for future skill management tasks.

Update (2026-11-10 21:58:38)

Deployment/runbook details discovered (2026-11-10 21:58:38)

Yes — I now have your question right, and I confirmed it by doing it rather than reading about it.

What you are asking for

A picker like pi-config's, but aimed at Paperclip agents: read your global skill and extension set, show what each agent has, and apply changes before the run — because Paperclip builds the Pi session folder at run time, so the TypeScript picker cannot reach in beforehand.

That is exactly right, and it is feasible. Here is the measured path.

The gap, measured

Set Where Count
what the pi-config picker lists ~/.agents/skills 21
what Paperclip can assign GET /api/companies/{id}/skills 9

Eleven skills you can see in the picker — nixos-workflow, caveman, tuios, archify, ste-writing and the rest — could not be given to a Paperclip agent, because Paperclip had never heard of them. That is the whole problem.

What works, proven in this session

  1. Local import has one door. POST /api/companies/{id}/skills/import with a path under ~/.agents returns 403 skill_workspace_boundary_denied — "Import from a configured Paperclip workspace or the company managed-skill directory". Copying the skill into ~/.paperclip/instances/default/skills/<companyId>/nixos-workflow and importing that path returned 201, with key local/9a9eba3826/nixos-workflow.

  2. Assignment is per agent, and it worked. On Documenter:

POST /api/agents/{id}/skills/sync
{"mode":"add","desiredSkills":["local/9a9eba3826/nixos-workflow"]}   → 200
desiredSkills: 3 → 4   … then remove → back to 3

So the per-agent list is real and I have driven it. My earlier claim that there was one set for everyone was wrong, and this is the proof.

  1. Extensions are the one genuine gap. ~/.agents/extensions has no Paperclip counterpart. The per-agent lever is adapterConfig.command — it defaults to pi and the adapter appends its own arguments — so a wrapper per agent that passes --extension … then exec pi "$@" is the equivalent of pi-config for Paperclip runs.

The picker, precisely

Read ~/.agents/skills and ~/.agents/extensions, plus the company list and the agent's desiredSkills. Mark each: available but not in Paperclip · in Paperclip · on this agent. Choosing an unavailable skill copies it into the managed directory, imports it, then syncs it onto the agent. Removing is a sync with remove. For an extension, add it to that agent's wrapper and point adapterConfig.command at it.

Every step of that is verified above. It is one command or one panel, not a new subsystem.

Change I made while checking: nixos-workflow is now imported into the company (copied into the managed directory), so it can be assigned to any agent. It is assigned to nobody, and Documenter's skills are back exactly as they were.

History — your instinct is right

~/.pi/paperclips/<timestamp>-<agentId>.jsonl — 178 files, one per run, and task-sessions maps task → file. So the cheap version is enough: a tool-pane entry that opens that folder in Yazi, or a pane running pi --resume. Minutes of work, nothing invented. A transcript tab inside Agents>agents is the integrated option, worth doing only if browsing files becomes a detour in practice.

Committed as e44b0df, documentation only — the running layout is untouched.

Source paths observed

  • paperclipai/paperclip/agentmail
  • company/d4921013-e5da-4d88-8016-57d5746e2d03/obsidian-cli
  • paperclipai/paperclip/paperclip
  • paperclipai/paperclip/paperclip-board
  • paperclipai/paperclip/paperclip-converting-plans-to-tasks
  • paperclipai/paperclip/paperclip-create-agent
  • paperclipai/paperclip/para-memory-files
  • company/d4921013-e5da-4d88-8016-57d5746e2d03/project-docs
  • company/d4921013-e5da-4d88-8016-57d5746e2d03/system-architect
  • local/9a9eba3826/nixos-workflow
  • .paperclip/instances/default/skills

Evidence

  • Confidence: 83%
  • Reason: Extracted repeatable deploy/configuration procedure from rich discovery

Update (2026-11-11 01:02:31)

Trigger: need to review or merge a completed task in paperclip-harness.

Procedure:

  1. In Work>work press o and choose the task from the list (IDs like AUD-168). All tabs switch to it.
  2. Check the review and change tabs.
  3. Press m to merge; watch the merge tab for live progress. Log file: /tmp/paperclip-harness-merge.log.
  4. Success line looks like merged into main at <sha> then merge finished <time>.

Expected log details: worktree <project>/.paperclip/worktrees/<TASK-id>-<slug>, branch <TASK-id>-<slug> rebased onto main; warnings no verify command and review saw nothing to review (0 files); recorded, not hidden are informational.

Troubleshooting:

  • refused: ocr is not installed then merge stopped: exit status 2 → PATH too small. bin/lib/harness.py should add ~/.local/bin, ~/.nix-profile/bin, system profile, per-user profile; verify ocr resolves in the environment the pane runs in.
  • Second merge attempt while one is running is refused by the merge lock — expected, not an error.
  • If Shell>shell behaves oddly, bin/board should detect cwd ≠ project folder and recreate that pane.

Rollback: merges land on main; revert with git as usual from the recorded sha.

Detailed procedure from completed turn

Both complaints are fair, and both are now fixed in the program rather than in the instructions.

Work>work selects its own task and merges

  • o in Work>work opens a list of the project's tasks. Choose one and the runs, change, facts, review and merge tabs all switch to it. You no longer start in Board>board.
  • m runs the merge for the task you are looking at, and the new merge tab shows it live:
merge of AUD-168 started 12:01:39
  worktree …/worktrees/AUD-168-tidy-the-readme-introduction
  rebased onto main
  no verify command: this would pass on the agent's word
  review saw nothing to review (0 files); recorded, not hidden
  merged into main at 4a44473
merge finished 12:01:43

That is a real merge I ran from the pane a minute ago, with no shell. Starting it twice is safe — the merge takes a lock and the second is refused.

Why the Shell step failed you

Two real bugs, not your memory:

  1. The pane that merges inherited the pane's small PATH, so bin/merge could not find ocr and refused with ocr is not installed. Fixed at the source: bin/lib/harness.py now adds ~/.local/bin, ~/.nix-profile/bin, the system profile and your per-user profile before looking for anything.
  2. Shell>shell was created in one folder and later adopted without checking. bin/board now compares the pane's real working directory with the project folder and replaces the shell pane when they differ, saying so.

The principle you are pointing at

The terminal was creeping in as the normal path. It should not be. From now on:

  • anything you do regularly is a key in a pane;
  • Shell>shell exists for the things we deliberately do not wrap, and for emergencies;
  • a test plan that needs me to write git log is a bad test plan.

docs/TEST-PLAN.md is rewritten accordingly: step 4 is now "press m and wait", and the whole plan contains no terminal commands at all. Shell>shell appears once, as the escape hatch it is.

What I would do next, in your order

  1. You run the plan as written (nine steps, keys only) and tell me where the screen and Paperclip disagree.
  2. Repair>cleanup — sessions listed with age and pane count, kill only what you select, and never touching the Pi session files.
  3. The Pi session browser — pi --resume in a tool pane, or the folder in Yazi.

Committed as 7198c34; the eight workspaces are rebuilt and running.

Evidence

  • Confidence: 85%
  • Reason: Repeatable procedure plus failure modes with concrete strings a future agent can match.; enriched with final-answer detail because persistence should be reusable

Update (2026-11-11 02:17:44)

Trigger: reviewing or merging a task's work without bouncing to the Board in paperclip-harness.

Procedure:

  1. Open Work>work — it is self-contained. Header shows Work · <TASK-ID> <title> · task N of M · read HH:MM:SS; tab title shows run count (the work · 1 run).
  2. Move between tasks: n (next) / p (previous); o picks from a list. Task choice here writes the same selection the Board writes, so Reply>reply and other panes follow.
  3. Move between runs: ↑/↓; Enter opens the selected run's log. Multiple runs occur when an agent was woken for different reasons (assignment, comment, automation, blockers resolved).
  4. Merge: press m. A confirm dialog shows what is merged (source branch, e.g. AUD-170-) and what happens (rebase onto base → verify command if any → ocr review [critical finding refuses] → merge into main → re-check on main with rollback on failure). Press y to proceed.

Safe/dangerous: n/p/o/↑/↓/Enter are read-only navigation. m then y is the dangerous step (writes to main). Merge is never automatic.

Rollback: bin/merge re-runs checks on main after merging and rolls back if they fail.

Troubleshooting: seeing only one run is expected for single-run tasks; check run count in the tab title. Docs: docs/RUNS-AND-MERGE.md; keys listed in README.

Detailed procedure from completed turn

Let me answer the three questions straight, because you were right that they were unclear.

What is a run?

One execution of one agent. A task can have several, each with its own log, because Paperclip wakes an agent for different reasons:

what woke it when
assignment the task was given to an agent
a comment someone replied on the task
automation Paperclip started a corrective run, e.g. the previous run ended without setting a status
blockers resolved the task was waiting on another task and it finished

Measured here: AUD-167 has 1 run, AUD-164 has 2 (assignment, then automation), AUD-166 has 2. So the runs tab showing one run is not it hiding anything — those agents finished in a single run. A task with a slower or messier job will show several, and each opens to its own log.

What is merge, and is it OCR?

No, merging is about code. An agent works in the task's own copy of the repository (a worktree) on its own branch. That branch holds commits that are not on the main line. Merging puts them there:

AUD-171-another-status-file-status-2   ──merge──▶   main

ocr is the reviewer that reads the change first. The order bin/merge follows:

  1. rebase the task's branch onto main;
  2. run the task's own verify command, if it has one;
  3. ocr reads the diff — a critical finding refuses the merge;
  4. merge the branch into main;
  5. re-check on main, and roll back if it fails.

Merging never happens on its own now: m asks, and shows the branch and the base branch before anything runs.

The Board-then-Work flow — fixed

That was cumbersome, and you should not have to do it. Work>work is now self-contained:

Work  ·  AUD-171  Another STATUS file…   task 1 of 13            read 13:16:47
╭─ the work · 1 run   runs   change   facts   review   log   merge ──·focus·╮
  • n / p — next / previous task, with task 3 of 13 in the header. You never leave the pane.
  • o — pick a task from a list.
  • ↑ / ↓ — choose a run; the tab title says how many there are (1 run).
  • Enter — open that run's log.
  • m — merge, after asking.

Choosing a task here writes the same choice the Board writes, so Reply>reply and the rest follow. The Board is still the best place to see everything at once; it is no longer the only way to move.

Written down

docs/RUNS-AND-MERGE.md holds all of this, including the measured run counts, and the README lists the new keys. Committed as 6aef6ce. The eight workspaces are running, with Work>work opening on runs for AUD-171.

Evidence

  • Confidence: 80%
  • Reason: Repeatable, evidence-backed procedure with safe vs dangerous actions and rollback.; enriched with final-answer detail because persistence should be reusable

Update (2026-11-11 03:16:34)

Deployment/runbook details discovered (2026-11-11 03:16:34)

Five of the six are in, and one has a caveat I want to be straight about.

Built

1. Repair>repair → cleanup tab. Sessions with pane count and last use; space ticks, x closes the ticked ones after asking. Protected and attached sessions are refused, and the screen says so:

session                          panes last used    state
paperclip-harness                    9 11 Nov 14:14 this project's session
tuios coding harness                 4 11 Nov 14:09 attached, or on another machine
office                              32 10 Nov 13:57 attached, or on another machine
office-test-1                       26 10 Nov 11:24
office-test-1-card-t3-cover-image…   1  9 Nov 19:50

space ticks a session, x closes the ticked ones after asking.
closing a TUIOS session ends its panes. It does not delete anything in
~/.pi/paperclips, where every Pi conversation is kept.

2. Pi sessions in t — two new entries: pi sessions opens ~/.pi/paperclips in Yazi, pi resume gives Pi's own session list ready to open.

4. v in Work>review — starts bin/review <task> --post and the tab shows the review running, then the verdict, which is also written on the task.

6. Board>board needs-you — enter on a card now selects that task and switches you to the Reply workspace, so the answer is one key away.

3. Project>project → projects tab — bin/office now records each adopted project in ~/.config/paperclip-harness/projects.toml, and the tab lists them:

project            machine      session        folder
spike-2026-11-10   nixos-desktop paperclip-har… /tmp/pc-sandbox-2026-…  (this machine)

a project on another machine can be listed here but not opened from this one.
to open one: attach to its session, or run bin/office <folder> there.

Caveat: that is a list, not a switch. Switching would mean tearing down these panes and building another project's — and a project on .27 cannot be opened from .13 at all. I would rather show you the truth than a button that half-works.

Not done: 7 (the spend chart)

Project>overview already has a runs sparkline. Spend-per-day needs cost per run, and the only cheap source is company runs, which cost 8 seconds to read. I did not put an 8-second read behind a tab without asking you first.

A finding that changes the key map

While testing the cleanup tab I found why tabs would not switch: TUIOS binds [, ], Tab, ,, ., =, ; and the digits in its own key map, and they never reached the program in that pane. Tabs are now switched with ( and ), which TUIOS leaves alone; [ and ] still work where the pane is in terminal mode, so both are accepted.

If a pane ever stops responding to letters, press i or Enter first — that is TUIOS's window mode, not a broken program.

Committed as 3ec960e; docs/NEXT.md updated with what is done and what remains (spend chart, the office scaffold, memory per agent, MOUSE-1).

Source paths observed

  • .config/paperclip-harness/projects.toml

Evidence

  • Confidence: 83%
  • Reason: Extracted repeatable deploy/configuration procedure from rich discovery

Update (2026-11-11 05:36:08)

Trigger: want to try Headroom compression for a single agent. Prerequisites: ~/.pi/agent/settings.json editable. Steps:

  1. Once, add provider to ~/.pi/agent/settings.json: "headroom": { "baseUrl": "http://192.168.20.13:8787/v1", "apiKey": "<same key the omni provider uses>", "api": "openai-completions" }.
  2. In Agents>settings, set that agent's model to headroom/auto/best-coding.
  3. Validate: run the same task with and without headroom, compare cost, duration, result; watch for tool-calling breakage and compression effects on large tool results. Rollback: restore the agent's model to the prior provider model; remove the provider entry if unused. Caveat: per-project selection is impossible — agent model is the finest granularity.

Detailed procedure from completed turn

Four answers.

1. The cut-off text

The overlay drew each line and then clipped it to the box width — no wrapping at all. It wraps now, and the box is as wide as the screen allows:

╭─ close 1 session? ───────────────────────────────────────────────────────·focus· ╮
│  what happens    these TUIOS sessions are closed and their panes end            │
│                  nothing in ~/.pi/paperclips is deleted; that is your Pi       │
│                  history                                                        │
│                  this project's session and attached sessions are never touched │
│  y               close them                                                     │
│  esc or n        do nothing                                                     │
│                  office-test-1-card-t3-cover-image-1x4e                         │
╰─────────────────────────────────────────────────────────────────────────────────╯

The text was also "cut off in chat" because I had truncated my own output with sed; the popup itself was the real fault and is fixed.

2. verify.sh — you are right, it was a hangover, and it is reframed

I had been describing it as "the thing that decides a task is finished". That is wrong, and it is exactly the wrongness you smelled. What actually decides a task is finished:

  1. the agent, by setting the task's status — Paperclip's own model;
  2. a reviewer, if a review stage is configured on the task;
  3. ocr, at merge time, which can refuse the change.

A project-wide script cannot overrule any of those. So its honest job is a regression check, the same job a CI pipeline does: after a task's work is applied, does the project still build and pass its tests? bin/merge runs it in the task's worktree and again on main, and rolls the merge back if it fails.

Renamed accordingly: bin/office now writes .harness/check.sh, and the old .harness/verify.sh is still honoured so an adopted folder keeps working. The merge dialog now says project check: … or no project check: the review decides.

3. Headroom — the concrete version

Measured just now:

GET http://192.168.20.13:8787/v1/models  → 200, 486 models (auto/best-coding, deepseek/…)
GET http://192.168.20.13:8787/health     → healthy, compression on, memory off
upstream                                 → http://192.168.20.13:20129/v1

The agents' omni provider already points at that same upstream, so headroom can sit in front of it. To use it for one agent, two steps, only the second per agent:

  1. once, in ~/.pi/agent/settings.json, add a provider:
"headroom": { "baseUrl": "http://192.168.20.13:8787/v1",
              "apiKey": "<the key the omni provider already uses>",
              "api": "openai-completions" }
  1. in Agents>settings, set that agent's model to headroom/auto/best-coding.

Would it work. The endpoint answers the OpenAI-style API that Pi's provider type speaks and lists the model ids Pi would ask for, so the pieces line up. It remains a hypothesis until one run goes 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, comparing cost, duration and result for the same task with and without.

Per project is not possible. The provider is chosen by the agent's model, and agents belong to the company, not a project. One agent is the finest granularity — which is what the model field already gives you.

4. Mouse-1 notes, read

They are precise and I have folded the facts into docs/PLAN-INTERFACE.md. The ones that shape the design:

  • POST https://api.mouse.dev/v1/decisions, key already in the environment — no software to install, I was wrong about that.
  • Cold start is minutes: first call after idle returned 503 engine_timeout after 30 s; retry 200 in 13 s; warm ~489 ms.
  • Stable answers at ~0.98 confidence. A classifier, not a writer.
  • Proven useful for failure routing (redo / stronger model / environment / person), unblock triage, intake clarity, task sizing.
  • Ruled out for the merge decision, judging test results, anything on a keystroke path, and as the only record of a decision.

The one place it earns its keep here: a failed run in Agents>agents offers f, asks one four-option question, and writes the recommendation, confidence and options onto the task — with the screen saying "asking, this can take half a minute", and a person still deciding.

Committed as 70e1f55.

Evidence

  • Confidence: 85%
  • Reason: Concrete repeatable procedure with rollback and validation.; enriched with final-answer detail because persistence should be reusable

Update (2026-11-11 05:36:08)

Deployment/runbook details discovered (2026-11-11 05:36:08)

Four answers.

1. The cut-off text

The overlay drew each line and then clipped it to the box width — no wrapping at all. It wraps now, and the box is as wide as the screen allows:

╭─ close 1 session? ───────────────────────────────────────────────────────·focus· ╮
│  what happens    these TUIOS sessions are closed and their panes end            │
│                  nothing in ~/.pi/paperclips is deleted; that is your Pi       │
│                  history                                                        │
│                  this project's session and attached sessions are never touched │
│  y               close them                                                     │
│  esc or n        do nothing                                                     │
│                  office-test-1-card-t3-cover-image-1x4e                         │
╰─────────────────────────────────────────────────────────────────────────────────╯

The text was also "cut off in chat" because I had truncated my own output with sed; the popup itself was the real fault and is fixed.

2. verify.sh — you are right, it was a hangover, and it is reframed

I had been describing it as "the thing that decides a task is finished". That is wrong, and it is exactly the wrongness you smelled. What actually decides a task is finished:

  1. the agent, by setting the task's status — Paperclip's own model;
  2. a reviewer, if a review stage is configured on the task;
  3. ocr, at merge time, which can refuse the change.

A project-wide script cannot overrule any of those. So its honest job is a regression check, the same job a CI pipeline does: after a task's work is applied, does the project still build and pass its tests? bin/merge runs it in the task's worktree and again on main, and rolls the merge back if it fails.

Renamed accordingly: bin/office now writes .harness/check.sh, and the old .harness/verify.sh is still honoured so an adopted folder keeps working. The merge dialog now says project check: … or no project check: the review decides.

3. Headroom — the concrete version

Measured just now:

GET http://192.168.20.13:8787/v1/models  → 200, 486 models (auto/best-coding, deepseek/…)
GET http://192.168.20.13:8787/health     → healthy, compression on, memory off
upstream                                 → http://192.168.20.13:20129/v1

The agents' omni provider already points at that same upstream, so headroom can sit in front of it. To use it for one agent, two steps, only the second per agent:

  1. once, in ~/.pi/agent/settings.json, add a provider:
"headroom": { "baseUrl": "http://192.168.20.13:8787/v1",
              "apiKey": "<the key the omni provider already uses>",
              "api": "openai-completions" }
  1. in Agents>settings, set that agent's model to headroom/auto/best-coding.

Would it work. The endpoint answers the OpenAI-style API that Pi's provider type speaks and lists the model ids Pi would ask for, so the pieces line up. It remains a hypothesis until one run goes 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, comparing cost, duration and result for the same task with and without.

Per project is not possible. The provider is chosen by the agent's model, and agents belong to the company, not a project. One agent is the finest granularity — which is what the model field already gives you.

4. Mouse-1 notes, read

They are precise and I have folded the facts into docs/PLAN-INTERFACE.md. The ones that shape the design:

  • POST https://api.mouse.dev/v1/decisions, key already in the environment — no software to install, I was wrong about that.
  • Cold start is minutes: first call after idle returned 503 engine_timeout after 30 s; retry 200 in 13 s; warm ~489 ms.
  • Stable answers at ~0.98 confidence. A classifier, not a writer.
  • Proven useful for failure routing (redo / stronger model / environment / person), unblock triage, intake clarity, task sizing.
  • Ruled out for the merge decision, judging test results, anything on a keystroke path, and as the only record of a decision.

The one place it earns its keep here: a failed run in Agents>agents offers f, asks one four-option question, and writes the recommendation, confidence and options onto the task — with the screen saying "asking, this can take half a minute", and a person still deciding.

Committed as 70e1f55.

Source paths observed

  • examples/extensions/custom-provider-anthropic
  • ../examples/extensions/custom-provider-anthropic
  • 8787/v1/models
  • .pi/agent/settings.json
  • headroom/auto/best-coding
  • api.mouse.dev/v1/decisions

Evidence

  • Confidence: 83%
  • Reason: Extracted repeatable deploy/configuration procedure from rich discovery

Update (2026-11-11 21:45:49)

Trigger: Need to (re)publish project documentation to Outline and refresh the Obsidian index note.

Prerequisites: Outline reachable at http://outline.lab.audasmedia.com.au (API accepts collections.list, collections.create, doc create at /api/...); Obsidian vault on host .27; auth token available (do not log it).

Steps:

  1. List collections via GET /api/collections.list to check for existing target.
  2. Create collection via API (POST returns 200). Record the returned collection id.
  3. Upload the five docs in order (00-index, 01-system-overview, 02-config-and-env, 03-code-map, 04-agent-runbook). Each returns 200 with a /doc/<slug> path.
  4. Rewrite 010 inbox/Paperclip Harness.md on .27 to include archify map URL, Mermaid source path, Outline collection link, and repository link.
  5. Verify byte count of note file.
  6. Commit and push repo to Gitea: git push to https://gitea.lab.audasmedia.com.au/sam/paperclip-harness.git main.

Known gotchas: Parsing non-JSON curl output as JSON throws JSONDecodeError: line 1 column 1 — check HTTP status separately from body. Port 3002 returns 404; port 3000 returns 401 (not Outline).

Rollback: Delete created Outline collection/docs via API; revert git commit.

Detailed procedure from completed turn

Headroom: what the failure actually is, and what you do

I tested three configurations on a throwaway container on a spare port, so the running one was untouched:

Configuration Result
default image (HEADROOM_BACKEND unset → anthropic) + your router key 502 proxy_error
plus OPENAI_API_KEY openai/deepseek-v4-flash → 404 No active credentials for provider: openai; deepseek/deepseek-v4-flash → 502
plus HEADROOM_BACKEND=openai 502 proxy_error

Two things that answer your questions directly:

  • openai/ is not OpenAI's model. In this stack the prefix names the protocol to speak — "use the OpenAI-compatible client" — and the rest is the model name your router uses. So openai/deepseek-v4-flash means "call the router with its deepseek-v4-flash model". That is why I wrote it that way, and it was a guess about headroom's routing, not a fact.
  • The 404 … provider: openai came from your router, not headroom. The router read openai/ as a provider name and had no key for it. So that path was never going to work with our router's ids.

/stats on the test container reported api_requests: 0 for all of those attempts — headroom refused or failed before forwarding. That is inside headroom's deployment, and it needs someone with its logs open; nothing in our software can fix it.

What does work, re-checked: POST /v1/compress answers 200 and reports tokens before and after. That is the endpoint headroom-bridge uses, and headroom-ai is now installed on .13, .27 and .51.

So: use Path A, not both. Agents>agents → ) to extensions → space on headroom-bridge. That agent, and no other, gets compress_for_agent. Path B is yours to fix in the headroom container if you still want automatic compression; I have recorded the three tests and their exact responses so nobody repeats them.

The rest

Archify in the Obsidian note — added, with the map URL, the Mermaid source path, the Outline collection and the repository link. Note is now 3,511 bytes at 010 inbox/Paperclip Harness.md on .27.

Outline — pushed. New collection Paperclip Harness with five published documents:

/doc/00-index-o6hhlKTFXR
/doc/01-system-overview-dGpfQOk1vt
/doc/02-configuration-and-environment-redTWei0y2
/doc/03-code-map-y6KPFbBbIC
/doc/04-agent-runbook-Sd9ZznhBqo

Point 4, specifically. Two tests, both about whether a person can get from "a folder" to "a working set of panes":

  1. A brand-new project. An empty folder, a new Paperclip project, then bin/office /tmp/fresh-project-2026-11-11 --project fresh-project. Expected: session created, eight panes built, Shell>shell in that folder. This already passed tonight — except the session opened at 78 columns and Board>board correctly said "this workspace needs 80 × 24", which is the too-small screen doing its job, not a failure. The real test is: switch to that session, press R to rebuild at full size, create one task, assign it, watch it run, merge it.
  2. An existing folder. Adopt this repository: bin/office ~/paseo/projects/paperclip-harness --project <new paperclip project>. Expected: .harness/ appears, nothing existing is overwritten, the panes read this repo's tasks. That proves adoption is safe on real code.

Pi versions. .51 is 0.87.1; .13 and .27 are 0.82.1. You rebuilt .51 — the Go addition there is in place (go gopls in hosts/sam-thinkpad/configuration.nix, dry-run evaluates). For Pi: on .13 and .27, Pi is the Nix package pi-coding-agent, so aligning means bumping that package in each machine's flake and rebuilding. Do you want me to prepare those two bumps (I can edit both flakes and run the dry-run evaluations), or leave .13/.27 on 0.82.1 since they work?

Committed as d2cf643 and pushed to Gitea.

Evidence

  • Confidence: 80%
  • Reason: Repeatable multi-step procedure with concrete endpoints and caveats.; enriched with final-answer detail because persistence should be reusable

Update (2026-11-11 21:45:49)

Deployment/runbook details discovered (2026-11-11 21:45:49)

Headroom: what the failure actually is, and what you do

I tested three configurations on a throwaway container on a spare port, so the running one was untouched:

Configuration Result
default image (HEADROOM_BACKEND unset → anthropic) + your router key 502 proxy_error
plus OPENAI_API_KEY openai/deepseek-v4-flash → 404 No active credentials for provider: openai; deepseek/deepseek-v4-flash → 502
plus HEADROOM_BACKEND=openai 502 proxy_error

Two things that answer your questions directly:

  • openai/ is not OpenAI's model. In this stack the prefix names the protocol to speak — "use the OpenAI-compatible client" — and the rest is the model name your router uses. So openai/deepseek-v4-flash means "call the router with its deepseek-v4-flash model". That is why I wrote it that way, and it was a guess about headroom's routing, not a fact.
  • The 404 … provider: openai came from your router, not headroom. The router read openai/ as a provider name and had no key for it. So that path was never going to work with our router's ids.

/stats on the test container reported api_requests: 0 for all of those attempts — headroom refused or failed before forwarding. That is inside headroom's deployment, and it needs someone with its logs open; nothing in our software can fix it.

What does work, re-checked: POST /v1/compress answers 200 and reports tokens before and after. That is the endpoint headroom-bridge uses, and headroom-ai is now installed on .13, .27 and .51.

So: use Path A, not both. Agents>agents → ) to extensions → space on headroom-bridge. That agent, and no other, gets compress_for_agent. Path B is yours to fix in the headroom container if you still want automatic compression; I have recorded the three tests and their exact responses so nobody repeats them.

The rest

Archify in the Obsidian note — added, with the map URL, the Mermaid source path, the Outline collection and the repository link. Note is now 3,511 bytes at 010 inbox/Paperclip Harness.md on .27.

Outline — pushed. New collection Paperclip Harness with five published documents:

/doc/00-index-o6hhlKTFXR
/doc/01-system-overview-dGpfQOk1vt
/doc/02-configuration-and-environment-redTWei0y2
/doc/03-code-map-y6KPFbBbIC
/doc/04-agent-runbook-Sd9ZznhBqo

Point 4, specifically. Two tests, both about whether a person can get from "a folder" to "a working set of panes":

  1. A brand-new project. An empty folder, a new Paperclip project, then bin/office /tmp/fresh-project-2026-11-11 --project fresh-project. Expected: session created, eight panes built, Shell>shell in that folder. This already passed tonight — except the session opened at 78 columns and Board>board correctly said "this workspace needs 80 × 24", which is the too-small screen doing its job, not a failure. The real test is: switch to that session, press R to rebuild at full size, create one task, assign it, watch it run, merge it.
  2. An existing folder. Adopt this repository: bin/office ~/paseo/projects/paperclip-harness --project <new paperclip project>. Expected: .harness/ appears, nothing existing is overwritten, the panes read this repo's tasks. That proves adoption is safe on real code.

Pi versions. .51 is 0.87.1; .13 and .27 are 0.82.1. You rebuilt .51 — the Go addition there is in place (go gopls in hosts/sam-thinkpad/configuration.nix, dry-run evaluates). For Pi: on .13 and .27, Pi is the Nix package pi-coding-agent, so aligning means bumping that package in each machine's flake and rebuilding. Do you want me to prepare those two bumps (I can edit both flakes and run the dry-run evaluations), or leave .13/.27 on 0.82.1 since they work?

Committed as d2cf643 and pushed to Gitea.

Source paths observed

  • nix/store/rgnappqqc5vbq60gza5fflyk84sylwl6-python3-3.14.6/lib/python3.14/json/__init__.py
  • nix/store/rgnappqqc5vbq60gza5fflyk84sylwl6-python3-3.14.6/lib/python3.14/json/decoder.py
  • outline.lab.audasmedia.com.au/api/collections.list
  • home/sam/obsidian/010
  • gitea.lab.audasmedia.com.au/sam/paperclip-harness.git
  • paseo/projects/paperclip-harness
  • hosts/sam-thinkpad/configuration.nix

Evidence

  • Confidence: 83%
  • Reason: Extracted repeatable deploy/configuration procedure from rich discovery

Update (2026-11-11 22:37:01)

Trigger: verifying a fresh install/offboarding of the paperclip-harness office after changes to bin/office.

Working dir: ~/paseo/projects/paperclip-harness. Full script with expected outputs: docs/CLEAN-TESTS.md.

Test 1 — new project:

  1. mkdir -p ~/paseo/projects/test-alpha && cd ~/paseo/projects/test-alpha && git init -b main && printf '# test alpha\n' > README.md && git add . && git commit -m "initial commit"
  2. cd ~/paseo/projects/paperclip-harness && bin/office ~/paseo/projects/test-alpha --new "test-alpha" — expect project: test-alpha created, session: test-alpha, and wrote .../.harness/{harness.toml,check.sh,NOTES.md} plus eight panes.
  3. tuios attach test-alpha (or the session switcher): Repair>repair all pass; Shell>shell shows .../test-alpha; Board>board says no tasks; press R once and confirm all eight panes return still on test-alpha.
  4. One task end to end: New task>new with title Add a STATUS.md with one line, description Create STATUS.md at the repository root with the line: alpha is running. Change no other file. Commit it., no blocker, no command, assign yes. Watch in Board>board then Agents>agents () runs, ↑, Enter for log). Work>work: change shows one file, v = review, m then y = merge.

Pass for Test 1: STATUS.md on main, task done, cost visible in Project>costs, nothing typed into a terminal after step 2.

Test 2 — existing repo (paperclip-harness itself, project already created as harness-repo):

  1. bin/office . --project harness-repo --session harness-repo (separate session on purpose; do not rebuild the working paperclip-harness session).
  2. git status --short — only new files under .harness/; anything modified existing = stop.
  3. tuios attach harness-repo; Project>project → projects tab lists spike-2026-11-10 and harness-repo with folder and machine; Board>board empty.
  4. Task Add a line to README: Add the line "tested by the harness" at the end of README.md. Change nothing else. Commit it., assign yes. Watch it, read diff in Work>work, then cancel with c in Board>board — do not merge — and confirm with git log that main is untouched.

Caveat: bin/office dry runs create the TUIOS session and need manual kill (see observation on dry-run side effects).

Evidence

  • Confidence: 80%
  • Reason: Repeatable, precise validation procedure with commands, expected outputs, and pass criteria.

Update (2026-11-12 00:35:25)

Trigger: applying config changes in /etc/nixos, or the .27 fontconfig evaluation failure.

Prerequisites: ssh access by IP (BatchMode probe ssh -o BatchMode=yes <addr> true), sudo on target host.

Steps (on .13, known good):

  1. Dry run to see scope: sudo nixos-rebuild dry-build --flake /etc/nixos#nixos-desktop (expected ~465 derivations / 5.5 GiB).
  2. Apply: sudo nixos-rebuild switch --flake /etc/nixos#nixos-desktop.
  3. Verify: check pi --version (evaluated vs running) after switch.

Fix path for .27 (fontconfig eval error at … fonts.fontconfig.configFile.fonts.settings): bump nixpkgs only in flake.lock, keep home-manager pinned, re-evaluate; do NOT change font config first. If it still fails, inspect home-manager fontconfig settings against newer nixpkgs attrsets (nixpkgs lib/attrsets.nix head).

Rollback: revert flake.lock and the initContent/initExtra edits in /etc/nixos (git tracked), then rebuild. Safe edits: home.nix Pi option name, flake.lock. Dangerous: font package removal, home-manager version bumps without testing.

Troubleshooting: 'already applied' claims must be verified — only edits in /etc/nixos count; /tmp copies are not applied.

Detailed procedure from completed turn

You are right that I was inconsistent. Here is the exact state, then the simple rule for office.

What is applied, precisely

/etc/nixos changed? Evaluates to pi Running pi Needs
.13 yes, now: lock bumped to 2026-11-07, initExtra → initContent 1.5.0 0.82.1 one sudo nixos-rebuild switch
.27 yes, now: same two edits 1.5.0 0.82.1 a fix first — see below
.51 no 0.87.1 0.87.1 the same two edits, then rebuild

Earlier I said "applied" when I had only tested a copy in /tmp. That was wrong wording on my part: nothing had been written to /etc/nixos. It has been now, on .13 and .27, and .13's dry-run build passes with 465 derivations to build and 5.5 GiB to fetch.

On .13, the rebuild is:

sudo nixos-rebuild switch --flake /etc/nixos#nixos-desktop

.27 cannot be rebuilt yet. Its evaluation fails in home-manager's fontconfig code:

… while evaluating the option `home-manager.users.sam.fonts.fontconfig.configFile.fonts.settings':
error: … head values (attrsets.nix:1711)

That is home-manager 2026-11-10 disagreeing with something in .27's font configuration, not the Pi change. The smaller next attempt is to bump nixpkgs only on .27 and leave home-manager where it is — I would try that before touching fonts.

.51 would need the same two edits to reach 1.5.0. Say whether you want .51 aligned in the same pass, or left at 0.87.1 until .13 is proven.

Your notes, adopted — and the commands simplified

I read docs/STARTUP-COMMANDS.md. Four things from it are now in our bin/office:

  • A host name → address table (nixos-desktop/.13 → 192.168.20.13, and so on), because the old version broke on names ssh could not resolve.
  • A check whether the named host is this machine, before any ssh. I hit exactly that trap tonight: office --remote 192.168.20.13 from .13 tried to log in to itself.
  • An ssh probe (ssh -o BatchMode=yes <addr> true) before doing anything remote, so a missing key says so instead of half-working.
  • bin/board --reset — closes every pane, waits until the session is empty, clears the workspace names, then builds. --replace alone can run out of workspace slots on a half-built session.

One thing is simpler than the old design: it needed tuios new --host … plus office … --host …, because office could not create a session. Ours can, through the daemon's own verb. So it is one command.

The rule, in one table

Situation Command
folder on this machine, shell on this machine bin/office <folder> --project "<Name>"
folder Paperclip has not seen yet bin/office <folder> --new "<Name>"
folder on another machine bin/office <folder> --project "<Name>" --host nixos-desktop
repair / rebuild bin/board, bin/board --replace, bin/board --reset
start again from nothing bin/project-reset <folder>

And the rule that removes the confusion: the folder and the session live on the same machine. If you are typing inside the session, on the machine that owns the folder, it is just bin/office <folder>. --host is only for the case where those two differ.

Written into docs/STARTUP.md, committed as 7b2cccf and pushed.

Your email_agent case, concretely

The folder is on .13, the Paperclip project is Email Agent (local_path, so no worktree policy), and the session must live on .13. From .27:

bin/office /home/sam/paseo/projects/email_agent --project "Email Agent" --host nixos-desktop --session email-agent

That runs the work on .13 over ssh, creates the session there, and builds eight panes. Then tuios attach --host nixos-desktop email-agent. No program can move your client, so that last step is always yours.

Evidence

  • Confidence: 80%
  • Reason: Repeatable procedure with commands, failure handling and a validation caveat.; enriched with final-answer detail because persistence should be reusable

Update (2026-11-12 01:11:14)

Trigger: a nixos-rebuild fails while building a specific derivation (e.g. a Rust/crate package reacting badly to the newer nixpkgs).

Procedure:

  1. Identify the failing package and its exact error text; send/triage the first error block.
  2. Build only that package with the intended override, e.g. via nix build of the flake package, before touching the system config. Validated example: lan-mouse override produced /nix/store/f6w228jqp5wq6xznf6arq69kck6axyyf-lan-mouse-0.11.0.
  3. Apply the minimal override in /etc/nixos/flake.nix, e.g. lm = lan-mouse.packages.${system}.default.overrideAttrs (o: { LIBGIT2_NO_VENDOR = "0"; });.
  4. Dry-run/evaluate the system build to confirm it computes, e.g. confirming 29 derivations to build and the expected pi version.
  5. Switch: sudo nixos-rebuild switch --flake /etc/nixos#<host> (e.g. #nixos-desktop for .13).

Rollback: switch back to the previous generation with sudo nixos-rebuild switch --rollback (or boot the previous generation) if the new generation misbehaves.

Troubleshooting: if the next rebuild fails elsewhere, repeat step 2 for the new failing package rather than guessing; eval errors in home-manager (e.g. fontconfig settings merge) are separate from package build errors and should be fixed deliberately.

Detailed procedure from completed turn

Both done, and the rebuild failure is fixed.

The commands are now plain commands

office, board, merge, review, triage, project-reset are on your path. Each is a two-line wrapper in ~/.local/bin that runs the real script in this repository — a wrapper, not a symlink, for the reason your old notes give: a symlinked script resolves its own directory to ~/.local/bin and cannot find its siblings. The previous project's three wrappers were moved to ~/.local/bin/.old-harness/, not deleted, in case you want to compare.

$ office --help
office — open a folder as a project

  office <folder> --project "<Name>"        adopt a project Paperclip already has
  office <folder> --new "<Name>"            create the Paperclip project too
  office <folder> --project "<Name>" --host nixos-desktop    the folder is on that machine
  office <folder> --dry                     say what would happen, write nothing

The folder and the session live on the same machine. If you are typing inside the
session, on the machine that owns the folder, it is just: office <folder>.

Two bugs fixed while doing it: office --help used to run with the current directory and create a stray session called tmp (I killed it), and the session was being created before the Paperclip project was checked — so a bad command left a session behind. Both corrected.

The .13 rebuild failure: found and fixed

lan-mouse 0.11.0 panics during build:

The environment variable LIBGIT2_NO_VENDOR has been set but no compatible system libgit2 could be found.

The new nixpkgs sets that variable, and lan-mouse's crate insists on a system libgit2. Letting it vendor its own fixes it. I built it with the override before touching anything:

/nix/store/f6w228jqp5wq6xznf6arq69kck6axyyf-lan-mouse-0.11.0   ← built

and applied the one-line change to /etc/nixos/flake.nix:

lm = lan-mouse.packages.${system}.default.overrideAttrs (o: { LIBGIT2_NO_VENDOR = "0"; });

Now the system dry-run evaluates with 29 derivations to build and pi at 1.5.0. Your rebuild:

sudo nixos-rebuild switch --flake /etc/nixos#nixos-desktop

If it fails again, send the first error block; the next likely candidate is another package that the newer nixpkgs treated differently, and each one is found the same way — build just that package with an override before switching.

Where the machines stand

config changed evaluates to running next
.13 lock + initContent + lan-mouse fix pi 1.5.0, 29 to build 0.82.1 rebuild now
.27 lock + initContent pi 1.5.0, but eval error in home-manager fontconfig 0.82.1 needs a fix; smaller attempt is bumping nixpkgs only
.51 not yet pi 0.87.1 0.87.1 the same three edits, then rebuild

.27's failure names home-manager.users.sam.fonts.fontconfig.configFile.fonts.settings — home-manager 2026-11-10 with a font declaration it cannot merge. It has nothing to do with Pi, and I would rather fix it deliberately than guess at its font config.

Committed as a129c4e and pushed. After .13 reboots into the new generation, tell me what pi --version says and I will do .27 and .51 to match.

Evidence

  • Confidence: 85%
  • Reason: Repeatable validated procedure used and generalised this turn.; enriched with final-answer detail because persistence should be reusable

Update (2026-11-12 01:50:43)

Trigger: bringing the NixOS fleet hosts to the same pi (pi package) version / fixing failed nixos-rebuild switch runs.

Prerequisites: flake at /etc/nixos with output #nixos-desktop; this repo committed & pushed for docs/ENVIRONMENT-FACTS.md updates.

Procedure & current host state:

  1. .13 (nixos-desktop) — config has: lock, initContent, and lan-mouse via lm (flake override with LIBGIT2_NO_VENDOR="0"). Evaluates to pi 1.5.0; running 0.82.1. Dry-run shows 12 derivations to build. Next action: rebuild now: sudo nixos-rebuild switch --flake /etc/nixos#nixos-desktop Then check pi --version.
  2. .27 — config has lock + initContent; evaluation fails in home-manager fontconfig: error names home-manager.users.sam.fonts.fontconfig.configFile.fonts.settings. Cause hypothesis: .27 declares fonts in two places — system fonts.packages and again inside systemPackages — which home-manager 2026-11-10 cannot merge. Fix the duplication (or only bump nixpkgs), then rebuild.
  3. .51 — unchanged: evaluates pi 0.87.1, running 0.87.1. Apply "the same three edits" (lock, initContent, and the lan-mouse/lm wiring) then rebuild.

Safe/dangerous: nixos-rebuild switch mutates live systems — do them one host at a time, .13 proven first. Change one thing at a time.

Troubleshooting: if a package build fails, build the offending package alone with an override first (see the lan-mouse LIBGIT2_NO_VENDOR case) and confirm the resulting store path, then wire it in. If it fails on something else, capture the first error block before changing anything else.

Evidence

  • Confidence: 85%
  • Reason: Repeatable multi-host procedure with per-host state, exact commands, and validation steps.

Update (2026-11-12 03:14:45)

Trigger: any nixpkgs bump or otherwise wide-reaching change on nixos-desktop (.13), which hosts Paperclip, Docker, postgres, tailscale, librespot and tuios sessions.

Prerequisites: ssh access to .13; confirm backups exist (/etc/nixos/flake.lock.bak-pi-align, /etc/nixos/home.nix.bak-pi-align).

Procedure:

  1. Build only: nixos-rebuild build or nix build the host config to prove evaluation.
  2. Inspect what will restart: systemctl list-units diff / nixos-rebuild dry-activate and note units touching network.
  3. Deploy with nixos-rebuild boot (never switch) for wide changes; reboot at a chosen maintenance window.
  4. On reboot, verify: ip route show default shows default via 192.168.20.1 dev enp42s0; systemctl --failed empty; curl -fsS localhost:<paperclip-port>/api/health returns 200 (/api/health expected 200); pi --version is 1.5.0; tuios list-sessions shows restored sessions.

Dangerous: nixos-rebuild switch on .13 while networkmanager.unmanaged coexists with legacy networking.interfaces static config — restarting NetworkManager wipes the default route that network-addresses-enp42s0 just set. If the route disappears mid-run, exit code 4 reflects failed units, not a build failure.

Rollback: nixos-rebuild switch --rollback (but this too restarts units); last resort reboot an older generation from the bootloader / nixos-rebuild boot --rollback then reboot.

Troubleshooting: if address present but no route, ip route add default via 192.168.20.1 dev enp42s0 and restart dependent services (e.g. librespot). Long-term: migrate enp42s0 to a NetworkManager manual profile first, tested with nmcli device modify/nmcli connection up before removing the legacy config.

Evidence

  • Confidence: 85%
  • Reason: Repeatable procedure derived from the outage with exact verification commands and rollback.

Update (2026-11-12 03:29:14)

Trigger: one user-level package (e.g. pi-coding-agent) must be a specific version across several NixOS hosts without moving the system nixpkgs pin.

Prerequisites: repo per host (nixos-desktop, nixos-4screen, nixos-thinkpad) cloned at /etc/nixos; a known-good nixpkgs commit for the desired package version.

Steps:

  1. In each host's flake.nix, add input nixpkgs-pi.url = "github:NixOS/nixpkgs/<commit>"; and in the per-system outputs piPkgs = import nixpkgs-pi { inherit system; config.allowUnfree = true; }; (allowUnfree needed if the package is unfree).
  2. In the home-manager users block, add piPkgs.<pkg> to home.packages.
  3. Remove any duplicate of the same package from home/sam/home.nix — otherwise two versions land on PATH and the result is ambiguous.
  4. Back up touched files (cp flake.nix flake.nix.bak-<tag> etc.) before editing.
  5. Verify WITHOUT switching: nix build the full system on each host and read the package's store path from the result (e.g. inspect result/sw/bin/<pkg> or result/home-path/home-manager generation links).
  6. Treat identical store paths across hosts as the success criterion; a matching --version string is not sufficient.
  7. Only if the system nixpkgs pin is unmoved (user-package-only change): sudo nixos-rebuild switch --flake /etc/nixos#<host> is acceptable. Otherwise use nixos-rebuild boot + reboot.
  8. Confirm with <pkg> --version on each host after activation.

Danger/caveats:

  • Never switch a nixpkgs-sized change on an in-use or remote host; use boot + reboot.
  • Exit code 4 from nixos-rebuild switch means units failed to start, not build failure — the generation may already have switched; check the running generation before retrying.
  • If a prior lockfile bump broke full-system builds on a host, restore that host's flake.lock before layering the new pin.

Rollback: restore the .bak-* files and git checkout the previous commit per repo; nixos-rebuild switch --flake /etc/nixos#<host> to the prior generation, or reboot into it.

Evidence

  • Confidence: 85%
  • Reason: Repeatable multi-host procedure with explicit verification, safe/dangerous commands, and rollback.