# Autonomy P3 — Daemon Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** `harnessd` — process ownership (A1), decision inbox (A5), notifications (W9/I11), trust ramp (W10), budget enforcement (V2), digest (V1), queues/windows (X1/X4/X6), handoff bundles + run reports (X11/X12), backups (W12), status cache (W5). After this lands, the babysitter is gone: full chaos suite passes end-to-end unattended.

**Architecture:** Canonical spec: `spec/DAEMON.md` — state machine, tick loop, sockets, re-attach, tests all pinned there; implement per spec. Daemon is stateless (I8): every behavior = library + CLI verb first, daemon schedules library calls (`spec/IMPLEMENTER-NOTES.md` § Transition seam). Extends `src/supervisor.js` + `src/control-api.js` — never replaces.

**Tech Stack:** Node, systemd user units, ntfy/webhook.

**Sequencing:** After `autonomy-p2b-resolver-watchdog` lands. NOTE: protected-path enforcement is live from P2b and this plan touches engine `src/` — the `g-protected-whitelist` gated decision must be RESOLVED at preflight before dispatch. After this plan lands, W13 dogfooding governs: P4+ plans are authored as runplan plans and executed BY the harness.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1, Task 2 | bin/harnessd, src/daemon.js · src/inbox.js | ✅ no overlap |
| 2 | Task 3, Task 4 | src/daemon.js, bin/runplan, src/runner.js · src/notify.js | ✅ no overlap |
| 3 | Task 5, Task 6 | src/control-api.js · systemd/ | ✅ no overlap |
| 4 | Task 7 | src/daemon.js, src/digest.js | single task |
| 5 | Task 8 | src/daemon.js | single task |
| 6 | Task 9 | src/report.js, src/daemon.js | single task |
| 7 | Task 10 | test/chaos/scenarios/ | single task |

---

### Task 1: Daemon core — state machine, tick loop, re-attach

**Wave:** 1 · **Blocks:** Task 3, Task 7 · **Blocked by:** —

**Files:**
- Create: `bin/harnessd`, `src/daemon.js` + colocated `src/daemon.test.js`.

**Contract:** `spec/DAEMON.md` is canonical and exhaustive: run state machine (every edge verbatim), tick loop (exact 8-step order, idempotent ticks), stateless invariant I8 (all state re-derivable — derived labels, no stored state field), re-attach procedure, journal-first transitions. Resolver invoked as library (P2b) in `resolving` state. Spec tests 1–3, 5 live here.

**Acceptance:**
- Run: `node --test src/daemon.test.js`
- Expected: PASS — table-driven transitions (every edge, illegal transitions assert), tick-twice idempotence, re-attach convergence from every state

- [ ] Tests FROM spec test list first · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 2: Decision inbox library (A5)

**Wave:** 1 · **Blocks:** Task 3 · **Blocked by:** —

**Files:**
- Create: `src/inbox.js` + colocated test.

**Contract:** `spec/DAEMON.md` § Decision inbox: `decision.requested`/`decision.answered` payloads verbatim, `d-<seq>` ids, first-answer-wins + 409 semantics, `default.afterSec` auto-answer ONLY at autonomyLevel ≥2, dependents wait while siblings proceed, escalation writes X11 handoff bundle BEFORE notification.

**Acceptance:**
- Run: `node --test src/inbox.test.js`
- Expected: PASS — double-answer 409; default never fires at level ≤1

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 3: Control socket + `--unattended` submit + `runplan answer`

**Wave:** 2 · **Blocks:** Task 5, Task 7 · **Blocked by:** Task 1, Task 2

**Files:**
- Modify: `src/daemon.js` (socket verbs), `bin/runplan`, `src/runner.js` (client verbs).

**Contract:** `spec/DAEMON.md` § Sockets: daemon socket `~/.harness/harnessd.sock`, verbs `submit`/`list`/`answer`/`status`/`shutdown {drain}`. `runplan <slug> --unattended` = client (validates via preflight, hands to daemon); foreground unchanged. `runplan answer <slug> <decisionId> <choice>` → inbox library. Per-run supervisor socket + verbs untouched.

**Acceptance:**
- Run: chaos unattended-submit scenario
- Expected: submit → daemon launches through preflight; dirty preflight → `blocked-on-decision`, never a silent launch

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 4: Notifications (W9, I11)

**Wave:** 2 · **Blocks:** Task 7 · **Blocked by:** — · **Gated:** `g-notify-channel` (channel config is operator input)

**Files:**
- Create: `src/notify.js` + colocated test.

**Contract:** `spec/DAEMON.md` § Notifications: channel config via X10 (`resolveConfig`), ntfy actions POST to control-api answer endpoint with auth header, send failure → `notify.failed` + retry ×3 next ticks → give up (decision stays answerable — latency loss, never decision loss). Non-blocking always.

**Acceptance:**
- Run: `node --test src/notify.test.js` (dead-channel fixture)
- Expected: PASS — `notify.failed` journaled, decision still answerable via CLI

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 5: Control-api token auth + answer/status proxy

**Wave:** 3 · **Blocks:** Task 7 · **Blocked by:** Task 3

**Files:**
- Modify: `src/control-api.js`.

