Two usage modes: conductor (navigates portals in real time via Geometra MCP) or standalone (script for already-collected URLs).
Never run batch as one long interactive session. Each offer gets its own opencode run worker via batch-runner.sh — that's the whole point of the architecture. Workers have clean ~200K-token contexts and exit after producing one report + PDF + tracker line, so prompt caching stays healthy.
If you find yourself doing geometra_fill_form or geometra_page_model for the Nth time in the same session, stop and delegate. See "Session Hygiene" in .opencode/skills/job-forge.md for the full rationale (cache-bust behavior with repeated Geometra tool calls).
opencode Conductor (opencode --dangerously-skip-permissions)
│
│ Geometra MCP: navigates portals (logged-in sessions)
│ Reads structured page model — the user sees everything in real time
│
├─ Offer 1: reads JD from DOM + URL
│ └─► opencode run worker → report .md + PDF + tracker-line
│
├─ Offer 2: click next, reads JD + URL
│ └─► opencode run worker → report .md + PDF + tracker-line
│
└─ End: merge tracker-additions → data/applications/ + summary
Each worker is a child opencode run with a clean 200K token context. The conductor only orchestrates.
.jobforge-runs/ # Durable iso-orchestrator records (gitignored)
batch/
batch-input.tsv # URLs (from conductor or manual)
batch-state.tsv # Progress (auto-generated, gitignored)
batch-runner.sh # Standalone orchestrator script
batch-prompt.md # Prompt template for workers
logs/ # One log per offer (gitignored)
tracker-additions/ # Tracker lines (gitignored)
- Read state:
batch/batch-state.tsv→ know what has already been processed - Navigate portal: Chrome → search URL
- Extract URLs: Read results DOM → extract URL list → append to
batch-input.tsv - For each pending URL:
a. Chrome: click on the offer → read JD text from DOM
b. Save JD to
/tmp/batch-jd-{id}.txtc. Calculate next sequential REPORT_NUM d. Execute via Bash:bash opencode run --dangerously-skip-permissions \ --file batch/batch-prompt.md \ "Process this offer. URL: {url}. JD: /tmp/batch-jd-{id}.txt. Report: {num}. ID: {id}"e. Updatebatch-state.tsv(completed/failed + score + report_num) f. Log tologs/{report_num}-{id}.logg. Chrome: go back → next offer - Pagination: If no more offers → click "Next" → repeat
- End: Merge
tracker-additions/→data/applications/(viamerge-tracker.mjs) + summary
batch/batch-runner.sh [OPTIONS]batch-runner.sh delegates to scripts/batch-orchestrator.mjs by default.
That Node runner uses @agent-pattern-labs/iso-orchestrator to persist workflow records in
.jobforge-runs/, cap bundle fan-out with workflow.forEach, and serialize
report-number/state writes while workers run in parallel. If a regression
requires the old shell loop, run with JOBFORGE_LEGACY_BATCH_RUNNER=1.
Options:
--dry-run— list pending without executing--retry-failed— only retry failed ones--start-from N— start from ID N--parallel N— N workers in parallel--max-retries N— attempts per offer (default: 2)--workflow-id ID— durable workflow id (default:jobforge-batch)
id url status started_at completed_at report_num score error retries
1 https://... completed 2026-... 2026-... 002 4.2 - 0
2 https://... failed 2026-... 2026-... - - Error msg 1
3 https://... pending - - - - - 0
- If it dies → re-run → reads
batch-state.tsv→ skips completed .jobforge-runs/keeps the durable run record, step outcomes, and bundle events- Lock file (
batch-runner.pid) prevents double execution - Each worker is independent: failure on offer #47 does not affect the rest
Each worker receives batch-prompt.md as system prompt. It is self-contained.
The worker produces:
- Report
.mdinreports/ - PDF in
output/ - Tracker line in
batch/tracker-additions/{id}.tsv - JSON result via stdout
| Error | Recovery |
|---|---|
| URL inaccessible | Worker fails → conductor marks failed, moves on |
| JD behind login | Conductor tries to read DOM. If it fails → failed |
| Portal changes layout | Conductor reasons about HTML, adapts |
| Worker crashes | Conductor marks failed, moves on. Retry with --retry-failed |
| Conductor dies | Re-run → reads state → skips completed |
| PDF fails | Report .md is saved. PDF remains pending |