job-forge 2.1.0 → 2.3.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.
@@ -10,7 +10,13 @@ alwaysApply: true
10
10
  The Hard Limits below are non-negotiable numeric rules. If you catch yourself about to violate one, STOP and restructure.
11
11
 
12
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 `data/pipeline.md` and today's `data/applications/*.md` for the URL and for `company+role`. If already APPLIED, skip that job and do not dispatch.
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)
18
+
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.
14
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.
15
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.
16
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.
@@ -342,6 +348,27 @@ Is the visible value correct?
342
348
 
343
349
  **The `invalidCount` from schema is a heuristic, not ground truth.** Always prefer direct observation of field values over the invalid count. If Submit becomes enabled, ignore any remaining invalid fields — the portal accepted the data.
344
350
 
351
+ **Text-field specific fix — `imeFriendly: true`.** For text fills where the React-controlled input swallows programmatic value assignment (visible value correct, but `invalidCount` stays >0 and Submit is rejected with "flagged as possible spam" or "field required"), pass `imeFriendly: true` to `geometra_fill_fields`. This fires proper composition events (`compositionstart` / `input` / `compositionend`) that clear React's internal validity state. Confirmed fix on Ashby for Supabase (2026-04-19): first submit rejected despite clean fills; refill with `imeFriendly: true` succeeded on retry. Safe to use as default on all Ashby text fields — no cost if not needed.
352
+
353
+ ### Ashby Anti-Bot Spam Filter — Two Failure Classes
354
+
355
+ **Symptom:** after a form is filled cleanly (`invalidCount: 0`, all values correct) and Submit is clicked, Ashby returns: *"We couldn't submit your application. Your application submission was flagged as possible spam."*
356
+
357
+ These blocks come from two distinct root causes and require different responses:
358
+
359
+ | Class | Root cause | Recoverable in-session? | Fix |
360
+ |---|---|---|---|
361
+ | **A. React-validation lag** | programmatic text input didn't fire composition events; React marks required fields internally missing even though values look correct | Yes | Refill with `imeFriendly: true` and resubmit once. |
362
+ | **B. Environment fingerprint** | datacenter IP / VPN / headless Chromium signatures / browser-extension tells detected server-side | No (in headless) | Mark `Failed` with note "Ashby env-fingerprint"; recommend manual submit from user's own browser. |
363
+
364
+ **How to tell them apart:** if you saw `invalidCount > 0` and the "required field" error BEFORE submit, class A is likely — retry with `imeFriendly: true`. If the form filled perfectly clean (`invalidCount: 0` on every step) and the spam flag fires only on submit, class B is likely — Ashby's "Learn more" dialog cites VPN/proxy, ad blockers, shared/public network, which `imeFriendly` cannot influence.
365
+
366
+ **Evidence (2026-04-19 session):**
367
+ - Class A confirmed: Supabase #793 (rejected → refilled with `imeFriendly` → applied).
368
+ - Class B confirmed: Unstructured #786 + ClickUp #787 — both filled cleanly with per-field `imeFriendly: true`, both still spam-flagged on submit with identical "VPN / ad blockers / shared network" messaging.
369
+
370
+ **Rule — do NOT loop retrying a class B block.** One retry with `imeFriendly: true` is the correct test for class A. If the same spam message fires after a clean `imeFriendly` refill, stop, mark Failed, move on. Repeated retries waste subagent time and do not change the outcome.
371
+
345
372
  ### Nested Scroll Containers (Greenhouse / Ashby)
346
373
 
347
374
  The major ATS portals (Greenhouse, Workday, Lever, Ashby) use nested scrollable regions. A field's `visibleBounds` may show it as off-screen even when it is actually visible within a child scroll container. Geometra's `scroll_to` operates on the outermost page scroll, so it cannot reach fields in inner scroll regions.
@@ -475,7 +502,7 @@ To check or modify MCP settings, edit `opencode.json` in the project root.
475
502
  - Output in `output/` (gitignored), Reports in `reports/`
476
503
  - JDs in `jds/` (referenced as `local:jds/{file}` in pipeline.md)
