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:
@@ -8,17 +8,28 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"antidrift/internal/domain"
|
||||
"antidrift/internal/session"
|
||||
"keel/internal/mode"
|
||||
"keel/internal/mode/focus"
|
||||
"keel/internal/mode/focus/domain"
|
||||
)
|
||||
|
||||
func activeState(deadline int64, driftStatus, nudge string) session.State {
|
||||
st := session.State{
|
||||
// focusEnv wraps a focus.State in the harness envelope the writer now consumes.
|
||||
func focusEnv(st focus.State) mode.Envelope {
|
||||
return mode.Envelope{ActiveMode: "focus", Mode: st}
|
||||
}
|
||||
|
||||
// focusState builds a focus.State with the given runtime state.
|
||||
func focusState(rt domain.RuntimeState) focus.State {
|
||||
return focus.State{RuntimeState: rt}
|
||||
}
|
||||
|
||||
func activeState(deadline int64, driftStatus, nudge string) focus.State {
|
||||
st := focus.State{
|
||||
RuntimeState: domain.RuntimeActive,
|
||||
Commitment: &session.CommitmentView{DeadlineUnixSecs: deadline},
|
||||
Commitment: &focus.CommitmentView{DeadlineUnixSecs: deadline},
|
||||
}
|
||||
if driftStatus != "" || nudge != "" {
|
||||
st.Drift = &session.DriftView{Status: driftStatus, Nudge: nudge}
|
||||
st.Drift = &focus.DriftView{Status: driftStatus, Nudge: nudge}
|
||||
}
|
||||
return st
|
||||
}
|
||||
@@ -29,25 +40,33 @@ func TestRender(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
st session.State
|
||||
env mode.Envelope
|
||||
want string
|
||||
}{
|
||||
{"locked", session.State{RuntimeState: domain.RuntimeLocked}, ""},
|
||||
{"empty runtime", session.State{}, ""},
|
||||
{"planning", session.State{RuntimeState: domain.RuntimePlanning}, "◔ planning"},
|
||||
{"review", session.State{RuntimeState: domain.RuntimeReview}, "✓ review"},
|
||||
{"active ontask", activeState(in24m, "ontask", ""), "● 24m"},
|
||||
{"active no drift view", activeState(in24m, "", ""), "● 24m"},
|
||||
{"active drifting", activeState(in24m, "drifting", ""), "⚠ DRIFT 24m"},
|
||||
{"active nudge", activeState(in24m, "ontask", "wandered off"), "● 24m ·?"},
|
||||
{"drift outranks nudge", activeState(in24m, "drifting", "wandered off"), "⚠ DRIFT 24m"},
|
||||
{"active no deadline", session.State{RuntimeState: domain.RuntimeActive}, "● 0m"},
|
||||
{"active past deadline", activeState(now.Add(-5*time.Minute).Unix(), "ontask", ""), "● 0m"},
|
||||
{"active partial minute rounds up", activeState(now.Add(10*time.Second).Unix(), "ontask", ""), "● 1m"},
|
||||
{"idle", mode.Envelope{}, "idle"},
|
||||
{"locked focus", focusEnv(focusState(domain.RuntimeLocked)), ""},
|
||||
{"empty focus runtime", focusEnv(focus.State{}), ""},
|
||||
{"planning", focusEnv(focusState(domain.RuntimePlanning)), "◔ planning"},
|
||||
{"review", focusEnv(focusState(domain.RuntimeReview)), "✓ review"},
|
||||
{"active ontask", focusEnv(activeState(in24m, "ontask", "")), "● 24m"},
|
||||
{"active no drift view", focusEnv(activeState(in24m, "", "")), "● 24m"},
|
||||
{"active drifting", focusEnv(activeState(in24m, "drifting", "")), "⚠ DRIFT 24m"},
|
||||
{"active nudge", focusEnv(activeState(in24m, "ontask", "wandered off")), "● 24m ·?"},
|
||||
{"drift outranks nudge", focusEnv(activeState(in24m, "drifting", "wandered off")), "⚠ DRIFT 24m"},
|
||||
{"active no deadline", focusEnv(focusState(domain.RuntimeActive)), "● 0m"},
|
||||
{"active past deadline", focusEnv(activeState(now.Add(-5*time.Minute).Unix(), "ontask", "")), "● 0m"},
|
||||
{"active partial minute rounds up", focusEnv(activeState(now.Add(10*time.Second).Unix(), "ontask", "")), "● 1m"},
|
||||
{"unknown kind falls back to kind name", mode.Envelope{ActiveMode: "house"}, "house"},
|
||||
{"focus with wrong payload falls back", mode.Envelope{ActiveMode: "focus", Mode: "bogus"}, "focus"},
|
||||
{"offscreen proposed", mode.Envelope{ActiveMode: "offscreen", Mode: map[string]any{"status": "proposed", "next_action": "call mum"}}, "off-screen: call mum"},
|
||||
{"offscreen pending", mode.Envelope{ActiveMode: "offscreen", Mode: map[string]any{"status": "pending"}}, "off-screen: thinking…"},
|
||||
{"offscreen error", mode.Envelope{ActiveMode: "offscreen", Mode: map[string]any{"status": "error", "error": "boom"}}, "off-screen: ⚠"},
|
||||
{"offscreen proposed no action", mode.Envelope{ActiveMode: "offscreen", Mode: map[string]any{"status": "proposed"}}, "off-screen: thinking…"},
|
||||
{"offscreen wrong payload", mode.Envelope{ActiveMode: "offscreen", Mode: "bogus"}, "off-screen: thinking…"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := Render(tc.st, now); got != tc.want {
|
||||
if got := Render(tc.env, now); got != tc.want {
|
||||
t.Errorf("Render() = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
@@ -57,8 +76,8 @@ func TestRender(t *testing.T) {
|
||||
func TestWriterWritesAndDedups(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "status")
|
||||
now := time.Unix(1_000_000, 0)
|
||||
state := session.State{RuntimeState: domain.RuntimePlanning}
|
||||
w := NewWriter(path, func() session.State { return state })
|
||||
env := focusEnv(focusState(domain.RuntimePlanning))
|
||||
w := NewWriter(path, func() mode.Envelope { return env })
|
||||
w.now = func() time.Time { return now }
|
||||
|
||||
w.write()
|
||||
@@ -77,7 +96,7 @@ func TestWriterWritesAndDedups(t *testing.T) {
|
||||
}
|
||||
|
||||
// Changed state: rewrites.
|
||||
state = session.State{RuntimeState: domain.RuntimeLocked}
|
||||
env = focusEnv(focusState(domain.RuntimeLocked))
|
||||
w.write()
|
||||
if got := readFile(t, path); got != "" {
|
||||
t.Errorf("changed write = %q, want empty", got)
|
||||
@@ -86,8 +105,8 @@ func TestWriterWritesAndDedups(t *testing.T) {
|
||||
|
||||
func TestWriterRemovesFileOnCancel(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "status")
|
||||
w := NewWriter(path, func() session.State {
|
||||
return session.State{RuntimeState: domain.RuntimePlanning}
|
||||
w := NewWriter(path, func() mode.Envelope {
|
||||
return focusEnv(focusState(domain.RuntimePlanning))
|
||||
})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
@@ -112,7 +131,7 @@ func TestWriterWakeRendersPromptly(t *testing.T) {
|
||||
|
||||
var mu sync.Mutex
|
||||
st := activeState(in10m, "ontask", "")
|
||||
w := NewWriter(path, func() session.State { mu.Lock(); defer mu.Unlock(); return st })
|
||||
w := NewWriter(path, func() mode.Envelope { mu.Lock(); defer mu.Unlock(); return focusEnv(st) })
|
||||
w.now = func() time.Time { return now }
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
Reference in New Issue
Block a user