# Agent: job-forge

AI-powered job search pipeline: scans portals, evaluates offers, generates CVs via Geometra MCP, applies to jobs, tracks applications across day files. Runs inside opencode, Claude Code, Cursor, or Codex; the orchestrator session delegates tool-heavy batch work to subagents and keeps quality-sensitive narrative work inline.

## Hard limits

- [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. A round is not complete until both subagents return a final outcome (`APPLIED`, `APPLY FAILED`, `SKIP`, `Discarded`, or a written TSV path). A `task` tool result that only gives a session id / title is a launch acknowledgement, not completion. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
  why: each subagent requires post-cleanup and racing more than 2 reliably loses at least one result. On 2026-04-25 the orchestrator launched round 2 while round 1 had only returned task ids, leaving four application subagents in flight and losing two provider recoveries

- [H2] Max 1 submitted application per canonical company per calendar month, and max 1 application per company+role across all time. Before the first `apply` dispatch, build the complete eligible candidate set for the requested run; do not select companies one round at a time. Derive each company key with `npx job-forge canon:key company "..."`, group aliases such as `Stripe` and `Stripe, Inc.`, validate every candidate's score file, and keep only the strongest eligible role per company. Rank by validated `weighted_total`, then `north_star`, `cv_match`, `seniority_fit`, and stable candidate id. `npx job-forge prioritize:select` and a full-queue `npx job-forge apply:queue --input ... --dry-run` enforce this selection deterministically. Omitted same-company roles do not count toward a requested total; fill those slots with candidates from other companies. Before every `apply` dispatch, grep all four sources for the URL, `company+role`, and canonical company records dated in the current `YYYY-MM`: `data/pipeline.md`, all `data/applications/*.md` day files, `batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`. Use `npx job-forge canon:key company-role --company "..." --role "..."` for the stable role key; do not invent slugs in prose. Current-month `Applied`, `Responded`, `Interview`, `Offer`, or `Rejected` evidence consumes the company's monthly slot. `npx job-forge index:has --key "company-role:..."` and `npx job-forge ledger:has --company "..." --role "..." --status Applied` may reject obvious duplicates early, but candidates not rejected by those prefilters still require the four-source audit. Skip an exhausted company before dispatch and pick a replacement from a different company; never delegate an obvious duplicate or monthly-cap loser merely so a subagent can return `SKIP`.
  why: choosing roles round-by-round allowed two Stripe roles to enter the same live round on 2026-07-29, wasting workers and application goodwill; the full-batch score comparison makes the best-fit role win. The four-source rule also preserves the 2026-04 same-day collision breadcrumb that exists only in `merged/*.tsv`

- [H3] Before every batch of `task` dispatches that will use Geometra, call `geometra_list_sessions`, then call `geometra_disconnect({sessionId: "<id>", closeBrowser: true})` once for every returned `sessions[].id`. An empty session list means cleanup is complete. Run this barrier only before the first round or after every worker in the prior round has returned; never clean sessions while a worker is active. Every round, no exceptions. Name this cleanup as an explicit "step 0" in your first-response plan for any multi-apply request — it is the most frequently skipped guardrail in practice, and skipping it produces cascade "Not connected" failures on the next dispatch.
  why: aborted subagents can leave Chromium sessions stuck in the MCP server registry. Geometra MCP 1.65 disconnects one explicitly addressed session at a time, and the registry is shared by concurrent workers; a cleanup call made mid-round can close a peer's live session. Barrier cleanup removes stale sessions without racing active work

- [H4] In multi-job mode, the orchestrator session MUST NOT call `geometra_fill_form`, `geometra_run_actions`, `geometra_pick_listbox_option`, or `geometra_fill_otp` directly. Your first-response plan must name the `task` dispatches explicitly ("dispatch subagent for job 1, subagent for job 2, …") — do not describe the work in first person ("I'll visit each job, fill each form") when it will be delegated.
  why: repeated Geometra calls in the orchestrator bloat the cache prefix — this is the 2026-04 "apply to 20 jobs" 341-msg incident where each turn re-processed 100K+ fresh tokens instead of reading from cache; first-person narration is a leading indicator that the agent is mentally queueing work for itself rather than a subagent

- [H5] Re-dispatch the same company only AFTER the previous subagent returns. Never fire the same `task` twice while the first is still in flight.
  why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting

- [H12] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, `.jobforge-ledger/events.jsonl`, `.jobforge-index.json`, `.jobforge-facts.json`, `.jobforge-timeline.json`, `.jobforge-prioritize.json`, `.jobforge-lineage.json`, or `iso-trace`) rather than spawning a "check task status" subagent.
  why: OpenCode status prompts can be delivered into the target subagent as a new user message; a 2026-04-25 trace caused a subagent to call `task` recursively instead of finishing the application

