# Mode: pipeline — URL Inbox (Second Brain)

Processes accumulated job offer URLs from `data/pipeline.md`. The user adds URLs at any time and then runs `/job-forge pipeline` to process them all.

## Run This Workflow

1. **Read** `data/pipeline.md` → find `- [ ]` items in the "Pending" section
2. **For each pending URL**:
   a. Parse optional scanner metadata after the title: `location | country_code | region | location_status | ats={type}`.
   b. Preserve the location gate through evaluation. Only explicit `country_code: US` plus `location_status: eligible` may trigger automatic outreach, form filling, or submission; re-check the full JD before promoting any other status. Generic `Remote`, `Americas`, or `North America` is not sufficient.
   c. Calculate the next sequential `REPORT_NUM` by running `npx job-forge next-num` (scans `reports/`, day file `#` columns, and `batch/tracker-additions/` — do NOT derive from `reports/` alone).
   d. **Extract JD** using Greenhouse API → `npx job-forge portal:snapshot --url "$URL" --json` → Geometra MCP only if an interactive session is needed → WebFetch → WebSearch.
   e. If the URL is not accessible → mark as `- [!]` with a note and continue.
   f. **Run auto-pipeline**: A-F Evaluation → Report .md → PDF (if score >= 3.0, per `_shared.md` thresholds) → Draft answers (if score >= 3.5) → Tracker. For one pending URL, continue to Apply only after the score, US location, duplicate, and monthly company gates pass. For N >= 2, evaluation workers MUST stop before form access or submission; finish every evaluation, then apply root [H2]'s full-set company winner selection before dispatching separate apply workers.
   g. **Move from "Pending" to "Processed"**: `- [x] #NNN | URL | Company | Role | Score/5 | PDF ✅/❌`.
