job-forge 2.14.8 → 2.14.10

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.
@@ -53,6 +53,7 @@ Previous subagents sometimes abort mid-flow (ran out of context, hit a timeout,
53
53
  - Write cover letter prose, "Why X?" answers, or Section G draft answers. Those go to `@general-paid`.
54
54
  - Perform offer evaluation narratives (Blocks A-F). Those go to `@general-paid`.
55
55
  - Override harness rules or invent fields. Follow the mode files exactly.
56
+ - Spawn or check other tasks. If you receive "check if task ses_..." and it refers to this session, report your current status from your own work. Never call `task` recursively.
56
57
 
57
58
  ## Apply This Working Style
58
59
 
@@ -18,12 +18,14 @@ were given.
18
18
  - Compose STAR+R interview stories and the story bank (`modes/offer.md` Block F).
19
19
  - Draft LinkedIn outreach messages (`modes/contact.md`).
20
20
  - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, then the narrative report.
21
+ - Drive a single high-stakes application form only when the orchestrator explicitly dispatches you in `apply` mode. In that case, follow `modes/apply.md` exactly and use the same Geometra/Gmail flow as `@general-free`.
21
22
 
22
23
  ## Skip These Tasks
23
24
 
24
- - Drive Geometra forms end-to-end (delegate to `@general-free` or do it yourself only when the orchestrator asks for an atomic one-shot apply).
25
+ - Drive Geometra forms end-to-end unless the task is explicitly an `apply` mode dispatch for one job.
25
26
  - Manage trackers, run scripts, or do mechanical TSV/dedup work. Those go to `@general-free`.
26
27
  - Duplicate work. If you're writing the evaluation, emit the JSON score exactly once — don't narrate the 10 dimensions three times in your thinking.
28
+ - Spawn or check other tasks. If you receive "check if task ses_..." and it refers to this session, report your current status from your own work. Never call `task` recursively.
27
29
 
28
30
  ## Apply This Working Style
29
31
 
@@ -52,6 +52,7 @@ Previous subagents sometimes abort mid-flow (ran out of context, hit a timeout,
52
52
  - Write cover letter prose, "Why X?" answers, or Section G draft answers. Those go to `@general-paid`.
53
53
  - Perform offer evaluation narratives (Blocks A-F). Those go to `@general-paid`.
54
54
  - Override harness rules or invent fields. Follow the mode files exactly.
55
+ - Spawn or check other tasks. If you receive "check if task ses_..." and it refers to this session, report your current status from your own work. Never call `task` recursively.
55
56
 
56
57
  ## Apply This Working Style
57
58
 
@@ -17,12 +17,14 @@ were given.
17
17
  - Compose STAR+R interview stories and the story bank (`modes/offer.md` Block F).
18
18
  - Draft LinkedIn outreach messages (`modes/contact.md`).
19
19
  - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, then the narrative report.
20
+ - Drive a single high-stakes application form only when the orchestrator explicitly dispatches you in `apply` mode. In that case, follow `modes/apply.md` exactly and use the same Geometra/Gmail flow as `@general-free`.
20
21
 
21
22
  ## Skip These Tasks
22
23
 
23
- - Drive Geometra forms end-to-end (delegate to `@general-free` or do it yourself only when the orchestrator asks for an atomic one-shot apply).
24
+ - Drive Geometra forms end-to-end unless the task is explicitly an `apply` mode dispatch for one job.
24
25
  - Manage trackers, run scripts, or do mechanical TSV/dedup work. Those go to `@general-free`.
25
26
  - Duplicate work. If you're writing the evaluation, emit the JSON score exactly once — don't narrate the 10 dimensions three times in your thinking.
27
+ - Spawn or check other tasks. If you receive "check if task ses_..." and it refers to this session, report your current status from your own work. Never call `task` recursively.
26
28
 
27
29
  ## Apply This Working Style
28
30
 
@@ -9,8 +9,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
9
9
 
10
10
  ## Hard limits
11
11
 
12
- - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
13
- why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result
12
+ - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. A round is not complete until both subagents return a final outcome (`APPLIED`, `APPLY FAILED`, `SKIP`, `Discarded`, or a written TSV path). A `task` tool result that only gives a session id / title is a launch acknowledgement, not completion. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
13
+ why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result. On 2026-04-25 the orchestrator launched round 2 while round 1 had only returned task ids, leaving four application subagents in flight and losing two provider-fallback recoveries
14
14
 
15
15
  - [H2] Max 1 application per company+role. Before every `apply` dispatch, grep all four sources for the URL and for `company+role`: `data/pipeline.md`, all `data/applications/*.md` day files, `batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`. If any source shows APPLIED / Applied, skip the dispatch.
16
16
  why: 2026-04 same-day batch collision — when two batches target the same role, `npx job-forge merge` updates the existing day-file row rather than appending, so grepping day files alone misses earlier-batch applies; merged/*.tsv is the only place the breadcrumb remains
@@ -24,12 +24,18 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
24
24
  - [H5] Re-dispatch the same company only AFTER the previous subagent returns. Never fire the same `task` twice while the first is still in flight.
25
25
  why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
26
26
 
27
+ - [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, or `iso-trace`) rather than spawning a "check task status" subagent.
28
+ why: OpenCode status prompts can be delivered into the target subagent as a new user message; a 2026-04-25 trace caused a subagent to call `task` recursively instead of finishing the application
29
+
27
30
  - [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
28
31
  why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
29
32
 
30
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, `batch/tracker-additions/*.tsv`.
31
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
32
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 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
38
+
33
39
  ## Defaults
34
40
 
35
41
  - [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
@@ -55,8 +61,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
55
61
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
56
62
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
57
63
  3. Read the active mode file [D3]; decide inline vs delegated work [D1].
58
- 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2].
59
- 5. Dispatch at most 2 tasks per round [H1]; wait per company [H5].
64
+ 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
65
+ 5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
60
66
  6. Keep multi-job form-filling out of the orchestrator [H4].
61
67
  7. Cross-check subagent facts against authoritative files [H7].
62
68
  8. Apply score gate [D4].
@@ -14,6 +14,7 @@ tools:
14
14
  geometra_wait_for_resume_parse: true
15
15
  gmail_list_messages: true
16
16
  gmail_get_message: true
17
+ task: false
17
18
  temperature: 0.1
18
19
  reasoningEffort: minimal
19
20
  fallback_models:
@@ -72,6 +73,7 @@ Previous subagents sometimes abort mid-flow (ran out of context, hit a timeout,
72
73
  - Write cover letter prose, "Why X?" answers, or Section G draft answers. Those go to `@general-paid`.
73
74
  - Perform offer evaluation narratives (Blocks A-F). Those go to `@general-paid`.
74
75
  - Override harness rules or invent fields. Follow the mode files exactly.
76
+ - Spawn or check other tasks. If you receive "check if task ses_..." and it refers to this session, report your current status from your own work. Never call `task` recursively.
75
77
 
76
78
  ## Apply This Working Style
77
79
 
@@ -3,8 +3,18 @@ description: Quality-sensitive worker on the strongest free-tier OpenCode model
3
3
  mode: subagent
4
4
  model: openrouter/qwen/qwen3-next-80b-a3b-instruct:free
5
5
  tools:
6
- geometra_*: false
7
- gmail_*: false
6
+ geometra_connect: true
7
+ geometra_page_model: true
8
+ geometra_form_schema: true
9
+ geometra_run_actions: true
10
+ geometra_fill_otp: true
11
+ geometra_upload_files: true
12
+ geometra_list_sessions: true
13
+ geometra_disconnect: true
14
+ geometra_wait_for_resume_parse: true
15
+ gmail_list_messages: true
16
+ gmail_get_message: true
17
+ task: false
8
18
  temperature: 0.3
9
19
  reasoningEffort: medium
10
20
  fallback_models:
@@ -30,12 +40,14 @@ were given.
30
40
  - Compose STAR+R interview stories and the story bank (`modes/offer.md` Block F).
31
41
  - Draft LinkedIn outreach messages (`modes/contact.md`).
32
42
  - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, then the narrative report.
43
+ - Drive a single high-stakes application form only when the orchestrator explicitly dispatches you in `apply` mode. In that case, follow `modes/apply.md` exactly and use the same Geometra/Gmail flow as `@general-free`.
33
44
 
34
45
  ## Skip These Tasks
35
46
 
36
- - Drive Geometra forms end-to-end (delegate to `@general-free` or do it yourself only when the orchestrator asks for an atomic one-shot apply).
47
+ - Drive Geometra forms end-to-end unless the task is explicitly an `apply` mode dispatch for one job.
37
48
  - Manage trackers, run scripts, or do mechanical TSV/dedup work. Those go to `@general-free`.
38
49
  - Duplicate work. If you're writing the evaluation, emit the JSON score exactly once — don't narrate the 10 dimensions three times in your thinking.
50
+ - Spawn or check other tasks. If you receive "check if task ses_..." and it refers to this session, report your current status from your own work. Never call `task` recursively.
39
51
 
40
52
  ## Apply This Working Style
41
53
 
@@ -152,10 +152,17 @@ Step 3 — Pre-flight cleanup (once, before the loop)
152
152
  Step 4 — Loop in rounds of 2 (Hard Limit #1)
153
153
  for round in ceil(len(candidates) / 2):
154
154
  pair = candidates[round*2 : round*2 + 2]
155
+ # If proxy is configured, do not paste proxy values into prompts.
156
+ # Say: "Proxy is configured; read config/profile.yml and pass its
157
+ # top-level proxy object to every geometra_connect call."
155
158
  # Dispatch 1 or 2 task() calls in ONE message (never 3+)
156
159
  task(subagent_type=<tier per AGENTS.md routing>, prompt=<apply prompt for pair[0]>)
157
160
  task(subagent_type=<tier>, prompt=<apply prompt for pair[1]>) # only if pair has 2
158
- # WAIT for both subagents to return before proceeding
161
+ # WAIT for both subagents to return final APPLIED / APPLY FAILED / SKIP /
162
+ # Discarded outcomes or TSV paths before proceeding.
163
+ # A returned task/session id is only a launch receipt, not completion.
164
+ # Do not create a "check task status" task; inspect tracker files or
165
+ # iso-trace if the user asks for status later.
159
166
  # Read their return values, log outcomes
160
167
 
161
168
  Step 5 — Between rounds: clean sessions again
package/AGENTS.md CHANGED
@@ -4,8 +4,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
4
4
 
5
5
  ## Hard limits
6
6
 
7
- - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
8
- why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result
7
+ - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. A round is not complete until both subagents return a final outcome (`APPLIED`, `APPLY FAILED`, `SKIP`, `Discarded`, or a written TSV path). A `task` tool result that only gives a session id / title is a launch acknowledgement, not completion. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
8
+ why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result. On 2026-04-25 the orchestrator launched round 2 while round 1 had only returned task ids, leaving four application subagents in flight and losing two provider-fallback recoveries
9
9
 
10
10
  - [H2] Max 1 application per company+role. Before every `apply` dispatch, grep all four sources for the URL and for `company+role`: `data/pipeline.md`, all `data/applications/*.md` day files, `batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`. If any source shows APPLIED / Applied, skip the dispatch.
11
11
  why: 2026-04 same-day batch collision — when two batches target the same role, `npx job-forge merge` updates the existing day-file row rather than appending, so grepping day files alone misses earlier-batch applies; merged/*.tsv is the only place the breadcrumb remains
@@ -19,12 +19,18 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
19
19
  - [H5] Re-dispatch the same company only AFTER the previous subagent returns. Never fire the same `task` twice while the first is still in flight.
20
20
  why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
21
21
 
22
+ - [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, or `iso-trace`) rather than spawning a "check task status" subagent.
23
+ why: OpenCode status prompts can be delivered into the target subagent as a new user message; a 2026-04-25 trace caused a subagent to call `task` recursively instead of finishing the application
24
+
22
25
  - [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
23
26
  why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
24
27
 
25
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, `batch/tracker-additions/*.tsv`.
26
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
27
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 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
33
+
28
34
  ## Defaults
29
35
 
30
36
  - [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
@@ -50,8 +56,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
50
56
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
51
57
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
52
58
  3. Read the active mode file [D3]; decide inline vs delegated work [D1].
53
- 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2].
54
- 5. Dispatch at most 2 tasks per round [H1]; wait per company [H5].
59
+ 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
60
+ 5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
55
61
  6. Keep multi-job form-filling out of the orchestrator [H4].
56
62
  7. Cross-check subagent facts against authoritative files [H7].
57
63
  8. Apply score gate [D4].
package/CLAUDE.md CHANGED
@@ -4,8 +4,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
4
4
 
5
5
  ## Hard limits
6
6
 
7
- - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
8
- why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result
7
+ - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. A round is not complete until both subagents return a final outcome (`APPLIED`, `APPLY FAILED`, `SKIP`, `Discarded`, or a written TSV path). A `task` tool result that only gives a session id / title is a launch acknowledgement, not completion. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
8
+ why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result. On 2026-04-25 the orchestrator launched round 2 while round 1 had only returned task ids, leaving four application subagents in flight and losing two provider-fallback recoveries
9
9
 
10
10
  - [H2] Max 1 application per company+role. Before every `apply` dispatch, grep all four sources for the URL and for `company+role`: `data/pipeline.md`, all `data/applications/*.md` day files, `batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`. If any source shows APPLIED / Applied, skip the dispatch.
11
11
  why: 2026-04 same-day batch collision — when two batches target the same role, `npx job-forge merge` updates the existing day-file row rather than appending, so grepping day files alone misses earlier-batch applies; merged/*.tsv is the only place the breadcrumb remains
@@ -19,12 +19,18 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
19
19
  - [H5] Re-dispatch the same company only AFTER the previous subagent returns. Never fire the same `task` twice while the first is still in flight.
20
20
  why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
21
21
 
22
+ - [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, or `iso-trace`) rather than spawning a "check task status" subagent.
23
+ why: OpenCode status prompts can be delivered into the target subagent as a new user message; a 2026-04-25 trace caused a subagent to call `task` recursively instead of finishing the application
24
+
22
25
  - [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
23
26
  why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
24
27
 
25
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, `batch/tracker-additions/*.tsv`.
26
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
27
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 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
33
+
28
34
  ## Defaults
29
35
 
30
36
  - [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
@@ -50,8 +56,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
50
56
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
51
57
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
52
58
  3. Read the active mode file [D3]; decide inline vs delegated work [D1].
53
- 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2].
54
- 5. Dispatch at most 2 tasks per round [H1]; wait per company [H5].
59
+ 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
60
+ 5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
55
61
  6. Keep multi-job form-filling out of the orchestrator [H4].
56
62
  7. Cross-check subagent facts against authoritative files [H7].
57
63
  8. Apply score gate [D4].
@@ -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
+ 'trace:list': 'job-forge trace:list',
114
+ 'trace:stats': 'job-forge trace:stats',
115
+ 'trace:show': 'job-forge trace:show',
113
116
  // One command to pull the latest harness, companion plugin, and any
114
117
  // locally-pinned MCP packages. npm update is a no-op on packages not
115
118
  // in package.json, so listing @razroo/gmail-mcp + @geometra/mcp is
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
  * tokens Run scripts/token-usage-report.mjs
20
+ * trace:* Inspect local agent transcripts via iso-trace
20
21
  * sync Re-run the harness symlink sync (bin/sync.mjs)
21
22
  * help, --help Show this message
22
23
  */
@@ -28,6 +29,7 @@ import { existsSync } from 'fs';
28
29
 
29
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
30
31
  const PKG_ROOT = resolve(__dirname, '..');
32
+ const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
31
33
 
32
34
  const commands = {
33
35
  merge: 'merge-tracker.mjs',
@@ -52,6 +54,12 @@ const commands = {
52
54
  'render-report-header': 'scripts/render-report-header.mjs',
53
55
  };
54
56
 
57
+ const traceAliases = {
58
+ 'trace:list': 'list',
59
+ 'trace:stats': 'stats',
60
+ 'trace:show': 'show',
61
+ };
62
+
55
63
  const [, , cmd, ...rest] = process.argv;
56
64
 
57
65
  function printHelp() {
@@ -68,6 +76,10 @@ Commands:
68
76
  pdf Generate ATS-optimized CV PDF from cv.md
69
77
  sync-check Lint: verify cv.md and profile.yml are filled in
70
78
  tokens Show opencode token usage and cost by session/day
79
+ trace Pass through to iso-trace (e.g. job-forge trace sources)
80
+ trace:list List recent local agent sessions (defaults: --since 7d --cwd project)
81
+ trace:stats Show trace stats (defaults: --since 7d --cwd project)
82
+ trace:show ID Show one trace by id or prefix
71
83
  sync Re-create harness symlinks in the current project
72
84
 
73
85
  Deterministic helpers (prefer these over LLM-derived values):
@@ -90,6 +102,8 @@ Pass --help after a command to see its own flags, e.g.:
90
102
  job-forge merge --help
91
103
  job-forge tokens --days 1
92
104
  job-forge slugify "Anthropic, PBC"
105
+ job-forge trace:list --since 24h
106
+ job-forge trace:show ses_...
93
107
 
94
108
  Project directory resolves to $JOB_FORGE_PROJECT or cwd.`);
95
109
  }
@@ -99,6 +113,21 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
99
113
  process.exit(0);
100
114
  }
101
115
 
116
+ if (cmd === 'trace' || traceAliases[cmd]) {
117
+ const traceArgs = cmd === 'trace'
118
+ ? (rest.length === 0 ? ['help'] : rest)
119
+ : [traceAliases[cmd], ...rest];
120
+
121
+ const scriptPath = join(PKG_ROOT, 'scripts/trace.mjs');
122
+ const result = spawnSync(process.execPath, [scriptPath, ...traceArgs], {
123
+ stdio: 'inherit',
124
+ cwd: PROJECT_DIR,
125
+ env: process.env,
126
+ });
127
+
128
+ process.exit(result.status ?? 1);
129
+ }
130
+
102
131
  const rel = commands[cmd];
103
132
  if (!rel) {
104
133
  console.error(`Unknown command: ${cmd}\n`);
@@ -203,6 +203,7 @@ Scripts maintain data consistency. In a consumer project they're invoked via the
203
203
  | `generate-pdf.mjs` | `npx job-forge pdf` | Renders HTML to PDF via Geometra MCP (`geometra_generate_pdf`) or standalone Playwright/Chromium (`npx job-forge pdf <input.html> <output.pdf>`) |
204
204
  | `cv-sync-check.mjs` | `npx job-forge sync-check` | Setup lint: `cv.md` + `config/profile.yml`, hardcoded-metric scan on `modes/_shared.md` and `batch/batch-prompt.md`, optional `article-digest.md` freshness |
205
205
  | `scripts/token-usage-report.mjs` | `npx job-forge tokens` | Per-session opencode token/cost report from the SQLite DB |
206
+ | `scripts/trace.mjs` | `npx job-forge trace:list` / `trace:stats` / `trace:show` | Local transcript observability via `@razroo/iso-trace`; common commands default to OpenCode sessions for the consumer project |
206
207
  | `tracker-lib.mjs` | _(library)_ | Shared helpers for reading/writing day-based tracker files — imported by merge/dedup/verify/normalize |
207
208
  | `bin/sync.mjs` | `npx job-forge sync` | Creates the harness symlinks in a consumer project (also runs as `postinstall`) |
208
209
  | `bin/create-job-forge.mjs` | `npx create-job-forge <dir>` | Scaffolds a new personal project |
@@ -98,13 +98,23 @@ Some forks use a **single** `data/applications.md` instead. That is fine if you
98
98
 
99
99
  ## Transcript observability (iso-trace)
100
100
 
101
- To inspect real agent sessions locally (tool mix, redundant fetches, Geometra churn) without uploading transcripts, use Razroo's [`@razroo/iso-trace`](https://github.com/razroo/iso/tree/main/packages/iso-trace).
101
+ To inspect real agent sessions locally (tool mix, redundant fetches, Geometra churn) without uploading transcripts, use the `job-forge trace:*` commands. JobForge depends on Razroo's [`@razroo/iso-trace`](https://github.com/razroo/iso/tree/main/packages/iso-trace), so consumer projects do not need to install it separately.
102
102
 
103
- This harness also ships npm scripts (from the package root): `npm run trace:list`, `npm run trace:stats`, and `npm run trace:show -- <id>` — they wrap `iso-trace` with sensible defaults when the CLI is on `PATH`.
103
+ Common commands default to OpenCode sessions for the current project and use a 7-day window:
104
+
105
+ ```bash
106
+ npx job-forge trace:list
107
+ npx job-forge trace:stats
108
+ npx job-forge trace:show <session-id-or-prefix>
109
+ ```
110
+
111
+ Scaffolded projects also include npm aliases: `npm run trace:list`, `npm run trace:stats`, and `npm run trace:show -- <id>`.
112
+
113
+ For raw iso-trace commands, use `npx job-forge trace sources`, `npx job-forge trace where`, or any other `iso-trace` subcommand after `trace`.
104
114
 
105
115
  **Where Claude Code writes JSONL:** `~/.claude/projects/<encoded-cwd>/*.jsonl`.
106
116
 
107
- **CLI (install once per machine):** `npx -y @razroo/iso-trace@latest stats --source "$HOME/.claude/projects/<encoded-dir>/<session>.jsonl"`
117
+ **Direct CLI fallback:** `npx -y @razroo/iso-trace@latest stats --source "$HOME/.claude/projects/<encoded-dir>/<session>.jsonl"`
108
118
 
109
119
  **Performance:** `iso-trace list --cwd /path/to/repo` walks all of `~/.claude/projects` before filtering; on large machines prefer `stats --source <one.jsonl>` or the library's `discoverSessions({ roots: ["<one encoded project dir>"] })` (see the iso-trace README).
110
120
 
package/docs/SETUP.md CHANGED
@@ -130,6 +130,9 @@ From your project root, these commands maintain the tracker and pipeline checks.
130
130
  | Generate ATS-optimized CV PDF | `npx job-forge pdf` | `npm run pdf` |
131
131
  | Setup lint (cv.md + profile.yml) | `npx job-forge sync-check` | `npm run sync-check` |
132
132
  | Token usage report (from opencode SQLite DB) | `npx job-forge tokens` | `npm run tokens` |
133
+ | List recent OpenCode traces for this project | `npx job-forge trace:list` | `npm run trace:list` |
134
+ | Summarize trace tool/file/token usage | `npx job-forge trace:stats` | `npm run trace:stats` |
135
+ | Show one trace by session id/prefix | `npx job-forge trace:show <id>` | `npm run trace:show -- <id>` |
133
136
  | Re-create harness symlinks | `npx job-forge sync` | `npm run sync` |
134
137
  | Build optional dashboard TUI (Go on `PATH`) | `(cd node_modules/job-forge/dashboard && go build .)` | `npm run build:dashboard` (harness repo only) |
135
138
 
@@ -31,6 +31,7 @@ targets:
31
31
  geometra_wait_for_resume_parse: true
32
32
  gmail_list_messages: true
33
33
  gmail_get_message: true
34
+ task: false
34
35
  ---
35
36
 
36
37
  You are the @general-free subagent. You run on a free-tier model, which means the orchestrator has delegated this task to you **specifically because the work is procedural**: deterministic steps, scripted outputs, no nuanced writing required.
@@ -82,6 +83,7 @@ Previous subagents sometimes abort mid-flow (ran out of context, hit a timeout,
82
83
  - Write cover letter prose, "Why X?" answers, or Section G draft answers. Those go to `@general-paid`.
83
84
  - Perform offer evaluation narratives (Blocks A-F). Those go to `@general-paid`.
84
85
  - Override harness rules or invent fields. Follow the mode files exactly.
86
+ - Spawn or check other tasks. If you receive "check if task ses_..." and it refers to this session, report your current status from your own work. Never call `task` recursively.
85
87
 
86
88
  ## Apply This Working Style
87
89
 
@@ -24,8 +24,18 @@ targets:
24
24
  - openrouter/google/gemma-4-31b-it:free
25
25
  - openrouter/meta-llama/llama-3.3-70b-instruct:free
26
26
  tools:
27
- geometra_*: false
28
- gmail_*: false
27
+ geometra_connect: true
28
+ geometra_page_model: true
29
+ geometra_form_schema: true
30
+ geometra_run_actions: true
31
+ geometra_fill_otp: true
32
+ geometra_upload_files: true
33
+ geometra_list_sessions: true
34
+ geometra_disconnect: true
35
+ geometra_wait_for_resume_parse: true
36
+ gmail_list_messages: true
37
+ gmail_get_message: true
38
+ task: false
29
39
  ---
30
40
 
31
41
  You are the @general-paid subagent. The orchestrator delegated this task to you because it requires quality writing or judgment — the kind of work `@general-free` isn't well-suited for.
@@ -42,12 +52,14 @@ were given.
42
52
  - Compose STAR+R interview stories and the story bank (`modes/offer.md` Block F).
43
53
  - Draft LinkedIn outreach messages (`modes/contact.md`).
44
54
  - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, then the narrative report.
55
+ - Drive a single high-stakes application form only when the orchestrator explicitly dispatches you in `apply` mode. In that case, follow `modes/apply.md` exactly and use the same Geometra/Gmail flow as `@general-free`.
45
56
 
46
57
  ## Skip These Tasks
47
58
 
48
- - Drive Geometra forms end-to-end (delegate to `@general-free` or do it yourself only when the orchestrator asks for an atomic one-shot apply).
59
+ - Drive Geometra forms end-to-end unless the task is explicitly an `apply` mode dispatch for one job.
49
60
  - Manage trackers, run scripts, or do mechanical TSV/dedup work. Those go to `@general-free`.
50
61
  - Duplicate work. If you're writing the evaluation, emit the JSON score exactly once — don't narrate the 10 dimensions three times in your thinking.
62
+ - Spawn or check other tasks. If you receive "check if task ses_..." and it refers to this session, report your current status from your own work. Never call `task` recursively.
51
63
 
52
64
  ## Apply This Working Style
53
65
 
@@ -155,10 +155,17 @@ Step 3 — Pre-flight cleanup (once, before the loop)
155
155
  Step 4 — Loop in rounds of 2 (Hard Limit #1)
156
156
  for round in ceil(len(candidates) / 2):
157
157
  pair = candidates[round*2 : round*2 + 2]
158
+ # If proxy is configured, do not paste proxy values into prompts.
159
+ # Say: "Proxy is configured; read config/profile.yml and pass its
160
+ # top-level proxy object to every geometra_connect call."
158
161
  # Dispatch 1 or 2 task() calls in ONE message (never 3+)
159
162
  task(subagent_type=<tier per AGENTS.md routing>, prompt=<apply prompt for pair[0]>)
160
163
  task(subagent_type=<tier>, prompt=<apply prompt for pair[1]>) # only if pair has 2
161
- # WAIT for both subagents to return before proceeding
164
+ # WAIT for both subagents to return final APPLIED / APPLY FAILED / SKIP /
165
+ # Discarded outcomes or TSV paths before proceeding.
166
+ # A returned task/session id is only a launch receipt, not completion.
167
+ # Do not create a "check task status" task; inspect tracker files or
168
+ # iso-trace if the user asks for status later.
162
169
  # Read their return values, log outcomes
163
170
 
164
171
  Step 5 — Between rounds: clean sessions again
@@ -4,8 +4,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
4
4
 
5
5
  ## Hard limits
6
6
 
7
- - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
8
- why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result
7
+ - [H1] Max 2 parallel `task` dispatches per message. For N jobs, run `ceil(N/2)` sequential rounds of 2. A round is not complete until both subagents return a final outcome (`APPLIED`, `APPLY FAILED`, `SKIP`, `Discarded`, or a written TSV path). A `task` tool result that only gives a session id / title is a launch acknowledgement, not completion. Applies in all modes, for all user phrasings ("urgent", "apply to 10 jobs now").
8
+ why: higher parallelism blows through free-tier rate limits; each subagent requires post-cleanup and racing more than 2 reliably loses at least one result. On 2026-04-25 the orchestrator launched round 2 while round 1 had only returned task ids, leaving four application subagents in flight and losing two provider-fallback recoveries
9
9
 
10
10
  - [H2] Max 1 application per company+role. Before every `apply` dispatch, grep all four sources for the URL and for `company+role`: `data/pipeline.md`, all `data/applications/*.md` day files, `batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`. If any source shows APPLIED / Applied, skip the dispatch.
11
11
  why: 2026-04 same-day batch collision — when two batches target the same role, `npx job-forge merge` updates the existing day-file row rather than appending, so grepping day files alone misses earlier-batch applies; merged/*.tsv is the only place the breadcrumb remains
@@ -19,12 +19,18 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
19
19
  - [H5] Re-dispatch the same company only AFTER the previous subagent returns. Never fire the same `task` twice while the first is still in flight.
20
20
  why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
21
21
 
22
+ - [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, or `iso-trace`) rather than spawning a "check task status" subagent.
23
+ why: OpenCode status prompts can be delivered into the target subagent as a new user message; a 2026-04-25 trace caused a subagent to call `task` recursively instead of finishing the application
24
+
22
25
  - [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
23
26
  why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
24
27
 
25
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, `batch/tracker-additions/*.tsv`.
26
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
27
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 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
33
+
28
34
  ## Defaults
29
35
 
30
36
  - [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
@@ -50,8 +56,8 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
50
56
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
51
57
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
52
58
  3. Read the active mode file [D3]; decide inline vs delegated work [D1].
53
- 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2].
54
- 5. Dispatch at most 2 tasks per round [H1]; wait per company [H5].
59
+ 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
60
+ 5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
55
61
  6. Keep multi-job form-filling out of the orchestrator [H4].
56
62
  7. Cross-check subagent facts against authoritative files [H7].
57
63
  8. Apply score gate [D4].
package/modes/apply.md CHANGED
@@ -16,8 +16,8 @@ Live application assistant. Reads the active application form in Chrome (via Geo
16
16
  - [H4] Before dispatching the first subagent in a multi-job run, the orchestrator MUST call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. Every dispatch-round, no exceptions.
17
17
  why: prior aborted subagents leave Chromium sessions stuck in the pool; next `geometra_connect` fails with "Not connected" (see root `[H3]`)
18
18
 
19
- - [H5] Max 2 parallel `task` dispatches per round. For N jobs, run `ceil(N/2)` sequential rounds of 2. Never emit 3+ dispatches in a single message.
20
- why: free-tier rate limits + subagent post-cleanup cost; racing more than 2 reliably loses at least one result (see root `[H1]`)
19
+ - [H5] Max 2 parallel `task` dispatches per round. For N jobs, run `ceil(N/2)` sequential rounds of 2. Never emit 3+ dispatches in a single message. Do not start the next round until both current-round subagents return final outcomes (`APPLIED`, `APPLY FAILED`, `SKIP`, `Discarded`, or a written TSV path); task/session ids are only launch receipts.
20
+ why: free-tier rate limits + subagent post-cleanup cost; racing more than 2 reliably loses at least one result (see root `[H1]`). A 2026-04-25 OpenCode trace launched round 2 while round 1 was still running, then lost two fallback recoveries
21
21
 
22
22
  ## Defaults
23
23
 
@@ -42,7 +42,7 @@ 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's task prompt includes a `proxy` object (sourced from `config/profile.yml`), pass it verbatim into every `geometra_connect` call — including Call 3 of the recovery sequence. If absent, run without one; never invent a proxy URL.
45
+ - [D7] If the orchestrator says a proxy is configured, read the top-level `proxy:` block from `config/profile.yml` and pass that object 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, but do not echo credentials in status text. If absent, run without one; never invent a proxy URL.
46
46
  why: class-B Ashby / Cloudflare-fronted portals need a residential outbound IP; the fix is wired in Geometra MCP v1.59.0 but 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.
@@ -115,6 +115,13 @@ Sections below are the detailed runbooks, decision tables, and portal-specific e
115
115
 
116
116
  **DO NOT dispatch 3+ `task` calls in one message.** Two is the absolute ceiling. This is non-negotiable, even when the user asks for "apply to 10 jobs" — that becomes 5 rounds of 2, not one message with 10 dispatches.
117
117
 
118
+ **A task/session id is not a result.** If OpenCode gives you a `ses_...`
119
+ id or title after dispatch, do not treat that as the subagent return.
120
+ Do not create another `task` to check it. Stop the round, report the
121
+ in-flight ids, and resume only after a real outcome is visible in the
122
+ subagent return or in an authoritative file (`batch/tracker-additions/`,
123
+ `batch/tracker-additions/merged/`, or the day file).
124
+
118
125
  For a single application interactively, carry on in the current session — the rule targets multi-job loops.
119
126
 
120
127
  ## Apply Preflight — Location Filter (orchestrator runs before dispatch)
@@ -176,7 +183,8 @@ Step 4 — For round in ceil(N/2):
176
183
  # ONE message, 1 or 2 task() calls. Never 3.
177
184
  task(apply to pair[0])
178
185
  task(apply to pair[1]) # only if pair has 2
179
- # WAIT for both returns. Do not proceed until both done.
186
+ # WAIT for both final outcomes. A session id is not completion.
187
+ # Do not dispatch round N+1 while round N is still in flight.
180
188
  Step 5 — Between rounds: geometra_list_sessions() + geometra_disconnect({closeBrowser: true})
181
189
  Step 6 — Reconcile outcomes (Hard Limit #6):
182
190
  bash: npx job-forge merge # TSVs → day file
@@ -188,7 +188,8 @@ Step 2: geometra_disconnect({ closeBrowser: true })
188
188
  Step 3: geometra_connect({ pageUrl: "<the URL the orchestrator gave you>", isolated: true, headless: true, slowMo: 350 })
189
189
  ```
190
190
 
191
- **If the orchestrator passed a `proxy` object in the task prompt** (sourced from `config/profile.yml`), add it to Step 3:
191
+ **If the orchestrator says proxy is configured,** read the top-level
192
+ `proxy:` block from `config/profile.yml` and add it to Step 3:
192
193
 
193
194
  ```
194
195
  Step 3: geometra_connect({
@@ -197,7 +198,7 @@ Step 3: geometra_connect({
197
198
  })
198
199
  ```
199
200
 
200
- Pass the proxy object through unchanged. Do NOT paraphrase or drop fields — `username`/`password`/`bypass` are optional, so only include what the orchestrator gave you. See the "BYO Residential Proxy" reference section for the why.
201
+ Pass the proxy object through unchanged. Do NOT paraphrase or drop fields — `username`/`password`/`bypass` are optional, so only include what exists in `config/profile.yml`. Do not echo proxy credentials in status text. See the "BYO Residential Proxy" reference section for the why.
201
202
 
202
203
  **DO NOT** skip Step 1 or Step 2. **DO NOT** think about whether it's needed. **DO NOT** look at `geometra_list_sessions` output and reason about it — just always call `geometra_disconnect({ closeBrowser: true })` next. The disconnect is a no-op if the pool is empty, and a poison-cure if it isn't.
203
204
 
@@ -75,16 +75,17 @@ See `config/profile.example.yml` for the commented-out template.
75
75
 
76
76
  **Orchestrator responsibilities:**
77
77
 
78
- 1. On session start, read `config/profile.yml` once. If a `proxy:` block is present, capture it as the `PROXY_CONFIG` for the session.
79
- 2. When dispatching any subagent whose work involves a `geometra_connect` call, include `PROXY_CONFIG` verbatim in the task prompt. Example dispatch prompt line: "Pass `proxy: { server: ..., username: ..., password: ..., bypass: ... }` to every `geometra_connect` call you make."
80
- 3. When the orchestrator itself opens a Chromium session (single-application interactive flow), include the same `proxy` object in its own `geometra_connect` call.
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 to every `geometra_connect` call. Example dispatch prompt line: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object 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` 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 includes a `proxy` object, pass it through to `geometra_connect` and any `geometra_prepare_browser` calls unchanged.
86
- 2. If the task prompt does NOT include a proxy object, run without one.
87
- 3. Never second-guess the proxy field if the orchestrator sourced it from `profile.yml`, it's authoritative.
85
+ 1. If the task prompt says proxy is configured, read `config/profile.yml` and pass the top-level `proxy:` object 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, 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 without one.
88
+ 4. Never second-guess the proxy field — if it comes from `profile.yml`, it's authoritative.
88
89
 
89
90
  ### When proxy use is load-bearing
90
91
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-forge",
3
- "version": "2.14.8",
3
+ "version": "2.14.10",
4
4
  "description": "AI-powered job search pipeline built on opencode",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,9 +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
- "trace:list": "iso-trace list --since 7d --cwd .",
22
- "trace:stats": "iso-trace stats --since 7d --cwd .",
23
- "trace:show": "iso-trace show",
21
+ "trace:list": "node bin/job-forge.mjs trace:list",
22
+ "trace:stats": "node bin/job-forge.mjs trace:stats",
23
+ "trace:show": "node bin/job-forge.mjs trace:show",
24
24
  "plan": "iso plan .",
25
25
  "lint:agentmd": "agentmd lint iso/instructions.md",
26
26
  "lint:modes": "isolint lint modes/",
@@ -85,6 +85,7 @@
85
85
  "node": ">=18"
86
86
  },
87
87
  "dependencies": {
88
+ "@razroo/iso-trace": "^0.4.0",
88
89
  "playwright": "^1.58.1"
89
90
  },
90
91
  "devDependencies": {
@@ -93,7 +94,6 @@
93
94
  "@razroo/iso-eval": "^0.4.0",
94
95
  "@razroo/iso-harness": "^0.6.1",
95
96
  "@razroo/iso-route": "^0.5.3",
96
- "@razroo/iso-trace": "^0.4.0",
97
97
  "@razroo/opencode-model-fallback": "^0.3.1"
98
98
  }
99
99
  }
@@ -0,0 +1,469 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'child_process';
4
+ import { existsSync } from 'fs';
5
+ import { createRequire } from 'module';
6
+ import { dirname, join, resolve } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import {
9
+ defaultOpenCodeDbPath,
10
+ findSessionById,
11
+ openCodeSessionLocator,
12
+ parseSinceCutoff,
13
+ } from '@razroo/iso-trace';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const require = createRequire(import.meta.url);
17
+ const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
18
+
19
+ const USAGE = `job-forge trace — local OpenCode transcript observability
20
+
21
+ Usage:
22
+ job-forge trace:list [--since 7d] [--cwd <dir>] [--json]
23
+ job-forge trace:stats [<id-or-prefix>...] [--since 7d] [--cwd <dir>] [--json]
24
+ job-forge trace:show <id-or-prefix> [--events <kinds>] [--grep <regex>]
25
+ job-forge trace <iso-trace args...>
26
+
27
+ Common aliases default to OpenCode sessions for the current JobForge project.
28
+ Use "job-forge trace sources" or "job-forge trace where" for raw iso-trace passthrough.`;
29
+
30
+ const [cmd = 'help', ...args] = process.argv.slice(2);
31
+
32
+ function parseFilters(rawArgs) {
33
+ const opts = { since: '7d', cwd: PROJECT_DIR, json: false };
34
+ const positional = [];
35
+
36
+ for (let i = 0; i < rawArgs.length; i++) {
37
+ const arg = rawArgs[i];
38
+ if (arg === '--since') {
39
+ opts.since = rawArgs[++i];
40
+ } else if (arg.startsWith('--since=')) {
41
+ opts.since = arg.slice('--since='.length);
42
+ } else if (arg === '--cwd') {
43
+ opts.cwd = rawArgs[++i];
44
+ } else if (arg.startsWith('--cwd=')) {
45
+ opts.cwd = arg.slice('--cwd='.length);
46
+ } else if (arg === '--json') {
47
+ opts.json = true;
48
+ } else if (arg === '--help' || arg === '-h') {
49
+ opts.help = true;
50
+ } else if (arg.startsWith('--')) {
51
+ opts.error = `unknown flag "${arg}"`;
52
+ } else {
53
+ positional.push(arg);
54
+ }
55
+ }
56
+
57
+ opts.cwd = resolve(opts.cwd || PROJECT_DIR);
58
+ return { opts, positional };
59
+ }
60
+
61
+ function parseShowArgs(rawArgs) {
62
+ const opts = {};
63
+ const positional = [];
64
+
65
+ for (let i = 0; i < rawArgs.length; i++) {
66
+ const arg = rawArgs[i];
67
+ if (arg === '--events') {
68
+ const raw = rawArgs[++i] || '';
69
+ opts.events = new Set(raw.split(',').map((s) => s.trim()).filter(Boolean));
70
+ } else if (arg.startsWith('--events=')) {
71
+ const raw = arg.slice('--events='.length);
72
+ opts.events = new Set(raw.split(',').map((s) => s.trim()).filter(Boolean));
73
+ } else if (arg === '--grep') {
74
+ opts.grep = compileRegex(rawArgs[++i], 'trace:show');
75
+ } else if (arg.startsWith('--grep=')) {
76
+ opts.grep = compileRegex(arg.slice('--grep='.length), 'trace:show');
77
+ } else if (arg === '--help' || arg === '-h') {
78
+ opts.help = true;
79
+ } else if (arg.startsWith('--')) {
80
+ opts.error = `unknown flag "${arg}"`;
81
+ } else {
82
+ positional.push(arg);
83
+ }
84
+ }
85
+
86
+ return { opts, positional };
87
+ }
88
+
89
+ function compileRegex(pattern, context) {
90
+ try {
91
+ return new RegExp(pattern || '', 'i');
92
+ } catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ return new Error(`${context}: invalid --grep regex: ${message}`);
95
+ }
96
+ }
97
+
98
+ async function discoverOpenCodeRefs(opts) {
99
+ const dbPath = defaultOpenCodeDbPath();
100
+ if (!existsSync(dbPath)) return [];
101
+
102
+ const where = [
103
+ 's.time_archived is null',
104
+ `s.directory = ${sqlString(resolve(opts.cwd || PROJECT_DIR))}`,
105
+ ];
106
+ const sinceMs = parseSinceCutoff(opts.since);
107
+ if (sinceMs !== undefined) {
108
+ where.push(`s.time_created >= ${Number(sinceMs)}`);
109
+ }
110
+
111
+ const rows = queryOpenCodeDb(dbPath, [
112
+ 'select',
113
+ ' s.id,',
114
+ ' s.directory,',
115
+ ' s.time_created,',
116
+ ' s.time_updated,',
117
+ ' (select count(*) from message m where m.session_id = s.id) as turn_count,',
118
+ ' (',
119
+ ' (select coalesce(sum(length(data)), 0) from message m where m.session_id = s.id) +',
120
+ ' (select coalesce(sum(length(data)), 0) from part p where p.session_id = s.id)',
121
+ ' ) as size_bytes',
122
+ 'from session s',
123
+ `where ${where.join(' and ')}`,
124
+ 'order by s.time_updated desc',
125
+ ].join(' '));
126
+
127
+ return rows.map((row) => ({
128
+ id: row.id,
129
+ source: {
130
+ harness: 'opencode',
131
+ format: 'opencode/sqlite-v1',
132
+ path: openCodeSessionLocator(row.id, dbPath),
133
+ },
134
+ cwd: row.directory,
135
+ startedAt: msToIso(row.time_created),
136
+ endedAt: msToIso(row.time_updated),
137
+ turnCount: row.turn_count ?? 0,
138
+ sizeBytes: row.size_bytes ?? 0,
139
+ }));
140
+ }
141
+
142
+ function queryOpenCodeDb(dbPath, sql) {
143
+ const result = spawnSync('sqlite3', ['-json', dbPath, sql], {
144
+ encoding: 'utf8',
145
+ maxBuffer: 16 * 1024 * 1024,
146
+ });
147
+ if ((result.status ?? 0) !== 0) {
148
+ const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? 1}`;
149
+ throw new Error(`job-forge trace: sqlite3 query failed: ${detail}`);
150
+ }
151
+ return JSON.parse(result.stdout || '[]');
152
+ }
153
+
154
+ function sqlString(value) {
155
+ return `'${String(value).replaceAll("'", "''")}'`;
156
+ }
157
+
158
+ function msToIso(ms) {
159
+ return new Date(Number(ms)).toISOString();
160
+ }
161
+
162
+ function sizeLabel(bytes) {
163
+ if (bytes < 1024) return `${bytes} B`;
164
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
165
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
166
+ }
167
+
168
+ function shorten(value, max) {
169
+ const text = String(value ?? '');
170
+ if (text.length <= max) return text;
171
+ return `${text.slice(0, max - 1)}...`;
172
+ }
173
+
174
+ function pad(value, width) {
175
+ const text = String(value ?? '');
176
+ return text.length >= width ? text : text + ' '.repeat(width - text.length);
177
+ }
178
+
179
+ function printSessionTable(refs) {
180
+ const rows = refs.map((ref) => [
181
+ ref.id,
182
+ ref.startedAt.replace('T', ' ').replace(/\.\d+Z$/, 'Z'),
183
+ shorten(ref.cwd, 42),
184
+ String(ref.turnCount),
185
+ sizeLabel(ref.sizeBytes),
186
+ ]);
187
+ const header = ['id', 'started', 'cwd', 'turns', 'size'];
188
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
189
+
190
+ console.log(header.map((h, i) => pad(h, widths[i])).join(' '));
191
+ console.log(widths.map((w) => '-'.repeat(w)).join(' '));
192
+ for (const row of rows) {
193
+ console.log(row.map((cell, i) => pad(cell, widths[i])).join(' '));
194
+ }
195
+ }
196
+
197
+ function printStats(result) {
198
+ console.log(`sessions: ${result.sessions}`);
199
+ console.log(`turns: ${result.turns}`);
200
+ console.log(`duration: ${Math.round(result.durationMs / 1000)}s`);
201
+ console.log(`tokens: input=${result.tokens.input} output=${result.tokens.output} cache_read=${result.tokens.cacheRead} cache_created=${result.tokens.cacheCreated}`);
202
+
203
+ console.log('\ntool calls:');
204
+ for (const [name, count] of Object.entries(result.toolCalls).sort((a, b) => b[1] - a[1])) {
205
+ console.log(` ${pad(name, 28)} ${count}`);
206
+ }
207
+
208
+ console.log('\nfile ops:');
209
+ for (const [name, count] of Object.entries(result.fileOps).sort((a, b) => b[1] - a[1])) {
210
+ console.log(` ${pad(name, 8)} ${count}`);
211
+ }
212
+ }
213
+
214
+ function computeOpenCodeStats(refs) {
215
+ const result = {
216
+ sessions: refs.length,
217
+ turns: 0,
218
+ durationMs: 0,
219
+ tokens: { input: 0, output: 0, cacheRead: 0, cacheCreated: 0 },
220
+ toolCalls: {},
221
+ fileOps: {},
222
+ };
223
+
224
+ for (const ref of refs) {
225
+ const rows = loadOpenCodeRows(ref.id);
226
+ result.turns += rows.messages.length;
227
+ result.durationMs += Math.max(0, Date.parse(ref.endedAt || ref.startedAt) - Date.parse(ref.startedAt));
228
+
229
+ for (const row of rows.messages) {
230
+ const data = parseJson(row.data);
231
+ const tokens = data.tokens;
232
+ if (!tokens || typeof tokens !== 'object') continue;
233
+ result.tokens.input += Number(tokens.input || 0);
234
+ result.tokens.output += Number(tokens.output || 0);
235
+ result.tokens.cacheRead += Number(tokens.cache?.read || 0);
236
+ result.tokens.cacheCreated += Number(tokens.cache?.write || 0);
237
+ }
238
+
239
+ for (const row of rows.parts) {
240
+ const data = parseJson(row.data);
241
+ if (data.type !== 'tool') continue;
242
+ const toolName = data.tool || 'unknown';
243
+ result.toolCalls[toolName] = (result.toolCalls[toolName] || 0) + 1;
244
+ const op = fileOpForTool(toolName);
245
+ if (op) result.fileOps[op] = (result.fileOps[op] || 0) + 1;
246
+ }
247
+ }
248
+
249
+ return result;
250
+ }
251
+
252
+ function printOpenCodeSession(ref, opts) {
253
+ const rows = loadOpenCodeRows(ref.id);
254
+ console.log(`id: ${ref.id}`);
255
+ console.log(`source: ${ref.source.harness} (${ref.source.format})`);
256
+ console.log(`path: ${ref.source.path}`);
257
+ console.log(`cwd: ${ref.cwd}`);
258
+ console.log(`started: ${ref.startedAt}`);
259
+ if (ref.endedAt) console.log(`ended: ${ref.endedAt}`);
260
+ console.log(`turns: ${rows.messages.length}`);
261
+ console.log('');
262
+
263
+ const events = openCodeEvents(rows);
264
+ for (const event of events) {
265
+ if (opts.events && !opts.events.has(event.kind)) continue;
266
+ const line = formatOpenCodeEvent(event);
267
+ if (opts.grep && !opts.grep.test(line)) continue;
268
+ console.log(line);
269
+ }
270
+ }
271
+
272
+ function loadOpenCodeRows(sessionId) {
273
+ const dbPath = defaultOpenCodeDbPath();
274
+ const id = sqlString(sessionId);
275
+ return {
276
+ messages: queryOpenCodeDb(dbPath, `select id, time_created, data from message where session_id = ${id} order by time_created, id`),
277
+ parts: queryOpenCodeDb(dbPath, `select id, message_id, time_created, data from part where session_id = ${id} order by time_created, id`),
278
+ };
279
+ }
280
+
281
+ function openCodeEvents(rows) {
282
+ const events = [];
283
+
284
+ for (const row of rows.messages) {
285
+ const data = parseJson(row.data);
286
+ const at = msToIso(row.time_created);
287
+ const model = data.modelID && data.providerID ? `${data.providerID}/${data.modelID}` : undefined;
288
+ const error = data.error?.data?.message || data.error?.message;
289
+ events.push({
290
+ kind: error ? 'error' : 'turn',
291
+ at,
292
+ text: error
293
+ ? `${data.role || 'assistant'} ${data.agent || ''} ${model || ''}: ${error}`
294
+ : `${data.role || 'unknown'} ${data.agent || ''} ${model || ''} finish=${data.finish || 'unknown'}`,
295
+ });
296
+ if (data.tokens) {
297
+ events.push({
298
+ kind: 'token_usage',
299
+ at,
300
+ text: `input=${data.tokens.input || 0} output=${data.tokens.output || 0} cache_read=${data.tokens.cache?.read || 0} cache_created=${data.tokens.cache?.write || 0}${model ? ` model=${model}` : ''}`,
301
+ });
302
+ }
303
+ }
304
+
305
+ for (const row of rows.parts) {
306
+ const data = parseJson(row.data);
307
+ const at = msToIso(row.time_created);
308
+ if (data.type === 'text') {
309
+ events.push({ kind: 'message', at, text: data.text || '' });
310
+ } else if (data.type === 'reasoning') {
311
+ events.push({ kind: 'reasoning', at, text: data.text || '' });
312
+ } else if (data.type === 'tool') {
313
+ const status = data.state?.status ? ` status=${data.state.status}` : '';
314
+ const input = data.state?.input ? ` ${JSON.stringify(data.state.input)}` : '';
315
+ const output = data.state?.output ? ` => ${data.state.output}` : '';
316
+ events.push({ kind: 'tool_call', at, text: `${data.tool || 'unknown'}${status}${input}${output}` });
317
+ const op = fileOpForTool(data.tool);
318
+ if (op) events.push({ kind: 'file_op', at, text: `${op} ${filePathFromTool(data) || ''}`.trim() });
319
+ } else if (data.__parseError) {
320
+ events.push({ kind: 'error', at, text: `unparseable part JSON: ${data.__parseError}` });
321
+ } else {
322
+ events.push({ kind: data.type || 'part', at, text: JSON.stringify(data) });
323
+ }
324
+ }
325
+
326
+ return events.sort((a, b) => a.at.localeCompare(b.at));
327
+ }
328
+
329
+ function formatOpenCodeEvent(event) {
330
+ return `${event.at} ${event.kind}: ${oneLine(event.text, 360)}`;
331
+ }
332
+
333
+ function parseJson(raw) {
334
+ try {
335
+ return JSON.parse(raw || '{}');
336
+ } catch (error) {
337
+ const message = error instanceof Error ? error.message : String(error);
338
+ return { __parseError: message, __raw: raw };
339
+ }
340
+ }
341
+
342
+ function fileOpForTool(toolName) {
343
+ if (toolName === 'read') return 'read';
344
+ if (toolName === 'write') return 'write';
345
+ if (toolName === 'edit') return 'edit';
346
+ if (toolName === 'glob') return 'list';
347
+ if (toolName === 'grep') return 'search';
348
+ return undefined;
349
+ }
350
+
351
+ function filePathFromTool(part) {
352
+ const input = part.state?.input;
353
+ if (!input || typeof input !== 'object') return undefined;
354
+ return input.filePath || input.path || input.pattern;
355
+ }
356
+
357
+ function oneLine(value, max) {
358
+ return shorten(String(value ?? '').replace(/\s+/g, ' ').trim(), max);
359
+ }
360
+
361
+ function resolveIsoTraceCli() {
362
+ const pkgJsonPath = require.resolve('@razroo/iso-trace/package.json');
363
+ return join(dirname(pkgJsonPath), 'dist/cli.js');
364
+ }
365
+
366
+ function passthroughIsoTrace(rawArgs) {
367
+ const cliPath = resolveIsoTraceCli();
368
+ const result = spawnSync(process.execPath, [cliPath, ...rawArgs], {
369
+ stdio: 'inherit',
370
+ cwd: PROJECT_DIR,
371
+ env: process.env,
372
+ });
373
+ return result.status ?? 1;
374
+ }
375
+
376
+ async function main() {
377
+ if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
378
+ console.log(USAGE);
379
+ return 0;
380
+ }
381
+
382
+ if (cmd === 'list') {
383
+ const { opts } = parseFilters(args);
384
+ if (opts.help) {
385
+ console.log(USAGE);
386
+ return 0;
387
+ }
388
+ if (opts.error) {
389
+ console.error(`job-forge trace:list: ${opts.error}`);
390
+ return 2;
391
+ }
392
+ const refs = await discoverOpenCodeRefs(opts);
393
+ if (opts.json) {
394
+ console.log(JSON.stringify(refs, null, 2));
395
+ return 0;
396
+ }
397
+ if (refs.length === 0) {
398
+ console.error('job-forge trace:list: no OpenCode sessions found for this project');
399
+ return 2;
400
+ }
401
+ printSessionTable(refs);
402
+ return 0;
403
+ }
404
+
405
+ if (cmd === 'stats') {
406
+ const { opts, positional } = parseFilters(args);
407
+ if (opts.help) {
408
+ console.log(USAGE);
409
+ return 0;
410
+ }
411
+ if (opts.error) {
412
+ console.error(`job-forge trace:stats: ${opts.error}`);
413
+ return 2;
414
+ }
415
+ const refs = await discoverOpenCodeRefs(opts);
416
+ const selected = positional.length === 0
417
+ ? refs
418
+ : positional.map((id) => {
419
+ const ref = findSessionById(refs, id);
420
+ if (!ref) throw new Error(`job-forge trace:stats: no OpenCode session matches "${id}"`);
421
+ return ref;
422
+ });
423
+ const result = computeOpenCodeStats(selected);
424
+ if (opts.json) {
425
+ console.log(JSON.stringify(result, null, 2));
426
+ } else {
427
+ printStats(result);
428
+ }
429
+ return 0;
430
+ }
431
+
432
+ if (cmd === 'show') {
433
+ const { opts, positional } = parseShowArgs(args);
434
+ if (opts.help) {
435
+ console.log(USAGE);
436
+ return 0;
437
+ }
438
+ if (opts.error) {
439
+ console.error(`job-forge trace:show: ${opts.error}`);
440
+ return 2;
441
+ }
442
+ if (opts.grep instanceof Error) {
443
+ console.error(opts.grep.message);
444
+ return 2;
445
+ }
446
+ if (positional.length === 0) {
447
+ console.error('job-forge trace:show: missing <id-or-prefix>');
448
+ return 2;
449
+ }
450
+ const refs = await discoverOpenCodeRefs({ cwd: PROJECT_DIR, since: undefined });
451
+ const ref = findSessionById(refs, positional[0]);
452
+ if (!ref) {
453
+ console.error(`job-forge trace:show: no OpenCode session matches "${positional[0]}"`);
454
+ return 2;
455
+ }
456
+ printOpenCodeSession(ref, opts);
457
+ return 0;
458
+ }
459
+
460
+ return passthroughIsoTrace([cmd, ...args]);
461
+ }
462
+
463
+ main()
464
+ .then((code) => process.exit(code))
465
+ .catch((error) => {
466
+ const message = error instanceof Error ? error.message : String(error);
467
+ console.error(message);
468
+ process.exit(1);
469
+ });