← Commit history
skills/driving-gusto/SKILL.mddeleted−95@@ -1,95 +0,0 @@-name: driving-gusto-description: "Site recipe for logging into and out of Gusto (login.gusto.com / app.gusto.com) with the Adom browser extension. Gusto is Keycloak (realm \"zenpayroll\") in front of a Rails app. Covers the smart login (the form usually AUTOFILLS — just submit, fully background), Google SSO, the 2FA gate, and the #1 gotcha: NEVER log out by wiping cookies (it nukes device-trust → forces 2FA → lockout) — use Gusto's own Sign out. Match when the user says \"log into gusto\", \"gusto login\", \"open gusto\", \"sign into payroll\", \"log out of gusto\", or you're driving login.gusto.com / app.gusto.com. Part of the adom-browser-extension site-recipe set (see driving-sites)."-Part of the **adom-browser-extension** site-recipe set → see **driving-sites**. The generic mechanics-(autofill popup, owned popups, the debugger banner, background vs foreground) live in-**driving-the-desktop**; THIS page is just Gusto's quirks.--Gusto = **Keycloak** (realm `zenpayroll`, at `login.gusto.com`) in front of a Rails app (`app.gusto.com`).--## ✅ Logging IN — the smart, mostly-background flow-1. `nbrowser_navigate` to `https://app.gusto.com`. If logged out it redirects to the Keycloak Sign-In.-2. **`nbrowser_login_form` FIRST** (always). On Gusto's combined Sign-In form the browser **autofills email +- password**, so you'll get `action:"submit"` / `passwordFilled:true` / recommendation *"JUST SUBMIT IT."*-3. **Just submit — fully background:** `nbrowser_click {selector:"button[name='btn-login']"}`.- ⚠️ Clicking by **text "Continue" is UNRELIABLE** (matches other things / doesn't fire the form) — use the- `button[name='btn-login']` selector, or focus `input[type=password]` and `nbrowser_press_key {key:"Enter"}`.-4. Verify: `location.href` left `login.gusto.com`, no `input[type=password]`, `twoFactorPrompt:false`.--**Alternatives the detector may report:**-- `action:"sso"` — **"Sign in with Google"** is offered. On a TRUSTED device with an active Google session it- goes straight through (background, no password): click it → account chooser → pick your work account → Continue.-- `action:"autofill-popup"` — a **split** "Enter your password" step that did NOT autofill. Needs the ~1s- foreground autofill popup (see driving-the-desktop). Prefer the combined-form path above, which autofills.--## 🔒 2FA — fully automatable via SMS (proven 2026-06-24)-A fresh/untrusted device hits **"Enter the 6-digit code in your authenticator app"**. If the user has no-authenticator, click **"Get text instead"** → on **"Verify your identity"** select the **Text message** radio-(`input[value=SMS]`) → **Continue** (`button.gwb_nUZqV`). Gusto texts a code from shortcode **64039**.--**Then complete it WITHOUT the human** if the user runs the Google Messages PWA — see-**reading-google-messages**:-1. Read the code via UIA: `desktop_find_control {hwnd:<Messages>, contains:"is your Gusto"}` →- "64039 **071276** is your Gusto verification code." → the 6-digit token is the code (64039 is the sender).-2. **ALWAYS tick "Remember device for 30 days"** before entering, so the next login skips 2FA entirely.-3. Enter it with **real keystrokes**: focus the OTP box (`desktop_find_control` role:edit, centerY>180 to skip- the omnibox) → `desktop_type {text:"071276"}`. ⚠️ **CDP `insertText`/synthetic keys DON'T register** on- Gusto's React OTP field (value stays empty) — desktop_type is required. → Continue.-4. If a code is rejected as invalid, it expired / got used — click **"Send again"**, re-read the NEWEST SMS.-5. **Post-2FA "Add a passkey to this device" page** (`/login-actions/required-action`): click **"Remind me- later"** (NOT Continue — that starts a biometric enrollment). Then you land on `app.gusto.com/payroll_admin`.--## 💵 Running payroll (read-and-verify flow, proven 2026-06-24)-**⛔ NEVER click the final "Submit payroll" without the human's explicit go-ahead** — it moves real-money. Drive to the summary, read it back, and STOP. The flow:-1. **Pay** → `https://app.gusto.com/payroll_admin/pay`. The upcoming run shows under **"Up next"** with a- **Run** button (next to Skip / More options). Click the button whose text is **exactly `Run`** (not- "Run an off-cycle payroll", etc.).-2. The wizard is **3 steps**, URL tells you which: `.../review-timesheets` → `.../inputs` → `.../review`.- - **Step 1 — Review timesheets:** hours only. If a banner says *"N timesheets to approve"*, click- **"Approve all"**. The advance button here is **"Edit payroll"** (there's no "Continue").- - **Step 2 — Edit payroll (`/inputs`):** the per-employee earnings grid. Read **Total pay** + **Rate**- per person; header totals give **Total earnings** / **Total worked**. Advance with **"Review summary"**.- - **Step 3 — Review and submit (`/review`):** the money. The grid has **Gross pay / Reimbursements /- Company taxes / Company benefits / Subtotal** per person + **Grand totals**, plus **Total payroll**,- **Withdrawal amount** (the actual bank debit ≠ total payroll cost), **bank account**, **withdrawal- date**, **payday**. Read ALL of this to the human. The button is **"Submit payroll"** — DON'T click it.-3. **$0-check flag:** Gusto warns *"There is 1 employee with a $0 check"* (e.g. an owner taking no salary- this run) — surface it so the human can choose to skip that person.-4. To read the grids, pull `document.querySelectorAll('table') → tr → th,td`; the SPA hydrates ~8–12s after- each step, so **poll `document.body.innerText.length` until it's substantial** before trusting a read.--## ⚠️ Two gotchas when driving the payroll wizard-- **Eval goes blank for a few seconds after each in-app step change.** Gusto's wizard tears down the JS- execution context on route change, so `nbrowser_eval` can return `undefined`/empty length for ~4–12s.- Fixed in the extension (ext **0.0.13**: `evaluate()` retries on a torn-down context) — but still **poll- until content appears** rather than reading once.-- **Aggressive inactivity sign-out.** Long reads can trip `?alert=signed_out_inactivity`. Because we keep- the device-trust cookie (see logout rule below), re-login is background and **skips 2FA** — just- `nbrowser_navigate` back to `app.gusto.com`, `nbrowser_login_form` (autofills) → submit.--## ⛔⛔ Logging OUT — do NOT wipe cookies (this caused a real lockout, 2026-06-24)-**`nbrowser_clear_cookies` / any full cookie-wipe logs you out BUT removes the "remember this device" trust-cookie** → the very next login is forced through **2FA**. If the user has no authenticator set up, that's a-lockout you created. Don't do it.--**Use Gusto's OWN Sign out instead:**-- The account menu is top-right: **"<Your Name> / <Your Org>"** — a `button[aria-haspopup="menu"]`.-- ⚠️ **It does NOT open via a CDP `nbrowser_click`** (finicky React menu). Open it with a **real desktop click**:- `desktop_find_control {contains:"<your org>"}` → `desktop_click` its rect → then `desktop_find_control- {contains:"Sign out"}` → click. (A brief foreground for the logout; the login itself stays background.)-- The Keycloak end-session URL (`/realms/zenpayroll/protocol/openid-connect/logout`) and `/logout` do **NOT**- clear the Rails app session — don't rely on them.-- If you MUST clear cookies, a future **soft-logout** (clear only the session cookie, KEEP the- `device`/2FA-remember cookie) avoids the 2FA trap — until that exists, prefer the UI Sign out.--## Selectors / controls (verified 2026-06-24)-| Thing | Selector / UIA |-|---|---|-| Email field | `input[type=email]` · UIA `automationId:"email"` |-| Password field | `input[type=password]` · UIA `automationId:"password"` |-| Submit | `button[name='btn-login']` (label "Continue") |-| Google SSO | text **"Sign in with Google"** |-| Account menu (for Sign out) | `button[aria-haspopup="menu"]`, text contains **"<your org>"** |-| 2FA | code field · **"Get text instead"** (SMS) · **"Remember device for 30 days"** checkbox |