← Commit history

New dev skill pup-user-foreground: the settled doctrine for the user-taskbar-click vs pup-demotion fight we relitigated twenty times - the contract, the recent-input heuristic inside the demotion primitive, the guard list, the ban list, the hardening history table, and the acceptance test

John Lauer ·070a50b5d4 ·2mo ago ·parent b2701b1
1 file changed +99
dev-skills/pup-user-foreground/SKILL.mdadded+99
@@ -0,0 +1,99 @@+---+name: pup-user-foreground+description: DEV skill (source-only) - THE doctrine for the user-raise vs pup-demotion fight. John clicks a pup taskbar button and the window must come up INSTANTLY and STAY up; pup's parks and re-asserts must never yank it back down. Revisited ~20 times before this was written; read this BEFORE touching any park, re-assert, z-order, foreground, or occlusion code, and BEFORE adding ANY new demotion path.+---++# The user's taskbar click always wins++The single most-revisited behavior in pup's history. This file exists because we relitigated it+about twenty times (2026-06 through 2026-08). The contract, the mechanism, and the bans below are+settled. Do not re-derive them; do not weaken them.++## The contract++1. pup windows open in the BACKGROUND and never steal focus.+2. But the user must be able to raise any pup window INSTANTLY by clicking its taskbar button,+   with zero delay and zero cooperation from pup.+3. And once the USER raises a window, pup must NEVER fight them for it. Not once, not "just one+   re-assert", never.++## Why pup demotes windows at all (the legitimate enemies)++- The first cross-origin paint can RAISE a fresh Chrome window.+- Chrome's PDF viewer SELF-ACTIVATES the window when a PDF loads (proven live: a 5-PDF window+  popped over John's work while pup's own log said "backgrounded").+- After an OS restart, Windows restores session-recovered Chrome windows FOREGROUND.+- Post-restart foreground reassignment can hand a brand-new window the foreground with no user+  intent.++Hence the machinery: the launch park, the +2.5s re-assert, the post-nav re-assert (+1.2s retry,++3s timer). Every one of those is aimed at CHROME's self-raises, and every one of them is a+loaded gun pointed at the user's click.++## The doctrine (current, settled)++1. **Background means BEHIND, never hidden.** Park = on-screen, full work-area size, z-BOTTOM+   (never off-screen, never minimized). A taskbar click then reveals a correctly-placed window+   with no help from pup (v1.9.133/134, after the "click the flash, see nothing" bug).+2. **User intent is read from the OS, atomically, inside the demotion primitive.**+   `GetForegroundWindow() == our hwnd` AND `GetLastInputInfo` idle < 5s means a HUMAN raised it+   (a keyboard/mouse was active). A programmatic self-raise happens with idle hands. This check+   lives INSIDE `osBottomUnlessForeground` (v2.0.26), in the same pass as the demotion, because+   the session-level flags LAG the click by seconds - guarding only on flags is exactly how the+   re-asserts kept yanking John's window back down.+3. **`force=true` means "override CHROME", never "override the USER".** Force exists for the PDF+   pop and launch reassignment. It must not bypass the recent-input check. If you add a new+   demotion call with force, it inherits the recent-input check automatically by going through+   the shared primitive - so GO THROUGH THE SHARED PRIMITIVE.+4. **On `user-foreground`: stop everything and remember.** The caller stops immediately (no+   retry pass, no ab fallback) and stamps `session._userActiveAt = Date.now()`, which makes+   `userIsUsingWindow()` true for 10 minutes - and EVERY demotion path checks that first+   (v1.9.181, after the unguarded-demotion audit of 2026-07-29: "pup windows still don't+   behave well" was exactly unguarded demotions).+5. **Every demotion path checks `userIsUsingWindow(session)` before acting.** The full list as+   of v2.0.26: `parkSessionWindowDirect`, `reassertBottomAfterNav` (both passes), the kickoffPark+   +2.5s re-assert, `healOffscreenWindow`, the unstick monitor, brand/park sweeps, recovery+   re-park (`sf.foreground` persisted flag). Adding a demotion that skips this check is a bug by+   definition.+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.++## The history that hardened each rule (do not re-learn these live)++| Era | Lesson |+|---|---|+| v1.8.53 | CDP `document.hasFocus()` LIES for CDP-created windows - never use it as the user signal. OS `GetForegroundWindow` is truth. |+| v1.9.133/134 | Parking OFF-screen made taskbar clicks reveal nothing. Background = z-bottom ON-screen, always. |+| v1.9.137 | 10px inset on placement so a deliberate park is visually distinct from an accident. |+| v1.9.181 | Granted-raise + observed-user-foreground flags, 10-min window, checked by every demotion. Unguarded demotions were the root of "pup windows still don't behave well". |+| v1.9.201/202 | The re-assert passes exist because PDFs self-activate AFTER the first park; do not delete them - they are why force exists. |+| 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. |++## Bans (each one shipped and hurt)++- No demotion path outside the shared primitive / without the `userIsUsingWindow` check.+- No off-screen or minimize-based backgrounding of a window the user can see on the taskbar.+- 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.++## ab 2.0.16 migration note++The PS implementations are being replaced by ab-native verbs (`desktop_get_foreground`,+`desktop_user_idle_ms`, `desktop_set_window_state {state:'bottom'}`,+`desktop_clear_window_attention`). THE HEURISTIC MUST SURVIVE THE MIGRATION: foreground check ++idle check + demotion should happen within tens of ms of each other, and the user-foreground+result must still short-circuit callers and stamp `_userActiveAt`. If ab ever grows a combined+"bottom-unless-user-foreground" verb, prefer it - one atomic call beats three.++## The test that must always pass++1. Open a background pup window; let all parks/re-asserts settle (~5s).+2. A HUMAN clicks its taskbar button. The window comes up instantly.+3. It STAYS up: through the next navigate, overlay refresh, and sweep, for 10+ minutes.+   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.