7d69a1f320
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>
302 lines
16 KiB
Markdown
302 lines
16 KiB
Markdown
---
|
|
title: Keel — Architecture
|
|
name: keel
|
|
category: architecture
|
|
created: 2026-06-04
|
|
updated: 2026-06-04
|
|
tags:
|
|
- keel
|
|
- human-harness
|
|
- architecture
|
|
- antidrift
|
|
- activitywatch
|
|
status: authoritative
|
|
---
|
|
|
|
# Keel — Architecture
|
|
|
|
> **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 `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.
|
|
|
|
---
|
|
|
|
## 1. What we're building (one sentence)
|
|
|
|
**A harness** — a framework that pulls Felix's real state out of his existing
|
|
tools, hands it to a swappable brain, and turns the reply into actions and two
|
|
surfaces. The brain, the storage, and the data sources are all **reused**; the
|
|
harness is the only thing we build. The target is one system (Keel — the single
|
|
screen), grown by **loosening AntiDrift's controller**, not by starting over.
|
|
|
|
The inversion holds: this is a *human-harness*. In an agent harness a model is
|
|
the observed worker; here Felix is the observed subject and the model is the
|
|
operator.
|
|
|
|
---
|
|
|
|
## 2. Core decisions (locked 2026-06-04)
|
|
|
|
1. **The product is the harness.** The brain (`claude` / `codex` / **Hermes**) is
|
|
rented, interchangeable compute reached over a CLI or the network — *not* the
|
|
point. The value is the framework that feeds information *in* and acts on what
|
|
comes *back*. Hermes is real (an agent harness on another machine); it is one
|
|
brain option, not a dependency.
|
|
|
|
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**. 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
|
|
(~1 yr / ~287k events). Keel gets its **own AW bucket(s)** for memory. **No new
|
|
database, no new event store.** AW is therefore both a *sensor* (screen-time
|
|
ground truth) and the *store* (Keel's memory).
|
|
|
|
4. **Two surfaces, both — no fork.**
|
|
- **Web UI** — configure, interact, discuss; phone-friendly. The place Felix
|
|
*talks to* Keel.
|
|
- **WM status bar** — reflect current status (AntiDrift already writes
|
|
`~/.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
|
|
"clawd". Values, goals, the 22 `resources/bug-*.md`, weekly reviews,
|
|
self-management frameworks, capture stream. **Reference, never duplicate.**
|
|
|
|
6. **Effectors: start gated, trend toward autonomy.** Day one: read + *propose,
|
|
Felix confirms in the web UI*. Target: the brain does a lot on its own —
|
|
**update Beeminder datapoints, update Marvin to-dos, update `~/owc` notes.**
|
|
The gate (propose-and-confirm vs. just-do-it) is a **config surface per
|
|
effector**, not a vibe — this is the agency-vs-cage line and it stays Felix's
|
|
to set.
|
|
|
|
7. **Still in force:** don't build a new event store; AntiDrift is the substrate;
|
|
reference source-of-truth; local-first; every session ends with visible proof;
|
|
the status line stays small.
|
|
|
|
---
|
|
|
|
## 3. The harness (the components we build)
|
|
|
|
```
|
|
SOURCES (read) KEEL (the framework = the product) BRAIN (swappable)
|
|
───────────── ────────────────────────────────────── ──────────────────
|
|
AW :5600 ───────┐ ┌──────────────────────────────────────┐ claude (local CLI)
|
|
AntiDrift ──────┤ feed │ ① COLLECTORS → ③ ASSEMBLER → [BRAIN] │ send ──▶ codex (local CLI)
|
|
HQ hq.db ───────┤ ───────▶ │ ▲ │ │ Hermes (remote API)
|
|
Consume :8000 ──┤ │ │ ▼ │ ◀─ reply
|
|
daily.db :2200 ─┤ │ ② MEMORY = AW buckets ④ EFFECTORS │ (just compute;
|
|
Beeminder ──────┤ │ (reuse AW; no new DB) │ │ interchangeable)
|
|
Marvin ammcp ───┘ └────────────────────────────────┼─────┘
|
|
~/owc (frame) ──── grounds the brief ────────────────────────┤
|
|
⑤ renders to ──┴──────────┐
|
|
▼ ▼
|
|
WEB UI WM STATUS BAR
|
|
(configure/interact/discuss) (current status)
|
|
```
|
|
|
|
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 ~/.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.
|
|
|
|
- **② Memory** — AW buckets owned by Keel. Keel writes its own derived events here
|
|
(mode_started, proposal_made, action_taken, spiral_flagged, …) so it *remembers
|
|
across runs* and can see trends a single tool can't. Rebuildable, queryable via
|
|
AW's REST API. Depends on AW only. (See §7.)
|
|
|
|
- **③ Context assembler** — builds the brief handed to the brain = relevant
|
|
collector signals + memory (recent + trend) + the `~/owc` frame (the value/goal
|
|
at stake, the relevant `bug-*.md`). Budgets it. Depends on ① ② and `~/owc`.
|
|
Privacy boundary lives here: titles that can leak secrets are filtered before
|
|
they reach the brain.
|
|
|
|
- **Brain adapter** — pluggable. Local: shell `claude --print` / `codex exec`
|
|
(exactly what AntiDrift's `ai.Backend` already does). Remote: call Hermes over
|
|
the network. Interface: `decide(brief) -> {status, proposals[]}`. Keel treats it
|
|
as a black box; swapping it changes nothing else.
|
|
|
|
- **④ Effectors** — one gated adapter per write target. Today's safe writes:
|
|
Marvin task create/complete (`am` / `ammcp`, **preserve the `fieldUpdates`
|
|
CRDT** on write), capture append (one file), AntiDrift start-session / enforce.
|
|
Roadmap writes: Beeminder datapoint (`beeline`), `~/owc` note edits. Each
|
|
effector reads its gate from config (propose-and-confirm | auto).
|
|
|
|
- **⑤ Surfaces** — render Keel's state. Web UI (rich, phone-friendly, the
|
|
interaction/approval surface) and the WM status bar (one live line). Both read
|
|
the same Keel state; neither owns data.
|
|
|
|
- **Control loop / scheduler** — drives the cycle: when to collect, when to think,
|
|
when to surface, when to act. Cheap triggers (a switch-count spike, a Beeminder
|
|
losedate approaching, a manual web-UI request) decide when to spend a brain
|
|
call, instead of polling the LLM blindly. This is AntiDrift's state machine,
|
|
generalized past the single session.
|
|
|
|
- **Policy / gates** — per-effector authority + the value frame. Felix's dial
|
|
from "read-only" to "act freely". Configured in the web UI.
|
|
|
|
---
|
|
|
|
## 4. AntiDrift is the seed, not a peer
|
|
|
|
Keel is AntiDrift's port set, widened. The parts already exist:
|
|
|
|
| Keel part | AntiDrift today | Keel (generalized) |
|
|
|---|---|---|
|
|
| Collectors | `evidence.Source` (X11), `tasks.Provider` (Marvin) | + AW, HQ, Consume, daily, Beeminder, `~/owc` |
|
|
| Frame | `knowledge.Source` → one file | `~/owc`: values, goals, 22 `bug-*.md`, frameworks |
|
|
| 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` + `~/.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; 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
|
|
|
|
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)
|
|
|
|
| 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 | `~/.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` |
|
|
| Beeminder | `beeline` | `beeline` datapoints | committed-evidence rollups | Beeminder API |
|
|
| Marvin | `ammcp` / `am --json` | `am` / `ammcp` (keep CRDT) | intent / to-dos / bugs | CouchDB |
|
|
| `~/owc` | markdown globs, `.zk` | note edits (later) | values/goals/bugs frame | `~/owc` files |
|
|
| Brain | — | — | rented compute | claude/codex/Hermes |
|
|
|
|
Known frictions Keel inherits (from the survey): beesync has **no scheduler**
|
|
(Beeminder data lags; `[daily]` bridge commented out); the life-bug concept is
|
|
**forked** (22 rich `~/owc` markdown bugs vs. 5 thin Marvin tasks); **capture is
|
|
broken** (`blurt` → clipboard, dead `inbox.md`, two rival `stream.md`); AW's year
|
|
of data is read by nothing today.
|
|
|
|
---
|
|
|
|
## 7. Memory in ActivityWatch (the storage design)
|
|
|
|
AW is a typed, append-only, timestamped event store with buckets + a REST/query
|
|
API — i.e. it is already the event store the §0 correction said not to rebuild.
|
|
Keel gets dedicated buckets (e.g. `keel.state`, `keel.events`) and writes
|
|
**derived** events: `mode_started`, `proposal_made`, `feedback_given`,
|
|
`action_taken`, `spiral_flagged`, `coach_line`. Properties:
|
|
- **Reference, don't duplicate** — these hold pointers (a Marvin id, a `bug-*.md`
|
|
path, an audit `session_id`), not copies of other tools' truth.
|
|
- **Rebuildable** — derivable from the underlying ledgers; AW is a cache/index of
|
|
Keel's own decisions plus a join surface over the sources.
|
|
- **Queryable over time** — this is what unlocks trend/spiral detection ("caffeine
|
|
bug + mood dip + rising switches, same shape as three weeks ago") that no single
|
|
tool can do, because each tool only answers "what now".
|
|
|
|
Open sub-decision: whether work-laptop AW data is peer-synced in (cross-device
|
|
behavioral story) or left out initially.
|
|
|
|
---
|
|
|
|
## 8. Open questions (not yet decided)
|
|
|
|
- **Effector gate policy** — exact default authority per write target on day one,
|
|
and how Felix raises/lowers it from the web UI.
|
|
- **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.
|
|
- **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/`)
|
|
|
|
- The whole `~/dev/higher/` planning directory: it was the scaffold for deciding
|
|
to extend AntiDrift. This doc is now the authoritative home; that dir retires to
|
|
a pointer.
|
|
- "clawd" → **`~/owc`** everywhere.
|
|
- The plan's premise that the operator "already knows Felix via `~/.hermes`" and
|
|
the survey finding "Hermes doesn't exist" are both resolved: **Hermes is real
|
|
but remote, and the brain is pluggable** — grounding comes from the `~/owc`
|
|
frame regardless of which brain runs.
|
|
- The plan's §6 recommendation ("a thin *reader* layer is the real architecture")
|
|
is **superseded**: the reader is merely the **collectors layer** of Keel. The
|
|
target is the full system (one screen, two surfaces, durable memory,
|
|
gated→autonomous effectors), grown from AntiDrift.
|
|
- "Don't build an event store" is **sharpened**: storage = **ActivityWatch
|
|
buckets**.
|
|
|
|
---
|
|
|
|
## 10. Smallest real slice (a vertical, not a summary)
|
|
|
|
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.
|