fe2fb4e250
Add the hexagonal architecture section (skeleton/nervous-system/cortex layering), name the tasks/knowledge/enforce ports as deferred, and extend the roadmap with M5-M7. Clarifies that the state machine owns transitions and the LLM advises within the rails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
248 lines
13 KiB
Markdown
248 lines
13 KiB
Markdown
# AntiDrift Go Reimagining — Design
|
||
|
||
Date: 2026-05-31
|
||
|
||
## Purpose
|
||
|
||
AntiDrift is being reimagined from its current Rust implementation (~7,500
|
||
lines) into Go, to become the user's focus operating system on the computer:
|
||
fast, good-looking, and deeply integrated with AI from the start.
|
||
|
||
This is **not a faithful 1:1 port**. The existing domain model and the
|
||
`commitment-os-design.md` spec remain the north star. The Rust code is a
|
||
reference, not a thing to replicate line-for-line. The move to Go is an
|
||
opportunity to shed incidental complexity — most notably the token-heavy
|
||
event-log replay/revalidation design — while preserving what is genuinely
|
||
valuable.
|
||
|
||
### Why Go, why now
|
||
|
||
- **Token efficiency for AI-assisted development.** The current pain is not
|
||
runtime cost; it is that any AI edit to the core must load
|
||
`session.rs` (3,475 lines, ~80% replay-validation logic and its tests).
|
||
Go's smaller idioms plus a redesigned, smaller core directly reduce the
|
||
context any change requires.
|
||
- **Predictable LLM codegen.** Go's rigid syntax produces functional code on
|
||
the first pass with fewer correction loops.
|
||
- **Runtime fit.** Concurrency and local HTTP serving are first-class, which
|
||
suits a long-running focus daemon that also talks to AI.
|
||
|
||
## What Carries Over vs. What Changes
|
||
|
||
**Preserved (high value, ports cleanly):**
|
||
|
||
- The domain model: `Commitment`, `PolicySnapshot`, `RuntimeState`,
|
||
`CommitmentState`, `AllowedContext`, `EnforcementLevel`.
|
||
- The pure runtime/commitment state machines (`state_machine.rs`) — a near 1:1
|
||
port.
|
||
- The `commitment-os-design.md` spec as the conceptual foundation, including
|
||
"no unchosen transitions" and the staged threat model.
|
||
- Hash-chained tamper evidence — but relocated to the audit log only.
|
||
|
||
**Reimagined:**
|
||
|
||
- **Persistence.** Replace replay-everything-and-revalidate-on-startup with an
|
||
in-memory state-of-truth, a persisted **snapshot**, and an append-only audit
|
||
log. This removes roughly 3,000 lines (the bulk of `session.rs`).
|
||
- **UI.** Replace the ratatui TUI with a **local web app** (Gin backend +
|
||
browser). This is the surface that must "look good."
|
||
- **AI.** AI is a first-class participant from the start, not a later add-on.
|
||
|
||
**Deferred for v1:**
|
||
|
||
- The AI **reviewer** role (session-end reflection). The three live roles ship
|
||
first.
|
||
- Privileged enforcement (guardian, IPC, nftables, delayed admin) — same Stage 2
|
||
boundary as the original spec.
|
||
|
||
## Process Model
|
||
|
||
A single Go binary, `antidriftd`, runs as a **local daemon** and owns all
|
||
state. The **browser** is its face.
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ antidriftd (one Go process) │
|
||
│ │
|
||
│ web (Gin) ──HTTP + SSE──▶ browser UI │
|
||
│ │ │
|
||
│ session ── statemachine ── domain │
|
||
│ │ │
|
||
│ store (snapshot + audit log) │
|
||
│ evidence (xdotool/X11) │
|
||
│ ai (CLI backend, async workers) │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
- The daemon holds live state **in memory** as the single source of truth.
|
||
- It persists a **snapshot** on every state change (crash/restart recovery).
|
||
- It appends every significant event to an **append-only audit log** (the
|
||
tamper-evident, hash-chained trail — for audit and later review, not for
|
||
state reconstruction).
|
||
- The browser is stateless: it renders what the daemon pushes over Server-Sent
|
||
Events (SSE) and POSTs user actions back. No business logic in the browser.
|
||
|
||
### Why snapshot instead of replay
|
||
|
||
The original Rust design reconstructs all state by replaying the entire event
|
||
log on startup and re-validating every transition, with a dedicated test per
|
||
illegal sequence. That is correct and tamper-aware, but it is the single
|
||
largest source of code and token weight. A snapshot of current state plus an
|
||
append-only audit trail gives the same recoverability and keeps tamper evidence
|
||
on the log, at a fraction of the code. State-machine *correctness* is still
|
||
enforced — by the pure transition functions at the point of transition, tested
|
||
directly — just not re-litigated on every startup.
|
||
|
||
## Architecture: Ports Around a Decision Core
|
||
|
||
AntiDrift is a **focus brain**: a decision core surrounded by pluggable
|
||
interfaces (ports) to the outside world. This is a ports-and-adapters
|
||
(hexagonal) architecture, and it is the organizing principle the whole system
|
||
grows along. New capability is almost always "a new port + adapter," not a
|
||
change to the core.
|
||
|
||
The core is layered, and the layering is load-bearing:
|
||
|
||
- **Skeleton — deterministic, no I/O** (`domain` + `statemachine`). The rails.
|
||
Owns what moves are *legal*. The original spec's safety property, "no
|
||
unchosen transitions," lives here: the system can only ever be in a legal
|
||
state, reached by a legal move.
|
||
- **Nervous system — the orchestrator** (`session.Controller`). The single hub.
|
||
Holds the in-memory state-of-truth, routes signals between ports and the
|
||
skeleton, persists snapshots, appends to the audit log, and broadcasts.
|
||
Everything connects through here.
|
||
- **Cortex — the advisor** (the LLM, via the `ai` port). Powerful *judgment* at
|
||
the decision points the state machine exposes — sharpen this commitment, is
|
||
this window drift, nudge me. It informs and proposes; **it can never force an
|
||
illegal transition.** The LLM is the most powerful adapter, not the kernel.
|
||
|
||
"The brain" is all three together. Critically, the state machine — not the LLM
|
||
— owns transitions; the LLM acts only within the rails the skeleton enforces.
|
||
|
||
### Ports
|
||
|
||
Each port is a small Go interface with one real adapter (and a fake for tests).
|
||
|
||
| Port | Interface | "Answers" | Adapter(s) | Milestone |
|
||
| ---- | --------- | --------- | ---------- | --------- |
|
||
| Activity | `evidence.Source` | What am I doing right now? | X11 / xgbutil (was xdotool) | M1 |
|
||
| Advisor | `ai.Assistant` (`Coach`/`JudgeDrift`/`Nudge`) | What's the smart call here? | `claude`/`codex` CLI | M2–M3 |
|
||
| Tasks | `tasks.Provider` | What *should* I be doing? | Amazing Marvin (existing `ampy` + marvin MCP) | deferred (M5) |
|
||
| Knowledge | `knowledge.Source` | Who am I; what are my priorities? | PKM / files | deferred (M6) |
|
||
| Enforcement | `enforce.Guard` | Make drift cost something | window-minimize now (legacy `minimize_other`); nftables/guardian later | deferred (M7) |
|
||
| UI | `web` | Show me; take my input | Gin + browser over SSE | M0, ongoing |
|
||
|
||
Persistence (`store`) is infrastructure shared by the orchestrator, not a port.
|
||
|
||
The `tasks`, `knowledge`, and `enforce` ports are **named now but built later**
|
||
— defining them keeps the architecture coherent without expanding near-term
|
||
scope. We resist designing their interfaces in detail until the milestone that
|
||
builds them, to avoid speculative abstraction (YAGNI). M1 ships the first real
|
||
port end-to-end (`evidence.Source` + X11 adapter + fake), establishing the
|
||
pattern every later port copies.
|
||
|
||
## Package Layout
|
||
|
||
| Package | Ports from | Size | Purpose |
|
||
| -------------- | ------------------------ | ------ | ------- |
|
||
| `domain` | `domain.rs` | small | Commitment, PolicySnapshot, runtime/commitment states, AllowedContext, EnforcementLevel, validation |
|
||
| `statemachine` | `state_machine.rs` | small | Pure transition functions (1:1 port) |
|
||
| `session` | reimagined `session.rs` | medium | In-memory controller; drives transitions, snapshots, audit appends; no replay validation |
|
||
| `store` | `event_log.rs` | small | Snapshot file (current state) + append-only hash-chained audit JSONL |
|
||
| `evidence` | `window/*` + `context.rs`| small | Active-window snapshot (xdotool/X11), evidence health, allowed-context matching |
|
||
| `ai` | new | small | `Coach` / `JudgeDrift` / `Nudge` behind one interface; CLI backend |
|
||
| `web` | new (replaces TUI) | medium | Gin routes, SSE stream, static browser UI |
|
||
| `tasks` | new (deferred, M5) | small | `Provider` port over current to-do items; Amazing Marvin adapter |
|
||
| `knowledge` | new (deferred, M6) | small | `Source` port over personal priorities / about-me context |
|
||
| `enforce` | `window/*` (minimize) | small | `Guard` port; make drift cost something (window-minimize → nftables/guardian) |
|
||
|
||
Design constraint: every package stays small and single-purpose so an AI edit
|
||
loads one focused file, not a monolith. This is the concrete mechanism for the
|
||
token-efficiency goal.
|
||
|
||
## AI Integration
|
||
|
||
AI is reached through one narrow interface with a single CLI backend to start:
|
||
|
||
```go
|
||
type Assistant interface {
|
||
// Planning: turn a vague intent into a concrete commitment.
|
||
Coach(ctx context.Context, intent string) (domain.Commitment, error)
|
||
// Live: is the current window on-task for this commitment?
|
||
JudgeDrift(ctx context.Context, c domain.Commitment, w evidence.WindowSnapshot) (Verdict, error)
|
||
// Ambient: periodic check-in based on recent activity.
|
||
Nudge(ctx context.Context, c domain.Commitment, recent []evidence.WindowSnapshot) (string, error)
|
||
}
|
||
```
|
||
|
||
- **Backend (v1):** shell out to `claude`/`codex` with a strict prompt that
|
||
demands JSON output. Reuses existing CLI auth; no API key plumbing.
|
||
- **Latency containment** (the CLI is slow, ~seconds, and AI is in the live hot
|
||
path): all AI calls run in **background goroutines**; the UI never blocks.
|
||
Drift judgments are **debounced** (no faster than ~10s) and **cached per
|
||
(commitment, window-class)** so the same window is not re-judged. The UI
|
||
shows a pending state and updates via SSE when a verdict lands.
|
||
- **Swap path:** the interface boundary lets an Anthropic API backend (faster,
|
||
structured, prompt-cached) drop in later without touching callers. Not built
|
||
in v1.
|
||
|
||
The three live roles ship first: planning **coach**, live **drift
|
||
interceptor**, ambient **nudge**. The reviewer is deferred. The advisor sits at
|
||
the **cortex** layer of the decision core (see "Architecture: Ports Around a
|
||
Decision Core"): it proposes and judges at the decision points the state
|
||
machine exposes, but never owns a transition.
|
||
|
||
## Roadmap
|
||
|
||
Each milestone is independently shippable and gets its own spec → plan → build
|
||
cycle. M0–M4 build the core plus the first two ports (activity, advisor) and the
|
||
UI; M5–M7 add the remaining ports, each a small interface + adapter following
|
||
the pattern M1 establishes.
|
||
|
||
- **M0 — Walking skeleton.** Daemon + Gin + minimal browser UI; port `domain` +
|
||
`statemachine`; snapshot persistence; manual commitment → timebox → end.
|
||
Proves the full stack end-to-end. No AI, no window tracking.
|
||
- **M1 — Evidence & audit.** X11 (xgbutil) active-window tracking via the
|
||
`evidence.Source` port, evidence health, per-window time stats, append-only
|
||
hash-chained audit log, live SSE updates. Establishes the port pattern.
|
||
- **M2 — AI planning coach.** `ai` port + CLI backend; "sharpen this
|
||
commitment" in the Planning view.
|
||
- **M3 — Drift interceptor + ambient nudge.** Allowed-context matching + live
|
||
AI drift judgment (debounced/cached) + violation friction UI.
|
||
- **M4 — Look good.** A real design pass on the web UI.
|
||
- **M5 — Tasks port.** `tasks.Provider` over current to-do items; Amazing Marvin
|
||
adapter. Pull the day's commitments from real tasks.
|
||
- **M6 — Knowledge port.** `knowledge.Source` over personal priorities and
|
||
about-me context, feeding the advisor richer grounding.
|
||
- **M7 — Enforcement port.** `enforce.Guard`: make drift cost something, starting
|
||
with window-minimize (porting legacy `minimize_other`) and later the
|
||
privileged guardian / nftables path.
|
||
|
||
The first sub-project to brainstorm and spec in detail is **M0**.
|
||
|
||
## Repo Strategy
|
||
|
||
- New Go module at the repository root.
|
||
- Move the existing Rust into a `legacy/` directory (or a `rust` branch) so it
|
||
remains available as reference while the Go code becomes the front door.
|
||
|
||
## Out of Scope (v1)
|
||
|
||
"v1" here means the first shippable arc, **M0–M4** (core + activity/advisor
|
||
ports + UI). The items below are deferred past it; some are now named ports with
|
||
their own later milestones (see Roadmap), others remain fully out of scope.
|
||
|
||
- **Tasks, knowledge, and enforcement ports** — named in the architecture and
|
||
slotted as M5–M7, but not built in v1. The `enforce.Guard` port starts with
|
||
window-minimize; its **privileged** adapters (guardian process, root-owned
|
||
Unix socket IPC, nftables/DNS domain blocking, delayed admin, break-glass)
|
||
remain out of scope until that milestone and keep the original Stage 2 threat
|
||
boundary.
|
||
- AI reviewer / session-end reflection.
|
||
- Wayland compositor adapters beyond the existing degraded reporting.
|
||
- Planner/project model and outcome writeback (beyond the M5 tasks port).
|
||
- Presence sensing.
|
||
|
||
These remain governed by `commitment-os-design.md` and may return as later
|
||
milestones.
|