# Why a machine full of self-cleaning machinery filled its disk three times in one day

**Date:** 2026-08-26 · **Author:** root-cause investigator (keeper desk dispatch) · **Method:** read-only.
Nothing was deleted, killed, or modified in producing this report.

---

## THE SHORT VERSION

**What's actually wrong:** the machine has six finished disk-cleanup instruments and five of them are
*forbidden from deleting anything* — deliberately, each one citing your own rule that no agent deletes.
The rule is right. What was never built is its other half: a scope you sign once, so the machinery can
act inside it without asking. So the tools watch the disk fill and fetch you.

**What I need from you:** one signature on a disk policy — the pools, their expiry, their caps, and the
never-touch list. That single ratification turns five report-only instruments into working ones. It is
not a per-incident ask; it is the last one.

**What lands without you:** two pure wiring changes I can make today that would have prevented all
three of today's freezes — turn on the run-directory sweeper that already exists, and stop the disk
guard from blocking its own cure.

**The one physical move worth more than all the software:** put the fleet's scratch on a size-capped
volume. Then an overrun is one failed job instead of your whole machine. macOS supports this natively;
it composes with the 08-23 spill plan you already ratified.

---

## THE ROOT CAUSE — one sentence

> **The safety rule "no agent deletes — resource pressure stops and reports" was implemented as
> *never delete* instead of *never delete outside a ratified scope*, so every disk instrument built on
> this box over the last three months — six of them, all finished, all adversarially checked — was
> deliberately wired to REPORT ONLY; nothing is bounded at creation, and the one instrument that does
> delete runs once a day with a 24-hour age floor against a fleet that writes ~400 MB/min.**

The machine is not missing cleanup machinery. It has **six** purpose-built disk instruments. **Five of
the six are structurally forbidden from reclaiming** — not by accident, but on purpose, each one
citing the same standing law in its own header. The sixth runs on a calendar.

This is not a bug in any one of them. It is what happens when a protective law has no lawful path
underneath it: the law holds, the pressure keeps arriving, and the machinery grows sideways into
elaborate instruments that watch the disk fill.

**The arithmetic makes the calendar leg hopeless independently of the law:**

| | |
|---|---|
| Fleet fill rate during the incident | **~400 MB/min** (~24 GB/hour) |
| Cadence of the only automatic *applying* reclaimer | **once per 24 hours** (04:30) |
| Age floor before that reclaimer will touch anything | **24 hours** |
| Worst-case reclaim latency | **~48 hours** |
| Bytes the fleet can write in 48 hours | **~1.1 TB** |
| Size of the disk | **460 GB** |

The reclaimer cannot win by any margin of tuning. It is the wrong *shape*.

### The day, measured — `~/next-arc/disk-watch.log`

The hourly watcher recorded the entire incident. Free space on `/`:

```
00:11  12 Gi   WARN-UNDER-15GI
01:11  11 Gi   WARN-UNDER-15GI
02:11  10 Gi   WARN-UNDER-15GI      ← four consecutive warnings overnight
03:11  10 Gi   WARN-UNDER-15GI
04:11  18 Gi
07:11  16 Gi
08:11  4.3 Gi  LOUD-UNDER-8GI       ← FREEZE 1   (16 → 4.3 Gi in one hour)
09:11  21 Gi                        ← manual reap
10:11  55 Gi                        ← manual reap
11:11  42 Gi                        (-13 GB/h)
12:11  34 Gi                        (-8 GB/h)
13:11  12 Gi   WARN-UNDER-15GI      (-22 GB in 60 min = 367 MB/min)
14:11  5.4 Gi  LOUD-UNDER-8GI       ← FREEZE 2/3
```

Two things to read off this:

1. **The 367 MB/min fill rate is measured, not estimated** — 34 Gi to 12 Gi between two hourly stamps,
   with the fleet running normally.
2. **The watcher produced six escalating alarms across the day and reclaimed nothing** — because its
   own header says so: *"WATCH-ONLY, deletes nothing... Keeper executes all deletions."* Its LOUD alarm
   text even reads *"reap early, lawfully (three disposal laws; keeper executes)"*. The instrument
   watching the disk die is explicitly built to fetch a human. At 08:11 and again at 14:11, the human
   it fetched was Robert.