**Contract:** `spec/DAEMON.md` § Sockets, auth paragraph verbatim: token generated at daemon first start into `~/.harness/token` (mode 0600), `Authorization: Bearer` on EVERY request, `crypto.timingSafeEqual`, 401 without, never default/empty. Bind localhost or Tailscale interface ONLY, never 0.0.0.0. Proxy `answer` + `status` to daemon socket.

**Acceptance:**
- Run: control-api tests
- Expected: request without/with-wrong token → 401; valid token answers a decision end-to-end

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 6: systemd units + resource-scoped spawns

**Wave:** 3 · **Blocks:** Task 7 · **Blocked by:** —

**Files:**
- Create: `systemd/harnessd.service`, `systemd/harnessd-daily.timer` (+ service), install/uninstall via `bin/harness-init.sh` additions.

**Contract:** `spec/DAEMON.md` § Processes: `Restart=on-failure`, `RestartSec=5`, `WantedBy=default.target`; daily timer → digest rollup + W12 backup + gc dry-run report. Agent spawns via `systemd-run --user --scope` with CPU/memory caps (I6), `setsid`+ulimit fallback, capability probed once at startup + journaled.

**Acceptance:**
- Run: `systemd-analyze verify systemd/harnessd.service` + install→status→uninstall round-trip
- Expected: unit valid; daemon restarts after induced crash

- [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 7: Trust ramp + budgets + digest + status cache + backup

**Wave:** 4 · **Blocks:** Task 8 · **Blocked by:** Task 1, Task 3, Task 4, Task 5, Task 6

**Files:**
- Create: `src/digest.js`; Modify: `src/daemon.js`.

**Contract:**
- W10 (spec § Trust ramp): submit gate `requested > allowed` → refuse with stats + thresholds; level 2 REQUIRES non-null budgets; daemon never edits `autonomyLevel`.
- V2 budgets (spec tick step 3): task ceiling → `budget-exceeded` quarantine; run ceiling → paused + decision (pause, never kill).
- V1 digest: per-run on terminal + daily rollup → `runstate/digests/<date>.md` + notification channel; content per handoff plan V1 (commits+diffstat, quarantines, `.warnignore` deltas, auto-resolved decisions, cost).
- W5 status cache `~/.harness/status-cache.json` (tick step 7 + journal watcher). W12 backup in daily timer path.

**Acceptance:**
- Run: chaos budget scenario + terminal-state scenario
- Expected: `maxUsdPerTask` breach quarantines; digest file lists injected quarantine + cost; status cache reflects live state

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 8: Queues + windows (X1, X4, X6)

**Wave:** 5 · **Blocks:** Task 9 · **Blocked by:** Task 7

**Files:**
- Modify: `src/daemon.js`.

**Contract (handoff plan PART VIII):**
- X1 meta-plans: `meta.depends_on: [<slug>...]` → `queued-on-deps`; released when every dep journal shows terminal-landed (or `meta.deps_accept_partial: true`); cycle detection at queue time; `plan.queued {on}` / `plan.released {by}` records; dep quarantined without accept_partial → escalate `blocked-on-dep`.
- X6 run windows: `runWindow {start, end, tz}` — outside-window unattended submits queue (`plan.queued {on: "window"}`), auto-launch at open, `--now` overrides.
- X4 maintenance window: open + zero user runs → housekeeping ladder ONE step per tick under `maint-` slug prefix; user submit preempts before next step.

**Acceptance:**
- Run: daemon queue tests (clock-mocked)
- Expected: fixture pair A→B launches B only after A lands; outside-window queues then launches at open; mid-ladder submit preempts

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 9: X11 handoff bundles + X12 run reports

**Wave:** 6 · **Blocks:** Task 10 · **Blocked by:** Task 8

**Files:**
- Create: `src/report.js`; Modify: `src/daemon.js` (terminal-state + escalation hooks).

**Contract (handoff plan X11/X12):**
- X11: every escalation writes `runstate/handoffs/<slug>-<decisionId>.md` — halt reason verbatim, journal tail, snapshot path (W6), config provenance (X10), suggested actions with resolver rationale — BEFORE the notification (which links it).
- X12: terminal state → `docs/plans/<slug>-report.md` (outcome table, per-task + total cost, decisions by whom, lessons, `.warnignore` deltas) generated from the SAME reducer as the journal summary (no second source of truth), committed with the landing.

**Acceptance:**
- Run: chaos escalation + terminal scenarios
- Expected: bundle contains exact injected failure string; report table matches journal summary exactly

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 10: Full unattended chaos + soak

**Wave:** 7 · **Blocks:** — · **Blocked by:** Task 9

**Files:**
- Modify: `test/chaos/scenarios/` — ungate P3 assertions incl. daemon-kill (C5-class) + soak per `spec/CHAOS-SUITE.md` phase map.

**Acceptance (the epic's definition of done for babysitter removal):**
- Run: `bash run-tests.sh` + full chaos plan end-to-end via `--unattended` (crash-resume + pre-authorized default decision included) + soak N cycles
- Expected: green unattended, zero orphaned worktrees/branches/processes/locks after soak

- [ ] Ungate, run, commit