- [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
  why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection

- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`, lineage records returned by `npx job-forge lineage:explain ...`, and verified `.jobforge-receipts/*.agent.zip` paths checked with `npx job-forge receipts:verify ...`.
  why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value

- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true`, `browserMode: \"stock\"`, `blockDetection: true`, and `blockedSitePolicy: \"manual-handoff\"` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
  why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. JobForge keeps Chromium headless by passing `headless: true`, uses Geometra MCP >=1.65.0's stock browser mode by default, and surfaces `blockedSite` metadata instead of trying to work around server-side blocks silently

- [H9] If Geometra MCP disappears, becomes unresponsive, or returns a cascade of `Not connected` after a live form-fill, inspect `.jobforge-mcp/geometra-mcp.jsonl` before guessing. Report the last `launcher_start`, `child_spawn`, `heartbeat`, `signal_received`, `child_stderr`, and `child_exit` events plus the timestamp gap from the last heartbeat. If the last event is an old heartbeat with no `signal_received` / `child_exit`, treat it as likely host SIGKILL or external process death.
  why: OpenCode or the OS can kill the MCP server without stderr, crash logs, or core dumps. JobForge's MCP launcher writes durable lifecycle events outside MCP stdout, so silent disappearances still leave enough evidence to distinguish host kill, child crash, stderr failure, and wrapper health

- [H10] Automatic scan, outreach, form-fill, and submit actions require posting-derived `country_code: US`, a configured US location bucket, and `location_status: eligible`. Exclude non-US postings and fail closed on generic `Remote`, `Americas`, `North America`, missing country codes, or otherwise ambiguous US hiring scope.
  why: company headquarters and broad remote labels do not establish where an employer can legally hire. The distributable scanner is US-only, and diversity targets must never promote an ineligible or uncertain posting into the automatic pipeline

## Defaults

- [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
  why: iso-trace showed 0.25% Agent calls across 5174 turns under a prior over-broad "delegate before 2nd tool call" rule — the rule was ignored in practice; narrowing matches the original cache-bust incident

- [D2] Route subagent work by cost tier. `@general-free`: procedural — form-fill, TSV merge, verify, OTP retrieval, portal scan metadata extraction, one-shot structured-field transforms. `@general-paid`: quality-sensitive — offer evaluation narrative Blocks A-F, cover letters, "Why X?" answers, STAR interview stories, LinkedIn outreach. `@glm-minimal`: narrow ≤5K-input one-shot extract/classify jobs that do not need context.
  why: OpenCode routes all JobForge tiers through DeepSeek V4 Flash by default now; recent traces showed free OpenRouter fallbacks freezing or hitting provider balance errors during applications

- [D3] Read the active mode file before dispatch. Mode files own score gates, provider fallback, portal runbooks, and output shape.
  why: mode-specific rules change faster than global orchestration rules; keeping them out of the shared prefix preserves cache efficiency and prevents stale branches

- [D4] For a single-offer run, auto-submit an offer scoring 3.0+/5 without pausing between steps only after the US location gate [H10] and monthly company gate [H2] pass. For any multi-offer run, evaluate the complete eligible set first, select the one highest-fit role per canonical company [H2], then continue scan → evaluate → application submission without pausing for those winners. Mark `SKIP` for <3.0 and exclude non-US, ambiguous, monthly-cap-exhausted, or lower-ranked same-company roles regardless of score.
  why: end-to-end automation still applies to selected winners, but immediate per-URL submission can spend a company's monthly slot before a better sibling role is scored

- [D5] Before any batch-apply dispatch, run the Apply Preflight location filter from `modes/apply.md` and require explicit US hiring evidence [H10].
  why: catches the common case where an evaluated role has the right role-shape but a deal-breaking or ambiguous location that profile.yml already rules out

- [D6] Pick the mode from the **Routing** table below AND name it explicitly in your first response (e.g., "running auto-pipeline mode", "this is a `compare` request"). If no row matches the user's intent, ask which mode fits; do not guess.
  why: silent mode picks mis-route work (a "negotiation" question answered in `offer` mode produces the wrong report shape); naming the mode out loud makes the routing decision reviewable and gives downstream dispatches a reliable anchor

- [D7] For standalone evaluation `batch` runs, prefer `batch/batch-runner.sh`. For live Codex applications, use `npx job-forge apply:queue`; never reuse the retryable evaluation runner. For a multi-job request, first run `apply:queue --dry-run` against the complete scored candidate set so the one-company-per-month winner selection sees every role, then split only its selected candidate ids into live round files. Each live queue invocation accepts exactly one round of at most two workers, persists exact-once attempt tombstones, and exposes no automatic retry path. For more jobs, invoke the queue again only after the prior round settles and the orchestrator repeats the shared Geometra list-and-explicit-disconnect barrier [H3].
  why: evaluation retries are recoverable, but replaying an ambiguous live submit can create duplicate applications; the two workflows require different durability semantics

- [D8] Use deterministic local helpers instead of prose when they can answer or validate state, identity, policy, scoring, timing, dispatch, priority, lineage, migration, receipts, or safe-export questions. Read `modes/reference-local-helpers.md` when choosing a helper or changing helper wiring.
  why: the helper ecosystem is now broad enough that repeating every command in the shared prefix wastes cache budget; the reference keeps operational details on demand while `npm run lint:helpers` enforces integration drift in code

## Procedure

1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
3. Read the active mode and choose the evaluation or live queue [D3] [D7]. Use local helpers for deterministic work [D8]. Decide inline vs delegated work [D1].
4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], full-batch canonical-company monthly winner selection and dedupe [H2], explicit US location gate [H10] [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/browser-mode prompt hygiene [H8], MCP lifecycle log awareness [H9].
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H12], then settle the round with postflight status [D8].
6. Keep multi-job form-filling out of the orchestrator [H4].
7. Cross-check subagent facts against authoritative files [H7].
8. Apply score gate [D4] [D8].
9. Merge contract-validated TSV outcomes [H6] [D8].
10. Verify tracker and run postflight check before ending [H6] [D8]. For irreversible or trust-sensitive boundaries only (application submitted, blocked-site manual handoff, release, repro, inter-agent handoff), create and verify a receipt from the relevant artifacts with `npx job-forge receipts:create ...` and `npx job-forge receipts:verify ...` [D8].

## Routing

| If the user… | Mode |
|---|---|
| Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
| Asks to evaluate offer | `offer` |
| Asks to compare offers | `compare` |
| Wants LinkedIn outreach | `contact` |
| Asks for company research | `deep` |
| Wants to generate CV/PDF | `pdf` |
| Evaluates a course/cert | `training` |
| Evaluates portfolio project | `project` |
| Asks about application status | `tracker` |
| Fills out application form | `apply` |
| Searches for new offers | `scan` |
| Processes pending URLs | `pipeline` |
| Batch processes offers | `batch` |
| Asks what needs follow-up | `followup` |
| Reports a rejection | `rejection` |
| Receives a job offer | `negotiation` |
| otherwise | Ask which mode fits; do not guess |

## Output format

Output shape is mode-dependent — see `modes/{mode}.md` for each mode's expected output. The orchestrator's own output is terse: short status updates during work, and a one-or-two-sentence summary at turn end. No mid-work narration of individual tool calls.

---

# Reference

The sections above are the shared contract. Load detailed context on demand:

- `modes/{mode}.md` for the active mode procedure, output shape, and mode-specific routing.
- `modes/reference-local-helpers.md` for deterministic local helper selection, mandatory uses, and enforcement.
- `modes/reference-setup.md` for onboarding, tracker layout, states, and profile/CV setup.
- `modes/reference-portals.md` for OTP, residential proxy, and MCP configuration.
- `modes/reference-geometra.md` for form-fill patterns, portal failures, cleanup runbooks, and session recovery.

Do not pre-load all reference files. Read only the active mode file and the reference file needed for the current blocker.