477
504
  - Batch in `batch/` (gitignored except scripts and prompt)
478
- - Report numbering: sequential 3-digit zero-padded, max existing + 1
505
+ - Report numbering: sequential 3-digit zero-padded. **Always use `npx job-forge next-num` to get the next number** — do NOT derive it yourself from `ls reports/`. The CLI scans all sources: `reports/*.md`, the `#` column of every `data/applications/*.md` day file, and pending + merged `batch/tracker-additions/*.tsv`. Deriving from `reports/` alone misses numbers assigned by prior-day tracker additions that were never written as report files (e.g., `SKIP` entries), which causes ID collisions downstream.
479
506
  - **RULE: After each batch of evaluations, run `npx job-forge merge`** to merge tracker additions and avoid duplications.
480
507
  - **RULE: NEVER create new entries in applications.md if company+role already exists.** Update the existing entry.
481
508
  - **RULE: NEVER attribute commits to opencode (no `Co-Authored-By: opencode` or similar).** All commits must be attributed solely to the person making the commit (e.g., CharlieGreenman).
package/AGENTS.md CHANGED
@@ -5,7 +5,13 @@
5
5
  The Hard Limits below are non-negotiable numeric rules. If you catch yourself about to violate one, STOP and restructure.
6
6
 
7
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 `data/pipeline.md` and today's `data/applications/*.md` for the URL and for `company+role`. If already APPLIED, skip that job and do not dispatch.
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)
13
+
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.
9
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.
10
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.
11
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.
@@ -337,6 +343,27 @@ Is the visible value correct?
337
343
 
338
344
  **The `invalidCount` from schema is a heuristic, not ground truth.** Always prefer direct observation of field values over the invalid count. If Submit becomes enabled, ignore any remaining invalid fields — the portal accepted the data.
339
345
 
346
+ **Text-field specific fix — `imeFriendly: true`.** For text fills where the React-controlled input swallows programmatic value assignment (visible value correct, but `invalidCount` stays >0 and Submit is rejected with "flagged as possible spam" or "field required"), pass `imeFriendly: true` to `geometra_fill_fields`. This fires proper composition events (`compositionstart` / `input` / `compositionend`) that clear React's internal validity state. Confirmed fix on Ashby for Supabase (2026-04-19): first submit rejected despite clean fills; refill with `imeFriendly: true` succeeded on retry. Safe to use as default on all Ashby text fields — no cost if not needed.
347
+
348
+ ### Ashby Anti-Bot Spam Filter — Two Failure Classes
349
+
350
+ **Symptom:** after a form is filled cleanly (`invalidCount: 0`, all values correct) and Submit is clicked, Ashby returns: *"We couldn't submit your application. Your application submission was flagged as possible spam."*
351
+
352
+ These blocks come from two distinct root causes and require different responses:
353
+
354
+ | Class | Root cause | Recoverable in-session? | Fix |
355
+ |---|---|---|---|
356
+ | **A. React-validation lag** | programmatic text input didn't fire composition events; React marks required fields internally missing even though values look correct | Yes | Refill with `imeFriendly: true` and resubmit once. |
357
+ | **B. Environment fingerprint** | datacenter IP / VPN / headless Chromium signatures / browser-extension tells detected server-side | No (in headless) | Mark `Failed` with note "Ashby env-fingerprint"; recommend manual submit from user's own browser. |
358
+
359
+ **How to tell them apart:** if you saw `invalidCount > 0` and the "required field" error BEFORE submit, class A is likely — retry with `imeFriendly: true`. If the form filled perfectly clean (`invalidCount: 0` on every step) and the spam flag fires only on submit, class B is likely — Ashby's "Learn more" dialog cites VPN/proxy, ad blockers, shared/public network, which `imeFriendly` cannot influence.
360
+
361
+ **Evidence (2026-04-19 session):**
362
+ - Class A confirmed: Supabase #793 (rejected → refilled with `imeFriendly` → applied).
363
+ - Class B confirmed: Unstructured #786 + ClickUp #787 — both filled cleanly with per-field `imeFriendly: true`, both still spam-flagged on submit with identical "VPN / ad blockers / shared network" messaging.
364
+
365
+ **Rule — do NOT loop retrying a class B block.** One retry with `imeFriendly: true` is the correct test for class A. If the same spam message fires after a clean `imeFriendly` refill, stop, mark Failed, move on. Repeated retries waste subagent time and do not change the outcome.
366
+
340
367
  ### Nested Scroll Containers (Greenhouse / Ashby)
