LinkedIn Unattended Drain — Codex Variant
Same leave-the-PC contract as linkedin-unattended-drain (one supervised
kickoff → autonomous discover → tailor → apply, park-don't-pause, watchdog,
caps) but the conductor/executor is an interactive Codex session and the
browser stack is the Codex Chrome plugin.
Why this variant exists (the upload tradeoff, stated honestly)
- Claude variant: uploads need one
request_directory grant at kickoff;
after that, every upload on any origin works for the session. Ceiling:
the grant only exists in a supervised session.
- Codex variant (this):
chooser.setFiles(...) uploads absolute local
paths with no share registry, but Codex asks per-origin: "Allow upload to
<origin>?". Kickoff warms the origins this run will hit most; any origin
that wasn't warmed parks as manual when its prompt appears with nobody
home.
Net: Claude's ceiling is one grant covering everything; Codex's ceiling is
per-origin approvals that cover the common ATS hosts (Greenhouse / Ashby /
Lever / LinkedIn Easy Apply) and park the long tail. Neither submits from a
fully headless run. Pick this variant when most of the queue lives on shared
ATS origins, or to A/B against the Claude variant.
Upload-approval semantics — knowns and unknowns
| Surface |
Behavior |
Status |
| Interactive conductor tab, warmed origin |
upload proceeds |
expected |
| Interactive conductor tab, new origin |
"Allow upload to <origin>?" prompt |
verified (base card 9e note) |
| Approval persistence across tabs, same origin, same session |
assumed yes |
UNVERIFIED |
| Approval persistence across Codex sessions / extension restart |
unknown |
UNVERIFIED |
Spawned codex exec worker hitting an upload prompt |
cannot be approved headless |
treat as park |
Treat every UNVERIFIED row pessimistically: if an upload prompt appears mid-run
and is not answered within the bounded wait, the item parks (operating card
C1). The first real run upgrades these rows from assumption to fact — record
what you observe in the run summary.
Operating Card
Read skills/linkedin-unattended-drain-codex/OPERATING_CARD.md before every
stage. It layers on the Codex base card
(skills/linkedin-early-career-weekly/OPERATING_CARD.md) and the Claude
variant's park-don't-pause rules.
Isolated Durable State
/tmp/linkedin_unattended_drain_codex_state.json # run state / cursor / items
/tmp/linkedin_unattended_drain_codex_worker.lock # single-actor lock
/tmp/linkedin_unattended_drain_codex_outputs/ # worker result notes
/tmp/linkedin_unattended_drain_codex_descriptions/ # captured job descriptions
/tmp/linkedin_unattended_drain_codex_ram_warning # flag file from watchdog
/tmp/linkedin_unattended_drain_codex_watchdog.log # watchdog log + digest
Do not use run_stages.py in this variant — its output/description paths
are hardcoded to the base weekly's /tmp locations and would collide. The
conductor drives stages itself; only build_run_state.py (fully
path-parameterized) is reused.
Persistence Goal (shared with the linkedin-early-career-weekly variants)
State this goal at kickoff and keep it active for the whole drain. The
blockquote below is the shared completion condition — keep it byte-identical
across variants:
Every fresh last-week LinkedIn Entry-level software-engineer posting from the
configured search is discovered, deduped against the tracker, and driven to a
terminal state — submitted with confirmation evidence, recorded as a precise
manual blocker, marked already-applied/duplicate, or archived with a reason —
until search saturation (search.stopRequested with a saturationReason) or
runPolicy.maxJobs, with the markdown tracker and visualizer cache
reconciled. Submit high-confidence applications when the tailored resume is
verified, all required answers are truthful/standing-answer covered, and no
true blocker remains. Stop early only on a systemic browser/auth/rate-limit
blocker or an explicit user stop. Honor the one-worker / one-browser-actor
rule; never spawn extra workers just to satisfy the goal.
Kickoff — the supervised minutes
Run in order while Liam is at the keyboard. A failed preflight is fixed now or
the run does not start.
Single-drain preflight. Verify none of these belong to a live process
(check any recorded pid with kill -0):
/tmp/linkedin_unattended_drain_codex_worker.lock,
/tmp/linkedin_unattended_drain_worker.lock (Claude variant),
/tmp/linkedin_early_career_weekly_worker.lock,
/tmp/linkedin_early_career_weekly_claude_worker.lock,
/tmp/fa_script_run_state.json. One drain per machine — a live lock from
any variant stops this kickoff. Kill orphan automation Chrome
(~/.codex/nodriver-chrome-ben).
Chrome + extension. Open Chrome on Liam's profile (Default,
liamvanpj@gmail.com). Confirm the Codex Chrome extension is connected and
"Allow access to file URLs" is enabled in chrome://extensions (base
card 10d — without it every upload fails as Not allowed). Create an
agent-owned Codex tab group; load https://www.linkedin.com/feed/ to prove
login. Logged out / 2FA / CAPTCHA = fix before leaving.
Build state (before warming — the warming event needs somewhere durable
to go):
python3 skills/application-visualizer-refresh/scripts/refresh_visualizer_data.py
python3 skills/linkedin-early-career-weekly/scripts/build_run_state.py \
--state /tmp/linkedin_unattended_drain_codex_state.json \
--lock-file /tmp/linkedin_unattended_drain_codex_worker.lock \
--output-dir /tmp/linkedin_unattended_drain_codex_outputs \
--description-dir /tmp/linkedin_unattended_drain_codex_descriptions \
--child-sandbox workspace-write \
--max-jobs 20
Add --resume to continue an interrupted run; --search-url URL for a
specific search. (--child-sandbox workspace-write matches the tailor
worker policy in the operating card — do not leave the default
danger-full-access in state.)
Origin warming (best-effort, not a contract). With Liam present, for
each shared ATS origin expected in this run — boards.greenhouse.io,
jobs.ashbyhq.com, jobs.lever.co, www.linkedin.com — open one real
application form and attach (do not submit) a resume via
chooser.setFiles, letting Liam answer each "Allow upload to <origin>?"
prompt once. Form sources, in order: items already in state when resuming,
then pending rows in application-trackers/job-intake.md; if neither has a
form on a given origin, skip it and note the reduced hands-off share. Record
warmed origins in a state event (warmedOrigins: [...]) — the state file
exists now. This is opportunistic: it raises the share of hands-off applies;
it does not guarantee any later upload (see unknowns table).
Start the shared watchdog (path-parameterized; no copy):
DRAIN_STATE_PREFIX=/tmp/linkedin_unattended_drain_codex \
bash skills/linkedin-unattended-drain/scripts/watchdog.sh &
Confirm a startup line in
/tmp/linkedin_unattended_drain_codex_watchdog.log. Note: watchdog
notifications are titled generically ("Drain …"); variant identity comes
from the prefixed log/state paths, not the notification text.
Tell Liam to leave. Summarize: warmed origins, file-URL setting, max
jobs, search URL, watchdog PID. Everything after this is autonomous.
The Loop
One stage per iteration, exactly one browser actor, lock held for the life of
each stage.
- Select from state:
apply_needed → tailor_needed → discover next.
Stop selecting on search.stopRequested, cap reached, or systemic blocker.
- Execute.
discover / apply: inline in this interactive conductor with the
Codex Chrome plugin (Computer Use fallback only). Fresh agent-owned tab,
do the stage, write the outcome atomically to the isolated state, close
the work tab.
tailor: may run as a spawned codex exec worker (no browser
dependency, --cd this repo, workspace-write sandbox) or inline —
whichever keeps the conductor responsive. Worker prompt must name the
isolated state path and require atomic writeback.
codex exec browser workers are off by default. Only enable after a
passing child-Chrome preflight (the browser-client.mjs bootstrap shape
from run_stages.py), and never for stages that may hit an upload or
permission prompt — a headless worker cannot answer one (card C1).
- Checkpoint (every stage). Re-read the changed item + cursor. Park
anything human-shaped (card C1/O1). Check the RAM flag
(
/tmp/linkedin_unattended_drain_codex_ram_warning). Every 5 terminal
items: full dedupe + evidence audit.
- Continue.
manual is never a stop signal.
End Of Run
Write search.stopRequested + saturationReason (or note the cap). The
watchdog runs the final reconcile (repo-root scripts/mirror_to_sqlite.py +
visualizer refresh), appends a digest, and notifies. Final summary must
include: submissions with evidence, manual rows with exact blockers
(including every unapproved-upload-origin park with the origin named), warmed
origins that worked vs didn't (upgrade the unknowns table), duplicates,
archived, integrity flags, and the exact resume command. No commits, no
pushes, no Notion unless Liam explicitly asked at kickoff.
Degraded Headless Mode (optional, prep-only)
A fully headless codex exec run cannot answer any upload or permission
prompt, so it can only prep: discover + tailor, park every apply as manual
with an upload blocker. Be explicit in the digest that such a run produced
zero submissions by design.
1---2name: linkedin-unattended-drain-codex3description: Codex-driven leave-the-PC LinkedIn early-career drain — an interactive Codex (GPT 5.5) session is the conductor AND the browser actor (Codex Chrome plugin first, Computer Use fallback), with best-effort upload-origin warming at a supervised kickoff, park-don't-pause on every manual blocker, isolated /tmp state, and the shared path-parameterized watchdog for caffeinate/RAM/stall/reconcile/notifications. Use when Liam says `$linkedin-unattended-drain-codex`, wants the unattended drain run by Codex instead of Claude, or wants to compare the Codex upload path (absolute local paths, per-origin approvals) against the Claude variant's one-grant model.4---56# LinkedIn Unattended Drain — Codex Variant78Same leave-the-PC contract as `linkedin-unattended-drain` (one supervised9kickoff → autonomous discover → tailor → apply, park-don't-pause, watchdog,10caps) but the conductor/executor is an **interactive Codex session** and the11browser stack is the **Codex Chrome plugin**.1213## Why this variant exists (the upload tradeoff, stated honestly)1415- **Claude variant:** uploads need one `request_directory` grant at kickoff;16 after that, *every* upload on *any* origin works for the session. Ceiling:17 the grant only exists in a supervised session.18- **Codex variant (this):** `chooser.setFiles(...)` uploads absolute local19 paths with no share registry, but Codex asks **per-origin**: "Allow upload to20 `<origin>`?". Kickoff warms the origins this run will hit most; any origin21 that wasn't warmed parks as `manual` when its prompt appears with nobody22 home.2324Net: Claude's ceiling is one grant covering everything; Codex's ceiling is25per-origin approvals that cover the *common* ATS hosts (Greenhouse / Ashby /26Lever / LinkedIn Easy Apply) and park the long tail. Neither submits from a27fully headless run. Pick this variant when most of the queue lives on shared28ATS origins, or to A/B against the Claude variant.2930### Upload-approval semantics — knowns and unknowns3132| Surface | Behavior | Status |33|---|---|---|34| Interactive conductor tab, warmed origin | upload proceeds | expected |35| Interactive conductor tab, new origin | "Allow upload to `<origin>`?" prompt | verified (base card 9e note) |36| Approval persistence across tabs, same origin, same session | assumed yes | **UNVERIFIED** |37| Approval persistence across Codex sessions / extension restart | unknown | **UNVERIFIED** |38| Spawned `codex exec` worker hitting an upload prompt | cannot be approved headless | treat as park |3940Treat every UNVERIFIED row pessimistically: if an upload prompt appears mid-run41and is not answered within the bounded wait, the item parks (operating card42C1). The first real run upgrades these rows from assumption to fact — record43what you observe in the run summary.4445## Operating Card4647Read `skills/linkedin-unattended-drain-codex/OPERATING_CARD.md` before every48stage. It layers on the Codex base card49(`skills/linkedin-early-career-weekly/OPERATING_CARD.md`) and the Claude50variant's park-don't-pause rules.5152## Isolated Durable State5354```text55/tmp/linkedin_unattended_drain_codex_state.json # run state / cursor / items56/tmp/linkedin_unattended_drain_codex_worker.lock # single-actor lock57/tmp/linkedin_unattended_drain_codex_outputs/ # worker result notes58/tmp/linkedin_unattended_drain_codex_descriptions/ # captured job descriptions59/tmp/linkedin_unattended_drain_codex_ram_warning # flag file from watchdog60/tmp/linkedin_unattended_drain_codex_watchdog.log # watchdog log + digest61```6263Do **not** use `run_stages.py` in this variant — its output/description paths64are hardcoded to the base weekly's `/tmp` locations and would collide. The65conductor drives stages itself; only `build_run_state.py` (fully66path-parameterized) is reused.6768## Persistence Goal (shared with the `linkedin-early-career-weekly` variants)6970State this goal at kickoff and keep it active for the whole drain. The71blockquote below is the shared completion condition — keep it byte-identical72across variants:7374> Every fresh last-week LinkedIn Entry-level software-engineer posting from the75> configured search is discovered, deduped against the tracker, and driven to a76> terminal state — submitted with confirmation evidence, recorded as a precise77> manual blocker, marked already-applied/duplicate, or archived with a reason —78> until search saturation (`search.stopRequested` with a `saturationReason`) or79> `runPolicy.maxJobs`, with the markdown tracker and visualizer cache80> reconciled. Submit high-confidence applications when the tailored resume is81> verified, all required answers are truthful/standing-answer covered, and no82> true blocker remains. Stop early only on a systemic browser/auth/rate-limit83> blocker or an explicit user stop. Honor the one-worker / one-browser-actor84> rule; never spawn extra workers just to satisfy the goal.8586## Kickoff — the supervised minutes8788Run in order while Liam is at the keyboard. A failed preflight is fixed now or89the run does not start.90911. **Single-drain preflight.** Verify none of these belong to a live process92 (check any recorded pid with `kill -0`):93 `/tmp/linkedin_unattended_drain_codex_worker.lock`,94 `/tmp/linkedin_unattended_drain_worker.lock` (Claude variant),95 `/tmp/linkedin_early_career_weekly_worker.lock`,96 `/tmp/linkedin_early_career_weekly_claude_worker.lock`,97 `/tmp/fa_script_run_state.json`. One drain per machine — a live lock from98 any variant stops this kickoff. Kill orphan automation Chrome99 (`~/.codex/nodriver-chrome-ben`).1002. **Chrome + extension.** Open Chrome on Liam's profile (`Default`,101 `liamvanpj@gmail.com`). Confirm the Codex Chrome extension is connected and102 **"Allow access to file URLs" is enabled** in `chrome://extensions` (base103 card 10d — without it every upload fails as `Not allowed`). Create an104 agent-owned Codex tab group; load `https://www.linkedin.com/feed/` to prove105 login. Logged out / 2FA / CAPTCHA = fix before leaving.1063. **Build state** (before warming — the warming event needs somewhere durable107 to go):108109 ```bash110 python3 skills/application-visualizer-refresh/scripts/refresh_visualizer_data.py111112 python3 skills/linkedin-early-career-weekly/scripts/build_run_state.py \113 --state /tmp/linkedin_unattended_drain_codex_state.json \114 --lock-file /tmp/linkedin_unattended_drain_codex_worker.lock \115 --output-dir /tmp/linkedin_unattended_drain_codex_outputs \116 --description-dir /tmp/linkedin_unattended_drain_codex_descriptions \117 --child-sandbox workspace-write \118 --max-jobs 20119 ```120121 Add `--resume` to continue an interrupted run; `--search-url URL` for a122 specific search. (`--child-sandbox workspace-write` matches the tailor123 worker policy in the operating card — do not leave the default124 `danger-full-access` in state.)1254. **Origin warming (best-effort, not a contract).** With Liam present, for126 each shared ATS origin expected in this run — `boards.greenhouse.io`,127 `jobs.ashbyhq.com`, `jobs.lever.co`, `www.linkedin.com` — open one real128 application form and attach (do **not** submit) a resume via129 `chooser.setFiles`, letting Liam answer each "Allow upload to `<origin>`?"130 prompt once. Form sources, in order: items already in state when resuming,131 then pending rows in `application-trackers/job-intake.md`; if neither has a132 form on a given origin, skip it and note the reduced hands-off share. Record133 warmed origins in a state event (`warmedOrigins: [...]`) — the state file134 exists now. This is opportunistic: it raises the share of hands-off applies;135 it does not guarantee any later upload (see unknowns table).1365. **Start the shared watchdog** (path-parameterized; no copy):137138 ```bash139 DRAIN_STATE_PREFIX=/tmp/linkedin_unattended_drain_codex \140 bash skills/linkedin-unattended-drain/scripts/watchdog.sh &141 ```142143 Confirm a startup line in144 `/tmp/linkedin_unattended_drain_codex_watchdog.log`. Note: watchdog145 notifications are titled generically ("Drain …"); variant identity comes146 from the prefixed log/state paths, not the notification text.1476. **Tell Liam to leave.** Summarize: warmed origins, file-URL setting, max148 jobs, search URL, watchdog PID. Everything after this is autonomous.149150## The Loop151152One stage per iteration, exactly one browser actor, lock held for the life of153each stage.1541551. **Select** from state: `apply_needed` → `tailor_needed` → discover next.156 Stop selecting on `search.stopRequested`, cap reached, or systemic blocker.1572. **Execute.**158 - `discover` / `apply`: **inline in this interactive conductor** with the159 Codex Chrome plugin (Computer Use fallback only). Fresh agent-owned tab,160 do the stage, write the outcome atomically to the isolated state, close161 the work tab.162 - `tailor`: may run as a spawned `codex exec` worker (no browser163 dependency, `--cd` this repo, workspace-write sandbox) or inline —164 whichever keeps the conductor responsive. Worker prompt must name the165 isolated state path and require atomic writeback.166 - `codex exec` **browser** workers are off by default. Only enable after a167 passing child-Chrome preflight (the `browser-client.mjs` bootstrap shape168 from `run_stages.py`), and never for stages that may hit an upload or169 permission prompt — a headless worker cannot answer one (card C1).1703. **Checkpoint (every stage).** Re-read the changed item + cursor. Park171 anything human-shaped (card C1/O1). Check the RAM flag172 (`/tmp/linkedin_unattended_drain_codex_ram_warning`). Every 5 terminal173 items: full dedupe + evidence audit.1744. **Continue.** `manual` is never a stop signal.175176## End Of Run177178Write `search.stopRequested` + `saturationReason` (or note the cap). The179watchdog runs the final reconcile (repo-root `scripts/mirror_to_sqlite.py` +180visualizer refresh), appends a digest, and notifies. Final summary must181include: submissions with evidence, manual rows with exact blockers182(including every unapproved-upload-origin park with the origin named), warmed183origins that worked vs didn't (upgrade the unknowns table), duplicates,184archived, integrity flags, and the exact resume command. **No commits, no185pushes, no Notion** unless Liam explicitly asked at kickoff.186187## Degraded Headless Mode (optional, prep-only)188189A fully headless `codex exec` run cannot answer any upload or permission190prompt, so it can only prep: discover + tailor, park every apply as `manual`191with an upload blocker. Be explicit in the digest that such a run produced192zero submissions by design.