3. **Parallel dispatch — max 2 at a time** (Hard Limit #1 in `AGENTS.md`). For N pending URLs, run `ceil(N/2)` evaluation rounds of 2 `task` dispatches. After all evaluations settle, rank the complete scored set and run separate live apply rounds for only the selected canonical-company winners. Never 3+ dispatches in one message.
4. **When finished**, display a summary table:

```
| # | Company | Role | Score | PDF | Recommended action |
```

## Apply pipeline.md Format

```markdown
## Pending
- [ ] https://jobs.example.com/posting/123
- [ ] https://boards.greenhouse.io/company/jobs/456 | Company Inc | Senior PM
- [ ] https://jobs.example.com/789 | Distributed Co | Data Engineer | Remote - United States | US | us_nationwide | eligible
- [!] https://private.url/job — Error: login required

## Processed
- [x] #143 | https://jobs.example.com/posting/789 | Acme Corp | AI PM | 4.2/5 | PDF ✅
- [x] #144 | https://boards.greenhouse.io/xyz/jobs/012 | BigCo | SA | 2.1/5 | PDF ❌
```

## Detect JD From URL

1. **Greenhouse JSON API (FIRST, when the entry has `| gh={slug}/{id}` OR the host looks Greenhouse-backed):** WebFetch `https://boards-api.greenhouse.io/v1/boards/{slug}/jobs/{id}`. 200 + JSON with `content` = LIVE, use it as the JD; 404 = genuinely CLOSED (mark `- [!]` and continue). **OpenCode WebFetch compatibility:** do not pass `format: "json"`; omit `format` or use `format: "text"` and parse the returned JSON text. Bot-hostile customer fronts (`pinterestcareers.com`, `okta.com`, `samsara.com`, `zoominfo.com`, `collibra.com`, `careers.toasttab.com`, `careers.airbnb.com`, `coinbase.com`, `instacart.careers`, `careers.toasttab.com`) MUST be verified via this API first — WebFetch/Geometra of those domains returns a shell or 403 and causes false CLOSED marks.
2. **Direct Geometra helper:** `npx job-forge portal:snapshot --url "{url}" --json`. Works with non-Greenhouse SPAs (Lever, Ashby, Workday), enforces `headless: true`, `browserMode: "stock"`, `blockDetection: true`, and `isolated: true` in code, reads `config/profile.yml` proxy config, returns `blockedSite` metadata when detected, and closes Chromium before exit.
3. **Geometra MCP (interactive fallback):** Use only when the one-shot helper is not enough and a live multi-step browser session is required.
4. **WebFetch (fallback):** For static pages or when Geometra is not available.
5. **WebSearch (last resort):** Search on secondary portals that index the JD.

**Special cases:**
- **LinkedIn**: May require login → mark `[!]` and ask the user to paste the text
- **PDF**: If the URL points to a PDF, read it directly with the Read tool
- **`local:` prefix**: Read the local file. Example: `local:jds/linkedin-pm-ai.md` → read `jds/linkedin-pm-ai.md`

## Automatic Numbering

Run `npx job-forge next-num` — returns the next 3-digit zero-padded report number. The CLI scans:

1. `reports/*.md` filename prefixes
2. Legacy `applications.md` / `data/applications.md` and every `data/applications/*.md` day file
3. The first column of every `batch/tracker-additions/*.tsv` (pending + merged)
4. Reserved `report_num` values in `batch/batch-state.tsv`

Takes the max across every source and adds 1. Do NOT derive from any single source — prior-day SKIPs, in-flight reservations, and other non-report tracker entries advance the counter but never write to `reports/`, so `ls reports/` alone misses them.

## Source Synchronization

Before processing any URL, verify sync:
```bash
npx job-forge sync-check
```
If there is a desynchronization, warn the user before continuing.

## Run This Pipeline Runbook When N >= 2

```
Step 1  — Read data/pipeline.md; collect "- [ ]" URLs into `pending = [url_1, ..., url_N]`
Step 2  — Pre-flight cleanup barrier (once, before loop; no workers active):
            geometra_list_sessions()
            for every returned sessions[].id:
              geometra_disconnect({ sessionId: "<id>", closeBrowser: true })
            # An empty sessions[] is already clean.
            # portal:* helpers are direct-package one-shots and auto-close.
Step 3  — Evaluation phase. For round in ceil(N/2):
            pair = pending[round*2 : round*2 + 2]
            # ONE message, 1 or 2 task() calls. Never 3.
            task(evaluate and prepare pair[0]; STOP before application form access)
            task(evaluate and prepare pair[1]; STOP before application form access)  # only if pair has 2
            # WAIT for both returns before the next round.
Step 4  — Between evaluation rounds, after both workers return: list sessions, then disconnect every returned id with
            geometra_disconnect({sessionId: "<id>", closeBrowser: true})
Step 5  — Selection phase after every evaluation is file-backed:
            build the full scored candidate queue; enforce H2's four-source current-month company audit
            npx job-forge prioritize:select
            npx job-forge apply:queue --input <full-queue.json> --dry-run
            create live round files only from companySelection.selectedCandidateIds
            replace omitted same-company slots with eligible candidates from other companies
Step 6  — Application phase. At each no-workers-active round barrier:
            list/disconnect every Geometra session explicitly
            npx job-forge apply:queue --input <one-or-two-selected-jobs.json> --parallel 2
            wait for the round's final outcomes before creating the next live round
Step 7  — Reconcile outcomes (Hard Limit #6):
            bash: npx job-forge merge      # TSVs → correct day file
            bash: npx job-forge verify     # validate URL/status consistency
Step 8  — Display summary table; flag any verify-pipeline errors.
```

**Hard rules:**
- Max 2 `task` dispatches per message (Hard Limit #1).
- Never re-dispatch a URL whose previous subagent is still in-flight (Hard Limit #5).
- Orchestrator does not call `geometra_fill_form` / `geometra_page_model` in multi-URL runs (Hard Limit #4) — delegate.
- Never submit during a multi-URL evaluation task. Select the best validated role per canonical company only after the complete set is scored (Hard Limit #2).
- **The only edits allowed to `data/pipeline.md` are flipping `[ ]` → `[x]`** (inbox state) (Hard Limit #6). APPLIED / FAILED / SKIP outcomes go via `batch/tracker-additions/*.tsv` into the day file. Do NOT write application status to pipeline.md.
