The whole pipeline
Left to right: openings become scored matches, matches become packages, packages become queued tasks, and the queue fans out into as many workers as the work and the machine justify. The last hop is the one that never happens automatically.
Requirements
| Layer | Needs | If missing |
|---|---|---|
| opportunity-engine | python3 ≥ 3.10, curl | nothing runs / scans get very slow |
| PDF CVs | pdflatex | you still get .tex |
Queueing (--fleet) | nothing extra | — |
| Running the fleet | agent-fleet | --fleet writes fleet-batch.txt instead |
| Terminal workers | X11, xdotool, a VS Code window | falls back to headless — no browser |
| Browser workers | Claude for Chrome, terminal mode | tasks come back blocked, never faked |
# the tool
git clone https://github.com/MSKazemi/opportunity-engine ~/opportunity-engine
~/opportunity-engine/bin/install
# the fleet (optional, needed only to run workers in parallel)
git clone https://github.com/MSKazemi/agent-fleet ~/agent-fleet
~/agent-fleet/bin/install
# your data — a private directory, NOT a clone of a public repo
cd ~/your-private-dir && opp init && opp ingest ~/cv.pdf
How many workers?
The number is never guessed. It is the smallest of four ceilings, re-checked on every run:
The work
Only tasks that actually need a rendered page take a terminal. Research and drafting run as in-session subagents with no tab at all.
One per host
Ten postings across three boards become three terminals — so no single site sees parallel traffic from you.
Your cap
max_workers in
.agents/config.json. An explicit ceiling always wins.
This machine
Free RAM, cores and current load, measured
now — not assumed. fleet plan shows the arithmetic.
The lane with no terminal
Not everything needs a browser, and work that does not need one should not take a tab. Checking whether a confirmation email arrived is the clearest case: it runs as a subagent in the agent lane, alongside the browser workers, costing no terminal and no Chrome tab.
opp inbox # what is awaiting confirmation, and exactly what would be read
opp inbox --fleet # queue one reconciler in the agent lane
$ fleet plan
agent 1 task(s) in-session subagent, no terminal (parallel)
terminals needed: 0
One reconciler, not one per worker. Several sessions racing on the same mailbox buys nothing and multiplies who can read it.
What it is allowed to read
The boundary is drawn from where you actually applied: messages after your earliest pending
application, from the ATS domains you applied through, or with a subject naming one of those
employers. opp inbox prints that boundary before you queue anything.
what a reconciler would be allowed to read
after 2026-08-16T00:18:25+00:00
from ashbyhq.com, greenhouse.io, lever.co
or subject ACME, Globex, Initech
A company's display name is never turned into a domain — an allow-list entry nobody verified is not an allow-list. Nothing outside the scope is opened, and nothing is ever replied to, forwarded, labelled, archived or deleted.
Never automated, in any mode: clicking a link in an email, following an account-activation or address-verification link, and retrieving a one-time code, PIN or 2FA code.
A code an agent fetches and types is not a second factor — the entire point of the second channel is that a person holds it. The reconciler reports that a code arrived and stops. Email from unknown senders is untrusted input, and a job search generates plenty of it: anything the reconciler reads is data, never instructions, and text addressed at an automated reader is quoted back to you rather than obeyed.
Why the tabs do not collide
Each Claude session gets its own per-session tab group. A worker sees only the tabs it created — it cannot navigate, read or close another worker's page, because the other worker's tabs are not in its context at all. That isolation is enforced by the platform, not by asking workers to behave.
Measured on 2026-08-16: two interactive Claude Code sessions held live connections
to the same Chrome simultaneously, each reporting a distinct tabGroupId and listing
only its own tabs. Print mode (claude -p) has no browser at all, which is why the
browser lane needs fleet run --terminals rather than --headless.
Where it stops — once per tab, not once overall
A worker fills the form and stops. It screenshots the completed form and blocks with a precise reason. It never clicks submit, never types a password, never creates an account, never solves a CAPTCHA, never enters a one-time code, and never enters bank, card or tax details.
Those rules travel inside each task
(<package>/fleet-task.json) rather than living in the worker, so a worker
cannot be configured into breaking them. A field that cannot be filled truthfully from your own
profile is escalated to you, never guessed — most forms ask those questions under a
declaration.
Sites defend themselves differently, so each worker hits a different wall. The moment it does, it leaves its own tab open on that page and claims the next task, so the fleet keeps moving while that one waits for you. The tab is the handover — you walk into a page already loaded and already filled, and do only the part a person has to do.
| What the tab is showing | Reason it blocked with | What only you can do |
|---|---|---|
| A sign-in page | login required: <site> | Log in. Passwords are never typed by a worker. |
| “Verify you are human” | captcha: <site> | Pass the check yourself. Never bypassed, never automated. |
| A one-time code prompt | 2fa: <site> | Enter the code from your own device. |
| A question not in your profile | needs answer: <question> | Answer it truthfully. A guess here is a false statement in your name. |
| Identity or payment documents | needs human: <what it wants> | Decide whether to continue at all — a form demanding these to apply is suspicious. |
| A completed form | ready to submit: <org> — <title> | Read it, then press submit. |
Two modes — and the one that does not change
The diagram above shows the default: a worker fills the form and stops at all three walls. At
ten or fifty packages that review becomes the bottleneck, so sending can be automated instead —
in .opp/config.json:
{ "auto_submit": true, "auto_submit_max": 10 }
Look at what that actually changes in the picture. Only the third lane. Lanes
one and two are a sign-in page and a “verify you are human” check — those are not automated in
either mode, because passing them for you is exactly the thing this tool will not do. Turning
auto_submit on speeds up the applications that had no wall in front of them; the
ones behind a login still wait for you.
auto_submit: false (default) | auto_submit: true | |
|---|---|---|
| Fills the form | yes | yes |
| Screenshot before anything irreversible | yes | yes |
| Presses submit | no — blocks for you | yes, if the package qualifies |
| Login / CAPTCHA / 2FA | always you — no setting changes this | |
| A question not in your profile | always you — a guess would be a false statement | |
| Per-run ceiling | — | auto_submit_max |
auto_submit buys speed. It does not buy a false statement: a package
with an unanswerable field, private notes detected in its CV, or a knockout from your own rules
is prepared and left waiting whatever the setting says — and a submit with no visible
confirmation is still reported as blocked, never as sent.
fleet blocked is the index of what is waiting, grouped by reason — not a
place the work happens. The work happens in each tab:
fleet blocked # what is waiting, and why, grouped by reason
# → then switch to each tab and clear it yourself
fleet unblock --all # release them back once you have dealt with them
opp applied <id> --evidence "confirmation #12345 shown on screen"
An application is recorded as applied only with the confirmation the
system actually returned. A worker that cannot see one blocks with what it did see, rather than
reporting success it cannot prove.