---

## THE RECEIPTS — four findings, each independently sufficient to explain a freeze

### 0. THE FRAME: six finished disk instruments, five forbidden from reclaiming — on purpose

This is the finding that explains the other three. Each of these was built by a different arc, months
apart, and each independently chose report-only, citing the same law.

| Instrument | Built | What it can do | What it is allowed to do |
|---|---|---|---|
| `scratch-registry.sh` | #383, 2026-08-16 | Registers every scratch dir at creation with `pid + starttime` liveness; emits certified orphan rows | **Nothing.** Header: *"OUTPUT ONLY -- this instrument never deletes anything."* |
| `sweep-finished-runs.sh` | 2026-08-02/03 | Reclaims a run dir the moment its evidence is provably banked elsewhere | **Nothing** — has no production caller at all |
| `bay-retention.sh` | Wave 1.4 | Age-based reclaim with a live-bench fence and a hard path fence | Scheduled as `--scan` only; the closed job table has **no apply variant** |
| `hygiene-report.sh` | — | Reports bay disk state | **Nothing** — *"REPORTS disk and removes nothing"* |
| `fleet-watchdog.sh` | — | Detects stranded runs and unread work | **Nothing** — *"this watchdog deletes nothing"* |
| `scratch-janitor.sh` | 2026-08-25 | Reaps dead-session scratch | **The only one with hands** — daily 04:30, 24 h age floor, one pool |

`scratch-registry.sh`'s own header states the reasoning in the clear:

> *"On this machine no agent deletes -- resource pressure stops and reports; this instrument is how the
> report gets rows."*

And `bay-schedule.sh`'s job table, line 127:

> `# Every body here is scan/report only: nothing deletes, nothing pushes.`

And `hygiene-report.sh`, per `sweep-finished-runs.sh`'s header: *"REPORTS disk and removes nothing."*

And the fleet watchdog, in every finding row it writes, today included:

> *"Finished work is sitting unread -- harvest or bury it deliberately; **this watchdog deletes
> nothing**."*

**Five independent instruments, built by different arcs over three months, each carrying the same
self-imposed clause in its own words.** That is not five oversights. That is one law, faithfully
obeyed, with no lawful path underneath it.

**The law is right.** *"No agent ever deletes or destroys outside its named scope"* has protected
Robert repeatedly — the REAP-REACH incident (ESC-2), the Five Arcs worktree, the sealed-room warm
tree, the three court benches destroyed on 2026-08-11. Every one of those scars is real, and every one
came from a delete that had no ratified scope behind it.

But the law was implemented as **"no agent deletes"** rather than **"no agent deletes outside a
ratified scope"** — and *no scope was ever ratified*. So each new reclaimer, correctly obeying the law
as written, shipped with its hands tied. The result is a machine that can describe its own disk
crisis in exquisite certified detail and cannot do one thing about it.

**The fix is not to weaken the law. It is to give the law the missing half — a bounded scope Robert
signs once (Layer 4 below).** That is the whole recommendation in one line.

### 1. The bay's daily retention job is `--scan`. It has never deleted a byte.

`~/.claude/engine-bay/bin/bay-schedule.sh` line 127, comment, verbatim:

> `# Every body here is scan/report only: nothing deletes, nothing pushes.`

Lines 130–131, the job body:

```bash
    bay-retention-scan)
      "${FLEET_HOME}/bin/bay-retention.sh" --scan
```

The closed job table is `JOB_NAMES='bay-retention-scan custody-pr-sweep fleet-watchdog-record
rehearsal-nightly-run'` — **there is no apply variant to switch to.** It cannot be scheduled.

Its own log from the morning of the incident,
`~/.claude/engine-bay/schedule-log/bay-retention-daily-2026-08-26.log`, last line:

```
bay-retention: SCAN -- 86 reclaimable (235 KiB), 6229 kept (4486365 KiB). horizon=7d reports=30d. Pass --apply to reclaim.
```

