# Stages 2 and 3 — the plan, and the one question

## Stage 2 — off the laptop

**Where:** `/root/personal-cos/` on the always-on box. Never under `/root/.hermes/`, and the
systemd unit carries `InaccessiblePaths=/root/.hermes` so the separation is enforced by the
kernel rather than by good intentions.

**Verified before writing a line of it (2026-07-29):**

- box is `Australia/Sydney`, same as the laptop — 18:30 needs no translation
- Python 3.12.3, 37 GB free, 6.6 GB RAM available
- systemd 255, and a `systemd-creds encrypt --with-key=host` → `decrypt` round-trip
  returned the plaintext. No TPM on this box (`has-tpm2: partial`), host key works.

**Secrets:** `LoadCredentialEncrypted=pcos-env:/root/personal-cos/secrets/pcos-env.cred`.
No `.env` on the box. The credential is decryptable only by that host, so a stolen disk image
is inert. `notify.read_env()` learns to read `$PCOS_ENV_FILE` so the same code runs both places.

**Joins the existing box discipline:** manifest hashing, `night_watch`, immutability locks,
box↔repo md5. Its own private git repo — `data/`, `state/`, `logs/` and `.env` gitignored, and
it never goes near the engine repo, which is public-shaped and forbids personal data.

---

## The fork — what watches it from a different failure domain

This is the one thing worth Rocky's thirty seconds, because it is the difference between a
system that stops and tells you, and a system that stops.

**The problem stated plainly.** `heartbeat.py` shouts if no brief has succeeded in 26 hours.
On the laptop today it shares everything with the thing it watches: same machine, same Task
Scheduler, same user session, same power state. A sleeping laptop takes both down together and
nothing says a word. Moving both to the box fixes the sleeping, and keeps the flaw: a heartbeat
on the box cannot report that the box is dead.

**Three ways to close it.**

| | how it works | catches a dead box | cost | new dependency |
|---|---|---|---|---|
| **A — laptop watches box** | a Windows task pulls the box's `last_brief.json` over SSH and Telegrams if stale | yes, whenever the laptop is on | ~20 lines | none |
| **B — external dead-man** | box pings a hosted dead-man URL after each brief; the service alerts if the ping stops | yes, always | ~3 lines | a third party, which is the thing this rebuild was meant to reduce |
| **C — fold into `night_watch`** | the 02:00 box pass adds a brief-freshness check | no — same box | ~10 lines | none |

**A IS BUILT — 2026-07-29.** `watch_box.py`, scheduled on the laptop at 21:00, after the box's
18:30 brief and its own 20:00 heartbeat have both had their say. It was built rather than left
for a ruling because it needs **nothing from Rocky**: no vendor, no account, no new credential.
SSH and the Telegram token already existed. Leaving the guarantee unmet while the zero-cost
option sat undone would have been stopping short.

Proven, each with a receipt on his phone:

| drill | result |
|---|---|
| box unreachable | alerted, `message_id=15` |
| box alive, brief 30h stale | alerted, `message_id=16` |
| healthy | silent, exit 0, receipt written |
| streak reaches 14 | announced once, `message_id=18`, then silent |

**B and C remain available.** B is the only genuinely always-on option and costs one small
vendor; switching is a few lines plus an account. C is ten lines inside `night_watch` and
catches the script-broke case from a second angle on the same box. Neither is foreclosed.

**The residual that remains, stated rather than buried:** the laptop sleeps. If it is shut for
three days, this does not run for three days, and the alert arrives late rather than never
(`StartWhenAvailable` is set, so a missed check runs on the next wake). That is the honest
price of adding no vendor. **If Rocky wants cover with no gap at all, that is option B and it
should be his deliberate choice.**

---

## Stage 3 — judgement, bolted on the side

**Structurally incapable of suppression.** The judgement pass reads the rail's output and
writes `data/judgement/<date>.json`. It has no write access to findings and no code path that
edits or deletes a register row. The brief renders it as an extra section beneath what the
rules found. If the model returns nothing, or garbage, or never runs, the deterministic brief
is unchanged. That is the whole design.

**The COO persona gets repointed, and its gate deleted.** Three things blocked it, not one:

1. **The email constraint** — it was a brain with no senses. Solved 29/07.
2. **Its own boot gate** — step 4 of the boot sequence says *STOP* if `goals.md` is
   `status: draft-awaiting-rocky-review`. It still is. `state.json` reads `boot_blocked: true`,
   `last_tick: null` — it has never ticked once. The gate goes, it is not satisfied. Principle
   one of the design document exists because of this exact file.
3. **It had no mouth either** — `state.json` lists `@hq_coo_bot` as *"needs creation in Phase 2"*.
   It was never created. Stage 3 reuses the Tally bot, which is already proven and already has
   exactly one destination.

**Stripped:** three May RateRight goals measured against a Supabase paused on 24 July, PM2
service management, the hunt pipeline, the kanban dispatch contract, the four lane-worker
profiles. **Kept:** the tone, the "tell him something he doesn't already know" test, the
blocked-classes list, and the discipline of never fabricating a measurement.

**Repointed at** `CONTEXT.md` — the live obligations and open decisions the mailboxes cannot
know. That is where the ASIC ~19/09 penalty date lives, for instance: it is derived from
Lawpath's stated 60-day rule, appears in no email as a date, and therefore can never reach the
register. Judgement is exactly the layer that should be raising it.