341
368
 
342
369
  The major ATS portals (Greenhouse, Workday, Lever, Ashby) use nested scrollable regions. A field's `visibleBounds` may show it as off-screen even when it is actually visible within a child scroll container. Geometra's `scroll_to` operates on the outermost page scroll, so it cannot reach fields in inner scroll regions.
@@ -470,7 +497,7 @@ To check or modify MCP settings, edit `opencode.json` in the project root.
470
497
  - Output in `output/` (gitignored), Reports in `reports/`
471
498
  - JDs in `jds/` (referenced as `local:jds/{file}` in pipeline.md)
472
499
  - Batch in `batch/` (gitignored except scripts and prompt)
473
- - Report numbering: sequential 3-digit zero-padded, max existing + 1
500
+ - Report numbering: sequential 3-digit zero-padded. **Always use `npx job-forge next-num` to get the next number** — do NOT derive it yourself from `ls reports/`. The CLI scans all sources: `reports/*.md`, the `#` column of every `data/applications/*.md` day file, and pending + merged `batch/tracker-additions/*.tsv`. Deriving from `reports/` alone misses numbers assigned by prior-day tracker additions that were never written as report files (e.g., `SKIP` entries), which causes ID collisions downstream.
474
501
  - **RULE: After each batch of evaluations, run `npx job-forge merge`** to merge tracker additions and avoid duplications.
475
502
  - **RULE: NEVER create new entries in applications.md if company+role already exists.** Update the existing entry.
476
503
  - **RULE: NEVER attribute commits to opencode (no `Co-Authored-By: opencode` or similar).** All commits must be attributed solely to the person making the commit (e.g., CharlieGreenman).
package/CLAUDE.md CHANGED
@@ -5,7 +5,13 @@
5
5
  The Hard Limits below are non-negotiable numeric rules. If you catch yourself about to violate one, STOP and restructure.
6
6
 
7
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 `data/pipeline.md` and today's `data/applications/*.md` for the URL and for `company+role`. If already APPLIED, skip that job and do not dispatch.
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)
13
+
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.
9
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.
10
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.
11
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.
@@ -337,6 +343,27 @@ Is the visible value correct?
337
343
 
338
344
  **The `invalidCount` from schema is a heuristic, not ground truth.** Always prefer direct observation of field values over the invalid count. If Submit becomes enabled, ignore any remaining invalid fields — the portal accepted the data.
339
345
 