Read that twice. On the morning the disk died three times, the bay's retention pass looked at **4.3 GB**
of its own debris, declared **235 kilobytes** reclaimable, and deleted **none of it**. Two independent
reasons: the job body is scan-only, and the horizon is **7 days** against a pool that fills in **hours**.

### 2. `sweep-finished-runs.sh` — the one tool with the right clock — has zero production callers.

`~/.claude/engine-bay/bin/sweep-finished-runs.sh` is exactly the right design: it reclaims a run dir
the moment its evidence is provably banked elsewhere (terminal ledger row + report/stdout exist
*outside* the dir + branch present in the repo), with a 10-minute settle window and a `BENCH_LIVE`
fence. Its own header says why it exists:

> `nothing in this bay has ever deleted a finished run directory ... the operator's disk went 48GB -> 12GB free twice in one night`

Grep for callers across `~/.claude`, `~/next-arc`, `~/Library/LaunchAgents`: **every hit is either its
own smoke test (`smoke/g-bench-guard.sh`) or a log file quoting it.** Built, verified, never wired.
This is the INC-005 class the repo already has a name for: *the orphan IS the bug.*

### 3. The 5 GB guard blocks its own cure. Verified, not inferred.

`~/.claude/hooks/disk-space-guard.py` allows commands by literal string prefix. Simulated against its
actual `SAFE_PREFIXES` tuple:

| Command | Verdict |
|---|---|
| `launchctl kickstart -k gui/501/com.keeper.scratch-janitor` | **BLOCKED** |
| `bash ~/next-arc/keeper-tools/scratch-janitor.sh apply` | **BLOCKED** |
| `~/.claude/engine-bay/bin/bay-retention.sh --apply` | **BLOCKED** |
| `npx tsx x.ts` | BLOCKED (correct — this is the point of the guard) |
| `rm -r /tmp/foo` | ALLOWED |

Under the block, the **only** lawful reclaim is a hand-typed `rm`. That collides head-on with the
constitution (*"resource pressure means stop and report, never improvised destruction"*) and with
Robert's standing rule that deletions are his word. **The guard structurally converts a resource event
into a Robert-blocking manual event.** That is precisely what happened, three times.

Two further defects in the guard: it is a **single cliff** at 5 GB with no soft mark and no hysteresis,
and its block message says *"Stop and report"* — a last-resort gate that only reports is a smoke alarm
with no sprinkler. (Its *measurement* is fine: it reads `/`, which on this APFS container reports
identically to `/System/Volumes/Data` — both 49.7 GB free at time of writing. The measurement is right;
the policy is wrong.)

---

## POOL INVENTORY — writer, claimed contract, actual failure mode

Sizes measured 2026-08-26 ~14:45 local, **after** Robert's manual reap. Peak-day figures noted where known.

