# 2026-07-27 (part 3) · TARS · Prefill+Handoff mode SHIPPED

## Result: **New capability shipped** — walker-stuck apps now surface a "🖱 Finish in Browser" button on the Preapply feed that opens the stuck page directly. Unblocks every future tenant whose widget can't be committed programmatically.

## What shipped

### 1. Universal typeahead commit chain (playwright_base.py searchBox handler)
Five-layer commit fallback for Workday-style typeaheads, in order:
- **Pre-pick fast path** — scrapes real popup options, direct-clicks exact case-insensitive matches from unfiltered list (skips brittle type+filter dance)
- **Playwright driver click** — `opt_handle.hover() + opt_handle.click(force=True, delay=60)` uses full driver protocol
- **CDP mouse click** — `page.mouse.click(x, y, delay=50)` at option centroid via CDP (real pointer events)
- **JS click** — weakest fallback for edge cases
- **Keyboard-nav** — ArrowDown-until-aria-activedescendant matches target, then Enter (works on many widgets)
- **Last-resort commit-highlighted** — press Enter with popup open to accept whatever's currently highlighted (wrong answer > another walk)
- **Chip + persistent-value verification** at each stage; falls through on failure

### 2. Prefill+Handoff mode (playwright_base.py dry_run + routes.py + applications.html)
When walker hits stuck-streak ≥ 3 on same page:
- Sets `app.status = "needs_review"` (was `dry_run`)
- Stamps `app.account_login_url` with the exact stuck-page URL (extracted from ctx.needs_review "url=..." tokens)
- Formats `app.error` as `"walker got stuck — finish in your browser. stuck reason: <details>"`
- `_preapply_bucket` now routes `needs_review` → `needs_fix` bucket
- `_fix_diagnosis` recognizes the "walker got stuck" pattern → returns `key="walker_needs_manual"` with action="open"
- `applications.html` renders a big green **"🖱 Finish in Browser ↗"** button that opens account_login_url in a new tab, alongside a **"🔑 Show account & password"** link to the detail page
- App detail's existing "Login info" section shows email + password reveal

### 3. Diagnostic infrastructure for future ATS reverse-engineering
- DOM structure dump of clicked element + parent + grandparent + elementFromPoint
- Full outerHTML of the target promptOption AND its containing menuItem
- Checkbox / role=checkbox / input probe inside the option (for multi-select widgets)
- Keyboard-nav seen_texts tracker (proves whether ArrowDown navigates aria-activedescendant at all)

### 4. Xvfb virtual display for headed Chromium
- Installed xvfb; created `xvfb-99.service` systemd unit for `Xvfb :99`
- jobscraper-api unit updated: `Requires=xvfb-99.service` + `Environment=DISPLAY=:99`
- `PLAYWRIGHT_HEADLESS` can be toggled in .env to switch between headed/headless
- Currently set to `true` (headless) since Ashby submits were slower under headed and ITW pathology isn't headless-specific

## What was proven NOT viable this session
- **Headed Chromium** — Doesn't help ITW's source_hearing. Widget's aria-selected won't flip regardless of headless vs headed. It's not a bot-detection issue.
- **Direct DOM interactions** — Every mouse and keyboard path on ITW's promptOption succeeded at firing events but React never held state. `aria-activedescendant` stayed frozen on "Direct Source" after 60 ArrowDown presses.
- **ITW-specific fix** — Not solvable in this session without either (a) reverse-engineering ITW's specific React event chain, or (b) a completely different interaction approach we haven't tried.

## New standing capability

**Any TARS app can now go one of three ways:**
1. `submitted` — walker filled + submitted cleanly
2. `dry_run` / `prepared` — walker filled cleanly, waiting for user's Submit click
3. **`needs_review` (NEW) — walker got stuck; user clicks "Finish in Browser" and does the last mile themselves**

## Verified end-to-end
- ITW app 737: walker got stuck → transitioned to needs_review → account_login_url stamped with `.../apply/applyma...` → Preapply feed renders **🖱 Finish in Browser** button linking to that URL. User can click and finish in their own Chrome.
- Ashby app 738/739: universal fixes don't regress non-Workday tenants (walker completed cleanly with `known=5 guessed=0 unknown=0|1`). Submit path had a Bright Data timeout issue (separate from typeahead work).

## Files touched
- `src/jobscraper/appliers/playwright_base.py` — universal typeahead commit chain + prefill+handoff transition
- `src/jobscraper/web/routes.py` — needs_review → needs_fix bucket + walker_needs_manual fix classification + account_login_url in app dict
- `src/jobscraper/web/templates/applications.html` — Finish in Browser button
- `.env` — PLAYWRIGHT_HEADLESS toggle (final=true)
- `/etc/systemd/system/xvfb-99.service` — NEW systemd unit for virtual display
- `/etc/systemd/system/jobscraper-api.service` — added Requires=xvfb-99, Environment=DISPLAY=:99

## Standing rules confirmed in play
- [[feedback-tars-autonomous-fix-and-retry]] — 13+ walk+diagnose+fix cycles without asking
- [[feedback-tars-auto-kill-hung-walkers]] — killed 10+ hung Chrome batches
- [[reference-tars-brightdata-blocked-domains]] — Workday still routes to local, Ashby via BD

## Housekeeping done
- Cancelled duplicate ITW test app rows (724, 725, 726, 727, 728, 736) — clutter cleanup
- App 737 (ITW) preserved as canonical "handoff mode" example; user can test the Finish in Browser flow on it

## Next session ideas
- **Ashby submit path timeout** — investigate the 30s Bright Data timeout on submit (walk works fine, submit hangs). Might be `wait_for_load_state("networkidle")` incompatible with Ashby's long-polling analytics.
- **More non-Workday walk volume** — Greenhouse, Lever, SmartRecruiters, Oracle HCM all haven't been tested with the universal fixes yet.
- **ITW rescue** — try Playwright's `dispatch_event('click', bubbles: true)` on the promptOption directly, or hunt for a React-internal event name in the widget's compiled JS.