346
+ **Text-field specific fix — `imeFriendly: true`.** For text fills where the React-controlled input swallows programmatic value assignment (visible value correct, but `invalidCount` stays >0 and Submit is rejected with "flagged as possible spam" or "field required"), pass `imeFriendly: true` to `geometra_fill_fields`. This fires proper composition events (`compositionstart` / `input` / `compositionend`) that clear React's internal validity state. Confirmed fix on Ashby for Supabase (2026-04-19): first submit rejected despite clean fills; refill with `imeFriendly: true` succeeded on retry. Safe to use as default on all Ashby text fields — no cost if not needed.
347
+
348
+ ### Ashby Anti-Bot Spam Filter — Two Failure Classes
349
+
350
+ **Symptom:** after a form is filled cleanly (`invalidCount: 0`, all values correct) and Submit is clicked, Ashby returns: *"We couldn't submit your application. Your application submission was flagged as possible spam."*
351
+
352
+ These blocks come from two distinct root causes and require different responses:
353
+
354
+ | Class | Root cause | Recoverable in-session? | Fix |
355
+ |---|---|---|---|
356
+ | **A. React-validation lag** | programmatic text input didn't fire composition events; React marks required fields internally missing even though values look correct | Yes | Refill with `imeFriendly: true` and resubmit once. |
357
+ | **B. Environment fingerprint** | datacenter IP / VPN / headless Chromium signatures / browser-extension tells detected server-side | No (in headless) | Mark `Failed` with note "Ashby env-fingerprint"; recommend manual submit from user's own browser. |
358
+
359
+ **How to tell them apart:** if you saw `invalidCount > 0` and the "required field" error BEFORE submit, class A is likely — retry with `imeFriendly: true`. If the form filled perfectly clean (`invalidCount: 0` on every step) and the spam flag fires only on submit, class B is likely — Ashby's "Learn more" dialog cites VPN/proxy, ad blockers, shared/public network, which `imeFriendly` cannot influence.
360
+
361
+ **Evidence (2026-04-19 session):**
362
+ - Class A confirmed: Supabase #793 (rejected → refilled with `imeFriendly` → applied).
363
+ - Class B confirmed: Unstructured #786 + ClickUp #787 — both filled cleanly with per-field `imeFriendly: true`, both still spam-flagged on submit with identical "VPN / ad blockers / shared network" messaging.
364
+
365
+ **Rule — do NOT loop retrying a class B block.** One retry with `imeFriendly: true` is the correct test for class A. If the same spam message fires after a clean `imeFriendly` refill, stop, mark Failed, move on. Repeated retries waste subagent time and do not change the outcome.
366
+
340
367
  ### Nested Scroll Containers (Greenhouse / Ashby)
341
368
 
342
369
  The major ATS portals (Greenhouse, Workday, Lever, Ashby) use nested scrollable regions. A field's `visibleBounds` may show it as off-screen even when it is actually visible within a child scroll container. Geometra's `scroll_to` operates on the outermost page scroll, so it cannot reach fields in inner scroll regions.
@@ -470,7 +497,7 @@ To check or modify MCP settings, edit `opencode.json` in the project root.
470
497
  - Output in `output/` (gitignored), Reports in `reports/`
471
498
  - JDs in `jds/` (referenced as `local:jds/{file}` in pipeline.md)
472
499
  - Batch in `batch/` (gitignored except scripts and prompt)
473
- - Report numbering: sequential 3-digit zero-padded, max existing + 1
500
+ - Report numbering: sequential 3-digit zero-padded. **Always use `npx job-forge next-num` to get the next number** — do NOT derive it yourself from `ls reports/`. The CLI scans all sources: `reports/*.md`, the `#` column of every `data/applications/*.md` day file, and pending + merged `batch/tracker-additions/*.tsv`. Deriving from `reports/` alone misses numbers assigned by prior-day tracker additions that were never written as report files (e.g., `SKIP` entries), which causes ID collisions downstream.
474
501
  - **RULE: After each batch of evaluations, run `npx job-forge merge`** to merge tracker additions and avoid duplications.
475
502
  - **RULE: NEVER create new entries in applications.md if company+role already exists.** Update the existing entry.
476
503
  - **RULE: NEVER attribute commits to opencode (no `Co-Authored-By: opencode` or similar).** All commits must be attributed solely to the person making the commit (e.g., CharlieGreenman).
@@ -5,7 +5,13 @@
5
5
  The Hard Limits below are non-negotiable numeric rules. If you catch yourself about to violate one, STOP and restructure.
6
6
 
7
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 `data/pipeline.md` and today's `data/applications/*.md` for the URL and for `company+role`. If already APPLIED, skip that job and do not dispatch.
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)
13
+
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.
9
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.
10
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.
11
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.
@@ -337,6 +343,27 @@ Is the visible value correct?
337
343
 
338
344
  **The `invalidCount` from schema is a heuristic, not ground truth.** Always prefer direct observation of field values over the invalid count. If Submit becomes enabled, ignore any remaining invalid fields — the portal accepted the data.
339
345
 
