job-forge 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,36 +3,100 @@ description: Project instructions
3
3
  alwaysApply: true
4
4
  ---
5
5
 
6
- # JobForge -- AI Job Search Pipeline
6
+ # Agent: job-forge
7
7
 
8
- ## Hard Limits NEVER exceed these numbers
8
+ 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.
9
9
 
10
- The Hard Limits below are non-negotiable numeric rules. If you catch yourself about to violate one, STOP and restructure.
10
+ ## Hard limits
11
11
 
12
- 1. **Max parallel subagents: 2.** Never emit 3+ `task` tool calls in a single message. For N jobs, run `ceil(N/2)` sequential rounds of 2. No exceptions not for "urgent", not for "the user asked for 10".
13
- 2. **Max 1 application per company+role.** Before every `task` dispatch for `apply`, Grep ALL of the following for the URL and for `company+role`:
14
- - `data/pipeline.md`
15
- - all `data/applications/*.md` day files (not just today's — prior-day Applies count too)
16
- - `batch/tracker-additions/*.tsv` (pending outcomes not yet merged)
17
- - `batch/tracker-additions/merged/*.tsv` (outcomes already consumed into day files — catches same-day earlier-batch Applies that merge collapsed into an existing row)
12
+ - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
13
+ why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result
18
14
 
19
- If any source shows an APPLIED / Applied outcome for this URL or company+role, skip that job and do not dispatch. **Why merged/ matters**: when two batches in the same day target the same role, `npx job-forge merge` updates the existing day-file row instead of creating a new one — so `grep data/applications/*.md` for the higher report number misses the earlier apply. The merged TSV is the only place the newer attempt's breadcrumb remains.
20
- 3. **Always clean Geometra sessions before dispatching.** Before every round of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. Every round. The disconnect is a no-op when the pool is empty.
21
- 4. **Orchestrator does NOT fill forms.** This session MUST NOT call `geometra_fill_form`, `geometra_run_actions`, `geometra_pick_listbox_option`, or `geometra_fill_otp` when handling a multi-job request. If you need to, it means you MUST have delegated — `task` out the remaining work instead.
22
- 5. **Re-dispatch only AFTER the previous subagent returns.** Never fire the same company's `task` twice while the first is still in-flight. Wait for the return value, then decide if a retry is warranted.
23
- 6. **Application outcomes flow through TSVs, not `data/pipeline.md`.** When a subagent returns APPLIED / FAILED / SKIP, the outcome goes to `batch/tracker-additions/{num}-{slug}.tsv`. `npx job-forge merge` then consumes the TSVs into the correct `data/applications/YYYY-MM-DD.md` day file. `data/pipeline.md` only tracks URL inbox state (`[ ]` pending → `[x]` processed). **NEVER append APPLIED / FAILED status lines to `pipeline.md`** — that's the day file's job, via the TSV pathway. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` followed by `npx job-forge verify` before ending the session.
24
- 7. **Load-bearing facts passed to downstream subagents must come from a file, not from a prior subagent's prose.** A URL, score, email ID, confirmation page snippet, JD salary range, exact answer submitted to a form question, or any other specific value that a downstream subagent will act on MUST originate from one of:
25
- - `data/pipeline.md` (URL inbox state)
26
- - `data/scan-history.tsv` (scan provenance)
27
- - `batch/scan-output-*.md` (scan-ranked candidates)
28
- - A report file (`reports/{num}-*.md`) with authoritative headers (`**URL:**`, `**Score:**`, etc.)
29
- - A TSV in `batch/tracker-additions/` (per-apply outcomes)
15
+ - [H2] Max 1 application per company+role. Before every `apply` dispatch, grep all four sources for the URL and for `company+role`: `data/pipeline.md`, all `data/applications/*.md` day files, `batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`. If any source shows APPLIED / Applied, skip the dispatch.
16
+ why: 2026-04 same-day batch collision when two batches target the same role, `npx job-forge merge` updates the existing day-file row rather than appending, so grepping day files alone misses earlier-batch applies; merged/*.tsv is the only place the breadcrumb remains
30
17
 
31
- **Not trustworthy by default**: anything quoted from a subagent's return message, any ID or score the orchestrator "remembers" from prose, any page-content snippet reproduced from a subagent's narrative. Subagents can hallucinate plausible-looking IDs, scores, and confirmation text. Before passing any such fact to another subagent, cross-check it exists in one of the authoritative files above, OR instruct the dispatching subagent to write its output to a structured file and re-read from that file.
18
+ - [H3] Before every batch of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. 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.
19
+ why: if any prior subagent aborted mid-flow, its Chromium session stays stuck in the MCP pool and the next `geometra_connect` fails with "Not connected"; the disconnect is a no-op when the pool is empty but a poison-cure when it isn't; vocalizing it up-front doubles the odds it actually runs
32
20
 
33
- **Why**: on 2026-04-18, a scan subagent returned 30 fabricated Greenhouse IDs in prose (correct role titles, plausible-looking invented IDs that didn't exist in the API). The orchestrator dispatched 30 downstream subagents that all hit 404s. Verification rules downstream (Hard Limit #6, API-first verify) caught the symptom. This rule prevents the *shape* of the bug hallucinations propagating through prose handoffs across all quantitative / identifier / specific-fact claims, not just URLs.
21
+ - [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.
22
+ 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
34
23
 
35
- Everything below is context and rationale. These seven numbers are the rules.
24
+ - [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.
25
+ why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
26
+
27
+ - [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.
28
+ 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
29
+
30
+ - [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, `batch/tracker-additions/*.tsv`.
31
+ 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
32
+
33
+ ## Defaults
34
+
35
+ - [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.
36
+ 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
37
+
38
+ - [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.
39
+ why: GLM 5.1 doesn't discount cache reads so procedural work there costs ~10×; free-tier models handle procedural work fine empirically (`opencode/big-pickle` processed 1000+ messages at $0)
40
+
41
+ - [D3] Upgrade `apply` routing to `@general-paid` when offer score ≥ 4.0/5, when user flags "top-tier / dream job / high-stakes", or when late-stage pipeline (post-screens).
42
+ why: form-fill flows are 6+ steps; free-tier sometimes aborts mid-flow on large Greenhouse/Workday schemas; paid tier has more headroom
43
+
44
+ - [D4] Auto-submit for offers scoring 3.0+/5 without pausing for confirmation between steps — scan → evaluate → apply is one continuous pipeline. Mark SKIP for <3.0 and move on.
45
+ why: JobForge is designed for end-to-end automation; pausing between steps defeats the purpose and the 3.0 gate already enforces quality
46
+
47
+ - [D5] Before any batch-apply dispatch, run the Apply Preflight location filter from `modes/apply.md` to exclude location-incompatible candidates.
48
+ why: catches the common case where an evaluated role has the right role-shape but a deal-breaking location that profile.yml already rules out
49
+
50
+ - [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.
51
+ 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
52
+
53
+ ## Procedure
54
+
55
+ 1. On start, check `cv.md`, `profile.yml`, `portals.yml` exist; onboard if any missing.
56
+ 2. Pick the mode from **Routing** [D6]. No match → ask; do not guess.
57
+ 3. Apply [D1]: batch/Geometra work → delegate; single/read-only/dev → inline.
58
+ 4. Before any `task` batch using Geometra, run cleanup [H3].
59
+ 5. Before `apply`, run duplicate check [H2] and location filter [D5].
60
+ 6. Route by cost tier [D2]; upgrade to `@general-paid` per [D3] for high-stakes offers.
61
+ 7. Cap parallelism at 2 per round [H1].
62
+ 8. One in-flight dispatch per company [H5].
63
+ 9. Orchestrator does not fill forms in multi-job mode [H4].
64
+ 10. Treat subagent prose as untrusted [H7]; cross-check facts against authoritative files.
65
+ 11. Write outcomes as TSVs [H6]; run `npx job-forge merge` then `verify` at end.
66
+ 12. Offers scoring 3.0+/5 continue without confirmation [D4]; <3.0 is SKIP.
67
+ 13. Confirm tracker is merged and verified before ending.
68
+
69
+ ## Routing
70
+
71
+ | If the user… | Mode |
72
+ |---|---|
73
+ | Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
74
+ | Asks to evaluate offer | `offer` |
75
+ | Asks to compare offers | `compare` |
76
+ | Wants LinkedIn outreach | `contact` |
77
+ | Asks for company research | `deep` |
78
+ | Wants to generate CV/PDF | `pdf` |
79
+ | Evaluates a course/cert | `training` |
80
+ | Evaluates portfolio project | `project` |
81
+ | Asks about application status | `tracker` |
82
+ | Fills out application form | `apply` |
83
+ | Searches for new offers | `scan` |
84
+ | Processes pending URLs | `pipeline` |
85
+ | Batch processes offers | `batch` |
86
+ | Asks what needs follow-up | `followup` |
87
+ | Reports a rejection | `rejection` |
88
+ | Receives a job offer | `negotiation` |
89
+ | otherwise | Ask which mode fits; do not guess |
90
+
91
+ ## Output format
92
+
93
+ 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.
94
+
95
+ ---
96
+
97
+ # Reference
98
+
99
+ Sections below are context, rationale, runbooks, and portal-specific empirical notes. The **Hard limits**, **Defaults**, **Procedure**, and **Routing** above are the contract; the material below is what the orchestrator and each mode consult during execution.
36
100
 
37
101
  ---
38
102
 
@@ -212,24 +276,7 @@ JobForge is designed to be customized by YOU (opencode). When the user asks you
212
276
 
213
277
  ### Skill Modes
214
278
 
215
- | If the user... | Mode |
216
- |----------------|------|
217
- | Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
218
- | Asks to evaluate offer | `offer` |
219
- | Asks to compare offers | `compare` |
220
- | Wants LinkedIn outreach | `contact` |
221
- | Asks for company research | `deep` |
222
- | Wants to generate CV/PDF | `pdf` |
223
- | Evaluates a course/cert | `training` |
224
- | Evaluates portfolio project | `project` |
225
- | Asks about application status | `tracker` |
226
- | Fills out application form | `apply` |
227
- | Searches for new offers | `scan` |
228
- | Processes pending URLs | `pipeline` |
229
- | Batch processes offers | `batch` |
230
- | Asks what needs follow-up | `followup` |
231
- | Reports a rejection | `rejection` |
232
- | Receives a job offer | `negotiation` |
279
+ Mode routing is specified in the top-level **## Routing** section. Each mode is implemented in `modes/{mode}.md` — consult those files for per-mode prompts, state, and expected outputs.
233
280
 
234
281
  ### CV Source of Truth
235
282
 
@@ -374,11 +421,13 @@ These blocks come from two distinct root causes and require different responses:
374
421
 
375
422
  **Known-block Ashby tenants (2026-04-19 empirical observations).** These tenants fired class B on every attempted submit from a headless datacenter-IP proxy. Orchestrators planning apply dispatches should assume these tenants will Fail in headless — prioritize other portals, or skip same-tenant siblings after a confirmed class B to avoid burning subagent slots:
376
423
 
377
- - Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS, **Ashby (self-tenant)**, **Perplexity**
424
+ - Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS, Ashby (self-tenant), Perplexity, **Goody**, **Starbridge**, **Graphite**, **Prompt Health**, **Vantage**
378
425
 
379
426
  **Known class-A-compatible Ashby tenants (same observations).** These tenants accepted headless submits cleanly, often with `imeFriendly: true` making the difference on the text-field subset:
380
427
 
381
- - Supabase, LangChain, Poolside, Runway Financial, **Sentry**, **Cognition**
428
+ - Supabase, LangChain, Poolside, Runway Financial, Sentry, Cognition
429
+
430
+ **Base rate for untested Ashby tenants (5/5 tested 2026-04-19 cycle 4 = class B).** The prior today is ~80-90% of untested Ashby tenants fingerprint-block headless submits. Orchestrators should treat any tenant not on the class-A-compatible list as likely class B — still dispatch to collect the data point, but don't burn multiple sibling-role slots on the same Ashby tenant.
382
431
 
383
432
  The pattern is tenant configuration, not role or company size. Lists drift as tenants tune their anti-bot — treat as probabilistic priors, not hard rules.
384
433
 
@@ -396,6 +445,29 @@ The pattern is tenant configuration, not role or company size. Lists drift as te
396
445
 
397
446
  **`geometra_fill_otp` char-drop on first fill.** Occasionally `fill_otp` lands only the first character of an 8-char code (seen on Instacart, 2026-04-19). Recovery: click the first cell to focus, then re-issue `fill_otp` with `perCharDelayMs: 120`. The form usually auto-submits once all 8 cells are populated.
398
447
 
448
+ **Breezy portal — tenant-dependent, native `<select>`, resume-auto-parse is primary.** A subset of companies (Avantos AI, Courted, Instinct Science confirmed 2026-04-19) host applications on `*.breezy.hr` or `applytojob.com`. Empirical rules:
449
+
450
+ - **Class is per-tenant, not uniform.** Avantos (Failed 2026-04-19 #854) returned Breezy's own "It looks like maybe you've already applied to this job?" banner from IP fingerprinting, even on a first submit — distinct failure mode from Ashby's "flagged as possible spam". Courted (Applied 2026-04-19 #855) went through cleanly on the same session. Don't pre-skip Breezy; the outcome is tenant-specific.
451
+ - **Native `<select>` elements, not React comboboxes.** `geometra_pick_listbox_option` sets the visible display but NOT the underlying form state — submit will fail with "A response is required" on every combobox. Use `geometra_select_option` with x,y + label value for every choice field on Breezy.
452
+ - **Resume-auto-parse carries the signal.** After resume upload, Breezy auto-parses work history and education into structured rows. Do NOT Add/Delete position rows via Geometra — row mutations reshuffle fieldIds mid-flow, sequential `fill_fields` calls land in wrong rows, and upstream pollution corrupts earlier positions. Trust the parsed resume and fill only Personal Details + salary.
453
+
454
+ **Mailto-apply portals — direct email via gmail-mcp `attachments`.** A subset of HN-listed companies (CoPlane, Gambit Robotics, Rinse, Digital Health Strategies confirmed 2026-04-19) don't host an ATS form — their careers page instructs sending resume by email to `founders@...` / `jobs@...` / `contact@...`. Detection: WebFetch the careers URL; if the Apply link resolves to `mailto:` or the copy reads "email your resume to …", skip Geometra entirely.
455
+
456
+ Use `gmail_send_message` with the `attachments` parameter (available from `@razroo/gmail-mcp@1.8.0`):
457
+
458
+ ```
459
+ gmail_send_message({
460
+ to: ["founders@example.com"],
461
+ subject: "Application — Forward Deployed AI Engineer — Charlie Greenman (Austin)",
462
+ body: "<Section G pitch, 4-8 short paragraphs>",
463
+ attachments: [{ path: "/abs/path/to/Charlie-Greenman-CV.pdf" }]
464
+ })
465
+ ```
466
+
467
+ The MCP reads the file from disk and builds multipart/mixed MIME server-side — do NOT manually base64-encode a PDF into the `raw` parameter (the inline blob exceeds tool-call argument limits for any real attachment). Subject is auto MIME-encoded for non-ASCII (em-dash, smart quotes) by the same version. For older gmail-mcp versions (< 1.8.0) the only path was a direct Gmail API POST with the stored OAuth token at `~/.gmail-mcp/credentials.json` — upgrade if you can.
468
+
469
+ Mark Applied with note `mailto portal — sent via gmail_send_message; Gmail msgId {id}`. Verify via `gmail_get_message` that the attachment intact-size matches what was on disk before writing the TSV.
470
+
399
471
  ### Greenhouse Bot-Detection Honeypots
400
472
 
401
473
  Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) inject a honeypot-style single-pick question on the application form, rendered as a listbox labeled something like "Which of the following best describes you?" with options resembling "I am a human being / I am a bot / I am a robot".
package/AGENTS.md CHANGED
@@ -1,33 +1,97 @@
1
- # JobForge -- AI Job Search Pipeline
1
+ # Agent: job-forge
2
2
 
3
- ## Hard Limits NEVER exceed these numbers
3
+ 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.
4
4
 
5
- The Hard Limits below are non-negotiable numeric rules. If you catch yourself about to violate one, STOP and restructure.
5
+ ## Hard limits
6
6
 
7
- 1. **Max parallel subagents: 2.** Never emit 3+ `task` tool calls in a single message. For N jobs, run `ceil(N/2)` sequential rounds of 2. No exceptions not for "urgent", not for "the user asked for 10".
8
- 2. **Max 1 application per company+role.** Before every `task` dispatch for `apply`, Grep ALL of the following for the URL and for `company+role`:
9
- - `data/pipeline.md`
10
- - all `data/applications/*.md` day files (not just today's — prior-day Applies count too)
11
- - `batch/tracker-additions/*.tsv` (pending outcomes not yet merged)
12
- - `batch/tracker-additions/merged/*.tsv` (outcomes already consumed into day files — catches same-day earlier-batch Applies that merge collapsed into an existing row)
7
+ - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
8
+ why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result
13
9
 
14
- If any source shows an APPLIED / Applied outcome for this URL or company+role, skip that job and do not dispatch. **Why merged/ matters**: when two batches in the same day target the same role, `npx job-forge merge` updates the existing day-file row instead of creating a new one — so `grep data/applications/*.md` for the higher report number misses the earlier apply. The merged TSV is the only place the newer attempt's breadcrumb remains.
15
- 3. **Always clean Geometra sessions before dispatching.** Before every round of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. Every round. The disconnect is a no-op when the pool is empty.
16
- 4. **Orchestrator does NOT fill forms.** This session MUST NOT call `geometra_fill_form`, `geometra_run_actions`, `geometra_pick_listbox_option`, or `geometra_fill_otp` when handling a multi-job request. If you need to, it means you MUST have delegated — `task` out the remaining work instead.
17
- 5. **Re-dispatch only AFTER the previous subagent returns.** Never fire the same company's `task` twice while the first is still in-flight. Wait for the return value, then decide if a retry is warranted.
18
- 6. **Application outcomes flow through TSVs, not `data/pipeline.md`.** When a subagent returns APPLIED / FAILED / SKIP, the outcome goes to `batch/tracker-additions/{num}-{slug}.tsv`. `npx job-forge merge` then consumes the TSVs into the correct `data/applications/YYYY-MM-DD.md` day file. `data/pipeline.md` only tracks URL inbox state (`[ ]` pending → `[x]` processed). **NEVER append APPLIED / FAILED status lines to `pipeline.md`** — that's the day file's job, via the TSV pathway. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` followed by `npx job-forge verify` before ending the session.
19
- 7. **Load-bearing facts passed to downstream subagents must come from a file, not from a prior subagent's prose.** A URL, score, email ID, confirmation page snippet, JD salary range, exact answer submitted to a form question, or any other specific value that a downstream subagent will act on MUST originate from one of:
20
- - `data/pipeline.md` (URL inbox state)
21
- - `data/scan-history.tsv` (scan provenance)
22
- - `batch/scan-output-*.md` (scan-ranked candidates)
23
- - A report file (`reports/{num}-*.md`) with authoritative headers (`**URL:**`, `**Score:**`, etc.)
24
- - A TSV in `batch/tracker-additions/` (per-apply outcomes)
10
+ - [H2] Max 1 application per company+role. Before every `apply` dispatch, grep all four sources for the URL and for `company+role`: `data/pipeline.md`, all `data/applications/*.md` day files, `batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`. If any source shows APPLIED / Applied, skip the dispatch.
11
+ why: 2026-04 same-day batch collision when two batches target the same role, `npx job-forge merge` updates the existing day-file row rather than appending, so grepping day files alone misses earlier-batch applies; merged/*.tsv is the only place the breadcrumb remains
25
12
 
26
- **Not trustworthy by default**: anything quoted from a subagent's return message, any ID or score the orchestrator "remembers" from prose, any page-content snippet reproduced from a subagent's narrative. Subagents can hallucinate plausible-looking IDs, scores, and confirmation text. Before passing any such fact to another subagent, cross-check it exists in one of the authoritative files above, OR instruct the dispatching subagent to write its output to a structured file and re-read from that file.
13
+ - [H3] Before every batch of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. 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.
14
+ why: if any prior subagent aborted mid-flow, its Chromium session stays stuck in the MCP pool and the next `geometra_connect` fails with "Not connected"; the disconnect is a no-op when the pool is empty but a poison-cure when it isn't; vocalizing it up-front doubles the odds it actually runs
27
15
 
28
- **Why**: on 2026-04-18, a scan subagent returned 30 fabricated Greenhouse IDs in prose (correct role titles, plausible-looking invented IDs that didn't exist in the API). The orchestrator dispatched 30 downstream subagents that all hit 404s. Verification rules downstream (Hard Limit #6, API-first verify) caught the symptom. This rule prevents the *shape* of the bug hallucinations propagating through prose handoffs across all quantitative / identifier / specific-fact claims, not just URLs.
16
+ - [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.
17
+ 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
29
18
 
30
- Everything below is context and rationale. These seven numbers are the rules.
19
+ - [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.
20
+ why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
21
+
22
+ - [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.
23
+ 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
24
+
25
+ - [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, `batch/tracker-additions/*.tsv`.
26
+ 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
27
+
28
+ ## Defaults
29
+
30
+ - [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.
31
+ 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
32
+
33
+ - [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.
34
+ why: GLM 5.1 doesn't discount cache reads so procedural work there costs ~10×; free-tier models handle procedural work fine empirically (`opencode/big-pickle` processed 1000+ messages at $0)
35
+
36
+ - [D3] Upgrade `apply` routing to `@general-paid` when offer score ≥ 4.0/5, when user flags "top-tier / dream job / high-stakes", or when late-stage pipeline (post-screens).
37
+ why: form-fill flows are 6+ steps; free-tier sometimes aborts mid-flow on large Greenhouse/Workday schemas; paid tier has more headroom
38
+
39
+ - [D4] Auto-submit for offers scoring 3.0+/5 without pausing for confirmation between steps — scan → evaluate → apply is one continuous pipeline. Mark SKIP for <3.0 and move on.
40
+ why: JobForge is designed for end-to-end automation; pausing between steps defeats the purpose and the 3.0 gate already enforces quality
41
+
42
+ - [D5] Before any batch-apply dispatch, run the Apply Preflight location filter from `modes/apply.md` to exclude location-incompatible candidates.
43
+ why: catches the common case where an evaluated role has the right role-shape but a deal-breaking location that profile.yml already rules out
44
+
45
+ - [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.
46
+ 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
47
+
48
+ ## Procedure
49
+
50
+ 1. On start, check `cv.md`, `profile.yml`, `portals.yml` exist; onboard if any missing.
51
+ 2. Pick the mode from **Routing** [D6]. No match → ask; do not guess.
52
+ 3. Apply [D1]: batch/Geometra work → delegate; single/read-only/dev → inline.
53
+ 4. Before any `task` batch using Geometra, run cleanup [H3].
54
+ 5. Before `apply`, run duplicate check [H2] and location filter [D5].
55
+ 6. Route by cost tier [D2]; upgrade to `@general-paid` per [D3] for high-stakes offers.
56
+ 7. Cap parallelism at 2 per round [H1].
57
+ 8. One in-flight dispatch per company [H5].
58
+ 9. Orchestrator does not fill forms in multi-job mode [H4].
59
+ 10. Treat subagent prose as untrusted [H7]; cross-check facts against authoritative files.
60
+ 11. Write outcomes as TSVs [H6]; run `npx job-forge merge` then `verify` at end.
61
+ 12. Offers scoring 3.0+/5 continue without confirmation [D4]; <3.0 is SKIP.
62
+ 13. Confirm tracker is merged and verified before ending.
63
+
64
+ ## Routing
65
+
66
+ | If the user… | Mode |
67
+ |---|---|
68
+ | Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
69
+ | Asks to evaluate offer | `offer` |
70
+ | Asks to compare offers | `compare` |
71
+ | Wants LinkedIn outreach | `contact` |
72
+ | Asks for company research | `deep` |
73
+ | Wants to generate CV/PDF | `pdf` |
74
+ | Evaluates a course/cert | `training` |
75
+ | Evaluates portfolio project | `project` |
76
+ | Asks about application status | `tracker` |
77
+ | Fills out application form | `apply` |
78
+ | Searches for new offers | `scan` |
79
+ | Processes pending URLs | `pipeline` |
80
+ | Batch processes offers | `batch` |
81
+ | Asks what needs follow-up | `followup` |
82
+ | Reports a rejection | `rejection` |
83
+ | Receives a job offer | `negotiation` |
84
+ | otherwise | Ask which mode fits; do not guess |
85
+
86
+ ## Output format
87
+
88
+ 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.
89
+
90
+ ---
91
+
92
+ # Reference
93
+
94
+ Sections below are context, rationale, runbooks, and portal-specific empirical notes. The **Hard limits**, **Defaults**, **Procedure**, and **Routing** above are the contract; the material below is what the orchestrator and each mode consult during execution.
31
95
 
32
96
  ---
33
97
 
@@ -207,24 +271,7 @@ JobForge is designed to be customized by YOU (opencode). When the user asks you
207
271
 
208
272
  ### Skill Modes
209
273
 
210
- | If the user... | Mode |
211
- |----------------|------|
212
- | Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
213
- | Asks to evaluate offer | `offer` |
214
- | Asks to compare offers | `compare` |
215
- | Wants LinkedIn outreach | `contact` |
216
- | Asks for company research | `deep` |
217
- | Wants to generate CV/PDF | `pdf` |
218
- | Evaluates a course/cert | `training` |
219
- | Evaluates portfolio project | `project` |
220
- | Asks about application status | `tracker` |
221
- | Fills out application form | `apply` |
222
- | Searches for new offers | `scan` |
223
- | Processes pending URLs | `pipeline` |
224
- | Batch processes offers | `batch` |
225
- | Asks what needs follow-up | `followup` |
226
- | Reports a rejection | `rejection` |
227
- | Receives a job offer | `negotiation` |
274
+ Mode routing is specified in the top-level **## Routing** section. Each mode is implemented in `modes/{mode}.md` — consult those files for per-mode prompts, state, and expected outputs.
228
275
 
229
276
  ### CV Source of Truth
230
277
 
@@ -369,11 +416,13 @@ These blocks come from two distinct root causes and require different responses:
369
416
 
370
417
  **Known-block Ashby tenants (2026-04-19 empirical observations).** These tenants fired class B on every attempted submit from a headless datacenter-IP proxy. Orchestrators planning apply dispatches should assume these tenants will Fail in headless — prioritize other portals, or skip same-tenant siblings after a confirmed class B to avoid burning subagent slots:
371
418
 
372
- - Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS, **Ashby (self-tenant)**, **Perplexity**
419
+ - Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS, Ashby (self-tenant), Perplexity, **Goody**, **Starbridge**, **Graphite**, **Prompt Health**, **Vantage**
373
420
 
374
421
  **Known class-A-compatible Ashby tenants (same observations).** These tenants accepted headless submits cleanly, often with `imeFriendly: true` making the difference on the text-field subset:
375
422
 
376
- - Supabase, LangChain, Poolside, Runway Financial, **Sentry**, **Cognition**
423
+ - Supabase, LangChain, Poolside, Runway Financial, Sentry, Cognition
424
+
425
+ **Base rate for untested Ashby tenants (5/5 tested 2026-04-19 cycle 4 = class B).** The prior today is ~80-90% of untested Ashby tenants fingerprint-block headless submits. Orchestrators should treat any tenant not on the class-A-compatible list as likely class B — still dispatch to collect the data point, but don't burn multiple sibling-role slots on the same Ashby tenant.
377
426
 
378
427
  The pattern is tenant configuration, not role or company size. Lists drift as tenants tune their anti-bot — treat as probabilistic priors, not hard rules.
379
428
 
@@ -391,6 +440,29 @@ The pattern is tenant configuration, not role or company size. Lists drift as te
391
440
 
392
441
  **`geometra_fill_otp` char-drop on first fill.** Occasionally `fill_otp` lands only the first character of an 8-char code (seen on Instacart, 2026-04-19). Recovery: click the first cell to focus, then re-issue `fill_otp` with `perCharDelayMs: 120`. The form usually auto-submits once all 8 cells are populated.
393
442
 
443
+ **Breezy portal — tenant-dependent, native `<select>`, resume-auto-parse is primary.** A subset of companies (Avantos AI, Courted, Instinct Science confirmed 2026-04-19) host applications on `*.breezy.hr` or `applytojob.com`. Empirical rules:
444
+
445
+ - **Class is per-tenant, not uniform.** Avantos (Failed 2026-04-19 #854) returned Breezy's own "It looks like maybe you've already applied to this job?" banner from IP fingerprinting, even on a first submit — distinct failure mode from Ashby's "flagged as possible spam". Courted (Applied 2026-04-19 #855) went through cleanly on the same session. Don't pre-skip Breezy; the outcome is tenant-specific.
446
+ - **Native `<select>` elements, not React comboboxes.** `geometra_pick_listbox_option` sets the visible display but NOT the underlying form state — submit will fail with "A response is required" on every combobox. Use `geometra_select_option` with x,y + label value for every choice field on Breezy.
447
+ - **Resume-auto-parse carries the signal.** After resume upload, Breezy auto-parses work history and education into structured rows. Do NOT Add/Delete position rows via Geometra — row mutations reshuffle fieldIds mid-flow, sequential `fill_fields` calls land in wrong rows, and upstream pollution corrupts earlier positions. Trust the parsed resume and fill only Personal Details + salary.
448
+
449
+ **Mailto-apply portals — direct email via gmail-mcp `attachments`.** A subset of HN-listed companies (CoPlane, Gambit Robotics, Rinse, Digital Health Strategies confirmed 2026-04-19) don't host an ATS form — their careers page instructs sending resume by email to `founders@...` / `jobs@...` / `contact@...`. Detection: WebFetch the careers URL; if the Apply link resolves to `mailto:` or the copy reads "email your resume to …", skip Geometra entirely.
450
+
451
+ Use `gmail_send_message` with the `attachments` parameter (available from `@razroo/gmail-mcp@1.8.0`):
452
+
453
+ ```
454
+ gmail_send_message({
455
+ to: ["founders@example.com"],
456
+ subject: "Application — Forward Deployed AI Engineer — Charlie Greenman (Austin)",
457
+ body: "<Section G pitch, 4-8 short paragraphs>",
458
+ attachments: [{ path: "/abs/path/to/Charlie-Greenman-CV.pdf" }]
459
+ })
460
+ ```
461
+
462
+ The MCP reads the file from disk and builds multipart/mixed MIME server-side — do NOT manually base64-encode a PDF into the `raw` parameter (the inline blob exceeds tool-call argument limits for any real attachment). Subject is auto MIME-encoded for non-ASCII (em-dash, smart quotes) by the same version. For older gmail-mcp versions (< 1.8.0) the only path was a direct Gmail API POST with the stored OAuth token at `~/.gmail-mcp/credentials.json` — upgrade if you can.
463
+
464
+ Mark Applied with note `mailto portal — sent via gmail_send_message; Gmail msgId {id}`. Verify via `gmail_get_message` that the attachment intact-size matches what was on disk before writing the TSV.
465
+
394
466
  ### Greenhouse Bot-Detection Honeypots
395
467
 
396
468
  Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) inject a honeypot-style single-pick question on the application form, rendered as a listbox labeled something like "Which of the following best describes you?" with options resembling "I am a human being / I am a bot / I am a robot".
package/CLAUDE.md CHANGED
@@ -1,33 +1,97 @@
1
- # JobForge -- AI Job Search Pipeline
1
+ # Agent: job-forge
2
2
 
3
- ## Hard Limits NEVER exceed these numbers
3
+ 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.
4
4
 
5
- The Hard Limits below are non-negotiable numeric rules. If you catch yourself about to violate one, STOP and restructure.
5
+ ## Hard limits
6
6
 
7
- 1. **Max parallel subagents: 2.** Never emit 3+ `task` tool calls in a single message. For N jobs, run `ceil(N/2)` sequential rounds of 2. No exceptions not for "urgent", not for "the user asked for 10".
8
- 2. **Max 1 application per company+role.** Before every `task` dispatch for `apply`, Grep ALL of the following for the URL and for `company+role`:
9
- - `data/pipeline.md`
10
- - all `data/applications/*.md` day files (not just today's — prior-day Applies count too)
11
- - `batch/tracker-additions/*.tsv` (pending outcomes not yet merged)
12
- - `batch/tracker-additions/merged/*.tsv` (outcomes already consumed into day files — catches same-day earlier-batch Applies that merge collapsed into an existing row)
7
+ - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
8
+ why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result
13
9
 
14
- If any source shows an APPLIED / Applied outcome for this URL or company+role, skip that job and do not dispatch. **Why merged/ matters**: when two batches in the same day target the same role, `npx job-forge merge` updates the existing day-file row instead of creating a new one — so `grep data/applications/*.md` for the higher report number misses the earlier apply. The merged TSV is the only place the newer attempt's breadcrumb remains.
15
- 3. **Always clean Geometra sessions before dispatching.** Before every round of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. Every round. The disconnect is a no-op when the pool is empty.
16
- 4. **Orchestrator does NOT fill forms.** This session MUST NOT call `geometra_fill_form`, `geometra_run_actions`, `geometra_pick_listbox_option`, or `geometra_fill_otp` when handling a multi-job request. If you need to, it means you MUST have delegated — `task` out the remaining work instead.
17
- 5. **Re-dispatch only AFTER the previous subagent returns.** Never fire the same company's `task` twice while the first is still in-flight. Wait for the return value, then decide if a retry is warranted.
18
- 6. **Application outcomes flow through TSVs, not `data/pipeline.md`.** When a subagent returns APPLIED / FAILED / SKIP, the outcome goes to `batch/tracker-additions/{num}-{slug}.tsv`. `npx job-forge merge` then consumes the TSVs into the correct `data/applications/YYYY-MM-DD.md` day file. `data/pipeline.md` only tracks URL inbox state (`[ ]` pending → `[x]` processed). **NEVER append APPLIED / FAILED status lines to `pipeline.md`** — that's the day file's job, via the TSV pathway. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` followed by `npx job-forge verify` before ending the session.
19
- 7. **Load-bearing facts passed to downstream subagents must come from a file, not from a prior subagent's prose.** A URL, score, email ID, confirmation page snippet, JD salary range, exact answer submitted to a form question, or any other specific value that a downstream subagent will act on MUST originate from one of:
20
- - `data/pipeline.md` (URL inbox state)
21
- - `data/scan-history.tsv` (scan provenance)
22
- - `batch/scan-output-*.md` (scan-ranked candidates)
23
- - A report file (`reports/{num}-*.md`) with authoritative headers (`**URL:**`, `**Score:**`, etc.)
24
- - A TSV in `batch/tracker-additions/` (per-apply outcomes)
10
+ - [H2] Max 1 application per company+role. Before every `apply` dispatch, grep all four sources for the URL and for `company+role`: `data/pipeline.md`, all `data/applications/*.md` day files, `batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`. If any source shows APPLIED / Applied, skip the dispatch.
11
+ why: 2026-04 same-day batch collision when two batches target the same role, `npx job-forge merge` updates the existing day-file row rather than appending, so grepping day files alone misses earlier-batch applies; merged/*.tsv is the only place the breadcrumb remains
25
12
 
26
- **Not trustworthy by default**: anything quoted from a subagent's return message, any ID or score the orchestrator "remembers" from prose, any page-content snippet reproduced from a subagent's narrative. Subagents can hallucinate plausible-looking IDs, scores, and confirmation text. Before passing any such fact to another subagent, cross-check it exists in one of the authoritative files above, OR instruct the dispatching subagent to write its output to a structured file and re-read from that file.
13
+ - [H3] Before every batch of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. 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.
14
+ why: if any prior subagent aborted mid-flow, its Chromium session stays stuck in the MCP pool and the next `geometra_connect` fails with "Not connected"; the disconnect is a no-op when the pool is empty but a poison-cure when it isn't; vocalizing it up-front doubles the odds it actually runs
27
15
 
28
- **Why**: on 2026-04-18, a scan subagent returned 30 fabricated Greenhouse IDs in prose (correct role titles, plausible-looking invented IDs that didn't exist in the API). The orchestrator dispatched 30 downstream subagents that all hit 404s. Verification rules downstream (Hard Limit #6, API-first verify) caught the symptom. This rule prevents the *shape* of the bug hallucinations propagating through prose handoffs across all quantitative / identifier / specific-fact claims, not just URLs.
16
+ - [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.
17
+ 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
29
18
 
30
- Everything below is context and rationale. These seven numbers are the rules.
19
+ - [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.
20
+ why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
21
+
22
+ - [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.
23
+ 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
24
+
25
+ - [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, `batch/tracker-additions/*.tsv`.
26
+ 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
27
+
28
+ ## Defaults
29
+
30
+ - [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.
31
+ 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
32
+
33
+ - [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.
34
+ why: GLM 5.1 doesn't discount cache reads so procedural work there costs ~10×; free-tier models handle procedural work fine empirically (`opencode/big-pickle` processed 1000+ messages at $0)
35
+
36
+ - [D3] Upgrade `apply` routing to `@general-paid` when offer score ≥ 4.0/5, when user flags "top-tier / dream job / high-stakes", or when late-stage pipeline (post-screens).
37
+ why: form-fill flows are 6+ steps; free-tier sometimes aborts mid-flow on large Greenhouse/Workday schemas; paid tier has more headroom
38
+
39
+ - [D4] Auto-submit for offers scoring 3.0+/5 without pausing for confirmation between steps — scan → evaluate → apply is one continuous pipeline. Mark SKIP for <3.0 and move on.
40
+ why: JobForge is designed for end-to-end automation; pausing between steps defeats the purpose and the 3.0 gate already enforces quality
41
+
42
+ - [D5] Before any batch-apply dispatch, run the Apply Preflight location filter from `modes/apply.md` to exclude location-incompatible candidates.
43
+ why: catches the common case where an evaluated role has the right role-shape but a deal-breaking location that profile.yml already rules out
44
+
45
+ - [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.
46
+ 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
47
+
48
+ ## Procedure
49
+
50
+ 1. On start, check `cv.md`, `profile.yml`, `portals.yml` exist; onboard if any missing.
51
+ 2. Pick the mode from **Routing** [D6]. No match → ask; do not guess.
52
+ 3. Apply [D1]: batch/Geometra work → delegate; single/read-only/dev → inline.
53
+ 4. Before any `task` batch using Geometra, run cleanup [H3].
54
+ 5. Before `apply`, run duplicate check [H2] and location filter [D5].
55
+ 6. Route by cost tier [D2]; upgrade to `@general-paid` per [D3] for high-stakes offers.
56
+ 7. Cap parallelism at 2 per round [H1].
57
+ 8. One in-flight dispatch per company [H5].
58
+ 9. Orchestrator does not fill forms in multi-job mode [H4].
59
+ 10. Treat subagent prose as untrusted [H7]; cross-check facts against authoritative files.
60
+ 11. Write outcomes as TSVs [H6]; run `npx job-forge merge` then `verify` at end.
61
+ 12. Offers scoring 3.0+/5 continue without confirmation [D4]; <3.0 is SKIP.
62
+ 13. Confirm tracker is merged and verified before ending.
63
+
64
+ ## Routing
65
+
66
+ | If the user… | Mode |
67
+ |---|---|
68
+ | Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
69
+ | Asks to evaluate offer | `offer` |
70
+ | Asks to compare offers | `compare` |
71
+ | Wants LinkedIn outreach | `contact` |
72
+ | Asks for company research | `deep` |
73
+ | Wants to generate CV/PDF | `pdf` |
74
+ | Evaluates a course/cert | `training` |
75
+ | Evaluates portfolio project | `project` |
76
+ | Asks about application status | `tracker` |
77
+ | Fills out application form | `apply` |
78
+ | Searches for new offers | `scan` |
79
+ | Processes pending URLs | `pipeline` |
80
+ | Batch processes offers | `batch` |
81
+ | Asks what needs follow-up | `followup` |
82
+ | Reports a rejection | `rejection` |
83
+ | Receives a job offer | `negotiation` |
84
+ | otherwise | Ask which mode fits; do not guess |
85
+
86
+ ## Output format
87
+
88
+ 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.
89
+
90
+ ---
91
+
92
+ # Reference
93
+
94
+ Sections below are context, rationale, runbooks, and portal-specific empirical notes. The **Hard limits**, **Defaults**, **Procedure**, and **Routing** above are the contract; the material below is what the orchestrator and each mode consult during execution.
31
95
 
32
96
  ---
33
97
 
@@ -207,24 +271,7 @@ JobForge is designed to be customized by YOU (opencode). When the user asks you
207
271
 
208
272
  ### Skill Modes
209
273
 
210
- | If the user... | Mode |
211
- |----------------|------|
212
- | Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
213
- | Asks to evaluate offer | `offer` |
214
- | Asks to compare offers | `compare` |
215
- | Wants LinkedIn outreach | `contact` |
216
- | Asks for company research | `deep` |
217
- | Wants to generate CV/PDF | `pdf` |
218
- | Evaluates a course/cert | `training` |
219
- | Evaluates portfolio project | `project` |
220
- | Asks about application status | `tracker` |
221
- | Fills out application form | `apply` |
222
- | Searches for new offers | `scan` |
223
- | Processes pending URLs | `pipeline` |
224
- | Batch processes offers | `batch` |
225
- | Asks what needs follow-up | `followup` |
226
- | Reports a rejection | `rejection` |
227
- | Receives a job offer | `negotiation` |
274
+ Mode routing is specified in the top-level **## Routing** section. Each mode is implemented in `modes/{mode}.md` — consult those files for per-mode prompts, state, and expected outputs.
228
275
 
229
276
  ### CV Source of Truth
230
277
 
@@ -369,11 +416,13 @@ These blocks come from two distinct root causes and require different responses:
369
416
 
370
417
  **Known-block Ashby tenants (2026-04-19 empirical observations).** These tenants fired class B on every attempted submit from a headless datacenter-IP proxy. Orchestrators planning apply dispatches should assume these tenants will Fail in headless — prioritize other portals, or skip same-tenant siblings after a confirmed class B to avoid burning subagent slots:
371
418
 
372
- - Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS, **Ashby (self-tenant)**, **Perplexity**
419
+ - Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS, Ashby (self-tenant), Perplexity, **Goody**, **Starbridge**, **Graphite**, **Prompt Health**, **Vantage**
373
420
 
374
421
  **Known class-A-compatible Ashby tenants (same observations).** These tenants accepted headless submits cleanly, often with `imeFriendly: true` making the difference on the text-field subset:
375
422
 
376
- - Supabase, LangChain, Poolside, Runway Financial, **Sentry**, **Cognition**
423
+ - Supabase, LangChain, Poolside, Runway Financial, Sentry, Cognition
424
+
425
+ **Base rate for untested Ashby tenants (5/5 tested 2026-04-19 cycle 4 = class B).** The prior today is ~80-90% of untested Ashby tenants fingerprint-block headless submits. Orchestrators should treat any tenant not on the class-A-compatible list as likely class B — still dispatch to collect the data point, but don't burn multiple sibling-role slots on the same Ashby tenant.
377
426
 
378
427
  The pattern is tenant configuration, not role or company size. Lists drift as tenants tune their anti-bot — treat as probabilistic priors, not hard rules.
379
428
 
@@ -391,6 +440,29 @@ The pattern is tenant configuration, not role or company size. Lists drift as te
391
440
 
392
441
  **`geometra_fill_otp` char-drop on first fill.** Occasionally `fill_otp` lands only the first character of an 8-char code (seen on Instacart, 2026-04-19). Recovery: click the first cell to focus, then re-issue `fill_otp` with `perCharDelayMs: 120`. The form usually auto-submits once all 8 cells are populated.
393
442
 
443
+ **Breezy portal — tenant-dependent, native `<select>`, resume-auto-parse is primary.** A subset of companies (Avantos AI, Courted, Instinct Science confirmed 2026-04-19) host applications on `*.breezy.hr` or `applytojob.com`. Empirical rules:
444
+
445
+ - **Class is per-tenant, not uniform.** Avantos (Failed 2026-04-19 #854) returned Breezy's own "It looks like maybe you've already applied to this job?" banner from IP fingerprinting, even on a first submit — distinct failure mode from Ashby's "flagged as possible spam". Courted (Applied 2026-04-19 #855) went through cleanly on the same session. Don't pre-skip Breezy; the outcome is tenant-specific.
446
+ - **Native `<select>` elements, not React comboboxes.** `geometra_pick_listbox_option` sets the visible display but NOT the underlying form state — submit will fail with "A response is required" on every combobox. Use `geometra_select_option` with x,y + label value for every choice field on Breezy.
447
+ - **Resume-auto-parse carries the signal.** After resume upload, Breezy auto-parses work history and education into structured rows. Do NOT Add/Delete position rows via Geometra — row mutations reshuffle fieldIds mid-flow, sequential `fill_fields` calls land in wrong rows, and upstream pollution corrupts earlier positions. Trust the parsed resume and fill only Personal Details + salary.
448
+
449
+ **Mailto-apply portals — direct email via gmail-mcp `attachments`.** A subset of HN-listed companies (CoPlane, Gambit Robotics, Rinse, Digital Health Strategies confirmed 2026-04-19) don't host an ATS form — their careers page instructs sending resume by email to `founders@...` / `jobs@...` / `contact@...`. Detection: WebFetch the careers URL; if the Apply link resolves to `mailto:` or the copy reads "email your resume to …", skip Geometra entirely.
450
+
451
+ Use `gmail_send_message` with the `attachments` parameter (available from `@razroo/gmail-mcp@1.8.0`):
452
+
453
+ ```
454
+ gmail_send_message({
455
+ to: ["founders@example.com"],
456
+ subject: "Application — Forward Deployed AI Engineer — Charlie Greenman (Austin)",
457
+ body: "<Section G pitch, 4-8 short paragraphs>",
458
+ attachments: [{ path: "/abs/path/to/Charlie-Greenman-CV.pdf" }]
459
+ })
460
+ ```
461
+
462
+ The MCP reads the file from disk and builds multipart/mixed MIME server-side — do NOT manually base64-encode a PDF into the `raw` parameter (the inline blob exceeds tool-call argument limits for any real attachment). Subject is auto MIME-encoded for non-ASCII (em-dash, smart quotes) by the same version. For older gmail-mcp versions (< 1.8.0) the only path was a direct Gmail API POST with the stored OAuth token at `~/.gmail-mcp/credentials.json` — upgrade if you can.
463
+
464
+ Mark Applied with note `mailto portal — sent via gmail_send_message; Gmail msgId {id}`. Verify via `gmail_get_message` that the attachment intact-size matches what was on disk before writing the TSV.
465
+
394
466
  ### Greenhouse Bot-Detection Honeypots
395
467
 
396
468
  Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) inject a honeypot-style single-pick question on the application form, rendered as a listbox labeled something like "Which of the following best describes you?" with options resembling "I am a human being / I am a bot / I am a robot".
@@ -1,33 +1,97 @@
1
- # JobForge -- AI Job Search Pipeline
1
+ # Agent: job-forge
2
2
 
3
- ## Hard Limits NEVER exceed these numbers
3
+ 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.
4
4
 
5
- The Hard Limits below are non-negotiable numeric rules. If you catch yourself about to violate one, STOP and restructure.
5
+ ## Hard limits
6
6
 
7
- 1. **Max parallel subagents: 2.** Never emit 3+ `task` tool calls in a single message. For N jobs, run `ceil(N/2)` sequential rounds of 2. No exceptions not for "urgent", not for "the user asked for 10".
8
- 2. **Max 1 application per company+role.** Before every `task` dispatch for `apply`, Grep ALL of the following for the URL and for `company+role`:
9
- - `data/pipeline.md`
10
- - all `data/applications/*.md` day files (not just today's — prior-day Applies count too)
11
- - `batch/tracker-additions/*.tsv` (pending outcomes not yet merged)
12
- - `batch/tracker-additions/merged/*.tsv` (outcomes already consumed into day files — catches same-day earlier-batch Applies that merge collapsed into an existing row)
7
+ - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
8
+ why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result
13
9
 
14
- If any source shows an APPLIED / Applied outcome for this URL or company+role, skip that job and do not dispatch. **Why merged/ matters**: when two batches in the same day target the same role, `npx job-forge merge` updates the existing day-file row instead of creating a new one — so `grep data/applications/*.md` for the higher report number misses the earlier apply. The merged TSV is the only place the newer attempt's breadcrumb remains.
15
- 3. **Always clean Geometra sessions before dispatching.** Before every round of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. Every round. The disconnect is a no-op when the pool is empty.
16
- 4. **Orchestrator does NOT fill forms.** This session MUST NOT call `geometra_fill_form`, `geometra_run_actions`, `geometra_pick_listbox_option`, or `geometra_fill_otp` when handling a multi-job request. If you need to, it means you MUST have delegated — `task` out the remaining work instead.
17
- 5. **Re-dispatch only AFTER the previous subagent returns.** Never fire the same company's `task` twice while the first is still in-flight. Wait for the return value, then decide if a retry is warranted.
18
- 6. **Application outcomes flow through TSVs, not `data/pipeline.md`.** When a subagent returns APPLIED / FAILED / SKIP, the outcome goes to `batch/tracker-additions/{num}-{slug}.tsv`. `npx job-forge merge` then consumes the TSVs into the correct `data/applications/YYYY-MM-DD.md` day file. `data/pipeline.md` only tracks URL inbox state (`[ ]` pending → `[x]` processed). **NEVER append APPLIED / FAILED status lines to `pipeline.md`** — that's the day file's job, via the TSV pathway. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` followed by `npx job-forge verify` before ending the session.
19
- 7. **Load-bearing facts passed to downstream subagents must come from a file, not from a prior subagent's prose.** A URL, score, email ID, confirmation page snippet, JD salary range, exact answer submitted to a form question, or any other specific value that a downstream subagent will act on MUST originate from one of:
20
- - `data/pipeline.md` (URL inbox state)
21
- - `data/scan-history.tsv` (scan provenance)
22
- - `batch/scan-output-*.md` (scan-ranked candidates)
23
- - A report file (`reports/{num}-*.md`) with authoritative headers (`**URL:**`, `**Score:**`, etc.)
24
- - A TSV in `batch/tracker-additions/` (per-apply outcomes)
10
+ - [H2] Max 1 application per company+role. Before every `apply` dispatch, grep all four sources for the URL and for `company+role`: `data/pipeline.md`, all `data/applications/*.md` day files, `batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`. If any source shows APPLIED / Applied, skip the dispatch.
11
+ why: 2026-04 same-day batch collision when two batches target the same role, `npx job-forge merge` updates the existing day-file row rather than appending, so grepping day files alone misses earlier-batch applies; merged/*.tsv is the only place the breadcrumb remains
25
12
 
26
- **Not trustworthy by default**: anything quoted from a subagent's return message, any ID or score the orchestrator "remembers" from prose, any page-content snippet reproduced from a subagent's narrative. Subagents can hallucinate plausible-looking IDs, scores, and confirmation text. Before passing any such fact to another subagent, cross-check it exists in one of the authoritative files above, OR instruct the dispatching subagent to write its output to a structured file and re-read from that file.
13
+ - [H3] Before every batch of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. 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.
14
+ why: if any prior subagent aborted mid-flow, its Chromium session stays stuck in the MCP pool and the next `geometra_connect` fails with "Not connected"; the disconnect is a no-op when the pool is empty but a poison-cure when it isn't; vocalizing it up-front doubles the odds it actually runs
27
15
 
28
- **Why**: on 2026-04-18, a scan subagent returned 30 fabricated Greenhouse IDs in prose (correct role titles, plausible-looking invented IDs that didn't exist in the API). The orchestrator dispatched 30 downstream subagents that all hit 404s. Verification rules downstream (Hard Limit #6, API-first verify) caught the symptom. This rule prevents the *shape* of the bug hallucinations propagating through prose handoffs across all quantitative / identifier / specific-fact claims, not just URLs.
16
+ - [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.
17
+ 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
29
18
 
30
- Everything below is context and rationale. These seven numbers are the rules.
19
+ - [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.
20
+ why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
21
+
22
+ - [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.
23
+ 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
24
+
25
+ - [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, `batch/tracker-additions/*.tsv`.
26
+ 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
27
+
28
+ ## Defaults
29
+
30
+ - [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.
31
+ 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
32
+
33
+ - [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.
34
+ why: GLM 5.1 doesn't discount cache reads so procedural work there costs ~10×; free-tier models handle procedural work fine empirically (`opencode/big-pickle` processed 1000+ messages at $0)
35
+
36
+ - [D3] Upgrade `apply` routing to `@general-paid` when offer score ≥ 4.0/5, when user flags "top-tier / dream job / high-stakes", or when late-stage pipeline (post-screens).
37
+ why: form-fill flows are 6+ steps; free-tier sometimes aborts mid-flow on large Greenhouse/Workday schemas; paid tier has more headroom
38
+
39
+ - [D4] Auto-submit for offers scoring 3.0+/5 without pausing for confirmation between steps — scan → evaluate → apply is one continuous pipeline. Mark SKIP for <3.0 and move on.
40
+ why: JobForge is designed for end-to-end automation; pausing between steps defeats the purpose and the 3.0 gate already enforces quality
41
+
42
+ - [D5] Before any batch-apply dispatch, run the Apply Preflight location filter from `modes/apply.md` to exclude location-incompatible candidates.
43
+ why: catches the common case where an evaluated role has the right role-shape but a deal-breaking location that profile.yml already rules out
44
+
45
+ - [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.
46
+ 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
47
+
48
+ ## Procedure
49
+
50
+ 1. On start, check `cv.md`, `profile.yml`, `portals.yml` exist; onboard if any missing.
51
+ 2. Pick the mode from **Routing** [D6]. No match → ask; do not guess.
52
+ 3. Apply [D1]: batch/Geometra work → delegate; single/read-only/dev → inline.
53
+ 4. Before any `task` batch using Geometra, run cleanup [H3].
54
+ 5. Before `apply`, run duplicate check [H2] and location filter [D5].
55
+ 6. Route by cost tier [D2]; upgrade to `@general-paid` per [D3] for high-stakes offers.
56
+ 7. Cap parallelism at 2 per round [H1].
57
+ 8. One in-flight dispatch per company [H5].
58
+ 9. Orchestrator does not fill forms in multi-job mode [H4].
59
+ 10. Treat subagent prose as untrusted [H7]; cross-check facts against authoritative files.
60
+ 11. Write outcomes as TSVs [H6]; run `npx job-forge merge` then `verify` at end.
61
+ 12. Offers scoring 3.0+/5 continue without confirmation [D4]; <3.0 is SKIP.
62
+ 13. Confirm tracker is merged and verified before ending.
63
+
64
+ ## Routing
65
+
66
+ | If the user… | Mode |
67
+ |---|---|
68
+ | Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
69
+ | Asks to evaluate offer | `offer` |
70
+ | Asks to compare offers | `compare` |
71
+ | Wants LinkedIn outreach | `contact` |
72
+ | Asks for company research | `deep` |
73
+ | Wants to generate CV/PDF | `pdf` |
74
+ | Evaluates a course/cert | `training` |
75
+ | Evaluates portfolio project | `project` |
76
+ | Asks about application status | `tracker` |
77
+ | Fills out application form | `apply` |
78
+ | Searches for new offers | `scan` |
79
+ | Processes pending URLs | `pipeline` |
80
+ | Batch processes offers | `batch` |
81
+ | Asks what needs follow-up | `followup` |
82
+ | Reports a rejection | `rejection` |
83
+ | Receives a job offer | `negotiation` |
84
+ | otherwise | Ask which mode fits; do not guess |
85
+
86
+ ## Output format
87
+
88
+ 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.
89
+
90
+ ---
91
+
92
+ # Reference
93
+
94
+ Sections below are context, rationale, runbooks, and portal-specific empirical notes. The **Hard limits**, **Defaults**, **Procedure**, and **Routing** above are the contract; the material below is what the orchestrator and each mode consult during execution.
31
95
 
32
96
  ---
33
97
 
@@ -207,24 +271,7 @@ JobForge is designed to be customized by YOU (opencode). When the user asks you
207
271
 
208
272
  ### Skill Modes
209
273
 
210
- | If the user... | Mode |
211
- |----------------|------|
212
- | Pastes JD or URL | auto-pipeline (evaluate + report + PDF + tracker) |
213
- | Asks to evaluate offer | `offer` |
214
- | Asks to compare offers | `compare` |
215
- | Wants LinkedIn outreach | `contact` |
216
- | Asks for company research | `deep` |
217
- | Wants to generate CV/PDF | `pdf` |
218
- | Evaluates a course/cert | `training` |
219
- | Evaluates portfolio project | `project` |
220
- | Asks about application status | `tracker` |
221
- | Fills out application form | `apply` |
222
- | Searches for new offers | `scan` |
223
- | Processes pending URLs | `pipeline` |
224
- | Batch processes offers | `batch` |
225
- | Asks what needs follow-up | `followup` |
226
- | Reports a rejection | `rejection` |
227
- | Receives a job offer | `negotiation` |
274
+ Mode routing is specified in the top-level **## Routing** section. Each mode is implemented in `modes/{mode}.md` — consult those files for per-mode prompts, state, and expected outputs.
228
275
 
229
276
  ### CV Source of Truth
230
277
 
@@ -369,11 +416,13 @@ These blocks come from two distinct root causes and require different responses:
369
416
 
370
417
  **Known-block Ashby tenants (2026-04-19 empirical observations).** These tenants fired class B on every attempted submit from a headless datacenter-IP proxy. Orchestrators planning apply dispatches should assume these tenants will Fail in headless — prioritize other portals, or skip same-tenant siblings after a confirmed class B to avoid burning subagent slots:
371
418
 
372
- - Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS, **Ashby (self-tenant)**, **Perplexity**
419
+ - Vellum, Linear, Vanta, River Financial, Higharc, Trace Labs, Solace Health, Unstructured, ClickUp, Zapier, Deepgram, Ramp, WorkOS, Ashby (self-tenant), Perplexity, **Goody**, **Starbridge**, **Graphite**, **Prompt Health**, **Vantage**
373
420
 
374
421
  **Known class-A-compatible Ashby tenants (same observations).** These tenants accepted headless submits cleanly, often with `imeFriendly: true` making the difference on the text-field subset:
375
422
 
376
- - Supabase, LangChain, Poolside, Runway Financial, **Sentry**, **Cognition**
423
+ - Supabase, LangChain, Poolside, Runway Financial, Sentry, Cognition
424
+
425
+ **Base rate for untested Ashby tenants (5/5 tested 2026-04-19 cycle 4 = class B).** The prior today is ~80-90% of untested Ashby tenants fingerprint-block headless submits. Orchestrators should treat any tenant not on the class-A-compatible list as likely class B — still dispatch to collect the data point, but don't burn multiple sibling-role slots on the same Ashby tenant.
377
426
 
378
427
  The pattern is tenant configuration, not role or company size. Lists drift as tenants tune their anti-bot — treat as probabilistic priors, not hard rules.
379
428
 
@@ -391,6 +440,29 @@ The pattern is tenant configuration, not role or company size. Lists drift as te
391
440
 
392
441
  **`geometra_fill_otp` char-drop on first fill.** Occasionally `fill_otp` lands only the first character of an 8-char code (seen on Instacart, 2026-04-19). Recovery: click the first cell to focus, then re-issue `fill_otp` with `perCharDelayMs: 120`. The form usually auto-submits once all 8 cells are populated.
393
442
 
443
+ **Breezy portal — tenant-dependent, native `<select>`, resume-auto-parse is primary.** A subset of companies (Avantos AI, Courted, Instinct Science confirmed 2026-04-19) host applications on `*.breezy.hr` or `applytojob.com`. Empirical rules:
444
+
445
+ - **Class is per-tenant, not uniform.** Avantos (Failed 2026-04-19 #854) returned Breezy's own "It looks like maybe you've already applied to this job?" banner from IP fingerprinting, even on a first submit — distinct failure mode from Ashby's "flagged as possible spam". Courted (Applied 2026-04-19 #855) went through cleanly on the same session. Don't pre-skip Breezy; the outcome is tenant-specific.
446
+ - **Native `<select>` elements, not React comboboxes.** `geometra_pick_listbox_option` sets the visible display but NOT the underlying form state — submit will fail with "A response is required" on every combobox. Use `geometra_select_option` with x,y + label value for every choice field on Breezy.
447
+ - **Resume-auto-parse carries the signal.** After resume upload, Breezy auto-parses work history and education into structured rows. Do NOT Add/Delete position rows via Geometra — row mutations reshuffle fieldIds mid-flow, sequential `fill_fields` calls land in wrong rows, and upstream pollution corrupts earlier positions. Trust the parsed resume and fill only Personal Details + salary.
448
+
449
+ **Mailto-apply portals — direct email via gmail-mcp `attachments`.** A subset of HN-listed companies (CoPlane, Gambit Robotics, Rinse, Digital Health Strategies confirmed 2026-04-19) don't host an ATS form — their careers page instructs sending resume by email to `founders@...` / `jobs@...` / `contact@...`. Detection: WebFetch the careers URL; if the Apply link resolves to `mailto:` or the copy reads "email your resume to …", skip Geometra entirely.
450
+
451
+ Use `gmail_send_message` with the `attachments` parameter (available from `@razroo/gmail-mcp@1.8.0`):
452
+
453
+ ```
454
+ gmail_send_message({
455
+ to: ["founders@example.com"],
456
+ subject: "Application — Forward Deployed AI Engineer — Charlie Greenman (Austin)",
457
+ body: "<Section G pitch, 4-8 short paragraphs>",
458
+ attachments: [{ path: "/abs/path/to/Charlie-Greenman-CV.pdf" }]
459
+ })
460
+ ```
461
+
462
+ The MCP reads the file from disk and builds multipart/mixed MIME server-side — do NOT manually base64-encode a PDF into the `raw` parameter (the inline blob exceeds tool-call argument limits for any real attachment). Subject is auto MIME-encoded for non-ASCII (em-dash, smart quotes) by the same version. For older gmail-mcp versions (< 1.8.0) the only path was a direct Gmail API POST with the stored OAuth token at `~/.gmail-mcp/credentials.json` — upgrade if you can.
463
+
464
+ Mark Applied with note `mailto portal — sent via gmail_send_message; Gmail msgId {id}`. Verify via `gmail_get_message` that the attachment intact-size matches what was on disk before writing the TSV.
465
+
394
466
  ### Greenhouse Bot-Detection Honeypots
395
467
 
396
468
  Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) inject a honeypot-style single-pick question on the application form, rendered as a listbox labeled something like "Which of the following best describes you?" with options resembling "I am a human being / I am a bot / I am a robot".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-forge",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "AI-powered job search pipeline built on opencode",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,6 +20,9 @@
20
20
  "tokens:log": "node scripts/token-usage-report.mjs --days 1 --append",
21
21
  "trace:list": "iso-trace list --since 7d --cwd .",
22
22
  "trace:stats": "iso-trace stats --since 7d --cwd .",
23
+ "lint:agentmd": "agentmd lint iso/instructions.md",
24
+ "test:agentmd": "agentmd test iso/instructions.md --fixtures fixtures/instructions.yml --via claude-code --model claude-haiku-4-5 --concurrency 2",
25
+ "test:agentmd:baseline": "agentmd test iso/instructions.md --fixtures fixtures/instructions.yml --via claude-code --model claude-haiku-4-5 --concurrency 2 --trials 3 --format json --out fixtures/baseline.json",
23
26
  "build:config": "iso build .",
24
27
  "prepack": "iso build .",
25
28
  "release:check-source": "node ./scripts/release/check-source.mjs",