| Pool | Size now | Who writes it | Contract that claims to bound it | Why the contract fails on this box |
|---|---|---|---|---|
| `$TMPDIR/tsx-501` | **9.0 GB** (65,535 entries) | `tsx` 4.23.12 — every pipeline script runs via `npx tsx` | **A 7-day TTL, self-enforced.** `expireDiskCache()` drops entries older than 7 days. **No size cap of any kind.** | **TTL-only bounding on a burst writer.** 9 GB and 65,535 entries accumulated *inside* the 7-day window, so the expiry never engages. No `TSX_*` env var caps size (only `TSX_DISABLE_CACHE` / `TSX_DEBUG` / `TSX_TSCONFIG_PATH`). Not in scope of any reaper here. Biggest single unattributed writer. **Good news: the cache root is `os.tmpdir()`, so it honors `TMPDIR` — a per-lane or per-run `TMPDIR` bounds it for free.** |
| `/tmp/engine-bay-runs` | **8.7 GB** (268+ FINISHED dirs, ~3 GB/run) | `run-sealed.sh` / bay dispatch | `bay-retention.sh` (7-day horizon), scheduled daily; `sweep-finished-runs.sh` (harvest-scoped) | Scheduled job is **`--scan`** (Finding 1). Horizon is **7 d** vs a pool that fills in **hours**. The right-clock sweeper has **no caller** (Finding 2). Dead escorts (7 RED on the watchdog) orphan their dirs with no terminal row, so even `--apply` would keep them. |
| `~/lane-worktrees` | **21 GB** (~12 arcs) | one worktree per unit, across arcs | `reap-worktrees.sh` at SessionStart → `reaper.sh` §E: remove only merged+clean+idle, `git worktree remove` (no force), behind `.prune-enabled` | **Stands down entirely while any sealed run is alive** (`pgrep -f 'run-sealed[.]sh' && exit 0`) — on a fleet day that is most of the day. Predicate is *inferred* from git state, so the pool shrinks only after merges land. And the REAP-REACH scar (ESC-2: a sweep killed a live rollback lever) made the whole lane hand-operated. |
| `$TMPDIR/*` mkdtemp fixtures | **~15 GB** (peak-day figure) | court/test suites via `mkdtemp` | **cleanup-on-exit** — `try/finally`, `afterEach` | **Exits here are kills.** Account switches on usage limits and helm reseats send SIGKILL; a killed process runs no `finally`. Plus macOS only purges this dir at boot. This is the purest instance of the class. |
| `/private/tmp/claude-501/…` | **1.3 GB** now; **~44 GB in one day** at peak | every session's scratchpad, agent clones, task outputs | three overlapping reapers (below) | **All three have a ≥24 h age floor or a scope that excludes the bytes** — see the breakout below. 44 GB accrued in *one day* is 100% invisible to every one of them. |
| `~/.claude/engine-bay/staging/reports` | **~4.3 GB** (6,229 items) | every dispatch's report + stdout log | `bay-retention.sh`, 30-day report horizon | Scan-only (Finding 1). 30 d horizon. |
| `~/Library/Application Support/Claude/vm_bundles` | **10.3 GB** | Claude Desktop | **none** — `disk-cleanup.sh` explicitly classifies it *"can't be auto-cleaned safely"* and only warns | Warn-only pool. Notification fires; nothing reclaims. Known upstream issue (#32263) referenced in the script's own header. |
| `~/.claude/projects` | **3.7 GB** (490 project dirs) | session transcripts | **none** — warn-only | Warn-only. Also load-bearing: the janitor's liveness predicate reads these, so they can't be blindly aged. |
| `~/.claude/handoffs` | **1.3 GB** | batons | **none** | Warn-only. |
| `~/factory-worktrees` | **2.2 GB** | factory arcs | same as lane-worktrees | Same class. |

### The `/private/tmp/claude-501` breakout — three reapers, three different blind spots

| Reaper | Trigger | Scope | Blind spot |
|---|---|---|---|
| `scratch-janitor.sh` | launchd **daily 04:30** | dead-session scratch dirs only | Requires transcript untouched **>24 h** AND no write in **24 h**. Worst-case latency ~48 h. Correct predicate, hopeless cadence. |
| `disk-cleanup.sh` | launchd **hourly** | `find -type f -mtime +1 -delete` | The **`-mtime +1` floor** — same-day bytes are structurally invisible. Also: its own log shows one run spanning **13:51:43 → 14:08:37** (17 minutes) burning that time on `du -sm` walks of multi-GB dirs while deleting almost nothing. |
| `reap-neuron-tmp-clones.sh` | SessionStart **+ every Agent spawn** | dirs whose `remote.origin.url` contains `ae-robertsanchez/neuron` | Only matches **git clones of one repo**. Test fixtures, capture outputs, artifacts, `node_modules` trees — the actual bulk — are out of scope by construction. |

The cadence is right on the third one and the scope is right on the first. No single reaper has both.

---

## THE ALARM LAYER THAT RECLAIMS NOTHING

Five launchd registrations touch disk. All five are **LOADED** (verified via `launchctl list`).
Three of them only *talk*:

| Job | Cadence | Does it free bytes? |
|---|---|---|
| `com.robertsanchez.disk-watch` | hourly :11 | **No.** Appends a line to `~/next-arc/disk-watch.log`. No notification. |
| `com.neuron.disk-keeper` | 03:30 | **No** (on internal). Mirror refresh + an 80% alarm → notification. |
| `com.claude.disk-cleanup` | hourly | **Barely.** Files >1 day only. Warns at <20 GB free. |
| `com.keeper.scratch-janitor` | 04:30 daily | **Yes** — the only real automatic reclaimer. One pool, 24 h floor. |
| `com.warp.bay-health` → `bay-retention-daily` | 30-min tick / 24 h job | **No.** `--scan`. |

**Five disk registrations; one and a half that free bytes; both on a daily clock.**

The hourly job's own log, on the day:

```
2026-08-26 14:09:08 CRITICAL: Only 5GB free on disk!
2026-08-26 14:09:09 Free disk: 5GB
```

It *observed* the crisis, wrote it down, notified — and reclaimed nothing, because everything in the
pool was less than a day old.

---

## THE DESIGN ALREADY EXISTS AND WAS NEVER INSTALLED

`~/.claude/specs/auto-cleanup/SPEC.md`, dated **2026-06-17**, status line verbatim:

> `**Status:** DRAFT for Robert's review. Nothing is installed.`

It is a complete two-watermark GC, with a working implementation beside it at `impl/`:
`disk-watch.mjs`, `disk-reap.mjs`, `lib.mjs`, `disk-config.json`, `INSTALL.md`, `SELF-CHECK.md`. It
cites kubelet image GC (85/80 thresholds), Docker BuildKit `defaultKeepStorage`, systemd-tmpfiles,
tmpreaper, and `git worktree prune --expire`. It documents its anti-pattern (threshold thrashing) and
its mitigation (high≠low gap + oldest-first). It uses only command forms `enforce-bash-safety.py`
permits, *specifically so it cannot deadlock under the guard.*

It is **not referenced in `settings.json`.** It has been sitting on disk, finished, for ten weeks.

Two things are stale about it and both are fixable in an afternoon:

1. Its clocks are calibrated to June's fill rate — watermarks 30/60 GB, `/tmp` age **3 days**. Today's
   fleet needs hours, not days.
2. Its scope covers `.claude/worktrees` + direct `/tmp` children only. It misses `tsx-501`,
   `/var/folders` fixtures, `lane-worktrees`, and `engine-bay-runs` — which together are the majority
   of today's bytes.

**The frame is right and the safety predicates are already adversarially self-checked. The move is to
widen its allowlist and tighten its clocks — not to author a new thing.**

---

## THE INDUSTRY-STANDARD DESIGN

Four layers, in the order the field actually stacks them. Each is a boring, named pattern with a
precedent. **Nothing here is novel** — that is the point.

### Layer 0 — HARD CEILING: put the fleet's scratch on a quota'd APFS volume

**CONFIGURE, not build.** Verified available on this machine:

```
diskutil apfs addVolume <containerRef> APFS <name> [-reserve <reserveSize>] [-quota <quotaSize>]
```

Create a scratch volume with a quota; point `TMPDIR`, `FLEET_RUN_ROOT`, the lane-worktree root, and
the Claude scratchpad root at it.

**Consequence:** the fleet *physically cannot* take the Mac down. When it overruns, it gets `ENOSPC`
inside its own box, one run fails loudly with a clear cause, and Robert's machine never freezes. This
converts a machine-wide outage into a single failed job.

Precedent: cgroup v2 io/storage limits, Kubernetes `ephemeral-storage` limits, ZFS dataset quotas.
**This is the one move that makes every layer below it a nicety rather than a load-bearing necessity.**

**Sizing caveat, stated plainly:** the APFS container is at **89.2% used** with **53.4 GB not
allocated**. A 60 GB quota volume cannot be carved from internal today — it must come *after* a reclaim,
be sized to what is actually free, or land on the Extreme SSD.

**Composition with the 08-23 ruling:** this *is* standing rule S1 (*"fan-outs never on internal disk"*)
and S4 (*"one env var names spill root"*), given teeth by the filesystem instead of by discipline.
`$NEURON_SPILL_ROOT` already exists in `~/.zshrc` and already points at the SSD spill volume. Layer 0
is that pointer made **enforceable** — the difference between a rule agents are asked to remember and a
wall they cannot walk through. It adds nothing new to the plan; it makes the plan's own rule real.

### Layer 1 — OWNER + TTL AT CREATION

**ALREADY BUILT — `scratch-registry.sh` (#383, 2026-08-16). Give it hands and call sites.**

Every allocation must register itself at *birth*, not at death. This already exists and is already
correct: a two-phase contract that records `{path, arc, pid, starttime, product_ref}` at creation and
emits certified rows for benches that are **provably dead** (`pid + starttime` — its header notes that
*"names never identify processes"*, which is the right subtlety; pids recycle).

**Why this and not cleanup-on-exit:** *on this box exits are kills.* A `finally`, a `trap EXIT`, an
`afterEach` — every one assumes a graceful exit that account switches and helm reseats structurally
deny. A registration is readable **after** the owner is dead. That inverts the whole problem, and it is
the only thing that does.

Two gaps to close, both small:

1. **Call sites.** Its only callers today are itself and `fleet-state.sh`. It needs to be called at
   every allocation: bay run dirs, lane worktrees, `mkdtemp` fixture roots, session scratchpads.
2. **A consumer with hands.** `candidates` already emits certified disposal rows. Under Layer 4 the GC
   consumes them directly instead of a human pasting them.

Add one field it lacks: `expires_at`. Today the only disposal signal is *owner is dead*; a TTL also
covers the live-but-abandoned case (a session that stays alive for hours holding a finished fixture).

This is also **warp#558's ratified altitude ruling** applied beyond worktrees: *ownership is DECLARED,
never inferred from git state.* Every place the current machinery *infers* ownership (merged+clean+idle,
`-mtime +1`, `pgrep run-sealed.sh`) is a place this layer replaces a guess with a record.

Precedent: Kubernetes `ownerReferences` + finalizers, systemd-tmpfiles age fields, S3 lifecycle rules,
Nomad allocation GC.

### Layer 2 — TWO-WATERMARK GC ON THE HEARTBEAT THAT ALREADY EXISTS

**MOSTLY CONFIGURE — the code exists.** `com.warp.bay-health` already fires `bay-health-tick.sh` every
**1800 s** at launchd's hand. Hang the GC there. **No new daemon.**

| Mark | Free space | Behavior |
|---|---|---|
| Soft | below 60 GB | Reclaim expired leases + past-horizon pools up to 90 GB free. Silent. |
| Hard | below 25 GB | Reclaim aggressively to 60 GB. One desktop notification. |
| Floor | below 5 GB | Existing guard. Should never fire again. |

High ≠ low is the anti-thrash mitigation — exactly kubelet's `imageGCHighThresholdPercent` 85 /
`imageGCLowThresholdPercent` 80, and Nix's `min-free` / `max-free`. Victim order: oldest-first within
each pool; pools ordered by (reclaimable bytes ÷ risk).

Reuse `~/.claude/specs/auto-cleanup/impl/disk-reap.mjs` — it already implements watermarks, oldest-first,
re-check-after-each-delete, dry-run default, and hook-safe command forms.

**Every pool needs BOTH an age rule and a size cap — never age alone.** The tsx cache is the proof on
this very machine: it *does* self-expire at 7 days, and it still reached 9 GB and 65,535 entries,
because a burst writer fills the window faster than the window closes. Age-only bounding is exactly
what failed here, in five different pools, for the same reason. The field learned this too: Docker
BuildKit carries `keepDuration` **and** `keepStorage`; Bazel added a disk-cache size limit on top of
its age policy. Age controls the tail; only a cap controls the burst.

### Layer 3 — REAP-ON-HARVEST, not reap-on-calendar

**WIRE THE EXISTING SWEEPER.** A run dir's correct expiry is *the moment its evidence is banked
elsewhere* — not seven days later. `sweep-finished-runs.sh` already computes exactly that predicate.
It has never had a caller.

Two edits:

1. Call it at the end of the harvest path in the bay dispatcher — **the run reaps itself when its own
   evidence lands.**
2. Add `bay-sweep-harvested` (and/or `bay-retention-apply` with an hours-scale horizon) to
   `bay-schedule.sh`'s closed `JOB_NAMES` table, fired on the existing 30-minute tick.

Note: the job table's comment — *"Every body here is scan/report only: nothing deletes"* — is currently
load-bearing doctrine. Changing it is a policy decision, which is exactly what Layer 4 authorizes.

Also needed: **dead escorts orphan their run dirs with no terminal ledger row**, and both existing
sweepers keep anything without one (fail-closed, correctly). The watchdog already knows which
dispatchers are dead (7 RED today). Feed that verdict in: *escort proven dead + no terminal row* is its
own disposition, not an eternal keep.

### Layer 4 — THE ALLOWLIST IS THE AUTHORIZATION (Robert's ONE tap)

One file Robert ratifies **once** — e.g. `~/next-arc/disk-policy.json`:

```json
{
  "pools": [
    {"root": "/tmp/engine-bay-runs",           "ttl": "harvested+2h",    "cap_gb": 15},
    {"root": "$TMPDIR/tsx-501",                "ttl": "24h",             "cap_gb": 3},
    {"root": "$TMPDIR", "match": "fixture-*",  "ttl": "12h",             "cap_gb": 10},
    {"root": "/private/tmp/claude-501",        "ttl": "dead-session+2h", "cap_gb": 20},
    {"root": "~/lane-worktrees",               "ttl": "lease",           "cap_gb": 25}
  ],
  "watermarks": {"soft_free_gb": 60, "hard_free_gb": 25, "target_free_gb": 90},
  "never_touch": ["~/vetharness-sites", "~/neuron-clean-for-bay", "*/BENCH_LIVE",
                  "*/LEASE.json:live", "~/.claude/engine-bay/ledger.*"],
  "ratified_by": "Robert", "ratified_at": "…"
}
```

**Everything inside the policy runs without asking. Anything outside it — a new root, a shortened TTL,
a raised cap — stops and asks.**

This satisfies the constitution's *"no agent ever deletes or destroys outside its named scope"* by
making the scope a **ratified document** rather than a per-incident conversation. And it satisfies
Robert's rule by making deletion authority **one standing authorization of a bounded policy**, not a
tap per crisis. He signs the shape of the box once; the machinery works inside it forever.

**The REAP-REACH protection, mechanized.** Before the GC deletes any path, it greps the *registration
surfaces* for that path — `settings.json` hooks + statusline, `crontab -l`,
`~/Library/LaunchAgents/*.plist`, `~/.claude/engine-bay/schedule-registry/`, and live `lsof`/cwd. **Any
hit = KEEP + report.** That turns the fence-brick incident (ESC-2) from a lesson somebody has to
remember into a predicate the machine enforces. Same idea as a package manager's reverse-dependency
check before a remove.

### And fix the guard so it cannot block its own cure

Two changes to `disk-space-guard.py`:

1. Allow the ratified reclaim commands by **exact path**, not prefix — `launchctl kickstart` of the
   named jobs, `bay-retention.sh --apply`, `disk-reap.mjs`, `scratch-janitor.sh`. *A cure the guard
   forbids is not a cure.*
2. On block, **fire the GC in the background first, then block.** The message becomes *"reclaiming N GB
   now, retry in 30 s"* instead of *"stop and report."*

---

## MIGRATION PATH — what lands first

| # | Move | Kind | Effort | What it buys |
|---|---|---|---|---|
| 1 | Add `bay-sweep-harvested` (`sweep-finished-runs.sh --apply`) to the bay job table; fire on the existing 30-min tick | **wire existing** | ~30 min | 8.7 GB now, and caps the single biggest recurring writer permanently |
| 2 | Guard: exact-path allowlist for reclaim commands + kick-then-block | **edit 1 file** | ~20 min | the guard stops blocking its own cure |
| 3 | Ratify `disk-policy.json` | **Robert's one tap** | 5 min | authorizes everything below |
| 4 | Point `TMPDIR` + `FLEET_RUN_ROOT` + scratch roots at a quota'd volume | **configure** | ~1 h | the Mac can never freeze again |
| 5 | Wire `disk-reap.mjs` to the 30-min tick with policy watermarks + registration-surface grep | **wire existing + 1 predicate** | ~2 h | steady state; the daily janitors retire |
| 6 | Call `scratch-registry.sh register` at every allocation (bay runs, worktrees, `mkdtemp` fixtures, scratchpads); add `expires_at`; let the GC consume `candidates` | **wire existing + 1 field** | ~half day | kills the cleanup-on-exit class outright |
| 7 | Bound `tsx-501`: set `TMPDIR` per lane (tsx roots its cache at `os.tmpdir()`), plus a policy pool with a size cap. Do **not** use `TSX_DISABLE_CACHE` — that trades disk for a large, permanent compile-time cost | **configure** | ~30 min | 9 GB, and caps the largest unattributed writer. tsx's own 7-day TTL is real but structurally cannot fire inside a burst |

**Moves 1 and 2 are both pure wiring, both doable today, and together they would have prevented all
three of today's freezes.** Move 3 is the only thing that needs Robert.

---

## WHAT EACH EXISTING PATCH BECOMES

| Patch | Fate | Why |
|---|---|---|
| `scratch-janitor.sh` (04:30 daily) | **ABSORBED** | Its predicate (dead transcript + no recent write + no open handle) is *correct*; it becomes the `dead-session` TTL rule in the policy, evaluated every 30 min instead of once a day. Keep the script, drop the plist. |
| `disk-space-guard.py` (5 GB block) | **KEPT, DEMOTED** | Stays as the floor; gains the allowlist fix and kick-then-block. Under the new design it should never fire. |
| `bay-retention.sh --scan` (daily) | **KEPT + GIVEN HANDS** | The scan stays as the census. An apply-mode job with an hours-scale horizon joins the table beside it. |
| `sweep-finished-runs.sh` | **PROMOTED** | From unreferenced code to the primary run-dir reclaimer, called on harvest. |
| `disk-cleanup.sh` (hourly, `-mtime +1`) | **RETIRED** | Its 1-day floor *is* the failure mode, and it burns 17 minutes per run on `du` walks. Its size warnings fold into the watermark layer. |
| `disk-watch-hourly.sh` | **RETIRED** | A log line nobody reads. Session headline + watermark notifications replace it. |
| `disk-keeper-nightly.sh` (80% alarm) | **KEPT** | Its mirror/bundle duty is unrelated and valuable. The alarm becomes redundant but harmless. |
| `reap-worktrees.sh` / `reaper.sh` §E | **KEPT, RE-BASED** | Swap the *inferred* merged+clean+idle predicate for the *declared* lease — warp#558's own ruling. The blanket `pgrep run-sealed.sh` stand-down then becomes unnecessary, which is what currently keeps it idle most of the day. |
| `reap-neuron-tmp-clones.sh` | **ABSORBED** | Its neuron-clone rule becomes one rule among several under the claude-501 pool; the pool rule catches the non-clone bytes it structurally misses. |
| Manual certified reaps | **RETIRED as routine** | Survive as the escape hatch for anything outside the ratified policy. |
| `scratch-registry.sh` (#383) | **PROMOTED** | From a report-row generator with two callers to the lease layer the whole GC reads. Add `expires_at`; call it at every allocation. |
| `~/.claude/specs/auto-cleanup/impl/` | **ACTIVATED** | Ten weeks old, finished, never installed. Widen scope, tighten clocks, wire it. |

---

## THE ONE THING TO REMEMBER

Five launchd registrations, six purpose-built disk instruments, one adversarially self-checked GC
implementation sitting finished on disk for ten weeks — and the disk still died three times in two
hours.

**Building more cleanup was never going to work, because the cleanup was never the missing piece.**
Five of the six instruments are excellent and are structurally forbidden from acting. They were built
that way on purpose, each citing the same law, and the law is *right*: every scar on this machine came
from a delete with no ratified scope behind it.

What was never built is the other half of that law — **the ratified scope itself.** One signed policy
naming the pools, their TTLs, their caps, and what is never touched. Sign it once and every instrument
already on disk wakes up with hands. Sign nothing and the next reclaimer someone builds will also ship
report-only, and the disk will fill again on Thursday.

Underneath that, one physical fact worth more than all the software: **a quota'd volume the fleet
cannot exceed.** With it, an overrun is one failed job. Without it, an overrun is Robert's whole
machine.