346
+ **Text-field specific fix — `imeFriendly: true`.** For text fills where the React-controlled input swallows programmatic value assignment (visible value correct, but `invalidCount` stays >0 and Submit is rejected with "flagged as possible spam" or "field required"), pass `imeFriendly: true` to `geometra_fill_fields`. This fires proper composition events (`compositionstart` / `input` / `compositionend`) that clear React's internal validity state. Confirmed fix on Ashby for Supabase (2026-04-19): first submit rejected despite clean fills; refill with `imeFriendly: true` succeeded on retry. Safe to use as default on all Ashby text fields — no cost if not needed.
347
+
348
+ ### Ashby Anti-Bot Spam Filter — Two Failure Classes
349
+
350
+ **Symptom:** after a form is filled cleanly (`invalidCount: 0`, all values correct) and Submit is clicked, Ashby returns: *"We couldn't submit your application. Your application submission was flagged as possible spam."*
351
+
352
+ These blocks come from two distinct root causes and require different responses:
353
+
354
+ | Class | Root cause | Recoverable in-session? | Fix |
355
+ |---|---|---|---|
356
+ | **A. React-validation lag** | programmatic text input didn't fire composition events; React marks required fields internally missing even though values look correct | Yes | Refill with `imeFriendly: true` and resubmit once. |
357
+ | **B. Environment fingerprint** | datacenter IP / VPN / headless Chromium signatures / browser-extension tells detected server-side | No (in headless) | Mark `Failed` with note "Ashby env-fingerprint"; recommend manual submit from user's own browser. |
358
+
359
+ **How to tell them apart:** if you saw `invalidCount > 0` and the "required field" error BEFORE submit, class A is likely — retry with `imeFriendly: true`. If the form filled perfectly clean (`invalidCount: 0` on every step) and the spam flag fires only on submit, class B is likely — Ashby's "Learn more" dialog cites VPN/proxy, ad blockers, shared/public network, which `imeFriendly` cannot influence.
360
+
361
+ **Evidence (2026-04-19 session):**
362
+ - Class A confirmed: Supabase #793 (rejected → refilled with `imeFriendly` → applied).
363
+ - Class B confirmed: Unstructured #786 + ClickUp #787 — both filled cleanly with per-field `imeFriendly: true`, both still spam-flagged on submit with identical "VPN / ad blockers / shared network" messaging.
364
+
365
+ **Rule — do NOT loop retrying a class B block.** One retry with `imeFriendly: true` is the correct test for class A. If the same spam message fires after a clean `imeFriendly` refill, stop, mark Failed, move on. Repeated retries waste subagent time and do not change the outcome.
366
+
340
367
  ### Nested Scroll Containers (Greenhouse / Ashby)
341
368
 
342
369
  The major ATS portals (Greenhouse, Workday, Lever, Ashby) use nested scrollable regions. A field's `visibleBounds` may show it as off-screen even when it is actually visible within a child scroll container. Geometra's `scroll_to` operates on the outermost page scroll, so it cannot reach fields in inner scroll regions.
@@ -470,7 +497,7 @@ To check or modify MCP settings, edit `opencode.json` in the project root.
470
497
  - Output in `output/` (gitignored), Reports in `reports/`
471
498
  - JDs in `jds/` (referenced as `local:jds/{file}` in pipeline.md)
472
499
  - Batch in `batch/` (gitignored except scripts and prompt)
473
- - Report numbering: sequential 3-digit zero-padded, max existing + 1
500
+ - Report numbering: sequential 3-digit zero-padded. **Always use `npx job-forge next-num` to get the next number** — do NOT derive it yourself from `ls reports/`. The CLI scans all sources: `reports/*.md`, the `#` column of every `data/applications/*.md` day file, and pending + merged `batch/tracker-additions/*.tsv`. Deriving from `reports/` alone misses numbers assigned by prior-day tracker additions that were never written as report files (e.g., `SKIP` entries), which causes ID collisions downstream.
474
501
  - **RULE: After each batch of evaluations, run `npx job-forge merge`** to merge tracker additions and avoid duplications.
475
502
  - **RULE: NEVER create new entries in applications.md if company+role already exists.** Update the existing entry.
476
503
  - **RULE: NEVER attribute commits to opencode (no `Co-Authored-By: opencode` or similar).** All commits must be attributed solely to the person making the commit (e.g., CharlieGreenman).
