Files
antidrift/docs/superpowers/plans/2026-05-31-m4-look-good.md
T
2026-05-31 21:03:34 -04:00

626 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M4 — "Look good" Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Restyle the AntiDrift web UI into a cockpit-style, state-aware stacked HUD, split CSS/JS out of the inline HTML, and polish the review screen — with zero behavior changes.
**Architecture:** The UI is a single embedded static bundle served by `internal/web`. Today everything lives inline in `static/index.html`. We split it into `index.html` + `app.css` + `app.js`, add Go routes to serve the two new assets from the existing `embed.FS`, then rework the client markup into reusable "band" rows whose accent color is driven by a `data-state` attribute on `<main>`. All SSE wiring, element ids, and POST endpoints are preserved, so the existing Go endpoint tests keep passing.
**Tech Stack:** Go 1.26, Gin, `embed.FS`, server-sent events; plain HTML/CSS/vanilla JS (no framework, no build step). Tests: stdlib `testing` + `net/http/httptest`.
---
## Reference: shared CSS class vocabulary
These class/attribute names are used by BOTH `app.css` and `app.js`. Keep them identical across tasks:
- `<main id="app" data-state="...">``data-state``locked | planning | active | nudge | drift | review`. Drives `--accent`.
- `.band` — a horizontal HUD row (top divider + padding). `.band:first-child` has no top border.
- `.statusband` — the top band; shows the state pill + status meta, takes a thicker accent top border.
- `.pill` — uppercase state label, colored by `--accent`.
- `.status-meta` — dim text beside the pill (e.g. "on task · 7 switches").
- `.timer` — large tabular-nums countdown.
- `.action` — the next-action line. `.meta` — dim secondary text (e.g. "done when: …").
- `.evidence` (band) with children `.now`, `.buckets` (`<ul>`/`<li>`), `.switches`; health spans `.health-ok` / `.health-bad`.
- `.summary` (review band) with `.summary-row` children.
- Buttons: `.btn`, `.btn-primary`, `.btn-ghost`.
- The `data-state` value is derived by `stateKey(state)` (defined in Task 2), which accounts for the client-side `dismissedNudge`.
Preserved element ids (do not rename): `#app`, `#intent`, `#sharpen`, `#coachStatus`, `#na`, `#sc`, `#mins`, `#apps`, `#start`, `#statusband`, `#t`, `#done`, `#end`, `#plan`, `#refocus`, `#ontask`, `#enddrift`, `#dismissnudge`.
Preserved endpoints: `GET /events`; `POST /planning /coach /commitment /complete /end /refocus /ontask`.
---
## Task 1: Split assets out of the inline HTML and serve them
Mechanical, behavior-preserving: move the inline `<style>`/`<script>` into `app.css`/`app.js`, link them, and add Go routes so they are served from the embedded FS. No visual or logic change yet.
**Files:**
- Create: `internal/web/static/app.css`
- Create: `internal/web/static/app.js`
- Modify: `internal/web/static/index.html` (replace whole file)
- Modify: `internal/web/web.go:39-56` (add two asset routes)
- Test: `internal/web/web_test.go` (add one test func)
- [ ] **Step 1: Write the failing test for asset serving**
Add to `internal/web/web_test.go`:
```go
func TestServesStaticAssets(t *testing.T) {
s := newTestServer(t)
r := s.Router()
cases := []struct {
path string
ctSubstr string
}{
{"/app.css", "css"},
{"/app.js", "javascript"},
}
for _, tc := range cases {
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("GET %s code = %d, want 200", tc.path, w.Code)
}
if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, tc.ctSubstr) {
t.Errorf("GET %s Content-Type = %q, want substring %q", tc.path, ct, tc.ctSubstr)
}
if w.Body.Len() == 0 {
t.Errorf("GET %s body is empty", tc.path)
}
}
}
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `go test ./internal/web/ -run TestServesStaticAssets -v`
Expected: FAIL — routes return 404 (and the embedded files don't exist yet).
- [ ] **Step 3: Create `app.css` by moving the inline styles verbatim**
Cut the entire current contents of the `<style>…</style>` block in `internal/web/static/index.html` (everything between the tags, currently lines 841) and paste it unchanged into the new file `internal/web/static/app.css`. Do not edit the rules in this task — this is a pure move. (They are replaced wholesale in Task 2.)
- [ ] **Step 4: Create `app.js` by moving the inline script verbatim**
Cut the entire current contents of the `<script>…</script>` block in `internal/web/static/index.html` (everything between the tags, currently lines 49227) and paste it unchanged into the new file `internal/web/static/app.js`.
- [ ] **Step 5: Replace `index.html` with the markup shell**
Replace the whole file `internal/web/static/index.html` with:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AntiDrift</title>
<link rel="stylesheet" href="/app.css">
</head>
<body>
<main id="app">
<h1>AntiDrift</h1>
<div class="card" id="view">connecting…</div>
</main>
<script src="/app.js"></script>
</body>
</html>
```
Note: the script must reference `view` and (from Task 2 on) `app`; both ids exist here. The script tag is at the end of `<body>`, so the DOM is parsed before it runs (matches the current inline-at-end-of-body behavior).
- [ ] **Step 6: Add asset routes in `web.go`**
In `internal/web/web.go`, the `Router()` method currently has (around lines 4346):
```go
sub, _ := fs.Sub(staticFS, "static")
r.GET("/", func(c *gin.Context) {
c.FileFromFS("/", http.FS(sub))
})
```
Add the two asset routes immediately after the `r.GET("/", …)` block:
```go
r.GET("/app.css", func(c *gin.Context) {
c.FileFromFS("/app.css", http.FS(sub))
})
r.GET("/app.js", func(c *gin.Context) {
c.FileFromFS("/app.js", http.FS(sub))
})
```
- [ ] **Step 7: Run the asset test and the full web suite**
Run: `go test ./internal/web/ -v`
Expected: PASS — `TestServesStaticAssets` passes and all pre-existing web tests still pass (they assert on endpoints/JSON, not markup).
- [ ] **Step 8: Verify vet and the whole module**
Run: `go vet ./... && go test -race ./...`
Expected: clean, all green.
- [ ] **Step 9: Commit**
```bash
git add internal/web/static/index.html internal/web/static/app.css internal/web/static/app.js internal/web/web.go internal/web/web_test.go
git commit -m "M4: split web UI assets into app.css and app.js
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 2: Cockpit visual system + locked/planning/active/drift/nudge
Replace `app.css` with the new design system and rewrite `app.js`'s rendering into stacked bands with a `data-state`-driven accent. Review stays a simple functional band here; Task 3 turns it into the recap. No Go changes — the existing endpoint tests are the regression guard.
**Files:**
- Modify: `internal/web/static/app.css` (replace whole file)
- Modify: `internal/web/static/app.js` (replace whole file)
- Test: existing `internal/web/web_test.go` (regression only — no new test)
- [ ] **Step 1: Replace `app.css` with the cockpit design system**
Replace the whole file `internal/web/static/app.css` with:
```css
:root {
color-scheme: dark;
--bg: #0d0f14;
--panel: #161922;
--line: #232733;
--ink: #e6e8ee;
--ink-dim: #8b91a6;
--ok: #34d399;
--warn: #f0b429;
--danger: #f06070;
--accent: #6b7280; /* default / locked */
}
/* The single state-driven knob: only --accent changes per state. */
[data-state="planning"] { --accent: #4c6ef5; }
[data-state="active"] { --accent: #34d399; }
[data-state="nudge"] { --accent: #f0b429; }
[data-state="drift"] { --accent: #f06070; }
[data-state="review"] { --accent: #a78bfa; }
* { box-sizing: border-box; }
body {
margin: 0;
font: 15px/1.5 system-ui, sans-serif;
background: var(--bg);
color: var(--ink);
}
main { max-width: 560px; margin: 7vh auto; padding: 0 20px; }
h1 {
font-size: 12px; letter-spacing: .28em; text-transform: uppercase;
color: var(--ink-dim); margin: 0 0 14px 6px;
}
/* The HUD card: a stack of bands. */
.card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 14px;
overflow: hidden;
}
.band { border-top: 1px solid var(--line); padding: 16px 20px; }
.band:first-child { border-top: 0; }
.statusband {
display: flex; align-items: baseline; gap: 12px;
border-top: 3px solid var(--accent);
background: color-mix(in srgb, var(--accent) 8%, var(--panel));
}
.pill {
font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
font-weight: 700; color: var(--accent);
}
.status-meta { font-size: 13px; color: var(--ink-dim); }
.timer {
font-size: 52px; font-weight: 700; line-height: 1;
font-variant-numeric: tabular-nums; color: var(--ink);
}
.action { font-size: 19px; font-weight: 600; }
.meta { color: var(--ink-dim); margin: 4px 0 0; }
label { display: block; font-size: 12px; color: var(--ink-dim); margin: 14px 0 5px; }
label:first-child { margin-top: 0; }
input {
width: 100%; padding: 10px 12px;
background: var(--bg); border: 1px solid var(--line);
border-radius: 8px; color: var(--ink); font: inherit;
}
input:focus { outline: 0; border-color: var(--accent); }
.btn {
margin-top: 16px; padding: 10px 16px; border: 0; border-radius: 8px;
font: inherit; font-weight: 600; cursor: pointer;
}
.btn-primary { background: var(--accent); color: #0d0f14; }
.btn-ghost { background: var(--line); color: var(--ink); }
.btn:disabled { background: var(--line); color: var(--ink-dim); cursor: not-allowed; }
/* Drift/nudge actions live in the status band; lay them out below the text. */
.band-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }
.band-actions .btn { margin-top: 0; padding: 7px 12px; }
.drift-reason, .nudge-msg { color: var(--ink); margin: 4px 0 0; font-weight: 400; }
.statusband.col { flex-direction: column; align-items: stretch; gap: 6px; }
/* Evidence band */
.evidence .now { font-size: 13px; color: var(--ink); }
.health-ok { color: var(--ok); }
.health-bad { color: var(--warn); }
.buckets { list-style: none; padding: 0; margin: 10px 0 0; font-size: 13px; color: var(--ink-dim); }
.buckets li {
display: flex; justify-content: space-between; padding: 2px 0;
font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums;
}
.switches { font-size: 12px; color: var(--ink-dim); margin-top: 8px; }
/* Review summary band (built out in Task 3) */
.summary { font-size: 14px; }
.summary-row {
display: flex; justify-content: space-between; padding: 3px 0;
font-variant-numeric: tabular-nums; color: var(--ink-dim);
}
.summary-row span:last-child { color: var(--ink); font-family: ui-monospace, monospace; }
```
- [ ] **Step 2: Replace `app.js` with the band-based renderer**
Replace the whole file `internal/web/static/app.js` with:
```js
const app = document.getElementById('app');
const view = document.getElementById('view');
let countdownTimer = null;
let renderedState = null; // which runtime_state the DOM currently shows
let dismissedNudge = null; // text of the nudge the user has dismissed
function post(path, body) {
return fetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
}
function fmt(secs) {
secs = Math.max(0, Math.floor(secs));
const m = String(Math.floor(secs / 60)).padStart(2, '0');
const s = String(secs % 60).padStart(2, '0');
return `${m}:${s}`;
}
// stateKey maps server state to the data-state accent bucket. Drift outranks
// a nudge, and a nudge the user already dismissed reverts to plain active.
function stateKey(state) {
const rs = state.runtime_state;
if (rs !== 'active') return rs || 'locked';
const d = state.drift || {};
if (d.status === 'drifting') return 'drift';
if (d.nudge && d.nudge !== dismissedNudge) return 'nudge';
return 'active';
}
function setStateAttr(state) {
app.dataset.state = stateKey(state);
}
function evidenceBlock(ev) {
if (!ev) return '';
const health = ev.available
? `<span class="health-ok">tracking</span>`
: `<span class="health-bad">evidence unavailable: ${ev.reason || 'unknown'}</span>`;
const now = ev.current && (ev.current.class || ev.current.title)
? `${ev.current.class || '?'} · ${ev.current.title || ''}` : '—';
const rows = (ev.buckets || []).map(b =>
`<li><span>${(b.class || '?')} · ${b.title || ''}</span><span>${fmt(b.seconds)}</span></li>`).join('');
return `<div class="band evidence">
<div class="now">now ${now} &nbsp; ${health}</div>
<ul class="buckets">${rows}</ul>
<div class="switches">context switches: ${ev.switch_count || 0}</div>
</div>`;
}
// updateActiveDrift owns the active status band: it renders on-task, nudge,
// drift, or pending content into #statusband and rebinds the buttons.
function updateActiveDrift(state) {
const el = document.getElementById('statusband');
if (!el) return;
setStateAttr(state);
const drift = state.drift || {};
const switches = (state.evidence && state.evidence.switch_count) || 0;
if (drift.status === 'drifting') {
el.className = 'band statusband col';
el.innerHTML = `<div><span class="pill">Drift</span></div>
<div class="drift-reason">${drift.reason || 'This looks off task.'}</div>
<div class="band-actions">
<button id="refocus" type="button" class="btn btn-primary">Back to task</button>
<button id="ontask" type="button" class="btn btn-ghost">This is on task</button>
<button id="enddrift" type="button" class="btn btn-ghost">End session</button>
</div>`;
document.getElementById('refocus').onclick = () => post('/refocus');
document.getElementById('ontask').onclick = () => post('/ontask');
document.getElementById('enddrift').onclick = () => post('/complete');
return;
}
const nudge = drift.nudge || '';
if (!nudge) dismissedNudge = null; // trajectory recovered; allow future nudges
if (nudge && nudge !== dismissedNudge) {
el.className = 'band statusband col';
el.innerHTML = `<div><span class="pill">Heads up</span></div>
<div class="nudge-msg">${nudge}</div>
<div class="band-actions">
<button id="dismissnudge" type="button" class="btn btn-ghost">Dismiss</button>
</div>`;
document.getElementById('dismissnudge').onclick = () => {
dismissedNudge = nudge;
setStateAttr(state);
updateActiveDrift(state);
};
return;
}
if (drift.status === 'pending') {
el.className = 'band statusband';
el.innerHTML = `<span class="pill">Active</span><span class="status-meta">checking focus…</span>`;
return;
}
el.className = 'band statusband';
el.innerHTML = `<span class="pill">Active</span>
<span class="status-meta">on task · ${switches} switches</span>`;
}
function updatePlanningCoach(coach) {
const statusEl = document.getElementById('coachStatus');
if (!statusEl) return;
if (!coach || coach.status === 'idle') { statusEl.textContent = ''; return; }
if (coach.status === 'pending') { statusEl.textContent = 'Sharpening…'; return; }
if (coach.status === 'error') { statusEl.textContent = coach.error || 'coach unavailable'; return; }
if (coach.status === 'ready' && coach.proposal) {
statusEl.textContent = 'AI suggestion applied — edit freely.';
const stamp = JSON.stringify(coach.proposal);
if (statusEl.dataset.applied === stamp) return;
statusEl.dataset.applied = stamp;
const na = document.getElementById('na'), sc = document.getElementById('sc'),
mins = document.getElementById('mins'), start = document.getElementById('start');
if (na && !na.value.trim()) na.value = coach.proposal.next_action || '';
if (sc && !sc.value.trim()) sc.value = coach.proposal.success_condition || '';
if (mins && coach.proposal.timebox_secs) mins.value = Math.round(coach.proposal.timebox_secs / 60);
if (start) start.disabled = !(na.value.trim() && sc.value.trim() && +mins.value > 0);
const apps = document.getElementById('apps');
if (apps && !apps.value.trim() && Array.isArray(coach.proposal.allowed_window_classes)) {
apps.value = coach.proposal.allowed_window_classes.join(', ');
}
}
}
function render(state) {
setStateAttr(state);
const rs = state.runtime_state;
if (rs === 'planning' && renderedState === 'planning') {
updatePlanningCoach(state.coach);
return;
}
if (rs === 'active' && renderedState === 'active') {
updateActiveDrift(state);
return;
}
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
renderedState = rs;
if (rs === 'locked') {
view.innerHTML = `<div class="band statusband"><span class="pill">Locked</span></div>
<div class="band">
<p class="meta">No active commitment.</p>
<button id="plan" class="btn btn-primary">Start planning</button>
</div>`;
document.getElementById('plan').onclick = () => post('/planning');
} else if (rs === 'planning') {
view.innerHTML = `<div class="band statusband"><span class="pill">Planning</span></div>
<div class="band">
<label>Rough intent</label>
<input id="intent" placeholder="e.g. work on the quarterly report">
<button id="sharpen" type="button" class="btn btn-ghost">Sharpen</button>
<div id="coachStatus" class="meta"></div>
</div>
<div class="band">
<label>Next action</label><input id="na" placeholder="What exactly will you do?">
<label>Success condition</label><input id="sc" placeholder="How do you know it's done?">
<label>Minutes</label><input id="mins" type="number" min="1" value="25">
<label>Allowed apps (comma-separated window classes)</label>
<input id="apps" placeholder="e.g. code, firefox">
<button id="start" class="btn btn-primary" disabled>Start commitment</button>
</div>`;
const na = document.getElementById('na'), sc = document.getElementById('sc'),
mins = document.getElementById('mins'), start = document.getElementById('start');
const check = () => { start.disabled = !(na.value.trim() && sc.value.trim() && +mins.value > 0); };
[na, sc, mins].forEach(el => el.oninput = check);
start.onclick = () => post('/commitment', {
next_action: na.value.trim(),
success_condition: sc.value.trim(),
timebox_secs: Math.round(+mins.value * 60),
allowed_window_classes: (document.getElementById('apps').value || '')
.split(',').map(s => s.trim()).filter(Boolean),
});
document.getElementById('sharpen').onclick = () => {
const intent = document.getElementById('intent').value.trim();
if (intent) post('/coach', { intent });
};
updatePlanningCoach(state.coach);
} else if (rs === 'active') {
const c = state.commitment || {};
view.innerHTML = `<div class="band statusband" id="statusband"></div>
<div class="band"><div class="timer" id="t">--:--</div></div>
<div class="band">
<div class="action">${c.next_action || ''}</div>
<p class="meta">done when: ${c.success_condition || ''}</p>
</div>
${evidenceBlock(state.evidence)}
<div class="band"><button id="done" class="btn btn-primary">Complete</button></div>`;
document.getElementById('done').onclick = () => post('/complete');
const t = document.getElementById('t');
const tick = () => { t.textContent = fmt((c.deadline_unix_secs || 0) - Date.now() / 1000); };
tick();
countdownTimer = setInterval(tick, 250);
updateActiveDrift(state);
} else if (rs === 'review') {
const c = state.commitment || {};
view.innerHTML = `<div class="band statusband"><span class="pill">Review</span></div>
<div class="band">
<div class="action">Session ended</div>
<p class="meta">${c.next_action || ''}</p>
</div>
${evidenceBlock(state.evidence)}
<div class="band"><button id="end" class="btn btn-primary">End</button></div>`;
document.getElementById('end').onclick = () => post('/end');
} else {
view.textContent = rs;
}
}
const es = new EventSource('/events');
es.onmessage = (e) => render(JSON.parse(e.data));
es.onerror = () => { view.textContent = 'disconnected — is antidriftd running?'; };
```
- [ ] **Step 3: Run the web suite (regression guard)**
Run: `go test ./internal/web/ -v`
Expected: PASS — all endpoint/asset tests still green (markup changed, contracts did not).
- [ ] **Step 4: Manual visual check**
Run the daemon and click through states:
```bash
go run ./cmd/antidriftd
```
Verify in the browser (http://localhost:7777):
- Locked: gray accent, "Start planning".
- Planning: blue accent, all fields, Sharpen + Start.
- Active on-task: green accent, "Active · on task · N switches", big timer, evidence band.
- Drift (force by setting allowed apps then switching to an off-list window): red accent, reason + three buttons.
- Nudge (semantic nudge within an allowed app): amber accent, message + Dismiss; after Dismiss the accent returns to green.
- [ ] **Step 5: Commit**
```bash
git add internal/web/static/app.css internal/web/static/app.js
git commit -m "M4: cockpit HUD with state-driven accent
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 3: Polish the review screen into a session recap
Turn the bare review state into a presentational summary built from data the state already carries (`commitment` + `evidence`). No new backend data.
**Files:**
- Modify: `internal/web/static/app.js` (add `reviewSummary` helper; swap the review branch's markup)
- Modify: `internal/web/static/app.css` (`.summary` styles already added in Task 2 — verify, no change expected)
- Test: existing `internal/web/web_test.go` (regression only)
- [ ] **Step 1: Add the `reviewSummary` helper to `app.js`**
Insert this function immediately after `evidenceBlock` in `internal/web/static/app.js`:
```js
// reviewSummary renders a presentational recap from already-available state:
// the commitment plus the per-window evidence buckets. No new backend data.
function reviewSummary(ev) {
if (!ev) return '';
const rows = (ev.buckets || []).map(b =>
`<div class="summary-row"><span>${(b.class || '?')} · ${b.title || ''}</span><span>${fmt(b.seconds)}</span></div>`).join('');
const switches = `<div class="summary-row"><span>context switches</span><span>${ev.switch_count || 0}</span></div>`;
return `<div class="band summary">${switches}${rows}</div>`;
}
```
- [ ] **Step 2: Swap the review branch markup to use the recap**
In `internal/web/static/app.js`, find the review branch (the `else if (rs === 'review')` block) and replace its `view.innerHTML = …` assignment. Change FROM:
```js
view.innerHTML = `<div class="band statusband"><span class="pill">Review</span></div>
<div class="band">
<div class="action">Session ended</div>
<p class="meta">${c.next_action || ''}</p>
</div>
${evidenceBlock(state.evidence)}
<div class="band"><button id="end" class="btn btn-primary">End</button></div>`;
```
TO:
```js
view.innerHTML = `<div class="band statusband"><span class="pill">Review</span></div>
<div class="band">
<div class="action">Session ended</div>
<p class="meta">${c.next_action || ''}</p>
<p class="meta">done when: ${c.success_condition || ''}</p>
</div>
${reviewSummary(state.evidence)}
<div class="band"><button id="end" class="btn btn-primary">End</button></div>`;
```
(The verbose live `evidenceBlock` is replaced by the calmer `reviewSummary` recap; the "now / tracking" line belongs to the active HUD, not the post-session summary.)
- [ ] **Step 3: Run the web suite (regression guard)**
Run: `go test ./internal/web/ -v`
Expected: PASS.
- [ ] **Step 4: Manual visual check of review**
Start a short commitment (1 minute) with allowed apps, switch between a couple of windows, let it expire (or click Complete), and confirm the review screen shows: violet accent, "Session ended", the action + success condition, then a summary band listing context switches and the per-window time buckets, then End.
- [ ] **Step 5: Final module verification**
Run: `go vet ./... && go test -race ./...`
Expected: clean, all green.
- [ ] **Step 6: Commit**
```bash
git add internal/web/static/app.js internal/web/static/app.css
git commit -m "M4: presentational review recap
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## After all tasks
Dispatch a final code reviewer over the three commits, then use **superpowers:finishing-a-development-branch** to close out M4.
Done when: the UI renders the cockpit HUD across all six `data-state` values, CSS/JS are served as separate embedded assets, the review screen shows a recap, and `go vet ./... && go test -race ./...` is clean.