From 4768f749177e093743671e4d2d7bdd4d63910f05 Mon Sep 17 00:00:00 2001 From: Ali Al Dallal Date: Wed, 12 Aug 2026 07:41:13 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20dev-liveness=20honesty=20=E2=80=94=20th?= =?UTF-8?q?ird=20badge=20state=20for=20a=20stale=20Go=20rebuild=20(goal=20?= =?UTF-8?q?0029)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The green DEV·live badge vouched for a dead watcher twice tonight: once during the disk-full episode, once when a normal-but-slow rebuild left the binary 15 commits behind while Settings looked broken. Goal 0019's 'DEV wins unconditionally' traded away Go-rebuild honesty for no-false-alarms; both incidents prove the trade needed a third state. goLiveness.ts compares the binary's own build info against the newest mtime of internal/**/*.go (vite dev-middleware-computed, never git HEAD — preserves goal 0019's own fix: docs-only commits stay green). Amber 'DEV · go-stale' names the remedy inline. task dev's start sweep now clears the orphaned vite port before boot and warns (non-blocking) under 2GB free, naming go clean -cache — tonight's own recurring disk-full culprit, caught live twice this session. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft --- .claude/rules/testing.md | 12 +++ Taskfile.yml | 20 +++++ docs/SPEC.md | 24 ++++++ docs/goals/0029-dev-liveness-honesty.md | 32 -------- docs/goals/BACKLOG.md | 17 +++- .../archive/0029-dev-liveness-honesty.md | 69 ++++++++++++++++ .../mill/internal/services/dataevent/index.ts | 6 ++ .../internal/services/dataevent/models.ts | 12 +++ .../mill/internal/services/mcpsvc/index.ts | 1 - .../mill/internal/services/mcpsvc/models.ts | 11 --- .../internal/services/settingssvc/models.ts | 15 ++++ .../wailsapp/wails/v3/internal/eventdata.d.ts | 5 +- frontend/src/app/BuildIdentityBadge.tsx | 24 ++++++ frontend/src/app/goLiveness.test.ts | 42 ++++++++++ frontend/src/app/goLiveness.ts | 79 +++++++++++++++++++ frontend/vite.config.ts | 58 +++++++++++++- .../settingssvc/settingsservice_buildinfo.go | 21 ++++- .../settingsservice_buildinfo_test.go | 12 +++ 18 files changed, 408 insertions(+), 52 deletions(-) delete mode 100644 docs/goals/0029-dev-liveness-honesty.md create mode 100644 docs/goals/archive/0029-dev-liveness-honesty.md create mode 100644 frontend/bindings/github.com/alicoding/mill/internal/services/dataevent/index.ts create mode 100644 frontend/bindings/github.com/alicoding/mill/internal/services/dataevent/models.ts create mode 100644 frontend/src/app/goLiveness.test.ts create mode 100644 frontend/src/app/goLiveness.ts diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index eb8d48d5..40d1ac08 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -124,6 +124,18 @@ layer per capability," never "a seed per thing": - **Manual-only registry** — OS-bound checks (hotkey delivery, real clipboard, tray) listed explicitly with reasons, never silently absent (see goal 0010's enforcement). +- **Dev-loop timing checks** — a non-seed instance of the same manual- + only discipline, outside goal 0010's seed/NodeType registry (that + machinery is keyed to seeded artifacts; this isn't one). Goal 0029's + BuildIdentityBadge third state (amber `DEV · go-stale`) depends on a + real `wails3 dev` rebuild wedging or running slow — CI has no live + file watcher or real Go recompile-and-relaunch cycle to reproduce + that timing deterministically. The pure comparison logic + (`isGoSourceStale`, `frontend/src/app/goLiveness.ts`) is unit-tested + directly (`goLiveness.test.ts`); the full live behavior — an actually + wedged watcher flipping the badge amber in a real window — stays a + manual desktop-mode check (`.claude/skills/run-mill`), named here + rather than silently absent. From the UX point of view the seed layer stays privileged — it's the one a human can SEE working — but correctness under change belongs to diff --git a/Taskfile.yml b/Taskfile.yml index cd215ac5..3700edde 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -127,6 +127,26 @@ tasks: # always begins from exactly one live instance. `|| true`: a clean # start (no orphan) is the normal case, not an error. - pkill -f "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}" || true + # Same failure mode, the vite side (goal 0029, hit live tonight): + # a supervisor killed via SIGHUP can leave the vite dev server + # itself still bound to VITE_PORT (strictPort: true in + # vite.config.ts), so the NEXT `wails3 dev` fails to bind instead + # of just starting -- kill whatever's still listening first. + - lsof -ti :{{.VITE_PORT}} | xargs kill -9 2>/dev/null || true + # Disk-space warning (goal 0029, hit live tonight): a Go rebuild + # can fail or wedge SILENTLY when the disk fills up mid-link, and + # the build-identity badge has no way to tell "wedged" apart from + # "idle" from the outside -- so warn loudly up front instead. + # Non-blocking by design (never refuse to start the dev loop over + # this) -- df -Pk's portable single-line-per-filesystem format + # avoids the wrapped-output parsing footgun plain `df` has on + # long device paths. + - | + FREE_KB=$(df -Pk . | awk 'NR==2 {print $4}') + if [ -n "$FREE_KB" ] && [ "$FREE_KB" -lt 2097152 ] 2>/dev/null; then + FREE_GB=$(( FREE_KB / 1024 / 1024 )) + echo "⚠️ Disk space low (~${FREE_GB}GB free) -- Go rebuilds can fail or wedge silently, and BuildIdentityBadge's go-stale detection can't tell that apart from a healthy idle watcher. Run 'go clean -cache' to free space." + fi - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} setup:docker: diff --git a/docs/SPEC.md b/docs/SPEC.md index d2a7e1fc..878b08b5 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -3933,6 +3933,30 @@ recorded as a real design input (`OPEN`), never silently dropped. moves cleanly from badge-DETECTION (which couldn't distinguish "committed forward" from "orphaned binary" anyway) to Taskfile PREVENTION (the sweep above) — prevention over false-alarm. + **Update (goal 0029, 2026-08-12): a third badge state closes the + remaining honesty gap — DEV·live proved the FRONTEND was live, never + the GO side.** Two real incidents in one night: a disk-full + `wails3 dev` rebuild wedged silently with the badge still green, and + separately a slow watcher cycle left the running binary 15 commits + behind while the badge stayed green — the owner debugged a working + Settings feature as broken because of it. Amber **`DEV · go-stale`** + (tooltip/label: "Go changes not yet in this binary — restart task + dev") now renders whenever `internal/**/*.go`'s newest mtime has + outlived `BuildInfo.BuiltAt` (the running binary's own executable + mtime, `settingsservice_buildinfo.go`) by more than a 30s grace + window — long enough to absorb a normal save-triggered rebuild + (including the ~20s bindings-regen path) without flapping. The + comparison input is served by a vite dev-only middleware computing + the mtime AT REQUEST TIME (`vite.config.ts`'s `goLivenessPlugin`, + `/__mill/go-source-mtime`) — chosen over a task-dev-written heartbeat + file (the goal's other candidate) because it needs no new watcher + process of its own to itself go stale. Deliberately Go-source-mtime, + never git HEAD, repeating goal 0019's own lesson: a docs-only or + frontend-only commit never touches a working-tree file's mtime, so it + can't move this comparison. Paired dev-loop guards from the same + incidents: `task dev`'s start sweep also clears an orphaned vite-port + listener (`lsof -ti :9245`), and a non-blocking pre-start disk check + warns (never blocks) below 2GB free, naming `go clean -cache`. ## 9.5 Platform kernel & extension contract diff --git a/docs/goals/0029-dev-liveness-honesty.md b/docs/goals/0029-dev-liveness-honesty.md deleted file mode 100644 index f4f2ffb8..00000000 --- a/docs/goals/0029-dev-liveness-honesty.md +++ /dev/null @@ -1,32 +0,0 @@ -# 0029 — Dev-liveness honesty: the DEV·live badge must not vouch for a dead watcher - -## Goal -The green DEV·live badge claimed liveness twice while the Go binary -was stale (once wedged by disk-full, once 15 commits behind — the -owner's "can't toggle" Settings report was this, not a bug). Goal -0019's "DEV wins unconditionally" traded away Go-rebuild honesty for -no-false-alarms; both scalps prove the trade needs a third state. - -## Plan -1. [ ] Research-first (small): the cheapest honest Go-liveness signal - in dev — candidates: the binary self-reports its build time via - GetBuildInfo (exists) and the frontend compares against the - NEWEST mtime of internal/**/*.go at bundle-serve time (vite can - compute at request time in dev middleware); or task dev writes a - heartbeat file the binary's own watcher-restart updates. Must NOT - false-alarm on docs-only commits (goal 0019's original trap) — - compare against Go-source state only, never git HEAD. -2. [ ] A third badge state: green DEV·live (both live), amber - DEV·go-stale ("Go changes not yet in this binary — the watcher - may be wedged; restart task dev"), red STALE unchanged. The amber - text names the remedy. -3. [ ] Dev-loop guards from tonight's incidents: the task dev - start-sweep also clears the orphaned vite port (lsof -ti :9245); - a pre-build disk-space check (< 2GB free → loud warning naming - `go clean -cache`, the recurring silent killer). -4. [ ] E2e where testable; manual-only registry for the rest. - -## Acceptance -A wedged watcher shows amber within one poll interval while docs-only -commits stay green; the owner never again debugs a working feature -against a stale binary. diff --git a/docs/goals/BACKLOG.md b/docs/goals/BACKLOG.md index 7fc95877..f02347e7 100644 --- a/docs/goals/BACKLOG.md +++ b/docs/goals/BACKLOG.md @@ -77,10 +77,19 @@ this pipeline and on this code)** verdict recorded in SPEC §3; both literal `/Users/ali` paths de-literalized. Second lint pass (gocritic/prealloc/contextcheck/ sqlclosecheck) named as explicit future work, not done here. -7. [ ] [0029 — Dev-liveness honesty](0029-dev-liveness-honesty.md) — - the DEV·live badge's Go-liveness blind spot, now having claimed a - second scalp (ADR-0035's Consequences note); a third badge state - (amber DEV·go-stale) not yet built. +7. [x] [0029 — Dev-liveness honesty](archive/0029-dev-liveness-honesty.md) + — DELIVERED 2026-08-12: third badge state amber `DEV · go-stale` + (`frontend/src/app/goLiveness.ts` + `BuildIdentityBadge.tsx`), + comparing `BuildInfo.BuiltAt` (the running binary's own executable + mtime) against the newest `internal/**/*.go` mtime served by a + vite dev-only middleware (`vite.config.ts`'s `goLivenessPlugin`) — + chosen over a task-dev-heartbeat-file candidate since it needs no + new watcher process; deliberately Go-source-mtime, never git HEAD, + so it can't repeat goal 0019's false-alarm. `task dev`'s start + sweep also clears an orphaned vite port and warns (non-blocking) + below 2GB free disk. Pure comparison unit-tested + (`goLiveness.test.ts`); the full live-wedge behavior entered + `.claude/rules/testing.md`'s manual-only note. 8. [x] [0030 — Node standard](archive/0030-node-standard.md) — DELIVERED 2026-08-12: `.claude/rules/node-standard.md` (8-item checklist, citing n8n's community-node/UX/error-handling diff --git a/docs/goals/archive/0029-dev-liveness-honesty.md b/docs/goals/archive/0029-dev-liveness-honesty.md new file mode 100644 index 00000000..06e3d098 --- /dev/null +++ b/docs/goals/archive/0029-dev-liveness-honesty.md @@ -0,0 +1,69 @@ +# 0029 — Dev-liveness honesty: the DEV·live badge must not vouch for a dead watcher + +## Goal +The green DEV·live badge claimed liveness twice while the Go binary +was stale (once wedged by disk-full, once 15 commits behind — the +owner's "can't toggle" Settings report was this, not a bug). Goal +0019's "DEV wins unconditionally" traded away Go-rebuild honesty for +no-false-alarms; both scalps prove the trade needs a third state. + +## Plan +1. [x] Research-first (small): the cheapest honest Go-liveness signal + in dev — candidates: the binary self-reports its build time via + GetBuildInfo (exists) and the frontend compares against the + NEWEST mtime of internal/**/*.go at bundle-serve time (vite can + compute at request time in dev middleware); or task dev writes a + heartbeat file the binary's own watcher-restart updates. Must NOT + false-alarm on docs-only commits (goal 0019's original trap) — + compare against Go-source state only, never git HEAD. + **Chosen: candidate 1 (binary mtime vs. vite-middleware mtime)** — + the binary already self-reports (`GetBuildInfo` existed; added + `BuiltAt`, its own executable's mtime, since `vcs.time` is pinned to + the last COMMIT and never moves across an uncommitted `wails3 dev` + relink). The heartbeat-file candidate would need teaching a + third-party supervisor (`wails3 dev`/`atterpac/refresh`) to write a + file on rebuild — no such hook exists. Two numbers, one comparison, + no new watcher process to itself go stale. +2. [x] A third badge state: green DEV·live (both live), amber + DEV·go-stale ("Go changes not yet in this binary — the watcher + may be wedged; restart task dev"), red STALE unchanged. The amber + text names the remedy. + Built: `frontend/src/app/goLiveness.ts` (`isGoSourceStale`, + `useGoSourceStale`, a 30s grace window absorbing a normal + save-triggered rebuild incl. the ~20s bindings-regen path), + `frontend/src/app/BuildIdentityBadge.tsx` (amber `Label + variant="attention"`, label + title both name the remedy), + `frontend/vite.config.ts` (`goLivenessPlugin`, dev-only + `/__mill/go-source-mtime` middleware, request-time mtime walk of + `internal/**/*.go` — never git HEAD, so a docs/frontend-only commit + can't move it), `internal/services/settingssvc/settingsservice_buildinfo.go` + (`BuildInfo.BuiltAt`). +3. [x] Dev-loop guards from tonight's incidents: the task dev + start-sweep also clears the orphaned vite port (lsof -ti :9245); + a pre-build disk-space check (< 2GB free → loud warning naming + `go clean -cache`, the recurring silent killer). + Built in `Taskfile.yml`'s `dev` task, both non-blocking. Verified + live against the actual tight-disk conditions this session ran + under (~1.8GB free triggered the real warning text). +4. [x] E2e where testable; manual-only registry for the rest. + The pure comparison (`isGoSourceStale`) is unit-tested directly + (`frontend/src/app/goLiveness.test.ts`, 6 cases incl. the + docs-commit-never-moves-it case). The full live behavior (an + actually wedged watcher flipping a real window's badge amber) is + impractical to reproduce deterministically in CI — entered in + `.claude/rules/testing.md`'s manual-only note with its reason, + pointing at `.claude/skills/run-mill` for the manual check. + +## Acceptance +A wedged watcher shows amber within one poll interval while docs-only +commits stay green; the owner never again debugs a working feature +against a stale binary. + +**Met 2026-08-12.** The comparison polls every 5s +(`GO_LIVENESS_POLL_MS`) so a genuinely wedged watcher (source mtime +outliving `BuiltAt` past the 30s grace) surfaces amber within one poll +interval of that threshold; the comparison only ever reads +`internal/**/*.go` mtimes (never git HEAD, never the frontend tree), so +a docs-only or frontend-only commit structurally cannot move it — +covered by `goLiveness.test.ts`'s dedicated case. `docs/SPEC.md` §3.8's +build-identity entry documents the third state and mechanism choice. diff --git a/frontend/bindings/github.com/alicoding/mill/internal/services/dataevent/index.ts b/frontend/bindings/github.com/alicoding/mill/internal/services/dataevent/index.ts new file mode 100644 index 00000000..18a72d4a --- /dev/null +++ b/frontend/bindings/github.com/alicoding/mill/internal/services/dataevent/index.ts @@ -0,0 +1,6 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export type { + Changed +} from "./models.js"; diff --git a/frontend/bindings/github.com/alicoding/mill/internal/services/dataevent/models.ts b/frontend/bindings/github.com/alicoding/mill/internal/services/dataevent/models.ts new file mode 100644 index 00000000..0db0128c --- /dev/null +++ b/frontend/bindings/github.com/alicoding/mill/internal/services/dataevent/models.ts @@ -0,0 +1,12 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * Changed is the live-sync event payload: which kind of entity changed + * (e.g. "workflow", "request", "list", "mcpserver", "decision", + * "execenv", "guardrail-rule", "run") and its ID. + */ +export interface Changed { + "entity": string; + "id": string; +} diff --git a/frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/index.ts b/frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/index.ts index 86b42909..a507ed0a 100644 --- a/frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/index.ts +++ b/frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/index.ts @@ -2,7 +2,6 @@ // This file is automatically generated. DO NOT EDIT export type { - DataChanged, MCPWriteActivity, MCPWriteRequest, MCPWriteResolved diff --git a/frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/models.ts b/frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/models.ts index d26df445..79da4ae3 100644 --- a/frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/models.ts +++ b/frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/models.ts @@ -1,17 +1,6 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -/** - * DataChanged is the live-sync event (docs/adr/0025): emitted after - * any MCP-driven mutation so an open Mill window refreshes what the - * LLM just changed -- §1's what-you-see-is-what-I-see thesis running - * in both directions. - */ -export interface DataChanged { - "entity": string; - "id": string; -} - /** * MCPWriteActivity is pushed for a resolved (denied/cancelled/expired, * or approved-but-failed) MCP write so it's no longer traceless diff --git a/frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/models.ts b/frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/models.ts index 467c2e9a..fb224b12 100644 --- a/frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/models.ts +++ b/frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/models.ts @@ -40,6 +40,21 @@ export interface BuildInfo { * genuinely knows. */ "Server": boolean; + + /** + * BuiltAt is the unix-millis mtime of THIS PROCESS'S OWN executable + * file -- when it was actually linked, not when its source was last + * committed. Deliberately not vcs.time (goal 0029, dev-liveness + * honesty): vcs.time reflects the last COMMIT's timestamp, which + * stays frozen across every `wails3 dev` relink of an uncommitted + * change (vcs.modified just goes true) -- exactly the case a wedged + * or slow rebuild watcher needs distinguished from "healthy." A + * binary's own mtime moves on every real relink regardless of git + * state, so the frontend can compare it against the newest mtime of + * internal/** /*.go (vite's dev-only middleware, vite.config.ts) to + * tell "rebuilt after this save" from "still running the old one." + */ + "BuiltAt": number; } /** diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts index 5b9a3cb5..026bf919 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -5,6 +5,9 @@ // @ts-ignore: Unused imports import type { Events } from "@wailsio/runtime"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import type * as dataevent$0 from "../../../../alicoding/mill/internal/services/dataevent/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import type * as executionsvc$0 from "../../../../alicoding/mill/internal/services/executionsvc/models.js"; @@ -22,7 +25,7 @@ declare module "@wailsio/runtime" { "hotkey-activity": triggersvc$0.HotkeyActivity; "mcp-write-activity": mcpsvc$0.MCPWriteActivity; "mcp-write-approval": mcpsvc$0.MCPWriteRequest; - "mill-data-changed": mcpsvc$0.DataChanged; + "mill-data-changed": dataevent$0.Changed; "mill-navigate": string; "time": string; } diff --git a/frontend/src/app/BuildIdentityBadge.tsx b/frontend/src/app/BuildIdentityBadge.tsx index dd96840b..528a0453 100644 --- a/frontend/src/app/BuildIdentityBadge.tsx +++ b/frontend/src/app/BuildIdentityBadge.tsx @@ -1,6 +1,7 @@ import { Label } from '@primer/react' import { SettingsService } from '../shared/bindings' import type { BuildInfo } from '../shared/bindings' +import { useGoSourceStale } from './goLiveness' import styles from './App.module.css' // Extracted from App.tsx along the 500-line seam (goal 0019). ONE rule @@ -25,6 +26,11 @@ import styles from './App.module.css' // by an old binary -- docs/SPEC.md §3.8). const isDevBuild = import.meta.env.DEV +// Third badge state (goal 0029, dev-liveness honesty): amber +// `DEV · go-stale` when Go source has moved since this binary was +// built but the rebuild hasn't landed yet -- the comparison logic and +// its own reasoning live in ./goLiveness (pulled out so it can be +// unit-tested without dragging @primer/react's CSS into Vitest). export function BuildIdentityBadge({ buildInfo }: { buildInfo: BuildInfo | null }) { // Go's own build tag (BuildInfo.Server), not a window-global sniff -- // `'_wails' in window` is true in server-mode browser tabs too (the @@ -32,6 +38,9 @@ export function BuildIdentityBadge({ buildInfo }: { buildInfo: BuildInfo | null // INSTALLED/SERVER split until goal 0021's dogfooding caught it. const isNativeWebview = buildInfo != null && !buildInfo.Server const binaryHead = buildInfo?.Revision ? buildInfo.Revision.slice(0, 7) : '' + // Called unconditionally (rules of hooks) -- it no-ops outside a dev + // build or before buildInfo has arrived, per its own guard. + const goSourceStale = useGoSourceStale(buildInfo?.BuiltAt) // DEV wins over the stale comparison, deliberately (fixed 2026-08-11 // after it false-alarmed on every commit): under `task dev`, vite @@ -45,6 +54,21 @@ export function BuildIdentityBadge({ buildInfo }: { buildInfo: BuildInfo | null // commit as the binary. Dev-orphan windows are handled by prevention // now (the Taskfile pkill sweep, SPEC §3.8), not by this badge. if (isDevBuild) { + // Third state (goal 0029): Go source has moved since this binary + // was linked and no rebuild has landed within the grace window -- + // named remedy in both the visible label and the tooltip, since + // amber (unlike green/red) isn't self-explanatory at a glance. + if (goSourceStale) { + return ( + + ) + } return (