← Commit history

pup-user-foreground: add the VISIBILITY INVARIANT (rule 7) after John's 'why can't i open this pup window' - the doctrine only covered demotions, so a nudge that HID the window slipped past every guard

John Lauer ·f88a1c8926 ·2mo ago ·parent 73bc802
1 file changed +40
dev-skills/pup-user-foreground/SKILL.md+40
@@ -58,6 +58,26 @@ loaded gun pointed at the user's click. 6. **The AI raising a window is a separate, GATED thing.** `foreground:true` requires    `foregroundReason` quoting the user's ask. A user-raise (observed) and an AI-raise (granted)    both set the same protective flags.+7. **THE VISIBILITY INVARIANT: no pup window may ever be left hidden, and pup never changes the+   show-state the user chose** (v2.0.251-253). Rules 1-6 all reason about DEMOTION, so for two+   months nothing here covered the other way to break a taskbar click: making the window+   *invisible*. The taskbar-identity nudge does `ShowWindow(SW_HIDE)` -> 80ms -> re-show, and its+   re-show was a separate statement in a PowerShell process node kills at 20s. Any interruption in+   that gap (timeout kill on a loaded box, crash, sleep) left the window `IsWindowVisible=false`+   behind a live taskbar button — John clicked it and got nothing, exactly the v1.9.133 symptom+   from a completely different cause. Three parts, all required:+   - **Any hide puts its re-show in a `finally`,** so a throw cannot strand the window;+   - **the caller verifies the window came back,** because a KILLED process runs no finally —+     verification has to live where the kill cannot reach;+   - **a 120s sweep asserts the invariant for the whole population** (`sweepVisibility`), so any+     future path that hides a window is repaired whether or not its author remembered this page.+   The re-show restores the state we FOUND: `SW_SHOWMINNOACTIVE` for a window the user had+   minimized, `SW_SHOWNOACTIVATE` otherwise. The old code always used the latter, so every nudge+   silently un-minimized windows John had deliberately put away.+   **Why the repair is unconditionally safe** (and why it needs no `userIsUsingWindow` check, and+   is deliberately NOT `minimalTouch`-gated): a human can minimize a window but a human can never+   HIDE one. A hidden pup window is therefore always pup's own mess, and re-showing it can only+   ever give the user back something pup took.  ## The history that hardened each rule (do not re-learn these live) @@ -71,6 +91,8 @@ loaded gun pointed at the user's click. | v1.9.292 | `osIdleMs` introduced to tell John's click from a JS raise. | | v2.0.11 (reverted) | Placement-first parking during load FAILED: the session is not registered until after navigation, so early parks have no hwnd and retreat. Do not retry that shape without moving session registration first. | | v2.0.26 | The force-flag fight: re-asserts with force demoted a window John had JUST raised, because flag guards lag. Fix = recent-input check inside the primitive, same pass, even under force. |+| v2.0.251 | A taskbar click revealing nothing is NOT always a demotion or a placement bug. The nudge's hide/show stranded windows invisible, and its always-`SW_SHOWNOACTIVATE` re-show un-minimized windows the user had put away. Hence rule 7. |+| v2.0.252/253 | The repair sweep itself failed twice, both times by asking "which session owns this window?" first: `windowHwnd` -> `bestSessionHwnd` falls back to enumerate-and-wiggle, which only sees VISIBLE windows (circular — 28 straight NOT-resolved misses on a hidden window sitting right there), and the cached/birth handles are exactly what a window in trouble lacks (park-failed never persisted one; a restart clears the birth capture). Fixed by asserting the invariant over the POPULATION by owning PID — no session mapping, one enumeration, and it repairs windows pup has lost track of entirely. |  ## Bans (each one shipped and hurt) @@ -79,6 +101,14 @@ loaded gun pointed at the user's click. - No re-park retry LOOPS (the playbook ban): one pass + the scheduled re-asserts, then stop. - No trusting session flags alone for "is the user using this" at demotion time - flags lag;   the OS check rides in the demotion pass itself.+- **No path may leave a pup window hidden.** If you must hide one, the re-show goes in a `finally`+  AND the caller verifies it landed. `ShowWindow(...,0)` appears exactly once in `server.js` (the+  nudge) and it should stay that way.+- **Never re-show a window in a different state than you found it.** Un-minimizing a window the+  user minimized is the same class of sin as raising one they backgrounded.+- **A repair must not depend on the bookkeeping that the broken state destroys.** Resolving "which+  session is this?" via the visible-window enumerator to fix an invisible window is circular; key+  population-wide repairs off the owning PROCESS instead.  ## ab 2.0.16 migration note @@ -97,3 +127,13 @@ result must still short-circuit callers and stamp `_userActiveAt`. If ab ever gr    Log must show `user-foreground — hands off`, never a `bottomed` for that window. 4. Counter-test: a local PDF opened in a background window still gets demoted (idle hands),    so the PDF protection is intact.+5. **Visibility (rule 7), and test it by STRANDING a window, not by hoping:** run the hide half+   alone against a live pup window (`ShowWindow(hwnd,0)` — this is precisely what a killed nudge+   leaves behind) and assert `IsWindowVisible=false`, then wait. The sweep must re-show it within+   120s and say so in the log (`[visibility] re-showed a HIDDEN pup window ...`). Measured 85s on+   AdomLapper, 2026-08-20.+6. **Minimize preservation:** minimize a pup window as a human would, then force a real nudge (a+   `taskbarMode` switch re-stamps and nudges). The log must show `button nudge: NUDGED` — a+   `SKIP-FG` proves nothing, and an audit that reports no findings does not nudge at all, so a+   "it stayed minimized" claim off that is worthless — and the window must still be minimized+   afterwards. Verified on ConfRoomROG across two NUDGED events, 2026-08-20.