# Babysitter demotion inventory (P5)

Companion to `docs/specs/2026-08-06-execution-plane-insulation.md`. P5 says: laptop mem-guard
agent-process alerts and laptop tmpjail become box-side telemetry; delete laptop-side special
cases that exist only to survive local agent load.

This document is the named list P5 owes you: every laptop-side special case that exists because
agent and build work runs on this laptop, each one tagged **removed** (with the measurement that
proved it dead weight) or **held** (with the condition that releases it).

## Current state: zero removals

`bin/exec-plane-probe --once` on this laptop reads **IMPURE**. Successive readings while this
document was written returned 31, 35 and 42 non-interactive workloads live in `agent.slice` and
`build.slice`, most of them `confine-agent-*.scope`, one `confine-build-*.scope`. P3 (harnessd +
factory dispatch on the boxes) and P4 (local-spawn deny) have not landed.

**Nothing in this inventory has been removed, and nothing may be removed while the probe reads
IMPURE.** Every guard below is tagged `held`. A guard whose triggering condition can still occur
stays. The removals happen in a later pass, one at a time, each with a probe transcript in its
commit message.

## The proof command

```
bin/exec-plane-probe                 # 15m window, 30s samples
bin/exec-plane-probe --once          # single sample, right now
bin/exec-plane-probe --window 8h --interval 5m --json
```

Read-only: it samples cgroup membership and prints evidence rows. Nothing is killed, signalled,
throttled or written to. Exit `0` = PURE, `1` = IMPURE, `2` = usage error.