package/modes/apply.md CHANGED
@@ -191,7 +191,8 @@ geometra_run_actions({
191
191
  sessionId: "...",
192
192
  actions: [
193
193
  { type: "upload_files", fieldLabel: "Resume/CV", paths: ["/abs/path/cv.pdf"] },
194
- { type: "fill_fields", valuesByLabel: { "First Name": "...", "Last Name": "...", ... } },
194
+ { type: "fill_fields", imeFriendly: true,
195
+ valuesByLabel: { "First Name": "...", "Last Name": "...", ... } },
195
196
  { type: "pick_listbox_option", fieldLabel: "Country", value: "United States" },
196
197
  ... (one entry per choice/listbox) ...
197
198
  { type: "click", labelOrText: "Submit application" }
@@ -199,6 +200,8 @@ geometra_run_actions({
199
200
  })
200
201
  ```
201
202
 
203
+ **Always pass `imeFriendly: true` on `fill_fields` for Ashby** (and safe as a default everywhere). Ashby's React form swallows programmatic text input silently — visible value looks correct, `invalidCount` stays >0, and Submit fails with "field required" or "flagged as possible spam." `imeFriendly: true` fires proper composition events that clear React's internal validity state. Confirmed fix: Supabase #793 (2026-04-19). Zero cost on other portals; no reason to leave it off.
204
+
202
205
  ### Use `fieldLabel` over `fieldId` (Rule B)
203
206
 
204
207
  Labels are stable across DOM refreshes; IDs are not. If `fieldLabel` works, use it everywhere. Only fall back to `fieldId` when two fields share the same label (rare — add a qualifier via sibling text instead).
package/modes/pipeline.md CHANGED
@@ -6,7 +6,7 @@ Processes accumulated job offer URLs from `data/pipeline.md`. The user adds URLs
6
6
 
7
7
  1. **Read** `data/pipeline.md` → find `- [ ]` items in the "Pending" section
8
8
  2. **For each pending URL**:
9
- a. Calculate the next sequential `REPORT_NUM` (read `reports/`, take the highest number + 1)
9
+ a. 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)
10
10
  b. **Extract JD** using Geometra MCP (geometra_connect + geometra_page_model) → WebFetch → WebSearch
11
11
  c. If the URL is not accessible → mark as `- [!]` with a note and continue
12
12
  d. **Run full auto-pipeline**: A-F Evaluation → Report .md → PDF (if score >= 3.0, per `_shared.md` thresholds) → Draft answers (if score >= 3.5) → Tracker
@@ -45,9 +45,13 @@ Processes accumulated job offer URLs from `data/pipeline.md`. The user adds URLs
45
45
 
46
46
  ## Automatic Numbering
47
47
 
48
- 1. List all files in `reports/`
49
- 2. Extract the number from the prefix (e.g., `142-medispend...` → 142)
50
- 3. New number = highest found + 1
48
+ Run `npx job-forge next-num` returns the next 3-digit zero-padded report number. The CLI scans:
49
+
50
+ 1. `reports/*.md` filename prefixes
51
+ 2. The `#` column of every `data/applications/*.md` day file
52
+ 3. The `{num}` prefix of every `batch/tracker-additions/*.tsv` (pending + merged)
53
+
54
+ Takes the max across all three sources and adds 1. Do NOT derive from any single source — prior-day SKIPs and other non-report tracker entries advance the counter but never write to `reports/`, so `ls reports/` alone misses them.
51
55
 
52
56
  ## Source Synchronization
53
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-forge",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "AI-powered job search pipeline built on opencode",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,23 +2,36 @@
2
2
  /**
3
3
  * next-num — print the next sequential report number (3-digit zero-padded).
4
4
  *
5
- * Reads reports/ and returns max(existing) + 1. Used by agents instead of
6
- * having the model figure this out by listing + parsing filenames.
5
+ * Scans three sources to find the max and returns max + 1:
6
+ * 1. reports/*.md — filename prefix `{num}-`
7
+ * 2. data/applications/*.md — `#` column of each table row
8
+ * 3. batch/tracker-additions/*.tsv — first tab-separated column (pending)
9
+ * batch/tracker-additions/merged/ — same, already consumed
10
+ *
11
+ * Why all three? Same-day batches can advance the counter without writing a
12
+ * report (e.g., SKIP entries skip PDF + report). Deriving from reports/ alone
13
+ * causes ID collisions when a later subagent picks a number already used in
14
+ * a tracker row or TSV. Scanning all three sources is O(N) on a small
15
+ * directory and eliminates the collision class.
7
16
  *
8
17
  * Usage:
9
18
  * job-forge next-num # prints e.g. "521"
10
- * job-forge next-num --padded # prints e.g. "521" (default, already padded)
11
19
  * job-forge next-num --raw # prints e.g. "521" without padding
12
20
  */
13
21
 
14
- import { readdirSync, existsSync } from 'fs';
22
+ import { readdirSync, readFileSync, existsSync, statSync } from 'fs';
15
23
  import { join } from 'path';
16
24
 
17
25
  const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
18
26
  const REPORTS_DIR = join(PROJECT_DIR, 'reports');
27
+ const APPS_DIR = join(PROJECT_DIR, 'data', 'applications');
28
+ const TSV_DIR = join(PROJECT_DIR, 'batch', 'tracker-additions');
29
+ const TSV_MERGED_DIR = join(TSV_DIR, 'merged');
19
30
  const RAW = process.argv.includes('--raw');
20
31
 
21
32
  let max = 0;
33
+
34
+ // 1. reports/*.md
22
35
  if (existsSync(REPORTS_DIR)) {
23
36
  for (const f of readdirSync(REPORTS_DIR)) {
24
37
  if (!f.endsWith('.md')) continue;
@@ -29,5 +42,47 @@ if (existsSync(REPORTS_DIR)) {
29
42
  }
30
43
  }
31
44
 
45
+ // 2. data/applications/*.md — first `|` column of each table row
46
+ if (existsSync(APPS_DIR)) {
47
+ for (const f of readdirSync(APPS_DIR)) {
48
+ if (!f.endsWith('.md')) continue;
49
+ const full = join(APPS_DIR, f);
50
+ if (!statSync(full).isFile()) continue;
51
+ const content = readFileSync(full, 'utf-8');
52
+ for (const line of content.split('\n')) {
53
+ // Match: "| 756 | 2026-04-18 | ..." — integer in first cell
54
+ const m = line.match(/^\|\s*(\d+)\s*\|/);
55
+ if (!m) continue;
56
+ const n = parseInt(m[1], 10);
57
+ if (n > max) max = n;
58
+ }
59
+ }
60
+ }
61
+
62
+ // 3. batch/tracker-additions/*.tsv (pending) + merged/*.tsv
63
+ for (const dir of [TSV_DIR, TSV_MERGED_DIR]) {
64
+ if (!existsSync(dir)) continue;
65
+ for (const f of readdirSync(dir)) {
66
+ if (!f.endsWith('.tsv')) continue;
67
+ const full = join(dir, f);
68
+ if (!statSync(full).isFile()) continue;
69
+ // Prefer the filename prefix (always present and canonical) over TSV
70
+ // contents — avoids reading the file for the common case.
71
+ const mName = f.match(/^(\d+)-/);
72
+ if (mName) {
73
+ const n = parseInt(mName[1], 10);
74
+ if (n > max) max = n;
75
+ continue;
76
+ }
77
+ // Fallback: parse first column of first non-empty line
78
+ const content = readFileSync(full, 'utf-8');
79
+ const firstLine = content.split('\n').find(l => l.trim().length > 0);
80
+ if (!firstLine) continue;
81
+ const cell = firstLine.split('\t')[0];
82
+ const n = parseInt(cell, 10);
83
+ if (!Number.isNaN(n) && n > max) max = n;
84
+ }
85
+ }
86
+
32
87
  const next = max + 1;
33
88
  console.log(RAW ? String(next) : String(next).padStart(3, '0'));