Generalize the focus controller into a harness hosting swappable modes

Loosen AntiDrift's session controller into Keel's general collect→brain→act
loop. A new internal/harness runs at most one mode.Mode at a time, fanning
async completions out to the web SSE and status-bar surfaces.

- internal/mode: the Mode contract (Kind/Command/View/Active) plus optional
  EvidenceConsumer and Expirer ports, and the surfacing Envelope.
- internal/mode/focus: the former session/domain/statemachine packages moved
  under the mode, now satisfying the harness contracts unchanged.
- internal/mode/offscreen: a one-shot away-from-desk mode built on the new
  ai.Proposer, which turns a life-domain brief into one off-screen action.
- cmd/keeld replaces cmd/antidriftd; daemon wires focus + offscreen factories
  with per-mode persistence under ~/.keel/modes/<kind>.
- Finish the rename: KEEL_* env refs in the README, /keeld build artifact
  ignored, stale antidriftd binary removed.

Include the design + implementation plan this refactor was built from under
docs/superpowers/{specs,plans}/2026-06-04-controller-refactor*.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 18:00:49 -04:00
parent 258de2c14b
commit 7d69a1f320
57 changed files with 4069 additions and 627 deletions
+56 -41
View File
@@ -18,12 +18,11 @@ status: authoritative
> **Name & home.** *Keel* is the system: a human-harness that helps Felix hold
> course toward his higher goals. The name answers AntiDrift's metaphor — *drift*
> is what a vessel does with nothing to hold it; the *keel* resists drift and
> keeps you on an even keel (steady mind). **This repo is now `keel`** — AntiDrift's
> focus harness becomes Keel's first *mode*. The directory and git repo are
> renamed; the **code identity is intentionally still `antidrift`** (Go module
> `antidrift`, binary `antidriftd`, runtime `~/.antidrift/`, `ANTIDRIFT_*` env).
> That *code* rename is **deferred** until the controller refactor (§8) because
> `~/.antidrift/` owns the live ledger/state and needs a migration, not a `mv`.
> keeps you on an even keel (steady mind). **This repo is `keel`** — AntiDrift's
> focus harness becomes Keel's first *mode*. The rename is **complete**: directory,
> git repo, Go module (`keel`), binary (`keeld`), runtime (`~/.keel/`), and env
> (`KEEL_*`) are all `keel`. The old `~/.antidrift/` ledger needs a one-time manual
> move to `~/.keel/` for existing installs.
> Moved from `~/dev/higher/` on 2026-06-04 — that directory was the scaffold where
> the decision to extend AntiDrift was made, and now retires.
@@ -54,13 +53,15 @@ operator.
2. **Reuse AntiDrift's engine; loosen its controller.** AntiDrift already is this
harness for one plane. Its controller is welded to *commitment = next_action +
success_condition + timebox*. We loosen that into a general **collect → brain →
act** loop where a focus-session is just **one mode**. Other modes: house,
body, review, capture, planning. The core architecture is salvageable — we
widen scope, we don't rebuild.
- Worked example (a non-focus mode): from the phone web UI, *"what work on the
house should I do next?"* → the harness reads the house bugs/tasks, the brain
proposes a strategy, the UI asks Felix for feedback, and on approval an
effector files the task.
act** loop where a focus-session is just **one mode**. The harness now hosts a
second, **off-screen** mode; further modes (body, review, capture) are
aspirational. The core architecture is salvageable — we widen scope, we don't
rebuild.
- Worked example (the shipped non-focus mode): from the phone web UI, *"what
worthwhile off-screen thing should I do now?"* → the harness reads today's
Marvin tasks + the `~/owc` goals and life-domain `bug-*.md`, the brain
proposes one off-screen action, the UI asks Felix for feedback, and on
approval the Marvin effector files the task.
3. **Storage = ActivityWatch.** AW is already a general append-only event store
with a REST API, and it is currently the single richest *unused* asset
@@ -72,7 +73,7 @@ operator.
- **Web UI** — configure, interact, discuss; phone-friendly. The place Felix
*talks to* Keel.
- **WM status bar** — reflect current status (AntiDrift already writes
`~/.antidrift_status`). The reminder sentence lives here; it is one *status
`~/.keel_status`). The reminder sentence lives here; it is one *status
element*, **not** a deliverable. (No "nice summaries" as a product.)
5. **Life-information root = `~/owc`** (owncloud markdown), formerly called
@@ -115,7 +116,7 @@ Each component has one job, a defined interface, and can be built/tested alone:
- **① Collectors** — one small read-only adapter per source. Contract:
`read(window) -> Signal[]`. Today's real interfaces: AW `POST :5600/api/0/query/`;
AntiDrift `tail ~/.antidrift/audit.jsonl` (+ live SSE `:7777/events`); HQ
AntiDrift `tail ~/.keel/audit.jsonl` (+ live SSE `:7777/events`); HQ
read-only `hq.db` (+ `:8765`); Consume `GET :8000/api/...`; daily read-only
`daily.db` (+ `:2200`); Beeminder `beeline`; Marvin `ammcp` MCP / `am --json`.
Depends only on the source being up; degrades to a dropped signal if not.
@@ -168,33 +169,38 @@ Keel is AntiDrift's port set, widened. The parts already exist:
| Brain | `ai.Backend` → local `claude`/`codex` | + Hermes (remote); pluggable interface |
| Memory | ephemeral (dies in `state.json`) | **AW buckets** (durable, queryable) |
| Effectors | `enforce.Guard` (window-minimize) | + Marvin, capture, Beeminder, `~/owc` notes |
| Surfaces | web UI `:7777` + `~/.antidrift_status` | richer web UI (phone) + status bar |
| Surfaces | web UI `:7777` + `~/.keel_status` | richer web UI (phone) + status bar |
| Control loop | session state machine (locked/planning/active/review) | **modes** (focus is one); a session is one mode |
**Loosening the controller** concretely means: generalize `Commitment` from a
work-session into a *mode invocation* (a mode has its own collectors, frame slice,
brain prompt, and allowed effectors); the focus-session remains the
`next_action/success_condition/timebox` mode; a "house" mode reads house bugs and
proposes a strategy; a "capture" mode just routes a thought. The runtime state
machine becomes the loop in §3.
`next_action/success_condition/timebox` mode; the off-screen mode reads the
life-domain bugs + goals and proposes one off-screen action; a future "capture"
mode would just route a thought. The runtime state machine becomes the loop in §3.
---
## 5. One loop, concrete
House mode, from the phone, end to end:
1. **Collect**`~/owc/resources/bug-*.md` house items + Marvin "house" tasks +
(memory) what was proposed/done last weekend.
2. **Remember** — read Keel's AW `keel.state` bucket for prior house proposals so
it doesn't repeat itself.
3. **Assemble** — those + the `house-integrity` goal from `~/owc/goals-2026.md`.
4. **Brain**chosen brain returns a proposed next house task + short strategy.
5. **Surface** — web UI shows the proposal and asks for feedback.
6. **Act** — on approval, the Marvin effector files the task; Keel writes an
`action_taken` event to its AW bucket (memory closes the loop).
Off-screen mode, from the phone, end to end**this is what ships today**:
1. **Collect**today's Marvin tasks (`am --json`) + the `~/owc/goals-2026.md`
goals + the life-domain `~/owc/resources/bug-*.md` notes, assembled into a
single budgeted brief.
2. **Brain** — the chosen brain (`ai.Proposer`) returns one worthwhile off-screen
action plus a short rationale.
3. **Surface**the phone-first web UI renders a proposal card and asks Felix to
confirm or dismiss.
4. **Act** — on confirm, the Marvin `Create` effector files the action as a task
(CRDT-preserving `am add`); the mode is one-shot and the harness returns idle.
No new store touched. The brain was rented for one call. Everything else was Keel.
**Next increment:** memory. Off-screen does not yet read or write Keel's AW
buckets, so it cannot remember what it proposed last time. Adding a *remember*
step (read `keel.state` for prior proposals) and an *act* event
(`proposal_made`/`action_taken` to `keel.events`) closes the loop per §7.
---
## 6. Sources & targets (the real ecosystem)
@@ -202,7 +208,7 @@ No new store touched. The brain was rented for one call. Everything else was Kee
| Tool | Read | Write | Role | Source of truth |
|---|---|---|---|---|
| ActivityWatch | `POST :5600/api/0/query/`, sqlite | new buckets via REST | sensor **+ store** | `peewee-sqlite.v2.db` |
| AntiDrift | `audit.jsonl`, SSE `:7777` | `POST :7777` cmds, enforce | focus mode + enforcement | `~/.antidrift/` |
| AntiDrift | `audit.jsonl`, SSE `:7777` | `POST :7777` cmds, enforce | focus mode + enforcement | `~/.keel/` |
| HQ | `hq.db` r/o, `:8765` | `dashboard.write_artifact` | agent-execution evidence | `hq.db` (~23k events) |
| Consume | `GET :8000/api` | (later) | intake/reflection signal | item frontmatter |
| daily | `daily.db` r/o, `:2200` | — | mood/habit checkout | `daily.db` |
@@ -246,15 +252,22 @@ behavioral story) or left out initially.
- **Capture unification** — which single file is canonical (resolve
`~/owc/stream.md` vs `~/wrk/pkb/stream.md` vs dead inboxes) and rewiring `blurt`
to append there.
- **Controller refactor shape** — how invasive the AntiDrift generalization is; do
we branch the daemon or grow it in place. **This is also when the *code* rename
lands** — Go module / `antidriftd` binary / `ANTIDRIFT_*` env and
`~/.antidrift/``~/.keel/` with a state migration. (The directory is already
`keel`; only the code identity still says `antidrift`.)
- **Cross-device AW** — sync work-laptop AW or not (see §7).
- **Secret hygiene** — an increasingly autonomous operator runs as the full Unix
user; plaintext keys exist on disk; the privacy boundary in ③ must be real.
### Resolved
- **Code rename** — complete as of Phase 0 of the controller refactor. Go module
is `keel`, binary is `keeld`, runtime is `~/.keel/`, env prefix is `KEEL_*`.
Existing installs with a live ledger under `~/.antidrift/` need a one-time
manual move to `~/.keel/`.
- **Controller refactor shape** — resolved: grown in place, not branched. The
focus-only `session.Controller` is now a generic `harness.Harness` that hosts
one `mode.Mode` at a time (the `collect → brain → act` loop of §3); focus is the
first mode and off-screen the second. Per-mode persistence lives under
`~/.keel/modes/<kind>/`.
---
## 9. What this supersedes (from `~/dev/higher/`)
@@ -278,9 +291,11 @@ behavioral story) or left out initially.
## 10. Smallest real slice (a vertical, not a summary)
To stay honest to "ship visible proof" without boiling the ocean: the first slice
is **house mode, end to end** (§5) on the existing AntiDrift web surface — one
collector (`~/owc` house bugs + Marvin), the brain, the web UI proposal, and the
Marvin effector behind a confirm gate. It exercises every Keel component once, on
the smallest mode, and produces a real filed task — interaction and action, not a
coaching sentence.
To stay honest to "ship visible proof" without boiling the ocean, the first slice
is **off-screen mode, end to end** (§5) on the existing AntiDrift web surface —
collectors (`~/owc` goals + life-domain bugs + Marvin), the brain (`ai.Proposer`),
the phone-first web UI proposal card, and the Marvin `Create` effector behind a
confirm gate. It exercises every Keel component once except memory, on the
smallest mode, and produces a real filed task — interaction and action, not a
coaching sentence. **This slice now ships;** durable AW memory (§7) is the next
increment that closes the loop.