It counts a workload when the unit sits under `agent.slice`, `build.slice` or `agent-seat.slice`,
or carries a `confine-agent-*` / `confine-build-*` / `v2-child-dispatch-*` scope name wherever it
was parked. `human.slice` (the owner's interactive session), `interactive.slice`, `app.slice` and
`session.slice` are exempt by construction and are never sampled — the owner's session is not a
violation and can never be reported as one.

PURE is not by itself a licence to delete. It is the precondition. It says the triggering
condition for the `release-on: laptop-purity` class no longer occurs here; each guard still needs
its own measurement over a window long enough to cover the load it was written for.

## Tags

- `release-on: laptop-purity` — exists only because non-interactive agent/build work runs on this
  laptop. Eligible for removal review once the probe reads PURE over a representative window.
- `release-on: never` — protects the owner's interactive session, or defends against something
  that is not local agent load. Stays regardless of where execution runs. Listed so the picture is
  complete, not because it is a demotion candidate.
- `moved` — the observing work now runs on the box it observes. Distinct from a removal: the
  laptop still reads the result, so the guard is not gone and the probe verdict is unaffected.
  Section F is the list of these.

## A. Runaway and memory guards — the "mem-guard agent-process alerts" P5 names

| Guard | Path | Exists for | Tag | Status |
|---|---|---|---|---|
| agent-reaper scanner | `~/.claude/bin/agent-reaper.py`, `agent-reaper.timer` (60s) | Five candidate classes of stuck/leaked local agent processes: no-forward-progress agents, leaked test orphans, blocked turns, orphaned sidecars, orphaned runtime trees | `release-on: laptop-purity` | **held** — every class is a local agent process; all five still occur (probe IMPURE) |
| reaper notifier daemon | `~/.claude/bin/reaper-notifier.py`, `reaper-notifier.service` | Desktop notification per newly-flagged local pid | `release-on: laptop-purity` | **held** — releases with the scanner it renders |
| agent-process-reaper / -report / reaper-ctl | `~/.claude/bin/agent-process-reaper`, `agent-process-report`, `reaper-ctl` | Manual inspection and reaping of local agent trees | `release-on: laptop-purity` | **held** — same trigger as the scanner |
| build-nice-sweep | `~/.claude/bin/build-nice-sweep.sh` + timer | Renices local build processes so they cannot outrank the desktop | `release-on: laptop-purity` | **held** — a local build still runs here |
| oomd PSI kill policy | `modules/workstation/systemd/**` (`SwapUsedLimit`, agent.slice `OOMPolicy=continue`) | Kills a local build under swap pressure instead of freezing the desktop | `release-on: never` | **held** — a kernel limit, not a reaper; the desktop needs it whatever else runs here |
| `MemoryMin` on the desktop slices | `modules/workstation/systemd/user/**` | Reserves memory for the owner's session | `release-on: never` | **held** — protects the interactive session by construction |

**Box-side replacement, landed:** the controller now samples each box's own `/proc/pressure/memory`
(PSI some60/full60/full300), `/tmp` fill, and per-slice `memory.current` / `memory.events` oom_kill /
`pids.current` for `agent.slice`, `build.slice`, `agent-seat.slice`. The collector's offload adapter
derives alerts from those, and the fleet panel carries the raw facts into the machine detail modal
in the web UI.

Nothing in that path kills anything, and no alert fires on a memory level. The three alertable
conditions are all failures that already happened on a box:

- **oom-kill** — the kernel already killed something. The counter is baselined on first sight, so a
  collector restart never renotifies; only a fresh delta alerts, and the item id embeds the new
  total so a repeat poll is the same episode and a new kill is a new one.
- **tmp-full** — 100% only, i.e. writes are already failing. A busy tmpfs at 90% is not an alert.
- **memory-stall** — PSI `full300` over threshold: the box is making no forward progress. A box at
  92% `some60` pressure with work getting done does not alert. A test pins that distinction.

No PID appears in any message. This is deliberately the same doctrine the owner set for the laptop
guards: kill only a real runaway, never a memory number; a box at 35G+20G doing agent work is
working, not sick.

## B. Filesystem containment

| Guard | Path | Exists for | Tag | Status |
|---|---|---|---|---|
| tmpjail | `~/.claude/bin/tmpjail`, `~/.local/bin/tmpjail`, `_tmpjail-shim.sh` | bwrap overlay so agent writes to `/tmp` land in the project tree, keeping the real `/tmp` (X11, Postgres, askpass sockets) intact | `release-on: laptop-purity` | **held** — every local agent process is still jailed; the sockets it protects are the owner's |
| tmpjail-gc | `~/.claude/bin/tmpjail-gc`, `tmpjail-gc.{service,timer}` | Daily sweep of jail upper dirs left behind by local agent runs | `release-on: laptop-purity` | **held** — releases with the jail |
| shm-pg-reaper | root timer (see `project_workstation_memory_protection`) | tmpfs leak from local pg-harness mmap dsm segments | `release-on: laptop-purity` | **held** — the harness still runs here |

**Box-side replacement, landed:** `tmpUsedBytes` / `tmpSizeBytes` per box, surfaced in the machine
detail modal, alerting only at 100%.

## C. Offload and admission — the machinery that moves work off the laptop

These are the opposite of babysitters: they are the mechanism that makes the laptop pure. They are
listed so the picture is complete. They are removal candidates only in the sense that they become
unnecessary once *nothing* is launched here at all — which is P4's surface, not P5's.

| Guard | Path | Exists for | Tag | Status |
|---|---|---|---|---|
| cpu-guard PATH shims | `~/.claude/bin/_cpu-guard-shim.sh` + ~30 symlinks (`tsc`, `vitest`, `pnpm`, `node`, …) | Intercepts heavy commands and routes them to a buildbox; pins affinity for what stays | `release-on: laptop-purity` | **held** — it is currently the thing doing the routing |
| buildslot admission queue | `~/.claude/lib/cpu-guard.sh` | Machine-global FIFO for heavy local work, nice-aware `/proc/stat` admission | `release-on: laptop-purity` | **held** — a local build still needs admission |
| local-gate | `~/.claude/bin/local-gate`, `local-gate.json` | Decides remote-vs-local per build; slot/memory/swap ceilings | `release-on: laptop-purity` | **held** — the decision still has a local branch |
| node-shim argv allowlist | `~/.claude/bin/node` via the cpu-guard shim | Default-deny on what may run node locally | `release-on: laptop-purity` | **held** |
| e2e-remote + headless-guard | `~/.claude/bin/e2e-remote`, `install-headless-guard` | Forces browser+dev-server pairs onto a buildbox; local headless launch exits 97 | `release-on: never` | **held** — a hard kernel-adjacent limit that keeps a whole workload class off the laptop; keeping it is the goal, not the cost |
| agent-sandbox / seat launchers | `~/.claude/bin/agent-sandbox`, `seat-launcher`, `seat-remote`, `provision-remote-seats`, `overdeck-seat-*` | P2/P3 seat primitives — run agent work on a box | `release-on: never` | **held** — this is the execution plane, not a guard |

## D. Kernel limits and slice layout

A limit is enforced by the kernel and cannot be exceeded; a reaper observes and can miss. Only
limits are invariants. None of these is demoted by P5.

| Guard | Path | Exists for | Tag | Status |
|---|---|---|---|---|
| `agent.slice` / `build.slice` / `agent-seat.slice` | `modules/workstation/systemd/user/**`, `~/.claude/bin/_agent-build-scope`, `apply-agent-seat-slice` | Confines local agent/build work so it can be limited and killed as a unit | `release-on: laptop-purity` | **held** — also the probe's own measurement surface; it must outlive the guards it proves dead |
| `human.slice` + `_human-session` + `install-human-slice` | `~/.claude/bin/` | Puts the owner's interactive session outside every limit and every reaper | `release-on: never` | **held** — the owner's session must never be killed, blocked or throttled |
| `app.slice` `TasksMax=8000` / pids caps | `modules/workstation/systemd/**` | Fork-bomb ceiling after the 20k-pid incident | `release-on: never` | **held** — defends against a bug in anything, not against agent load |
| pids-cap-guard hook | `~/.claude/hooks/pids-cap-guard.mjs` | Refuses a dispatch scope with no pids ceiling | `release-on: never` | **held** — same |

## E. Session-safety guards — never demoted

Listed for completeness. None of these exists because of local agent load; they exist because the
owner's session shares a machine with automation.

| Guard | Path | Tag |
|---|---|---|
| human-kill-guard hook | `~/.claude/hooks/human-kill-guard.mjs` | `release-on: never` |
| `pkill` / `killall` kill-guard shims | `~/.claude/bin/{pkill,killall}`, `_kill-guard-shim.sh` | `release-on: never` |
| main-checkout-guard, worktree-lock-gate | `~/.claude/hooks/` | `release-on: never` |
| deny-gate | `~/.claude/hooks/deny-gate.mjs` | `release-on: never` |
| secret-file-gate | `~/.claude/hooks/secret-file-gate.mjs` | `release-on: never` |
| git-guard / git-hook-confine | `~/.claude/bin/_git-guard-shim.sh`, `git-hook-confine.sh` | `release-on: never` |

## F. Remote-work telemetry — moved 2026-08-07

| Former laptop observer | Before | Replacement | Status |
|---|---|---|---|
| controller host-metrics sampler | Every 15s, `overdeck-controller` opened one SSH process per box and ran a one-second `/proc`, `df`, thermal and cgroup scan through `sh -c` | Each box runs `buildbox-telemetry.timer` every 15s and atomically writes `~/.local/state/overdeck/buildbox-telemetry.json`; the laptop reads only that completed summary once per 30s collector cadence | **moved** |

Process topology, per enrolled box:

```
before: laptop  overdeck-controller -> ssh box sh -c <one-second /proc scan>  (4/min)
after:  box     systemd --user -> buildbox-telemetry.sh -> local JSON         (4/min)
        laptop  overdeck-controller -> ssh box cat .local/state/...json       (2/min)
```

The laptop now starts half as many telemetry SSH children and no longer sends or waits for the
remote sampling program. The box keeps the same 15-second fact freshness because it owns both the
work and the kernel/cgroup files being observed. Summary publication is tempfile-plus-rename, so a
reader gets either the previous complete sample or the next complete sample, never a partial JSON
document.

This is a reduction of the laptop-side poller, not its elimination. The laptop still opens one
`ssh … cat` per box every 30 seconds. What moved is the expensive part: the one-second `/proc`,
`df`, thermal and cgroup scan that used to run three times over, through a remote shell, driven
from here.

**Staleness is the cost of reading a file instead of running a probe.** A failed probe used to be
self-announcing — the box was unreachable, the sample was absent. A file keeps answering after the
sampler that writes it has stopped, so every summary carries `sampledAtEpochMs` and the controller
drops a host whose summary is more than 90 seconds off its own clock in either direction. A box
with a dead sampler therefore disappears from the fleet panel and from placement, exactly as an
unreachable box does, instead of pinning every reader to the numbers it last held. The `buildbox`
declared-state check reports the same fact from the box side, with the age in the verdict, so the
condition is named rather than silent.

### Deliberately left on the laptop

- `buildbox-watch.timer` stays for now. It is not a pure observer: when a reachable box fails its
  declared-state checks it invokes the guarded one-host `buildbox harden` recovery path and owns
  workstation notifications. Moving that would alter SSH/boot recovery authority, which this task
  does not authorize.
- `buildbox-parity.timer` stays. It actively converges fleet configuration and records its result;
  it is a reconciler, not telemetry.
- Admission capability probes and the on-demand host-log endpoint stay. They run only while
  admitting a host or serving an explicit user request, not as periodic remote-work watchers.

## Removal protocol

When the probe reads PURE over a window that covers a representative working day:

1. Pick **one** guard tagged `release-on: laptop-purity`.
2. Record a probe transcript covering a window in which that guard's specific triggering condition
   would have fired if it could — not merely a quiet window.
3. Remove that guard alone. Put the measurement in the commit message.
4. Re-run the probe. Repeat.

Never bulk-delete. A guard whose triggering condition can still occur stays.

## Spec exit criterion

P5's exit is complaint-registry machine-takeover categories flat for 30 days. That clock starts
after removals, not after this document.
