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.
- package/.cursor/rules/main.mdc +29 -2
- package/AGENTS.md +29 -2
- package/CLAUDE.md +29 -2
- package/iso/instructions.md +29 -2
- package/modes/apply.md +4 -1
- package/modes/pipeline.md +8 -4
- package/package.json +1 -1
- package/scripts/next-num.mjs +59 -4
package/.cursor/rules/main.mdc
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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/iso/instructions.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
|
|
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,
|
|
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",
|
|
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` (
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
package/scripts/next-num.mjs
CHANGED
|
@@ -2,23 +2,36 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* next-num — print the next sequential report number (3-digit zero-padded).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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'));
|