The first Control loop / scheduler component (architecture §3): an
always-on sentinel beside the one-mode harness that judges activity
against the ~/owc frame when no session is declared, and surfaces drift
to the status bar, a notify-send toast, and a web-UI banner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The X11 source only re-read the active window on _NET_ACTIVE_WINDOW
changes, so switching a browser tab — which changes the window title
but not the active window — was invisible. All the time on the new tab
was credited to the stale title (e.g. reading a Consume article showed
up as time on the Keel tab).
Mirror the Windows sensor: poll the active window every 750ms and emit
only when the window or its title changes. A shared, display-free
pollLoop carries the change-dedup and is unit-tested for the exact
regression (a title-only change must emit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A UI save was silently blanking the configured AW URL because
handlePostSettings bound the whole Settings from the request and saved
it as-is. Now reads s.settings.AWURL under the existing settingsMu lock
and carries it forward when req.AWURL is empty, so fields not exposed in
the form are never erased.
Nine TDD tasks: internal/aw REST client (EnsureBucket/Insert/Recent),
internal/memory port (Store + awStore over-fetch filter + nop + fake),
KEEL_AW_URL setting, harness.Services.Memory wiring with nop fallback,
off-screen write path (proposal_made/action_taken/proposal_dismissed) and
read path (## Recently proposed + follow-up prompt rule), plus full and
live verification. Derived from the 2026-06-05 design spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brainstormed design for Keel's first cross-run memory: a thin memory.Store
port backed by an internal/aw REST client writing to the keel.events bucket,
with off-screen as the single wired consumer (records proposal_made /
action_taken / proposal_dismissed and reads its recent history back into the
brief for fresher, follow-up-aware proposals). Brain-only, best-effort,
degrades to nop when AW is down. Realizes keel-architecture.md §5/§7 at the
smallest honest vertical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- README: lead with Keel (the human-harness), frame AntiDrift as the focus
mode, keep the milestone history. Fix the "rename deferred" wording (the
dir/repo is already keel; only the code identity stays antidrift) and drop
the dangling 2026-05-31 spec reference (that spec was intentionally removed).
- keel-architecture.md: update the rename status in the header and §8 — the
directory is keel; the Go module / antidriftd binary / ANTIDRIFT_* env /
~/.antidrift -> ~/.keel migration land with the controller refactor.
- add AGENTS.md (agent orientation) + CLAUDE.md symlink so claude / codex /
Hermes auto-load Keel context when launched in this repo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the higher-goals cockpit architecture into this repo: AntiDrift is
Keel's seed, with its focus harness becoming one mode of a general
collect -> brain -> act loop. Records the locked decisions: the product
is the harness around a swappable brain (claude/codex/Hermes); storage is
ActivityWatch; two surfaces (web UI + status bar); gated -> autonomous
effectors. Code/repo rename to keel deferred until the controller-refactor
decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three independent defects made the focus state feel flaky and let the OS
status bar disagree with the web UI:
- Status file lagged the web by up to a minute: it rendered only on a 60s
ticker with no hook into state changes. notify() now fans out to multiple
listeners (AddOnChange) and the writer has a coalesced Wake() so drift
reaches the bar as promptly as the browser.
- A brief off-task visit could latch drift for the whole session. Sibling
windows of one app (a browser's reading tab vs its chat tab) share a window
class, but the judge verdict was cached by class alone, so one tab's verdict
poisoned the rest and never re-judged. Cache is now keyed by class + scrubbed
title (judgedWindows) so siblings are judged independently.
- Allowed-class matching was exact equality, so a short token ("brave") never
matched the real WM_CLASS ("Brave-browser") and every window was routed to
the LLM. Matching is now substring-based, and planning surfaces the live
window class with a click-to-add chip so users pick a token that matches.
Also fix the planning checkbox alignment: the global full-width input rule was
stretching the "Enforce focus" checkbox; scope it out for checkboxes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the legacy/ Rust tree (Cargo manifests, .rs sources, desktop
entry), drop Rust-specific .gitignore blocks and README note, and strip
now-dangling "ported from Rust" comments from the Go sources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Polling active-window sensor and window-minimize guard as //go:build windows
adapters over a shared internal/winapi Win32 binding. Pure logic (class
normalization, emit-on-change tracking) is TDD-tested on Linux; syscall code is
cross-compile verified. No consumer changes.
- GOOS=windows GOARCH=amd64 go build ./... (whole module links)
- GOOS=darwin GOARCH=amd64 go build ./... (!linux && !windows fallback intact)
- go build ./... && go test ./... (Linux host green, incl. new winapi/evidence tests)
- go vet ./... on linux and windows (clean)
- cmd/antidriftd cross-compiles to a real PE32+ x86-64 .exe
Six TDD/cross-compile tasks: pure class normalization and emit-on-change
tracker (unit-tested on Linux), Win32 binding, the two windows-tagged port
adapters, build-tag narrowing, and a whole-module cross-compile gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full-parity design: evidence.Source (polling) and enforce.Guard
(ShowWindow) behind the existing X11/no-op port boundaries, pure Go,
verified compile-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
antidriftd binds localhost only and sits behind no proxy, so trusting all
proxies (gin's default) is both wrong and a startup warning. Set an empty
trusted-proxy list to disable forwarded-IP header trust and silence it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bucket keys are {Class, ScrubTitle(Title)}, so each animated progress frame a
terminal app cycles into its title (Claude Code's braille and dingbat-star
glyphs) became a distinct bucket — fragmenting one task into dozens of rows and
inflating the switch count, since applyEvent counts a switch per key change.
ScrubTitle now drops any non-ASCII symbol or control rune and collapses leftover
whitespace. This is category-based rather than range-based, so future spinner
sets are covered too, while letters and digits of every script survive and ASCII
symbols (e.g. "C++") are left intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The active view's fast-path repainted only the status band, so the "now"
window, per-window buckets, and their times froze at the moment the active
screen first rendered — only the status-band switch count and the countdown
kept moving. Give the evidence band a stable id and repaint it every tick via
updateActiveEvidence, mirroring updateActiveDrift.
The evidence band's own "context switches" readout duplicated the live count
in the status band, so remove it (and its now-dead .switches CSS rule).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the one coverage gap from the final review: the spec's
'one async call per session' efficiency claim is now directly
verified via the reviewer call count, not just implied. Also drops a
stale CarryForward-preview mention from the Review projection in the
design spec to match the shipped (cleaner) behaviour.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Review screen renders the reviewer's one-line recap (quiet pending
line, then the recap); the Planning screen renders last session's
carry-forward as a 'Last time: …' one-liner, mirroring the knowledge
indicator. Updates the README Status section for M7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main injects the AI service as the reviewer alongside the other roles.
A web test drives a session to Review and asserts the recap rides the
state payload, then to the next Planning and asserts the carry-forward
does too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The controller fetches a reflection asynchronously on enterReview
(generation-guarded, non-blocking, graceful) and caches a one-line
recap plus a latest-wins carry-forward. The recap projects onto Review,
the carry-forward onto the next Planning, and both ride the snapshot.
RequestCoach composes the carry-forward into the coach's existing
free-form grounding string, so ai.Coach is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Exposes the last n session summaries (oldest-first) for the reviewer to
read as recent-history context. Missing/empty chain or n<=0 yields nil.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fourth leaf role: Review(finished, history) returns a two-line
Reflection (recap + carry_forward) over the same CLI backend as the
coach. Primitive-only, with a JSON prompt and a tolerant parser that
rejects a blank recap but allows an empty carry_forward.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The planning view renders a quiet line for the standing-profile state
(loading/grounded/absent/unreadable) with a "change" affordance to
repoint the file via POST /knowledge/path. The profile text never
reaches the browser — only status, path, and size. Also updates the
README Status section to describe the M6 knowledge port.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main constructs the FileSource (default via ANTIDRIFT_KNOWLEDGE_FILE)
and injects it. Adds POST /knowledge/path to repoint the profile file at
runtime, and web tests asserting the planning payload carries the
knowledge object (status + path, never the text) and that path selection
reloads.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The controller loads the profile asynchronously on entering planning,
mirroring the tasks fetch: generation-guarded goroutine, status enum
(idle/pending/ready/absent/error), projected into State only while
planning and only when a source is set. The loaded text is cached and
passed to the coach as grounding; SetKnowledgePath repoints the file at
runtime. nil source degrades to no view and an ungrounded coach.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ai.Coach interface gains a grounding parameter carrying standing
context about the user (the knowledge port). buildPrompt injects an
"About the user" block only when grounding is non-empty, so an empty
grounding produces a byte-for-byte unchanged prompt. Drift judge and
nudge are untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The knowledge.Source port answers "who am I; what are my priorities?".
The FileSource adapter reads one profile file (path selectable per call),
treats a missing/blank file as absent (not an error), and caps the text.
Leaf package mirroring tasks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The planning view renders Marvin's today list as clickable chips;
clicking one fills the intent field, feeding the existing coach flow.
idle/error/empty render nothing, pending shows a quiet loading line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main constructs the Marvin adapter (command overridable via
ANTIDRIFT_MARVIN_CMD) and injects it. Adds a web test asserting the
planning state payload carries today's tasks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Exercises the generation/left-planning guard in startTasksFetchLocked
via the fakeProvider gate: a fetch in flight when its generation is
superseded must not clobber the current tasks state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>