job-forge 2.14.41 → 2.14.43

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.
@@ -33,8 +33,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
33
33
  - [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, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`, and lineage records returned by `npx job-forge lineage:explain ...`.
34
34
  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
35
35
 
36
- - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `stealth: true` to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
37
- why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`, which belongs with JobForge portal sessions instead of stock Playwright Chromium
36
+ - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true` and `stealth: true` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
37
+ why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP opens visible Chromium unless `headless: true` is explicit, and Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`; both flags belong with JobForge portal sessions instead of stock visible Playwright Chromium
38
38
 
39
39
  ## Defaults
40
40
 
@@ -67,7 +67,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
67
67
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
68
68
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
69
69
  3. Read the active mode file [D3]. Use local helpers when they can replace broad file reads, prose math, manual policy checks, or artifact reuse decisions [D8]. Decide inline vs delegated work [D1].
70
- 4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/stealth prompt hygiene [H8].
70
+ 4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8].
71
71
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D8].
72
72
  6. Keep multi-job form-filling out of the orchestrator [H4].
73
73
  7. Cross-check subagent facts against authoritative files [H7].
@@ -231,7 +231,8 @@ Step 5 — Loop in rounds of 2 (Hard Limit #1)
231
231
  pair = candidates[round*2 : round*2 + 2]
232
232
  # If proxy is configured, do not paste proxy values into prompts.
233
233
  # Say: "Proxy is configured; read config/profile.yml and pass its
234
- # top-level proxy object plus stealth: true to every geometra_connect call."
234
+ # top-level proxy object plus headless: true and stealth: true to every
235
+ # Geometra connect or auto-connect call."
235
236
  # Dispatch 1 or 2 task() calls in ONE message (never 3+)
236
237
  task(subagent_type=<tier per AGENTS.md routing>, prompt=<apply prompt for pair[0]>)
237
238
  task(subagent_type=<tier>, prompt=<apply prompt for pair[1]>) # only if pair has 2
package/AGENTS.md CHANGED
@@ -28,8 +28,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
28
28
  - [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, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`, and lineage records returned by `npx job-forge lineage:explain ...`.
29
29
  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
30
30
 
31
- - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `stealth: true` to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
32
- why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`, which belongs with JobForge portal sessions instead of stock Playwright Chromium
31
+ - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true` and `stealth: true` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
32
+ why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP opens visible Chromium unless `headless: true` is explicit, and Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`; both flags belong with JobForge portal sessions instead of stock visible Playwright Chromium
33
33
 
34
34
  ## Defaults
35
35
 
@@ -62,7 +62,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
62
62
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
63
63
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
64
64
  3. Read the active mode file [D3]. Use local helpers when they can replace broad file reads, prose math, manual policy checks, or artifact reuse decisions [D8]. Decide inline vs delegated work [D1].
65
- 4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/stealth prompt hygiene [H8].
65
+ 4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8].
66
66
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D8].
67
67
  6. Keep multi-job form-filling out of the orchestrator [H4].
68
68
  7. Cross-check subagent facts against authoritative files [H7].
package/CLAUDE.md CHANGED
@@ -28,8 +28,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
28
28
  - [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, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`, and lineage records returned by `npx job-forge lineage:explain ...`.
29
29
  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
30
30
 
31
- - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `stealth: true` to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
32
- why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`, which belongs with JobForge portal sessions instead of stock Playwright Chromium
31
+ - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true` and `stealth: true` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
32
+ why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP opens visible Chromium unless `headless: true` is explicit, and Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`; both flags belong with JobForge portal sessions instead of stock visible Playwright Chromium
33
33
 
34
34
  ## Defaults
35
35
 
@@ -62,7 +62,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
62
62
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
63
63
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
64
64
  3. Read the active mode file [D3]. Use local helpers when they can replace broad file reads, prose math, manual policy checks, or artifact reuse decisions [D8]. Decide inline vs delegated work [D1].
65
- 4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/stealth prompt hygiene [H8].
65
+ 4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8].
66
66
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D8].
67
67
  6. Keep multi-job form-filling out of the orchestrator [H4].
68
68
  7. Cross-check subagent facts against authoritative files [H7].
