Files
antidrift/docs/keel-architecture.md

16 KiB

title, name, category, created, updated, tags, status
title name category created updated tags status
Keel — Architecture keel architecture 2026-06-04 2026-06-04
keel
human-harness
architecture
antidrift
activitywatch
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 — plus recent proposals and their outcomes recalled from the keel.events AW bucket — 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.

Shipped: memory. Off-screen now writes its decisions to the keel.events AW bucket (proposal_made / action_taken / proposal_dismissed, correlated by a random proposal_id) and reads its recent history back into the brief — so a proposal can avoid repeating itself and follow up on what was dismissed or left undone. Storage sits behind a thin memory.Store port (AW-backed, or a nop when AW is down), the seam every later mode reuses; see §7. The keel.state bucket and the trend/spiral detection that reads this data remain future increments.


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.