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:
+56
-41
@@ -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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,302 @@
|
||||
# Controller Refactor — Harness + Modes — Design
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**Status:** Approved, ready for implementation planning
|
||||
**Scope:** Rewrite the focus-only session controller into a generic
|
||||
**harness** that hosts swappable **modes**. Focus becomes the first mode
|
||||
(behavior-preserving extraction); **off-screen mode** becomes the second
|
||||
(new, the proof the abstraction generalizes). The `antidrift`→`keel` code
|
||||
rename and `~/.antidrift/`→`~/.keel/` runtime move land in the same pass,
|
||||
clean-slate (no state migration).
|
||||
|
||||
## Problem
|
||||
|
||||
`session.Controller` (≈1,400 LOC across `session.go`, `roles.go`, `drift.go`,
|
||||
`stats.go`, `views.go`) is a god-object. It conflates three things:
|
||||
|
||||
1. **Generic runtime orchestration** — holding the ports, the async
|
||||
brain-call primitive (`runFetchAsync`), persistence, change-notify, the
|
||||
evidence ingestion callback, the clock.
|
||||
2. **The focus state machine** — `locked→planning→active→transition→review`,
|
||||
commitment lifecycle, timebox/deadline, drift/nudge/enforcement, evidence
|
||||
stats, reflection, session sealing.
|
||||
3. **The focus domain payload** — `domain.Commitment`
|
||||
(`next_action / success_condition / timebox`), sitting in a package named
|
||||
`domain` as if it were generic.
|
||||
|
||||
Focus mode is not *a* mode — it **is** the controller. There is no seam to add
|
||||
a second mode. Keel's thesis (the harness is the product; focus is one mode
|
||||
among many) cannot be expressed until (1) is separated from (2)+(3).
|
||||
|
||||
The directory and git repo are already `keel`; only the code identity still
|
||||
says `antidrift`. AntiDrift was never in production and its `~/.antidrift/`
|
||||
ledger is disposable, so the rename and runtime move can be done outright with
|
||||
no migration ceremony.
|
||||
|
||||
## Goals
|
||||
|
||||
- A generic `internal/harness` host that owns shared services and exactly one
|
||||
active mode, and routes I/O to it.
|
||||
- A small `Mode` contract that holds both a long-lived stateful mode (focus)
|
||||
and a one-shot request/response mode (off-screen) without crippling either.
|
||||
- Focus re-homed as `mode/focus` with **identical behavior** — existing focus
|
||||
tests are the regression oracle.
|
||||
- Off-screen mode built end-to-end: collect → assemble → brain → surface →
|
||||
gated Marvin write.
|
||||
- The `antidrift`→`keel` rename complete in code and runtime, clean-slate.
|
||||
|
||||
## Non-goals (YAGNI)
|
||||
|
||||
- **AW-bucket memory** (the §7 "remember across runs" layer). Off-screen v1 is
|
||||
stateless. AW memory is the immediate fast-follow, its own spec.
|
||||
- **Concurrent / ambient modes.** Exactly one active mode at a time
|
||||
(idle ↔ focus *or* off-screen). Matches today's single-commitment reality.
|
||||
- **Any third mode.**
|
||||
- **State migration** from `~/.antidrift/`. Clean slate by decision.
|
||||
- Renaming the off-screen mode's working code name (`offscreen`) — placeholder,
|
||||
rename freely later.
|
||||
|
||||
## Decisions that shaped this design
|
||||
|
||||
- **One thin `Mode` interface + capability interfaces**, not a stateless
|
||||
collect/assemble/effector pipeline framework (Procrustean for focus's
|
||||
stateful machine, and a YAGNI framework for two modes), and not two formal
|
||||
mode *kinds* (fragments the concept before the seams are felt). Capability
|
||||
interfaces (`EvidenceConsumer`, `Expirer`) give the heterogeneity honesty
|
||||
without the taxonomy. If a third one-shot mode appears, off-screen's flow can
|
||||
be promoted into a real pipeline helper then — driven by evidence.
|
||||
- **Generalize the web routes** (`/mode/{kind}/start`, `/mode/command/{name}`)
|
||||
rather than keep focus's bespoke endpoints. The frontend is rewritten under
|
||||
the state envelope regardless.
|
||||
- **`Locked` collapses into harness idle.** "No active mode" is the resting
|
||||
state; focus is `Planning→Active→Review→done`. One fewer state.
|
||||
- **Off-screen reuses the existing single-file `knowledge.Source`** for the
|
||||
`~/owc/goals-2026.md` frame, plus a small in-package glob read
|
||||
(`filepath.Glob` + read) for the curated life-bug set. `knowledge.Source.Load`
|
||||
is single-path by contract; no new adapter package — smallest real slice.
|
||||
- **The confirm click is the effector gate.** No auto-write in v1.
|
||||
|
||||
## Package layout
|
||||
|
||||
```
|
||||
cmd/keeld/main.go # wires harness + modes + surfaces + settings
|
||||
|
||||
internal/harness/
|
||||
harness.go # Harness: services, the one active mode, change-notify, store root
|
||||
services.go # Services handle passed to modes
|
||||
async.go # generation-guarded async primitive (was runFetchAsync)
|
||||
|
||||
internal/mode/
|
||||
mode.go # Mode interface, capability interfaces, Envelope
|
||||
|
||||
internal/mode/focus/ # today's session/* + domain/* + statemachine/* re-homed
|
||||
focus.go statemachine.go drift.go stats.go views.go roles.go ...
|
||||
|
||||
internal/mode/offscreen/
|
||||
offscreen.go # NEW: off-screen mode
|
||||
|
||||
internal/ai/ tasks/ knowledge/ evidence/ enforce/ # shared ports — only module path changes
|
||||
internal/store/ # persistence primitives + per-mode namespacing
|
||||
internal/settings/ statusfile/ web/ # web becomes mode-aware
|
||||
```
|
||||
|
||||
`internal/domain` dissolves into `mode/focus`: `Commitment`, `RuntimeState`,
|
||||
`EnforcementLevel`, `AllowedContext`, `PolicySnapshot` are all focus concepts.
|
||||
`internal/statemachine` likewise moves under `mode/focus` (its states are the
|
||||
focus lifecycle). Off-screen touches none of them.
|
||||
|
||||
## The Mode contract
|
||||
|
||||
```go
|
||||
package mode
|
||||
|
||||
// Mode is one collect→brain→act configuration. The harness runs at most one.
|
||||
type Mode interface {
|
||||
Kind() string // "focus", "offscreen"
|
||||
Command(ctx context.Context, name string, payload json.RawMessage) error
|
||||
View() any // JSON-marshalable projection
|
||||
Active() bool // false ⇒ harness returns to idle
|
||||
}
|
||||
|
||||
// Optional capabilities — the harness asserts for these.
|
||||
type EvidenceConsumer interface { OnWindow(evidence.WindowSnapshot) } // focus only
|
||||
type Expirer interface { Deadline() time.Time; Expire(context.Context) error } // focus only
|
||||
```
|
||||
|
||||
`Command` returns an error for illegal/invalid commands; the web layer maps it
|
||||
to HTTP (today's `IllegalTransitionError`→409 mapping moves with focus). `View`
|
||||
returns the mode's own shape, wrapped by the harness in the envelope below.
|
||||
|
||||
## The Harness + Services
|
||||
|
||||
```go
|
||||
type Harness struct {
|
||||
mu sync.Mutex
|
||||
active mode.Mode // nil = idle
|
||||
factories map[string]func(Services) mode.Mode // "focus", "offscreen"
|
||||
services Services
|
||||
onChange []func()
|
||||
}
|
||||
|
||||
func (h *Harness) Start(kind string) error // idle → mode (single-active invariant)
|
||||
func (h *Harness) Command(ctx, name, payload) error // routes to active; cleans up active→done
|
||||
func (h *Harness) State() mode.Envelope
|
||||
func (h *Harness) RecordWindow(w evidence.WindowSnapshot) // forwards iff active is EvidenceConsumer
|
||||
func (h *Harness) Deadline() time.Time // delegates iff active is Expirer
|
||||
func (h *Harness) Expire(ctx) error // delegates iff active is Expirer
|
||||
```
|
||||
|
||||
After any `Command`/`Start`, if `active.Active()` is false the harness clears
|
||||
`active` to nil (idle). `Start` on a non-idle harness is an error unless the
|
||||
requested kind is already active.
|
||||
|
||||
`Services` is what a mode receives — the ports plus relocated infrastructure:
|
||||
|
||||
```go
|
||||
type Services struct {
|
||||
AI *ai.Service // coach/judge/nudge/reviewer + new proposer role
|
||||
Tasks tasks.Provider // read (Today) + new write (Create)
|
||||
Knowledge knowledge.Source
|
||||
Enforce enforce.Guard
|
||||
Clock func() time.Time
|
||||
Store *store.Scope // mode-namespaced (~/.keel/modes/<kind>/)
|
||||
Notify func() // fires harness change-listeners (SSE + status bar)
|
||||
}
|
||||
```
|
||||
|
||||
### The async primitive
|
||||
|
||||
`runFetchAsync` (today `session/roles.go:99`) moves to `harness/async.go` as a
|
||||
helper parameterized by **the mode's own mutex** plus `Services.Notify`:
|
||||
|
||||
```go
|
||||
type Async struct { mu *sync.Mutex; notify func() }
|
||||
func (a Async) Run(timeout time.Duration, fetch func(context.Context), stale func() bool, apply func())
|
||||
```
|
||||
|
||||
This preserves today's exact lock discipline: `fetch` runs with no lock held;
|
||||
`stale`/`apply` run under the mode's mutex; `notify` fires once, after unlock.
|
||||
Each mode constructs one `Async{mu: &m.mu, notify: svc.Notify}`. Generation
|
||||
counters and status enums stay inside each mode, exactly as today.
|
||||
|
||||
## Focus mode (behavior-preserving extraction)
|
||||
|
||||
`session.Controller` minus the host plumbing becomes `focus.Mode`. The state
|
||||
machine, drift/nudge/enforcement, evidence stats, reflection, and the planning
|
||||
coach/tasks/knowledge fetches move verbatim. Focus implements `Mode`,
|
||||
`EvidenceConsumer` (today's `RecordWindow`), and `Expirer` (today's
|
||||
deadline/`Expire`). Its `View()` returns today's `session.State` shape,
|
||||
unchanged, so the existing focus screens render identically.
|
||||
|
||||
Command mapping (today's routes → focus `Command` names): `planning`, `coach`,
|
||||
`commitment`, `complete`, `end`, `refocus`, `ontask`.
|
||||
|
||||
`Locked` is removed: harness idle replaces it. `Planning→Active→Review→done`,
|
||||
then the harness returns to idle. `admin_override` stays inside focus (an
|
||||
enforcement-escape concern). **Existing focus tests move with the package and
|
||||
must pass unchanged** — they are the proof the extraction preserved behavior.
|
||||
|
||||
## Off-screen mode (the proof)
|
||||
|
||||
The away-from-the-desk complement to focus. A one-shot request/response
|
||||
lifecycle — this is what proves the abstraction holds for a mode that is *not*
|
||||
a long-lived session.
|
||||
|
||||
```
|
||||
start → assembling → proposed → (confirm | dismiss) → done
|
||||
```
|
||||
|
||||
- **`Command "start"`** — collect + assemble + brain via the async primitive;
|
||||
view status `pending`.
|
||||
- **Collect (real frame elements):**
|
||||
- `tasks.Provider.Today()` — life / non-work items.
|
||||
- `~/owc/goals-2026.md` (Body/Spirit, People, Via Negativa) via the existing
|
||||
single-file `knowledge.Source`.
|
||||
- `~/owc` life-domain bugs via a small in-package glob read:
|
||||
`physical-reset`, `workout-effort`, `nutrition-effort`,
|
||||
`missing-social-life`, `make-environment-beautiful`, `marriage-strategy`.
|
||||
- **Brain:** a new thin `ai.Proposer` role —
|
||||
`Propose(ctx, brief) → Proposal{NextAction, Rationale}` — reusing
|
||||
`ai.Backend`. A sibling to `Coach`, not a bend of it.
|
||||
- **Surface:** phone-friendly proposal card (off-screen ⇒ you're not at the
|
||||
desk; the phone surface earns its keep here).
|
||||
- **`Command "confirm"` (gated effector):** a **new**
|
||||
`tasks.Provider.Create(ctx, Task)` files the proposed task in Marvin,
|
||||
preserving the `fieldUpdates` CRDT on write. The confirm click *is* the gate.
|
||||
- **`Command "dismiss"`:** done, nothing written.
|
||||
- **Memory:** none in v1 (AW buckets deferred).
|
||||
|
||||
Off-screen implements only `Mode` (no evidence stream, no timebox), so it
|
||||
exercises the capability-assertion path: the harness forwards neither window
|
||||
events nor an expiry timer to it.
|
||||
|
||||
## Surfacing
|
||||
|
||||
```go
|
||||
package mode
|
||||
type Envelope struct {
|
||||
ActiveMode string `json:"active_mode"` // "" = idle
|
||||
Mode any `json:"mode,omitempty"` // active mode's View()
|
||||
}
|
||||
```
|
||||
|
||||
Web routes generalize:
|
||||
|
||||
- `POST /mode/{kind}/start` → `harness.Start`
|
||||
- `POST /mode/command/{name}` → `harness.Command` (request body = payload)
|
||||
- `GET /events` → SSE of the envelope (broadcaster mechanism unchanged)
|
||||
- The server-owned expiry timer arms from `harness.Deadline()` and fires
|
||||
`harness.Expire()` via the `Expirer` assertion (today's `web.armExpiry`
|
||||
logic, retargeted).
|
||||
|
||||
`app.js` switches on `active_mode`: `""` → launcher (pick focus / off-screen);
|
||||
`"focus"` → today's screens, fed by `.mode`; `"offscreen"` → the proposal card.
|
||||
The status-file writer renders one line per active mode (focus's drift line,
|
||||
off-screen's proposal summary, or `idle`). **This is the largest single chunk
|
||||
of new work**: the focus UI is reshaped under the envelope and a small
|
||||
off-screen view is added.
|
||||
|
||||
## Persistence (clean slate)
|
||||
|
||||
- Root `~/.keel/`; settings `~/.keel/settings.json`; status `~/.keel_status`.
|
||||
- Per-mode namespace via `store.Scope`:
|
||||
`~/.keel/modes/focus/{state.json, audit.jsonl, sessions/}`.
|
||||
- Off-screen v1 persists nothing (ephemeral; restart = idle).
|
||||
- **No migration** from `~/.antidrift/`.
|
||||
|
||||
## The rename (lands in this pass)
|
||||
|
||||
In-repo:
|
||||
|
||||
- `go.mod` module `antidrift`→`keel`; all import paths.
|
||||
- `cmd/antidriftd`→`cmd/keeld`; binary `keeld`; log strings.
|
||||
- `store`/`settings`/`statusfile` default paths → `~/.keel*`.
|
||||
- `settings.SeedFromEnv` `ANTIDRIFT_*`→`KEEL_*`.
|
||||
- `CLAUDE.md` naming-status section (rename now *done*).
|
||||
- `keel-architecture.md`: §1/§8 (rename resolved), §5/§10 (remove the phantom
|
||||
"house mode"; describe off-screen mode with the real collectors above).
|
||||
|
||||
External — Felix updates by hand (out of repo, listed in the plan):
|
||||
|
||||
- WM status-bar config reading `~/.antidrift_status` → `~/.keel_status`.
|
||||
- Any systemd unit / shell alias / launcher invoking `antidriftd` → `keeld`.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Focus:** existing tests relocate with the package (import paths only) and
|
||||
must pass unchanged — the regression oracle for the extraction.
|
||||
- **Harness:** new unit tests with a fake mode — single-active invariant,
|
||||
command routing, capability assertion (window events only to
|
||||
`EvidenceConsumer`s; expiry only to `Expirer`s), idle↔active transitions,
|
||||
`Active()==false` cleanup to idle.
|
||||
- **Off-screen:** new unit tests with fake `ai`/`tasks` — collect→propose then
|
||||
`confirm` files exactly one task (CRDT-preserving); `dismiss` writes nothing;
|
||||
the gate holds.
|
||||
- **Web:** envelope routing test (idle / focus / off-screen shapes).
|
||||
|
||||
## Open question carried into the plan
|
||||
|
||||
Build order. Recommended: (1) rename in place (mechanical, keeps tests green),
|
||||
(2) extract harness + `Mode` with focus as the only mode (tests green proves
|
||||
the extraction), (3) add off-screen mode, (4) reshape the frontend under the
|
||||
envelope. Each step leaves the daemon runnable.
|
||||
Reference in New Issue
Block a user