@@ -110,6 +110,9 @@ const consumerPkg = {
110
110
  tokens: 'job-forge tokens',
111
111
  'tokens:today': 'job-forge tokens --days 1',
112
112
  'tokens:log': 'job-forge tokens --days 1 --append',
113
+ 'portal:snapshot': 'job-forge portal:snapshot',
114
+ 'portal:form-schema': 'job-forge portal:form-schema',
115
+ 'portal:explain': 'job-forge portal:explain',
113
116
  'trace:list': 'job-forge trace:list',
114
117
  'trace:stats': 'job-forge trace:stats',
115
118
  'trace:show': 'job-forge trace:show',
@@ -5,7 +5,7 @@ import { existsSync, readFileSync } from 'node:fs';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
 
8
- const DEFAULT_FALLBACK_PACKAGE = '@geometra/mcp@1.61.3';
8
+ const DEFAULT_FALLBACK_PACKAGE = '@geometra/mcp@1.62.1';
9
9
  const RESOLVE_ONLY_FLAG = '--job-forge-resolve-target';
10
10
 
11
11
  function normalizeEnv(value) {
package/bin/job-forge.mjs CHANGED
@@ -17,6 +17,7 @@
17
17
  * pdf Run generate-pdf.mjs
18
18
  * sync-check Run cv-sync-check.mjs
19
19
  * mcp:geometra Launch Geometra MCP via JobForge's local/npm resolver
20
+ * portal:* One-shot direct-Geometra browser snapshots/form schemas
20
21
  * tokens Run scripts/token-usage-report.mjs
21
22
  * trace:* Inspect local agent transcripts via iso-trace
22
23
  * telemetry:* Summarize JobForge pipeline status from traces + tracker files
@@ -91,6 +92,12 @@ const guardAliases = {
91
92
  'guard:explain': 'explain',
92
93
  };
93
94
 
95
+ const portalAliases = {
96
+ 'portal:snapshot': 'snapshot',
97
+ 'portal:form-schema': 'form-schema',
98
+ 'portal:explain': 'explain',
99
+ };
100
+
94
101
  const ledgerAliases = {
95
102
  'ledger:status': 'status',
96
103
  'ledger:rebuild': 'rebuild',
@@ -247,6 +254,9 @@ Commands:
247
254
  pdf Generate ATS-optimized CV PDF from cv.md
248
255
  sync-check Lint: verify cv.md and profile.yml are filled in
249
256
  mcp:geometra Launch Geometra MCP, preferring local JobForge/Geometra dev wiring
257
+ portal:snapshot Render a URL with direct Geometra and print page model/snapshot
258
+ portal:form-schema Render a URL with direct Geometra and print form schema
259
+ portal:explain Show direct Geometra module/defaults
250
260
  tokens Show opencode token usage and cost by session/day
251
261
  trace Pass through to iso-trace (e.g. job-forge trace sources)
252
262
  trace:list List recent local agent sessions (defaults: --since 7d --cwd project)
@@ -354,6 +364,8 @@ Pass --help after a command to see its own flags, e.g.:
354
364
  job-forge telemetry:show ses_...
355
365
  job-forge guard:audit
356
366
  job-forge guard:explain
367
+ job-forge portal:snapshot --url https://example.test/jobs/123 --json
368
+ job-forge portal:form-schema --url https://example.test/apply --json
357
369
  job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied
358
370
  job-forge capabilities:explain general-free
359
371
  job-forge capabilities:check general-free --tool browser --mcp geometra --command "npx job-forge merge" --filesystem write
@@ -438,6 +450,21 @@ if (cmd === 'guard' || guardAliases[cmd]) {
438
450
  process.exit(result.status ?? 1);
439
451
  }
440
452
 
453
+ if (cmd === 'portal' || portalAliases[cmd]) {
454
+ const portalArgs = cmd === 'portal'
455
+ ? (rest.length === 0 ? ['help'] : rest)
456
+ : [portalAliases[cmd], ...rest];
457
+
458
+ const scriptPath = join(PKG_ROOT, 'scripts/portal.mjs');
459
+ const result = spawnSync(process.execPath, [scriptPath, ...portalArgs], {
460
+ stdio: 'inherit',
461
+ cwd: PROJECT_DIR,
462
+ env: process.env,
463
+ });
464
+
465
+ process.exit(result.status ?? 1);
466
+ }
467
+
441
468
  if (cmd === 'ledger' || ledgerAliases[cmd]) {
442
469
  const ledgerArgs = cmd === 'ledger'
443
470
  ? (rest.length === 0 ? ['help'] : rest)
@@ -92,17 +92,18 @@ location_constraints:
92
92
  # mobile / SOCKS proxy you already pay for. Bypasses the datacenter-IP
93
93
  # fingerprinting that drives ~80-90% of Ashby / Lever / Cloudflare-fronted
94
94
  # "flagged as possible spam" submit failures in headless mode. JobForge passes
95
- # `stealth: true` by default so Geometra MCP >= 1.61.3 launches CloakBrowser's
96
- # patched Chromium for portal sessions.
95
+ # `headless: true` and `stealth: true` by default so Geometra MCP >= 1.61.3
96
+ # launches CloakBrowser's patched Chromium for portal sessions without opening
97
+ # visible browser windows.
97
98
  #
98
99
  # BYO — JobForge does NOT bundle or resell proxy bandwidth. Pick a residential
99
100
  # or mobile provider (Bright Data, Oxylabs, SOAX, Smartproxy, etc.), or a
100
101
  # mobile hotspot, or your own SOCKS relay. Required: Geometra MCP >= 1.61.3.
101
102
  #
102
103
  # When present, the apply / scan / auto-pipeline modes thread this into every
103
- # `geometra_connect` call as `proxy: {...}` alongside `stealth: true`. Pool is
104
- # partitioned by proxy identity and stealth mode so direct and proxied sessions
105
- # never share a Chromium.
104
+ # `geometra_connect` call as `proxy: {...}` alongside `headless: true` and
105
+ # `stealth: true`. Pool is partitioned by proxy identity and stealth mode so
106
+ # direct and proxied sessions never share a Chromium.
106
107
  #
107
108
  # proxy:
108
109
  # server: "http://residential.example.com:8080" # http://, https://, or socks5://
@@ -115,7 +115,7 @@ For customization (archetypes, weights, tone), start with `_shared.md` and [CUST
115
115
  ## Evaluation Flow (Single Offer)
116
116
 
117
117
  1. **Input**: User pastes JD text or URL
118
- 2. **Extract**: Geometra MCP/WebFetch extracts JD from URL
118
+ 2. **Extract**: `job-forge portal:*`, Geometra MCP, or WebFetch extracts JD/form context from URL
119
119
  3. **Classify**: Detect archetype (one row from the archetype table in `modes/_shared.md`)
120
120
  4. **Evaluate**: 6 blocks (A-F).
121
121
  - A: Role summary.
@@ -251,6 +251,7 @@ Scripts maintain data consistency. In a consumer project they're invoked via the
251
251
  | `scripts/trace.mjs` | `npx job-forge trace:list` / `trace:stats` / `trace:show` | Local transcript observability via `@agent-pattern-labs/iso-trace`; common commands default to project-local sessions across supported harnesses |
252
252
  | `scripts/telemetry.mjs` | `npx job-forge telemetry:status` / `telemetry:show` | JobForge operational telemetry derived from normalized local traces plus tracker TSV state |
253
253
  | `scripts/guard.mjs` | `npx job-forge guard:audit` / `guard:explain` | Deterministic `@agent-pattern-labs/iso-guard` policy audits over local normalized traces (with OpenCode `task` rules still available where relevant) |
254
+ | `scripts/portal.mjs` | `npx job-forge portal:snapshot` / `portal:form-schema` | Deterministic direct-Geometra one-shot browser snapshots and form schemas with JobForge browser defaults enforced in code |
254
255
  | `scripts/ledger.mjs` | `npx job-forge ledger:status` / `ledger:has` / `ledger:rebuild` | Deterministic `@agent-pattern-labs/iso-ledger` state over tracker, TSV, and pipeline files |
255
256
  | `scripts/capabilities.mjs` | `npx job-forge capabilities:check` / `capabilities:explain` | Deterministic `@agent-pattern-labs/iso-capabilities` role boundary checks for tools, MCPs, commands, filesystem, and network access |
256
257
  | `scripts/cache.mjs` | `npx job-forge cache:has` / `cache:get` / `cache:put` | Deterministic `@agent-pattern-labs/iso-cache` JD and artifact reuse keyed by stable job/url inputs |
package/docs/SETUP.md CHANGED
@@ -214,7 +214,7 @@ Use it to identify which sessions or models are consuming the most tokens. The `
214
214
  `sync-check` requires `cv.md` and `config/profile.yml` with the fields checked in `cv-sync-check.mjs`. Until you finish the profile and CV steps, that is normal.
215
215
 
216
216
  **PDF generation fails**
217
- The scaffolded `opencode.json` already registers Geometra MCP; if it's not running, check `opencode mcp list` and verify the scaffolded config under the `mcp.geometra` key — its `command` MUST be `["npx", "--no-install", "job-forge", "mcp:geometra"]`, `enabled: true`, and its `environment` should include `GEOMETRA_STEALTH=1` (or equivalently `GEOMETRA_BROWSER=stealth`) so proxy-backed portal sessions default to CloakBrowser's patched Chromium. `job-forge mcp:geometra` resolves Geometra in this order: `JOB_FORGE_GEOMETRA_MCP_PATH`, then a consumer-project override from `package.json -> jobForge.geometraMcpPath`, then `opencode.json -> mcp.geometra.environment.JOB_FORGE_GEOMETRA_MCP_PATH`, then a sibling `../geometra/mcp/dist/index.js` checkout for local JobForge development, and finally the pinned npm package. Geometra manages Chromium via its built-in proxy. JobForge still passes `stealth: true` for portal sessions explicitly; the env block keeps the default aligned for auto-spawned sessions and local debugging. For standalone CLI usage (outside opencode), `generate-pdf.mjs` also works with standalone Playwright/Chromium — install with `npx playwright install chromium`.
217
+ The scaffolded `opencode.json` already registers Geometra MCP; if it's not running, check `opencode mcp list` and verify the scaffolded config under the `mcp.geometra` key — its `command` MUST be `["npx", "--no-install", "job-forge", "mcp:geometra"]`, `enabled: true`, and its `environment` should include `GEOMETRA_STEALTH=1` (or equivalently `GEOMETRA_BROWSER=stealth`) so proxy-backed portal sessions default to CloakBrowser's patched Chromium. `job-forge mcp:geometra` resolves Geometra in this order: `JOB_FORGE_GEOMETRA_MCP_PATH`, then a consumer-project override from `package.json -> jobForge.geometraMcpPath`, then `opencode.json -> mcp.geometra.environment.JOB_FORGE_GEOMETRA_MCP_PATH`, then a sibling `../geometra/mcp/dist/index.js` checkout for local JobForge development, and finally the pinned npm package. Geometra manages Chromium via its built-in proxy. JobForge still passes `headless: true` and `stealth: true` for portal sessions explicitly; the env block keeps the default aligned for auto-spawned sessions and local debugging. For standalone CLI usage (outside opencode), `generate-pdf.mjs` also works with standalone Playwright/Chromium — install with `npx playwright install chromium`.
218
218
 
219
219
  For consumer projects that should always use a local Geometra checkout across Opencode, Codex, Cursor, and Claude, prefer a local `package.json` override instead of editing symlinked MCP configs:
220
220
 
@@ -234,7 +234,8 @@ Step 5 — Loop in rounds of 2 (Hard Limit #1)
234
234
  pair = candidates[round*2 : round*2 + 2]
235
235
  # If proxy is configured, do not paste proxy values into prompts.
236
236
  # Say: "Proxy is configured; read config/profile.yml and pass its
237
- # top-level proxy object plus stealth: true to every geometra_connect call."
237
+ # top-level proxy object plus headless: true and stealth: true to every
238
+ # Geometra connect or auto-connect call."
238
239
  # Dispatch 1 or 2 task() calls in ONE message (never 3+)
239
240
  task(subagent_type=<tier per AGENTS.md routing>, prompt=<apply prompt for pair[0]>)
240
241
  task(subagent_type=<tier>, prompt=<apply prompt for pair[1]>) # only if pair has 2
@@ -28,8 +28,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
28
28
  - [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, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`, and lineage records returned by `npx job-forge lineage:explain ...`.
29
29
  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
30
30
 
31
- - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `stealth: true` to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
32
- why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`, which belongs with JobForge portal sessions instead of stock Playwright Chromium
31
+ - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true` and `stealth: true` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
32
+ why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP opens visible Chromium unless `headless: true` is explicit, and Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`; both flags belong with JobForge portal sessions instead of stock visible Playwright Chromium
33
33
 
34
34
  ## Defaults
35
35
 
@@ -62,7 +62,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
62
62
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
63
63
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
64
64
  3. Read the active mode file [D3]. Use local helpers when they can replace broad file reads, prose math, manual policy checks, or artifact reuse decisions [D8]. Decide inline vs delegated work [D1].
65
- 4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/stealth prompt hygiene [H8].
65
+ 4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8].
66
66
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D8].
67
67
  6. Keep multi-job form-filling out of the orchestrator [H4].
68
68
  7. Cross-check subagent facts against authoritative files [H7].
package/modes/apply.md CHANGED
@@ -42,8 +42,8 @@ Live application assistant. Reads the active application form in Chrome (via Geo
42
42
  - [D6] Use `fieldLabel` over `fieldId` everywhere it works.
43
43
  why: labels are stable across DOM refreshes; IDs are regenerated
44
44
 
45
- - [D7] If the orchestrator says a proxy is configured, read the top-level `proxy:` block from `config/profile.yml` and pass that object plus `stealth: true` into every `geometra_connect` call — including Call 3 of the recovery sequence. If the task prompt includes a legacy inline `proxy` object, pass it through and still set `stealth: true`, but do not echo credentials in status text. If absent, run with `stealth: true` and no proxy; never invent a proxy URL.
46
- why: class-B Ashby / Cloudflare-fronted portals need a residential outbound IP plus a stealth Chromium fingerprint. Geometra MCP v1.59.0 added proxy plumbing, and v1.61.3 added CloakBrowser stealth Chromium via `stealth: true`; the orchestrator owns the config pipe. See "BYO Residential Proxy" in modes/reference-portals.md.
45
+ - [D7] If the orchestrator says a proxy is configured, read the top-level `proxy:` block from `config/profile.yml` and pass that object plus `headless: true` and `stealth: true` into every `geometra_connect` call — including Call 3 of the recovery sequence — and every Geometra auto-connect call that passes `pageUrl` or `url`. If the task prompt includes a legacy inline `proxy` object, pass it through and still set `headless: true` and `stealth: true`, but do not echo credentials in status text. If absent, run with `headless: true`, `stealth: true`, and no proxy; never invent a proxy URL.
46
+ why: class-B Ashby / Cloudflare-fronted portals need a residential outbound IP plus a stealth Chromium fingerprint, and Geometra opens visible Chromium unless `headless: true` is explicit. Geometra MCP v1.59.0 added proxy plumbing, and v1.61.3 added CloakBrowser stealth Chromium via `stealth: true`; the orchestrator owns the config pipe. See "BYO Residential Proxy" in modes/reference-portals.md.
47
47
 
48
48
  - [D8] Upgrade application routing to `@general-paid` when the offer score is ≥ 4.0/5, the user flags "top-tier", "dream job", or "high-stakes", or the candidate is late-stage/post-screen.
49
49
  why: high-stakes applications need the quality-sensitive prompt and medium reasoning budget even though OpenCode now routes both application tiers through DeepSeek V4 Flash by default
@@ -53,23 +53,24 @@ Live application assistant. Reads the active application form in Chrome (via Geo
53
53
 
54
54
  ## Procedure
55
55
 
56
- 1. `geometra_connect` with `stealth: true` + `geometra_page_model`; thread `proxy` if present [D7]; no WebFetch [D5].
57
- 2. If Geometra is unavailable, ask for screenshot or pasted text [D2].
58
- 3. Extract company + role; Grep `reports/` for a matching evaluation.
59
- 4. Load full report + Section G if present.
60
- 5. Compare role on screen vs evaluated role [D3].
61
- 6. If different, pause for the candidate's decision [D3].
62
- 7. Before dispatch, run Geometra cleanup [H4] and location filter [D1].
63
- 8. Route high-stakes applications through `@general-paid` [D8].
64
- 9. Extract form questions; classify each Section-G vs new.
65
- 10. Generate answers from Block B + Block F + Section G + JD.
66
- 11. Submit as ONE `run_actions` call [H1] using labels [D6] with `imeFriendly: true` [D4].
67
- 12. On session error, run the 4-step recovery; only one retry [H2].
68
- 13. On provider failure, stop and inspect telemetry before any retry [D9].
69
- 14. On OTP prompt, fetch the code from Gmail via `gmail_get_message`.
70
- 15. Submit the OTP with `geometra_fill_otp` and click Submit.
71
- 16. Write outcome as `batch/tracker-additions/*.tsv` [H3].
72
- 17. Cap parallelism at 2 per round [H5]; one in-flight per company.
56
+ 1. `geometra_connect`: `headless: true`, `stealth: true`, `isolated: true` [D7].
57
+ 2. Run `geometra_page_model`; do not WebFetch the URL [D5].
58
+ 3. If Geometra is unavailable, ask for screenshot or pasted text [D2].
59
+ 4. Extract company + role; Grep `reports/` for a matching evaluation.
60
+ 5. Load full report + Section G if present.
61
+ 6. Compare role on screen vs evaluated role [D3].
62
+ 7. If different, pause for the candidate's decision [D3].
63
+ 8. Before dispatch, run Geometra cleanup [H4] and location filter [D1].
64
+ 9. Route high-stakes applications through `@general-paid` [D8].
65
+ 10. Extract form questions; classify each Section-G vs new.
66
+ 11. Generate answers from Block B + Block F + Section G + JD.
67
+ 12. Submit as ONE `run_actions` call [H1] using labels [D6] with `imeFriendly: true` [D4].
68
+ 13. On session error, run the 4-step recovery; only one retry [H2].
69
+ 14. On provider failure, stop and inspect telemetry before any retry [D9].
70
+ 15. On OTP prompt, fetch the code from Gmail via `gmail_get_message`.
71
+ 16. Submit the OTP with `geometra_fill_otp` and click Submit.
72
+ 17. Write outcome as `batch/tracker-additions/*.tsv` [H3].
73
+ 18. Cap parallelism at 2 per round [H5]; one in-flight per company.
73
74
 
74
75
  ## Routing
75
76
 
@@ -211,7 +212,7 @@ If a subagent fails, report it in the summary and let the user decide whether to
211
212
 
212
213
  ## Verify these requirements
213
214
 
214
- - **Best with Geometra MCP**: In visible proxy mode, the candidate sees the browser and opencode can interact with the page via `geometra_connect`, `geometra_form_schema`, and `geometra_fill_form`.
215
+ - **Best with Geometra MCP**: In headless proxy mode, opencode can interact with the page via `geometra_connect`, `geometra_form_schema`, and `geometra_fill_form` without opening a visible browser window.
215
216
  - **Without Geometra**: the candidate shares a screenshot or pastes the questions manually.
216
217
 
217
218
  ## Run this workflow
@@ -9,7 +9,7 @@ Fetch the JD content once. If the input is a **URL** (not pasted JD text), fetch
9
9
  **Pick exactly one method, in this priority order:**
10
10
 
11
11
  1. **Greenhouse JSON API (first try, if the URL is Greenhouse-backed):** If the pipeline.md entry carries `| gh={slug}/{id}` OR the URL host matches `*.greenhouse.io` / a known Greenhouse customer front-end (`*.pinterestcareers.com`, `okta.com/company/careers/opportunity/*`, `samsara.com/company/careers/roles/*`, `zoominfo.com/careers?gh_jid=*`, `collibra.com/.../?gh_jid=*`, `careers.toasttab.com/jobs?gh_jid=*`, `careers.airbnb.com/positions/*?gh_jid=*`, `coinbase.com/careers/positions/*?gh_jid=*`, `instacart.careers/job/?gh_jid=*`), extract `slug` and `id` and WebFetch `https://boards-api.greenhouse.io/v1/boards/{slug}/jobs/{id}`. 200 + JSON with `content` is the authoritative JD. 404 = genuinely closed (mark CLOSED and stop). **OpenCode WebFetch compatibility:** do not pass `format: "json"`; omit `format` or use `format: "text"` and parse the returned JSON text. **If 200, STOP — do not fall back to Geometra or WebFetch of the front-end.** The API is faster, cheaper (no Geometra session), and never returns a bot-shell.
12
- 2. **Geometra MCP:** Most non-Greenhouse job portals (Lever, Ashby, Workday) are SPAs. Use `geometra_connect({ ..., stealth: true })` + `geometra_page_model` to render and read the JD. **If this returns non-empty JD text, STOP — do not WebFetch the same URL.**
12
+ 2. **Direct Geometra helper:** Most non-Greenhouse job portals (Lever, Ashby, Workday) are SPAs. Use `npx job-forge portal:snapshot --url "{url}" --json` to render and read the page model/snapshot. This helper enforces `headless: true`, `stealth: true`, and `isolated: true` in code, reads `config/profile.yml` proxy config, and closes Chromium before exit. **If this returns non-empty JD text, STOP — do not WebFetch the same URL.**
13
13
  3. **WebFetch (only if Geometra is unavailable OR returned only a shell with no JD text):** For static pages (ZipRecruiter, WeLoveProduct, company career pages).
14
14
  4. **WebSearch (only if methods 1–3 all failed):** Search for the role title + company on secondary portals that index the JD in static HTML.
15
15
 
@@ -38,7 +38,7 @@ Execute the full `pdf` pipeline (read `modes/pdf.md`).
38
38
 
39
39
  Generate draft answers for the application form when the final score is >= 3.5. If the final score is >= 3.5 (per Canonical Scoring Model thresholds in `_shared.md`), generate draft answers for the application form:
40
40
 
41
- 1. **Extract form questions**: Use Geometra MCP (`geometra_connect({ ..., stealth: true })` + `geometra_form_schema`) to discover all form fields. **Reuse the same `sessionId` from Step 0** when the apply URL is the same rendered page; only connect again if the prior session ended or the URL changed. If questions cannot be extracted, use the generic questions.
41
+ 1. **Extract form questions**: Prefer `npx job-forge portal:form-schema --url "{apply_url}" --json` to discover all form fields without leaving a browser open. Use Geometra MCP only when a live multi-step browser session already exists and must be reused. If questions cannot be extracted, use the generic questions.
42
42
  2. **Generate answers** following the tone guidelines (see below).
43
43
  3. **Save in the report** as a `## G) Draft Application Answers` section.
44
44
 
package/modes/pipeline.md CHANGED
@@ -7,7 +7,7 @@ Processes accumulated job offer URLs from `data/pipeline.md`. The user adds URLs
7
7
  1. **Read** `data/pipeline.md` → find `- [ ]` items in the "Pending" section
8
8
  2. **For each pending URL**:
9
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
- b. **Extract JD** using Geometra MCP (`geometra_connect({ ..., stealth: true })` + geometra_page_model) → WebFetch → WebSearch
10
+ b. **Extract JD** using Greenhouse API `npx job-forge portal:snapshot --url "$URL" --json` Geometra MCP only if an interactive session is needed → 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
13
13
  e. **Move from "Pending" to "Processed"**: `- [x] #NNN | URL | Company | Role | Score/5 | PDF ✅/❌`
@@ -34,9 +34,10 @@ Processes accumulated job offer URLs from `data/pipeline.md`. The user adds URLs
34
34
  ## Detect JD From URL
35
35
 
36
36
  1. **Greenhouse JSON API (FIRST, when the entry has `| gh={slug}/{id}` OR the host looks Greenhouse-backed):** WebFetch `https://boards-api.greenhouse.io/v1/boards/{slug}/jobs/{id}`. 200 + JSON with `content` = LIVE, use it as the JD; 404 = genuinely CLOSED (mark `- [!]` and continue). **OpenCode WebFetch compatibility:** do not pass `format: "json"`; omit `format` or use `format: "text"` and parse the returned JSON text. Bot-hostile customer fronts (`pinterestcareers.com`, `okta.com`, `samsara.com`, `zoominfo.com`, `collibra.com`, `careers.toasttab.com`, `careers.airbnb.com`, `coinbase.com`, `instacart.careers`, `careers.toasttab.com`) MUST be verified via this API first — WebFetch/Geometra of those domains returns a shell or 403 and causes false CLOSED marks.
37
- 2. **Geometra MCP:** `geometra_connect({ ..., stealth: true })` + `geometra_page_model`. Works with non-Greenhouse SPAs (Lever, Ashby, Workday), uses fewer tokens than raw DOM snapshots.
38
- 3. **WebFetch (fallback):** For static pages or when Geometra is not available.
39
- 4. **WebSearch (last resort):** Search on secondary portals that index the JD.
37
+ 2. **Direct Geometra helper:** `npx job-forge portal:snapshot --url "{url}" --json`. Works with non-Greenhouse SPAs (Lever, Ashby, Workday), enforces `headless: true`, `stealth: true`, and `isolated: true` in code, reads `config/profile.yml` proxy config, and closes Chromium before exit.
38
+ 3. **Geometra MCP (interactive fallback):** Use only when the one-shot helper is not enough and a live multi-step browser session is required.
39
+ 4. **WebFetch (fallback):** For static pages or when Geometra is not available.
40
+ 5. **WebSearch (last resort):** Search on secondary portals that index the JD.
40
41
 
41
42
  **Special cases:**
42
43
  - **LinkedIn**: May require login → mark `[!]` and ask the user to paste the text
@@ -68,6 +69,7 @@ Step 1 — Read data/pipeline.md; collect "- [ ]" URLs into `pending = [url_1,
68
69
  Step 2 — Pre-flight cleanup (once, before loop):
69
70
  geometra_list_sessions()
70
71
  geometra_disconnect({ closeBrowser: true })
72
+ # portal:* helpers are direct-package one-shots and auto-close.
71
73
  Step 3 — For round in ceil(N/2):
72
74
  pair = pending[round*2 : round*2 + 2]
73
75
  # ONE message, 1 or 2 task() calls. Never 3.
@@ -50,7 +50,7 @@ These blocks come from two distinct root causes and require different responses:
50
50
 
51
51
  **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.
52
52
 
53
- **Class B fix — BYO residential proxy + stealth Chromium.** When the candidate has configured `proxy:` in `config/profile.yml`, every `geometra_connect` call threads that proxy through to Chromium, which flips the outbound IP from datacenter to residential/mobile. JobForge also passes `stealth: true` so Geometra MCP >=1.61.3 launches CloakBrowser's patched Chromium instead of stock Playwright Chromium. See the "BYO Residential Proxy" reference section below. Without a configured proxy, stealth still helps browser fingerprinting, but the outbound IP remains datacenter.
53
+ **Class B fix — BYO residential proxy + headless stealth Chromium.** When the candidate has configured `proxy:` in `config/profile.yml`, every `geometra_connect` call threads that proxy through to Chromium, which flips the outbound IP from datacenter to residential/mobile. JobForge also passes `headless: true` and `stealth: true` so Geometra MCP runs without opening a visible browser and Geometra MCP >=1.61.3 launches CloakBrowser's patched Chromium instead of stock Playwright Chromium. See the "BYO Residential Proxy" reference section below. Without a configured proxy, stealth still helps browser fingerprinting, but the outbound IP remains datacenter.
54
54
 
55
55
  **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:
56
56
 
@@ -143,7 +143,7 @@ geometra_connect({ pageUrl: "https://...", isolated: true, headless: true, slowM
143
143
 
144
144
  **Wrong:** running `geometra_connect` without `isolated: true` when submitting multiple forms concurrently. The forms may share state and produce incorrect submissions.
145
145
 
146
- **With a configured proxy,** add `proxy: { server, username?, password?, bypass? }` to the same call — see "BYO Residential Proxy" below. The reusable-proxy pool is partitioned by proxy identity, so mixing direct and proxied sessions across parallel rounds is safe. Keep `stealth: true` either way so JobForge uses Geometra's CloakBrowser Chromium path for portal sessions.
146
+ **With a configured proxy,** add `proxy: { server, username?, password?, bypass? }` to the same call — see "BYO Residential Proxy" below. The reusable-proxy pool is partitioned by proxy identity, so mixing direct and proxied sessions across parallel rounds is safe. Keep `headless: true` and `stealth: true` either way so JobForge uses Geometra's CloakBrowser Chromium path for portal sessions without opening visible windows.
147
147
 
148
148
  ### Session Reuse — When Subagents Cannot Reach Existing Sessions
149
149
 
@@ -10,6 +10,7 @@ Prefer a local helper when the workflow needs:
10
10
  - Machine-readable artifact validation.
11
11
  - Context, capability, or migration policy.
12
12
  - Dispatch planning or settlement.
13
+ - One-shot rendered browser snapshots or form schemas.
13
14
  - Scoring, timing, priority, or lineage decisions.
14
15
  - Safe export checks.
15
16
 
@@ -26,6 +27,7 @@ Do not paste whole helper outputs into prompts unless the downstream agent needs
26
27
  | Artifact contracts | `templates/contracts.json` | `npx job-forge tracker-line ... --write`; `npx job-forge verify` |
27
28
  | Role capability policy | `templates/capabilities.json` | `npx job-forge capabilities:*` |
28
29
  | Context bundle policy | `templates/context.json` | `npx job-forge context:*` |
30
+ | Browser snapshots / form schemas | Direct `@geometra/mcp` session module | `npx job-forge portal:*` |
29
31
  | JD/artifact reuse | `.jobforge-cache/` | `npx job-forge cache:*` |
30
32
  | Artifact lookup | `.jobforge-index.json` from `templates/index.json` | `npx job-forge index:*` |
31
33
  | Source-backed facts | `.jobforge-facts.json` from `templates/facts.json` | `npx job-forge facts:*` |
@@ -49,6 +51,7 @@ Do not paste whole helper outputs into prompts unless the downstream agent needs
49
51
  - For generated reports or PDFs reused after input changes, run `lineage:check --artifact <file>` if lineage exists; after creating derived artifacts, record them with `lineage:record --artifact <file> --input <source>...`.
50
52
  - Before exporting traces, prompts, reports, or fixtures outside the project, run `redact:scan`, `redact:apply`, or `redact:verify`.
51
53
  - When diagnosing consumer harness drift, run `migrate:plan` or `migrate:check`; `job-forge sync` applies safe migrations automatically unless `JOB_FORGE_SKIP_MIGRATIONS=1` is set.
54
+ - When you only need a rendered page model, compact snapshot, or form schema from one URL, prefer `portal:snapshot` / `portal:form-schema` over Geometra MCP tool calls. Use MCP for interactive multi-step browser sessions.
52
55
 
53
56
  ## Enforcement
54
57
 
@@ -53,9 +53,9 @@ When a form says "enter the code we sent to your email", you MUST retrieve the c
53
53
 
54
54
  **Problem:** on 2026-04-19 cycle 4, 5/5 untested Ashby tenants and 100% of Dropbox-class Cloudflare-fronted portals fingerprint-blocked headless Chromium from datacenter IPs. `imeFriendly: true` fixes class A (React validation lag) but has zero effect on class B (environment fingerprint). There is no in-session software-only fix for class B: the server decided the session is a bot before the form response was rendered.
55
55
 
56
- **Fix:** route the spawned Chromium through a residential or mobile proxy the candidate already pays for, and launch Geometra's CloakBrowser stealth Chromium with `stealth: true`. Geometra MCP v1.59.0 added a `proxy: { server, username?, password?, bypass? }` parameter on `geometra_connect` and `geometra_prepare_browser`; v1.61.3 added `stealth: true` for CloakBrowser. The outbound IP becomes residential/mobile, the browser fingerprint moves off stock Playwright Chromium, and the class-B checks have fewer signals to trip.
56
+ **Fix:** route the spawned Chromium through a residential or mobile proxy the candidate already pays for, and launch Geometra's headless CloakBrowser stealth Chromium with `headless: true` and `stealth: true`. Geometra MCP v1.59.0 added a `proxy: { server, username?, password?, bypass? }` parameter on `geometra_connect` and `geometra_prepare_browser`; v1.61.3 added `stealth: true` for CloakBrowser. The outbound IP becomes residential/mobile, the browser fingerprint moves off stock Playwright Chromium, and the class-B checks have fewer signals to trip.
57
57
 
58
- **Proxy is opt-in, stealth is default.** JobForge does NOT bundle or resell proxy bandwidth — the candidate brings their own provider (Bright Data, Oxylabs, SOAX, Smartproxy, mobile hotspot, self-hosted SOCKS). Without a configured proxy, JobForge still passes `stealth: true`, but the outbound IP remains the machine or hosting environment running Chromium.
58
+ **Proxy is opt-in; headless and stealth are default.** JobForge does NOT bundle or resell proxy bandwidth — the candidate brings their own provider (Bright Data, Oxylabs, SOAX, Smartproxy, mobile hotspot, self-hosted SOCKS). Without a configured proxy, JobForge still passes `headless: true` and `stealth: true`, but the outbound IP remains the machine or hosting environment running Chromium.
59
59
 
60
60
  ### Where the proxy config lives
61
61
 
@@ -76,15 +76,15 @@ See `config/profile.example.yml` for the commented-out template.
76
76
  **Orchestrator responsibilities:**
77
77
 
78
78
  1. On session start, read `config/profile.yml` once. If a `proxy:` block is present, remember that a proxy is configured, but do not paste username/password values into task prompts or user-visible status.
79
- 2. When dispatching any subagent whose work involves a `geometra_connect` call, tell it to read `config/profile.yml` and pass the top-level `proxy:` block plus `stealth: true` to every `geometra_connect` call. Example dispatch prompt line: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `stealth: true` to every `geometra_connect` call."
80
- 3. When the orchestrator itself opens a Chromium session (single-application interactive flow), include the same `proxy` object from `config/profile.yml` and `stealth: true` in its own `geometra_connect` call.
79
+ 2. When dispatching any subagent whose work involves a `geometra_connect` call or a Geometra auto-connect call with `pageUrl` / `url`, tell it to read `config/profile.yml` and pass the top-level `proxy:` block plus `headless: true` and `stealth: true` to every connect. Example dispatch prompt line: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true` and `stealth: true` to every Geometra connect or auto-connect call."
80
+ 3. When the orchestrator itself opens a Chromium session (single-application interactive flow), include the same `proxy` object from `config/profile.yml`, `headless: true`, and `stealth: true` in its own `geometra_connect` call.
81
81
  4. If `proxy:` is absent from `profile.yml`, skip the param entirely. Do NOT invent a proxy URL or leave a stale placeholder.
82
82
 
83
83
  **Subagent responsibilities:**
84
84
 
85
- 1. If the task prompt says proxy is configured, read `config/profile.yml` and pass the top-level `proxy:` object plus `stealth: true` through to `geometra_connect` and any `geometra_prepare_browser` calls unchanged.
86
- 2. If the task prompt includes a legacy inline `proxy` object, pass it through unchanged and still set `stealth: true`, but never print the credentials back in status text.
87
- 3. If the task prompt does NOT mention a proxy and `config/profile.yml` has no `proxy:` block, run with `stealth: true` and no proxy.
85
+ 1. If the task prompt says proxy is configured, read `config/profile.yml` and pass the top-level `proxy:` object plus `headless: true` and `stealth: true` through to `geometra_connect`, any Geometra auto-connect call with `pageUrl` / `url`, and any `geometra_prepare_browser` calls unchanged.
86
+ 2. If the task prompt includes a legacy inline `proxy` object, pass it through unchanged and still set `headless: true` and `stealth: true`, but never print the credentials back in status text.
87
+ 3. If the task prompt does NOT mention a proxy and `config/profile.yml` has no `proxy:` block, run with `headless: true`, `stealth: true`, and no proxy.
88
88
  4. Never second-guess the proxy field — if it comes from `profile.yml`, it's authoritative.
89
89
 
90
90
  ### When proxy use is load-bearing
@@ -100,6 +100,10 @@ Apply these rules when deciding whether the proxy is worth waiting for:
100
100
 
101
101
  The Geometra MCP partitions its reusable-proxy pool by proxy identity and browser flavor — proxy partitioning landed in `@geometra/mcp@1.59.0`, and stealth partitioning is available in `@geometra/mcp@1.61.3`. A direct session and a proxied session NEVER share a Chromium instance, and stock and stealth sessions do not pool together. Practical consequence: flipping `proxy:` on or off in `profile.yml` mid-session is safe — the next `geometra_connect` just opens a fresh Chromium in its own pool partition.
102
102
 
103
+ ### Direct helper for one-shot reads
104
+
105
+ Use `npx job-forge portal:snapshot --url "{url}" --json` or `npx job-forge portal:form-schema --url "{url}" --json` when you only need a rendered page model, compact snapshot, or form schema. These commands import Geometra's session module directly instead of going through MCP, enforce `headless: true`, `stealth: true`, and `isolated: true`, pass the `config/profile.yml` proxy block if configured, and close Chromium before exit. Keep MCP for interactive multi-step browser automation where a live `sessionId` must be driven across actions.
106
+
103
107
  ### Troubleshooting
104
108
 
105
109
  | Symptom | Diagnosis |
package/modes/scan.md CHANGED
@@ -25,7 +25,7 @@ Read `portals.yml` which contains:
25
25
 
26
26
  ### Use Level 1 — Direct Geometra (PRIMARY)
27
27
 
28
- **For each company in `tracked_companies`:** Connect to its `careers_url` with Geometra MCP (`geometra_connect({ ..., stealth: true })` + `geometra_page_model` / `geometra_list_items`), read ALL visible job listings, and extract the title + URL of each one. Direct Geometra is the most reliable method because:
28
+ **For each company in `tracked_companies`:** Connect to its `careers_url` with Geometra MCP (`geometra_connect({ ..., headless: true, stealth: true })` + `geometra_page_model` / `geometra_list_items`), read ALL visible job listings, and extract the title + URL of each one. Direct Geometra is the most reliable method because:
29
29
 
30
30
  - It sees the page in real time (not cached Google results).
31
31
  - It works with SPAs (Ashby, Lever, Workday).
@@ -138,7 +138,7 @@ The levels are additive — all are executed, results are merged and deduplicate
138
138
 
139
139
  4. **Level 1 — Geometra scan** (sequential, or ≤2 parallel via `task` subagents per Hard Limit #1 in `AGENTS.md`):
140
140
  For each company in `tracked_companies` with `enabled: true` and `careers_url` defined:
141
- a. `geometra_connect` to the `careers_url` with `stealth: true`
141
+ a. `geometra_connect` to the `careers_url` with `headless: true` and `stealth: true`
142
142
  b. `geometra_page_model` or `geometra_list_items` to read all job listings
143
143
  c. If the page has filters/departments, navigate the relevant sections
144
144
  d. For each job listing extract: `{title, url, company}`
@@ -317,7 +317,7 @@ Each company in `tracked_companies` MUST have a `careers_url` — the direct URL
317
317
  **If `careers_url` doesn't exist** for a company:
318
318
  1. Try the pattern for its known platform
319
319
  2. If that fails, do a quick WebSearch: `"{company}" careers jobs`
320
- 3. Navigate with Geometra (`geometra_connect` with `stealth: true`) to confirm it works
320
+ 3. Navigate with Geometra (`geometra_connect` with `headless: true` and `stealth: true`) to confirm it works
321
321
  4. **Save the found URL in portals.yml** for future scans
322
322
 
323
323
  **If `careers_url` returns 404 or redirect:**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-forge",
3
- "version": "2.14.41",
3
+ "version": "2.14.43",
4
4
  "description": "AI-powered job search pipeline built on opencode",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,9 @@
18
18
  "tokens": "node scripts/token-usage-report.mjs",
19
19
  "tokens:today": "node scripts/token-usage-report.mjs --days 1",
20
20
  "tokens:log": "node scripts/token-usage-report.mjs --days 1 --append",
21
+ "portal:snapshot": "node bin/job-forge.mjs portal:snapshot",
22
+ "portal:form-schema": "node bin/job-forge.mjs portal:form-schema",
23
+ "portal:explain": "node bin/job-forge.mjs portal:explain",
21
24
  "trace:list": "node bin/job-forge.mjs trace:list",
22
25
  "trace:stats": "node bin/job-forge.mjs trace:stats",
23
26
  "trace:show": "node bin/job-forge.mjs trace:show",
@@ -196,6 +199,7 @@
196
199
  "@agent-pattern-labs/iso-score": "^0.1.1",
197
200
  "@agent-pattern-labs/iso-timeline": "^0.1.1",
198
201
  "@agent-pattern-labs/iso-trace": "^0.5.1",
202
+ "@geometra/mcp": "1.62.1",
199
203
  "playwright": "^1.58.1"
200
204
  },
201
205
  "devDependencies": {
@@ -20,6 +20,7 @@ const groups = [
20
20
  helper('trace', '@agent-pattern-labs/iso-trace', ['list', 'stats', 'show']),
21
21
  helper('telemetry', '', ['list', 'status', 'show', 'watch']),
22
22
  helper('guard', '@agent-pattern-labs/iso-guard', ['audit', 'explain'], { template: 'templates/guards/jobforge-baseline.yaml' }),
23
+ helper('portal', '@geometra/mcp', ['snapshot', 'form-schema', 'explain'], { migrated: true }),
23
24
  helper('ledger', '@agent-pattern-labs/iso-ledger', ['status', 'rebuild', 'verify', 'has', 'query'], { artifacts: ['.jobforge-ledger/'] }),
24
25
  helper('capabilities', '@agent-pattern-labs/iso-capabilities', ['list', 'explain', 'check', 'render'], { template: 'templates/capabilities.json', migrated: true }),
25
26
  helper('context', '@agent-pattern-labs/iso-context', ['list', 'explain', 'plan', 'check', 'render'], { template: 'templates/context.json', migrated: true }),
@@ -21,12 +21,13 @@ const checks = [
21
21
  ["H5 blocks same-company concurrent retry", () => every(files.instructions, ["Re-dispatch the same company only AFTER", "previous subagent returns"])],
22
22
  ["H6 requires merge and verify", () => every(files.instructions, ["batch/tracker-additions/*.tsv", "npx job-forge merge", "npx job-forge verify"])],
23
23
  ["H7 distrusts subagent prose", () => every(files.instructions, ["must originate from a file", "not from prior subagent prose"])],
24
- ["H8 keeps proxy secret and requires stealth", () => every(files.instructions, ["[H8]", "Do not transcribe `server`, `username`, `password`, or `bypass`", "`stealth: true`"])],
24
+ ["H8 keeps proxy secret and requires headless stealth", () => every(files.instructions, ["[H8]", "Do not transcribe `server`, `username`, `password`, or `bypass`", "`headless: true`", "`stealth: true`"])],
25
25
  ["OpenCode addendum exists for task semantics", () => every(files.instructionsOpencode, ["OpenCode", "`task`", "launch acknowledgement", "Do not use `task` to poll status"])],
26
26
  ["root points to consolidated helper reference", () => every(files.instructions, ["[D8]", "modes/reference-local-helpers.md", "deterministic local helpers"])],
27
27
  ["helper reference covers score/timeline/prioritize/lineage", () => every(files.helpers, ["templates/score.json", "npx job-forge score:*", "templates/timeline.json", "npx job-forge timeline:*", "templates/prioritize.json", "npx job-forge prioritize:*", ".jobforge-lineage.json", "npx job-forge lineage:*"])],
28
28
  ["root helper defaults are consolidated", () => !/\[D(?:9|1\d|2[0-9])\]/.test(files.instructions)],
29
29
  ["shared prompt points to on-demand references", () => every(files.instructions, ["modes/{mode}.md", "modes/reference-setup.md", "modes/reference-portals.md", "modes/reference-geometra.md"])],
30
+ ["apply mode requires headless stealth Geometra", () => every(files.apply, ["`headless: true`", "`stealth: true`", "`isolated: true`", "every Geometra auto-connect call"])],
30
31
  ["apply mode owns high-stakes upgrade", () => every(files.apply, ["[D8]", "@general-paid", "4.0/5", "high-stakes"])],
31
32
  ["apply mode blocks provider auto-downgrade", () => every(files.apply, ["[D9]", "do not auto-downgrade", "inspect telemetry before retrying"])],
32
33
  ["models policy pins OpenCode to DeepSeek V4 Flash", () => /extends:\s*standard/.test(files.models) && count(files.models, "opencode-go/deepseek-v4-flash") >= 4],
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'node:module';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+ import { PROJECT_DIR } from '../tracker-lib.mjs';
8
+
9
+ const DEFAULT_WIDTH = 1024;
10
+ const DEFAULT_HEIGHT = 768;
11
+ const DEFAULT_SLOW_MO = 350;
12
+ const DEFAULT_MAX_NODES = 120;
13
+
14
+ const USAGE = `job-forge portal - deterministic direct-Geometra browser helpers
15
+
16
+ Usage:
17
+ job-forge portal:snapshot --url <url> [--json] [--forms] [--max-nodes N]
18
+ job-forge portal:form-schema --url <url> [--json] [--include-options]
19
+ job-forge portal:explain [--json]
20
+
21
+ Defaults are enforced in code for every browser launch:
22
+ isolated: true
23
+ headless: true
24
+ stealth: true
25
+ slowMo: 350
26
+
27
+ The helper imports Geometra's session module directly. It does not call the
28
+ MCP tool protocol, does not leave a reusable browser pool behind, and closes
29
+ its isolated Chromium before exit. If config/profile.yml contains a top-level
30
+ proxy: block with server/username/password/bypass, it is threaded into the
31
+ browser unless --no-profile-proxy is passed.`;
32
+
33
+ const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
34
+ const opts = parseArgs(rawArgs);
35
+
36
+ if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
37
+ console.log(USAGE);
38
+ process.exit(0);
39
+ }
40
+
41
+ try {
42
+ if (cmd === 'snapshot') {
43
+ await snapshot(opts);
44
+ } else if (cmd === 'form-schema') {
45
+ await formSchema(opts);
46
+ } else if (cmd === 'explain') {
47
+ await explain(opts);
48
+ } else {
49
+ console.error(`unknown portal command "${cmd}"\n`);
50
+ console.error(USAGE);
51
+ process.exit(2);
52
+ }
53
+ } catch (error) {
54
+ console.error(error instanceof Error ? error.message : String(error));
55
+ process.exit(1);
56
+ }
57
+
58
+ async function snapshot(opts) {
59
+ if (!opts.url) throw new Error('portal:snapshot requires --url <url>');
60
+ const geometra = await loadGeometraSessionModule();
61
+ const proxy = opts.profileProxy ? readProfileProxy(PROJECT_DIR) : null;
62
+ const session = await connect(geometra, opts, proxy);
63
+
64
+ try {
65
+ const root = buildRoot(geometra, session);
66
+ const pageModel = geometra.buildPageModel(root, {
67
+ maxPrimaryActions: opts.maxPrimaryActions,
68
+ maxSectionsPerKind: opts.maxSectionsPerKind,
69
+ });
70
+ const compact = geometra.buildCompactUiIndex(root, {
71
+ maxNodes: opts.maxNodes,
72
+ viewportWidth: opts.width,
73
+ viewportHeight: opts.height,
74
+ });
75
+ const result = {
76
+ url: opts.url,
77
+ session: connectionSummary(session, proxy),
78
+ defaults: launchDefaults(opts, proxy),
79
+ pageModel,
80
+ compact,
81
+ ...(opts.forms ? { forms: geometra.buildFormSchemas(root, formOptions(opts)) } : {}),
82
+ };
83
+ output(result, opts, () => {
84
+ console.log(`url: ${opts.url}`);
85
+ console.log(`session: ${session.id}`);
86
+ console.log(`defaults: isolated=true headless=true stealth=true slowMo=${opts.slowMo}`);
87
+ if (proxy) console.log(`proxy: ${redactProxy(proxy)}`);
88
+ console.log(geometra.summarizePageModel(pageModel, 12));
89
+ console.log(geometra.summarizeCompactIndex(compact.nodes, 24));
90
+ if (opts.forms) {
91
+ console.log(`forms: ${result.forms.length}`);
92
+ }
93
+ });
94
+ } finally {
95
+ geometra.disconnect({ sessionId: session.id, closeProxy: true });
96
+ }
97
+ }
98
+
99
+ async function formSchema(opts) {
100
+ if (!opts.url) throw new Error('portal:form-schema requires --url <url>');
101
+ const geometra = await loadGeometraSessionModule();
102
+ const proxy = opts.profileProxy ? readProfileProxy(PROJECT_DIR) : null;
103
+ const session = await connect(geometra, opts, proxy);
104
+
105
+ try {
106
+ const root = buildRoot(geometra, session);
107
+ const forms = geometra.buildFormSchemas(root, formOptions(opts));
108
+ const result = {
109
+ url: opts.url,
110
+ session: connectionSummary(session, proxy),
111
+ defaults: launchDefaults(opts, proxy),
112
+ forms,
113
+ };
114
+ output(result, opts, () => {
115
+ console.log(`url: ${opts.url}`);
116
+ console.log(`session: ${session.id}`);
117
+ console.log(`defaults: isolated=true headless=true stealth=true slowMo=${opts.slowMo}`);
118
+ if (proxy) console.log(`proxy: ${redactProxy(proxy)}`);
119
+ for (const form of forms) {
120
+ const name = form.name ? ` "${form.name}"` : '';
121
+ console.log(`${form.formId}${name}: ${form.fieldCount} fields, ${form.requiredCount} required, ${form.invalidCount} invalid`);
122
+ for (const field of form.fields.slice(0, opts.maxFields)) {
123
+ const required = field.required ? ' required' : '';
124
+ const invalid = field.invalid ? ' invalid' : '';
125
+ const label = field.label || field.name || field.id;
126
+ console.log(` - ${field.kind}: ${label}${required}${invalid}`);
127
+ }
128
+ }
129
+ });
130
+ } finally {
131
+ geometra.disconnect({ sessionId: session.id, closeProxy: true });
132
+ }
133
+ }
134
+
135
+ async function explain(opts) {
136
+ const moduleTarget = resolveGeometraSessionModule();
137
+ const proxy = opts.profileProxy ? readProfileProxy(PROJECT_DIR) : null;
138
+ const result = {
139
+ projectDir: PROJECT_DIR,
140
+ module: moduleTarget,
141
+ defaults: launchDefaults(opts, proxy),
142
+ profileProxy: proxy ? redactProxy(proxy) : null,
143
+ };
144
+ output(result, opts, () => {
145
+ console.log(`project: ${PROJECT_DIR}`);
146
+ console.log(`module: ${moduleTarget.source} ${moduleTarget.path}`);
147
+ console.log(`defaults: isolated=true headless=true stealth=true slowMo=${opts.slowMo}`);
148
+ console.log(`profile proxy: ${proxy ? redactProxy(proxy) : 'none'}`);
149
+ });
150
+ }
151
+
152
+ async function connect(geometra, opts, proxy) {
153
+ return await geometra.connectThroughProxy({
154
+ pageUrl: opts.url,
155
+ isolated: true,
156
+ headless: true,
157
+ stealth: true,
158
+ slowMo: opts.slowMo,
159
+ width: opts.width,
160
+ height: opts.height,
161
+ awaitInitialFrame: true,
162
+ eagerInitialExtract: true,
163
+ ...(proxy ? { proxy } : {}),
164
+ });
165
+ }
166
+
167
+ function buildRoot(geometra, session) {
168
+ if (!session.tree || !session.layout) {
169
+ throw new Error(`Geometra session ${session.id} did not return an accessibility tree`);
170
+ }
171
+ return geometra.buildA11yTree(session.tree, session.layout);
172
+ }
173
+
174
+ async function loadGeometraSessionModule() {
175
+ const target = resolveGeometraSessionModule();
176
+ try {
177
+ return await import(pathToFileURL(target.path).href);
178
+ } catch (error) {
179
+ const detail = error instanceof Error ? error.message : String(error);
180
+ throw new Error(`Failed to load Geometra session module from ${target.path}: ${detail}`);
181
+ }
182
+ }
183
+
184
+ function resolveGeometraSessionModule() {
185
+ const explicit = normalizeEnv(process.env.JOB_FORGE_GEOMETRA_SESSION_MODULE);
186
+ if (explicit) return existingModulePath('env', resolve(explicit));
187
+
188
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
189
+ const siblingPath = resolve(scriptDir, '../../geometra/mcp/dist/session.js');
190
+ if (existsSync(siblingPath)) return { source: 'sibling-repo', path: siblingPath };
191
+
192
+ const require = createRequire(import.meta.url);
193
+ try {
194
+ return {
195
+ source: 'package',
196
+ path: require.resolve('@geometra/mcp/dist/session.js'),
197
+ };
198
+ } catch (error) {
199
+ const detail = error instanceof Error ? error.message : String(error);
200
+ throw new Error(`Could not resolve @geometra/mcp/dist/session.js. Install dependencies with npm install. Resolution error: ${detail}`);
201
+ }
202
+ }
203
+
204
+ function existingModulePath(source, path) {
205
+ if (!existsSync(path)) throw new Error(`${source} Geometra session module not found: ${path}`);
206
+ return { source, path };
207
+ }
208
+
209
+ function parseArgs(args) {
210
+ const opts = {
211
+ help: false,
212
+ json: false,
213
+ forms: false,
214
+ includeOptions: false,
215
+ profileProxy: true,
216
+ width: DEFAULT_WIDTH,
217
+ height: DEFAULT_HEIGHT,
218
+ slowMo: DEFAULT_SLOW_MO,
219
+ maxNodes: DEFAULT_MAX_NODES,
220
+ maxFields: 80,
221
+ maxPrimaryActions: 6,
222
+ maxSectionsPerKind: 8,
223
+ };
224
+
225
+ for (let i = 0; i < args.length; i++) {
226
+ const arg = args[i];
227
+ if (arg === '--url' || arg === '-u') {
228
+ opts.url = valueAfter(args, ++i, arg);
229
+ } else if (arg.startsWith('--url=')) {
230
+ opts.url = arg.slice('--url='.length);
231
+ } else if (arg === '--json') {
232
+ opts.json = true;
233
+ } else if (arg === '--forms') {
234
+ opts.forms = true;
235
+ } else if (arg === '--include-options') {
236
+ opts.includeOptions = true;
237
+ } else if (arg === '--no-profile-proxy') {
238
+ opts.profileProxy = false;
239
+ } else if (arg === '--width') {
240
+ opts.width = parsePositiveInt(valueAfter(args, ++i, arg), arg);
241
+ } else if (arg.startsWith('--width=')) {
242
+ opts.width = parsePositiveInt(arg.slice('--width='.length), '--width');
243
+ } else if (arg === '--height') {
244
+ opts.height = parsePositiveInt(valueAfter(args, ++i, arg), arg);
245
+ } else if (arg.startsWith('--height=')) {
246
+ opts.height = parsePositiveInt(arg.slice('--height='.length), '--height');
247
+ } else if (arg === '--slow-mo') {
248
+ opts.slowMo = parseNonNegativeInt(valueAfter(args, ++i, arg), arg);
249
+ } else if (arg.startsWith('--slow-mo=')) {
250
+ opts.slowMo = parseNonNegativeInt(arg.slice('--slow-mo='.length), '--slow-mo');
251
+ } else if (arg === '--max-nodes') {
252
+ opts.maxNodes = parsePositiveInt(valueAfter(args, ++i, arg), arg);
253
+ } else if (arg.startsWith('--max-nodes=')) {
254
+ opts.maxNodes = parsePositiveInt(arg.slice('--max-nodes='.length), '--max-nodes');
255
+ } else if (arg === '--max-fields') {
256
+ opts.maxFields = parsePositiveInt(valueAfter(args, ++i, arg), arg);
257
+ } else if (arg.startsWith('--max-fields=')) {
258
+ opts.maxFields = parsePositiveInt(arg.slice('--max-fields='.length), '--max-fields');
259
+ } else if (arg === '--help' || arg === '-h') {
260
+ opts.help = true;
261
+ } else {
262
+ throw new Error(`unknown flag "${arg}"`);
263
+ }
264
+ }
265
+
266
+ return opts;
267
+ }
268
+
269
+ function formOptions(opts) {
270
+ return {
271
+ includeOptions: opts.includeOptions,
272
+ maxFields: opts.maxFields,
273
+ };
274
+ }
275
+
276
+ function readProfileProxy(projectDir) {
277
+ const profilePath = join(projectDir, 'config', 'profile.yml');
278
+ if (!existsSync(profilePath)) return null;
279
+ const proxy = parseTopLevelProxy(readFileSync(profilePath, 'utf8'));
280
+ return proxy?.server ? proxy : null;
281
+ }
282
+
283
+ function parseTopLevelProxy(source) {
284
+ const lines = source.split(/\r?\n/);
285
+ const start = lines.findIndex((line) => /^proxy:\s*(?:#.*)?$/.test(line));
286
+ if (start === -1) return null;
287
+ const proxy = {};
288
+ for (const line of lines.slice(start + 1)) {
289
+ if (/^\S/.test(line) && line.trim() !== '') break;
290
+ const match = line.match(/^\s+([A-Za-z_][A-Za-z0-9_]*):\s*(.*?)\s*$/);
291
+ if (!match) continue;
292
+ const key = match[1];
293
+ if (!['server', 'username', 'password', 'bypass'].includes(key)) continue;
294
+ const value = parseYamlScalar(match[2]);
295
+ if (value !== '') proxy[key] = value;
296
+ }
297
+ return Object.keys(proxy).length > 0 ? proxy : null;
298
+ }
299
+
300
+ function parseYamlScalar(raw) {
301
+ const withoutComment = raw.replace(/\s+#.*$/, '').trim();
302
+ if ((withoutComment.startsWith('"') && withoutComment.endsWith('"')) ||
303
+ (withoutComment.startsWith("'") && withoutComment.endsWith("'"))) {
304
+ return withoutComment.slice(1, -1);
305
+ }
306
+ return withoutComment;
307
+ }
308
+
309
+ function output(result, opts, textPrinter) {
310
+ if (opts.json) {
311
+ console.log(JSON.stringify(result, null, 2));
312
+ } else {
313
+ textPrinter();
314
+ }
315
+ }
316
+
317
+ function launchDefaults(opts, proxy) {
318
+ return {
319
+ isolated: true,
320
+ headless: true,
321
+ stealth: true,
322
+ slowMo: opts.slowMo,
323
+ width: opts.width,
324
+ height: opts.height,
325
+ profileProxy: Boolean(proxy),
326
+ };
327
+ }
328
+
329
+ function connectionSummary(session, proxy) {
330
+ return {
331
+ id: session.id,
332
+ url: session.url,
333
+ proxy: Boolean(proxy),
334
+ };
335
+ }
336
+
337
+ function redactProxy(proxy) {
338
+ try {
339
+ const url = new URL(proxy.server);
340
+ const auth = proxy.username || proxy.password || url.username || url.password ? ' auth=present' : '';
341
+ return `${url.protocol}//${url.host}${auth}${proxy.bypass ? ' bypass=present' : ''}`;
342
+ } catch {
343
+ return 'configured';
344
+ }
345
+ }
346
+
347
+ function normalizeEnv(value) {
348
+ if (typeof value !== 'string') return null;
349
+ const trimmed = value.trim();
350
+ return trimmed.length > 0 ? trimmed : null;
351
+ }
352
+
353
+ function valueAfter(args, index, flag) {
354
+ const value = args[index];
355
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
356
+ return value;
357
+ }
358
+
359
+ function parsePositiveInt(value, flag) {
360
+ const parsed = Number.parseInt(value, 10);
361
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${flag} must be a positive integer`);
362
+ return parsed;
363
+ }
364
+
365
+ function parseNonNegativeInt(value, flag) {
366
+ const parsed = Number.parseInt(value, 10);
367
+ if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} must be a non-negative integer`);
368
+ return parsed;
369
+ }
@@ -19,6 +19,9 @@
19
19
  "context:plan": "job-forge context:plan",
20
20
  "context:check": "job-forge context:check",
21
21
  "context:render": "job-forge context:render",
22
+ "portal:snapshot": "job-forge portal:snapshot",
23
+ "portal:form-schema": "job-forge portal:form-schema",
24
+ "portal:explain": "job-forge portal:explain",
22
25
  "cache:key": "job-forge cache:key",
23
26
  "cache:has": "job-forge cache:has",
24
27
  "cache:get": "job-forge cache:get",