job-forge 2.14.15 → 2.14.17

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.
@@ -12,7 +12,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
12
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
13
  why: 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 recoveries
14
14
 
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 and pick a replacement from the remaining candidate list. Do not count duplicates toward a requested "apply to N jobs" total, and do not delegate obvious duplicates just so a subagent can return SKIP.
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 `.jobforge-ledger/events.jsonl` exists, `npx job-forge ledger:has --company "..." --role "..." --status Applied` may be used as a fast prefilter; a match is enough to drop that duplicate before dispatch. For candidates not rejected by the ledger, the four-source grep is still mandatory. If any source shows APPLIED / Applied, skip the dispatch and pick a replacement from the remaining candidate list. Do not count duplicates toward a requested "apply to N jobs" total, and do not delegate obvious duplicates just so a subagent can return SKIP.
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
17
17
 
18
18
  - [H3] Before every batch of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. Every round, no exceptions. Name this cleanup as an explicit "step 0" in your first-response plan for any multi-apply request — it is the most frequently skipped guardrail in practice, and skipping it produces cascade "Not connected" failures on the next dispatch.
@@ -24,7 +24,7 @@ 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.
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, `.jobforge-ledger/events.jsonl`, or `iso-trace`) rather than spawning a "check task status" subagent.
28
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
29
 
30
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.
@@ -59,17 +59,23 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
59
59
  - [D7] For standalone `batch` runs, prefer `batch/batch-runner.sh` instead of hand-rolling the loop. It delegates to `@razroo/iso-orchestrator`, persists workflow records in `.jobforge-runs/`, caps bundle fan-out, and mutexes state/report-number writes. Use `JOBFORGE_LEGACY_BATCH_RUNNER=1` only as a fallback.
60
60
  why: the old Bash loop encoded resumability and parallelism manually; the iso-orchestrator path makes the durable control state inspectable and prevents report-number collisions under parallel bundles
61
61
 
62
+ - [D8] Use `job-forge ledger:*` for cheap local workflow-state checks when available. `iso-ledger` is not an MCP and adds no prompt/tool schema tokens; it records tracker TSV writes, merge outcomes, rebuilt tracker snapshots, and pipeline items in `.jobforge-ledger/events.jsonl`.
63
+ why: state-trace remains working memory, while iso-ledger is deterministic append-only workflow truth that can answer duplicate/status questions without loading growing markdown/TSV files into the model context
64
+
65
+ - [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
66
+ why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
67
+
62
68
  ## Procedure
63
69
 
64
70
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
65
71
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
66
72
  3. Read the active mode file [D3]; decide inline vs delegated work [D1].
67
- 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
73
+ 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
68
74
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
69
75
  6. Keep multi-job form-filling out of the orchestrator [H4].
70
76
  7. Cross-check subagent facts against authoritative files [H7].
71
77
  8. Apply score gate [D4].
72
- 9. Merge TSV outcomes [H6].
78
+ 9. Merge contract-validated TSV outcomes [H6, D9].
73
79
  10. Verify tracker before ending [H6].
74
80
 
75
81
  ## Routing
@@ -68,6 +68,14 @@ Or paste a JD directly to run the full pipeline.
68
68
  Token usage check (terminal, outside opencode):
69
69
  npx job-forge tokens --days 1 # today's sessions with input/cache breakdown
70
70
  npx job-forge tokens --session <id> # drill into one session for cache-bust hunting
71
+
72
+ Local workflow ledger (terminal, outside opencode):
73
+ npx job-forge ledger:status # .jobforge-ledger/events.jsonl summary
74
+ npx job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied
75
+
76
+ Artifact contracts (terminal, outside opencode):
77
+ npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json
78
+ npx job-forge tracker-line ... --write # renders + validates tracker TSV locally
71
79
  ```
72
80
 
73
81
  ---
@@ -142,6 +150,9 @@ Step 1 — Enumerate candidates
142
150
  - Build ordered list: candidates = [job_1, job_2, ..., job_N]
143
151
 
144
152
  Step 2 — Dedup against already-applied
153
+ - If .jobforge-ledger/events.jsonl exists, use npx job-forge ledger:has as a
154
+ fast prefilter for obvious company+role Applied duplicates. A ledger match
155
+ can be dropped before dispatch without loading tracker files into context.
145
156
  - For each candidate, grep all four sources for URL and company+role:
146
157
  data/pipeline.md, data/applications/*.md, batch/tracker-additions/*.tsv,
147
158
  batch/tracker-additions/merged/*.tsv
package/AGENTS.md CHANGED
@@ -7,7 +7,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
7
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
8
  why: 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 recoveries
9
9
 
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 and pick a replacement from the remaining candidate list. Do not count duplicates toward a requested "apply to N jobs" total, and do not delegate obvious duplicates just so a subagent can return SKIP.
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 `.jobforge-ledger/events.jsonl` exists, `npx job-forge ledger:has --company "..." --role "..." --status Applied` may be used as a fast prefilter; a match is enough to drop that duplicate before dispatch. For candidates not rejected by the ledger, the four-source grep is still mandatory. If any source shows APPLIED / Applied, skip the dispatch and pick a replacement from the remaining candidate list. Do not count duplicates toward a requested "apply to N jobs" total, and do not delegate obvious duplicates just so a subagent can return SKIP.
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
12
12
 
13
13
  - [H3] Before every batch of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. Every round, no exceptions. Name this cleanup as an explicit "step 0" in your first-response plan for any multi-apply request — it is the most frequently skipped guardrail in practice, and skipping it produces cascade "Not connected" failures on the next dispatch.
@@ -19,7 +19,7 @@ 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.
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, `.jobforge-ledger/events.jsonl`, or `iso-trace`) rather than spawning a "check task status" subagent.
23
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
24
 
25
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.
@@ -54,17 +54,23 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
54
54
  - [D7] For standalone `batch` runs, prefer `batch/batch-runner.sh` instead of hand-rolling the loop. It delegates to `@razroo/iso-orchestrator`, persists workflow records in `.jobforge-runs/`, caps bundle fan-out, and mutexes state/report-number writes. Use `JOBFORGE_LEGACY_BATCH_RUNNER=1` only as a fallback.
55
55
  why: the old Bash loop encoded resumability and parallelism manually; the iso-orchestrator path makes the durable control state inspectable and prevents report-number collisions under parallel bundles
56
56
 
57
+ - [D8] Use `job-forge ledger:*` for cheap local workflow-state checks when available. `iso-ledger` is not an MCP and adds no prompt/tool schema tokens; it records tracker TSV writes, merge outcomes, rebuilt tracker snapshots, and pipeline items in `.jobforge-ledger/events.jsonl`.
58
+ why: state-trace remains working memory, while iso-ledger is deterministic append-only workflow truth that can answer duplicate/status questions without loading growing markdown/TSV files into the model context
59
+
60
+ - [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
61
+ why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
62
+
57
63
  ## Procedure
58
64
 
59
65
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
60
66
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
61
67
  3. Read the active mode file [D3]; decide inline vs delegated work [D1].
62
- 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
68
+ 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
63
69
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
64
70
  6. Keep multi-job form-filling out of the orchestrator [H4].
65
71
  7. Cross-check subagent facts against authoritative files [H7].
66
72
  8. Apply score gate [D4].
67
- 9. Merge TSV outcomes [H6].
73
+ 9. Merge contract-validated TSV outcomes [H6, D9].
68
74
  10. Verify tracker before ending [H6].
69
75
 
70
76
  ## Routing
package/CLAUDE.md CHANGED
@@ -7,7 +7,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
7
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
8
  why: 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 recoveries
9
9
 
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 and pick a replacement from the remaining candidate list. Do not count duplicates toward a requested "apply to N jobs" total, and do not delegate obvious duplicates just so a subagent can return SKIP.
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 `.jobforge-ledger/events.jsonl` exists, `npx job-forge ledger:has --company "..." --role "..." --status Applied` may be used as a fast prefilter; a match is enough to drop that duplicate before dispatch. For candidates not rejected by the ledger, the four-source grep is still mandatory. If any source shows APPLIED / Applied, skip the dispatch and pick a replacement from the remaining candidate list. Do not count duplicates toward a requested "apply to N jobs" total, and do not delegate obvious duplicates just so a subagent can return SKIP.
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
12
12
 
13
13
  - [H3] Before every batch of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. Every round, no exceptions. Name this cleanup as an explicit "step 0" in your first-response plan for any multi-apply request — it is the most frequently skipped guardrail in practice, and skipping it produces cascade "Not connected" failures on the next dispatch.
@@ -19,7 +19,7 @@ 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.
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, `.jobforge-ledger/events.jsonl`, or `iso-trace`) rather than spawning a "check task status" subagent.
23
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
24
 
25
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.
@@ -54,17 +54,23 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
54
54
  - [D7] For standalone `batch` runs, prefer `batch/batch-runner.sh` instead of hand-rolling the loop. It delegates to `@razroo/iso-orchestrator`, persists workflow records in `.jobforge-runs/`, caps bundle fan-out, and mutexes state/report-number writes. Use `JOBFORGE_LEGACY_BATCH_RUNNER=1` only as a fallback.
55
55
  why: the old Bash loop encoded resumability and parallelism manually; the iso-orchestrator path makes the durable control state inspectable and prevents report-number collisions under parallel bundles
56
56
 
57
+ - [D8] Use `job-forge ledger:*` for cheap local workflow-state checks when available. `iso-ledger` is not an MCP and adds no prompt/tool schema tokens; it records tracker TSV writes, merge outcomes, rebuilt tracker snapshots, and pipeline items in `.jobforge-ledger/events.jsonl`.
58
+ why: state-trace remains working memory, while iso-ledger is deterministic append-only workflow truth that can answer duplicate/status questions without loading growing markdown/TSV files into the model context
59
+
60
+ - [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
61
+ why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
62
+
57
63
  ## Procedure
58
64
 
59
65
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
60
66
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
61
67
  3. Read the active mode file [D3]; decide inline vs delegated work [D1].
62
- 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
68
+ 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
63
69
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
64
70
  6. Keep multi-job form-filling out of the orchestrator [H4].
65
71
  7. Cross-check subagent facts against authoritative files [H7].
66
72
  8. Apply score gate [D4].
67
- 9. Merge TSV outcomes [H6].
73
+ 9. Merge contract-validated TSV outcomes [H6, D9].
68
74
  10. Verify tracker before ending [H6].
69
75
 
70
76
  ## Routing
package/README.md CHANGED
@@ -31,6 +31,8 @@ The scaffolded `opencode.json` already has three MCPs wired up — they launch a
31
31
  - **Gmail** — reads replies from recruiters
32
32
  - **state-trace** — typed working memory for cross-session context (resumed batches, recent decisions, repeated portal quirks). Install once with `python3 -m pip install "state-trace[mcp]"`; the MCP command is `state-trace-mcp`.
33
33
 
34
+ JobForge also keeps MCP-free local workflow state: `templates/contracts.json` defines tracker/apply artifact shapes via `@razroo/iso-contract`, and `.jobforge-ledger/events.jsonl` records deterministic duplicate/status events via `@razroo/iso-ledger`. Neither adds prompt or tool-schema tokens.
35
+
34
36
  `npm install` also materializes symlinks for every supported agent harness — OpenCode, Cursor, Claude Code, and Codex — so you can run `opencode`, `cursor`, `claude`, or `codex` in the same project and each picks up the shared MCP config and instructions.
35
37
 
36
38
  Then fill in `cv.md`, `config/profile.yml`, and `portals.yml` with your personal data, paste a job URL into opencode, and JobForge evaluates + tracks it.
@@ -76,7 +78,7 @@ JobForge turns opencode into a full job search command center. Instead of manual
76
78
  | **Durable Batch Orchestration** | `batch-runner.sh` uses `@razroo/iso-orchestrator` for resumable bundle execution, bounded fan-out, mutexed state writes, and workflow records in `.jobforge-runs/`. |
77
79
  | **Pipeline Integrity** | Automated merge, dedup, status normalization, health checks |
78
80
  | **Cost-Aware Agent Routing** | Three subagents (`@general-free`, `@general-paid`, `@glm-minimal`) with per-task tool surfaces. On OpenCode, JobForge pins all tiers to `opencode-go/deepseek-v4-flash` so application runs avoid overloaded free-model pools. See [Subagent Routing in AGENTS.md](AGENTS.md) for the task-to-agent mapping. |
79
- | **Trace + Telemetry + Guard** | `job-forge trace:*` exposes local OpenCode transcripts, `job-forge telemetry:*` summarizes runs, and `job-forge guard:*` audits deterministic JobForge policy rules with `@razroo/iso-guard`. |
81
+ | **Trace + Telemetry + Guard + Contract + Ledger** | `job-forge trace:*` exposes local OpenCode transcripts, `job-forge telemetry:*` summarizes runs, `job-forge guard:*` audits deterministic policy rules, `templates/contracts.json` enforces artifact shape with `iso-contract`, and `job-forge ledger:*` queries append-only workflow state without MCP/token overhead. |
80
82
  | **Token Cost Visibility** | `job-forge tokens --days 1` for per-session breakdown; `job-forge session-report --since-minutes 60 --log` to flag sessions over budget and append history to `data/token-usage.tsv`. Auto-logged after every batch run. |
81
83
 
82
84
  ## Usage
@@ -143,6 +145,7 @@ my-search/
143
145
  ├── portals.yml # companies to scan (personal)
144
146
  ├── config/profile.yml # your identity, target roles (personal)
145
147
  ├── data/ # applications, pipeline, scan history (personal, gitignored)
148
+ ├── .jobforge-ledger/ # append-only local workflow events (personal, gitignored)
146
149
  ├── reports/ # generated evaluation reports (personal, gitignored)
147
150
  ├── batch/{batch-input,batch-state}.tsv, tracker-additions/, logs/ # personal
148
151
  ├── .jobforge-runs/ # durable batch workflow records (generated)
@@ -190,6 +193,8 @@ JobForge/
190
193
  ├── batch/{batch-prompt.md,batch-runner.sh} # batch orchestrator
191
194
  ├── scripts/
192
195
  │ ├── batch-orchestrator.mjs # iso-orchestrator-backed batch control loop
196
+ │ ├── tracker-line.mjs # iso-contract-backed tracker TSV renderer
197
+ │ ├── ledger.mjs # iso-ledger-backed workflow-state CLI
193
198
  │ ├── token-usage-report.mjs # opencode cost analyzer
194
199
  │ └── release/check-source.mjs # version gate for npm publish
195
200
  ├── tracker-lib.mjs / merge-tracker.mjs / dedup-tracker.mjs / verify-pipeline.mjs
@@ -119,6 +119,11 @@ const consumerPkg = {
119
119
  'telemetry:watch': 'job-forge telemetry:watch',
120
120
  'guard:audit': 'job-forge guard:audit',
121
121
  'guard:explain': 'job-forge guard:explain',
122
+ 'ledger:status': 'job-forge ledger:status',
123
+ 'ledger:rebuild': 'job-forge ledger:rebuild',
124
+ 'ledger:verify': 'job-forge ledger:verify',
125
+ 'ledger:has': 'job-forge ledger:has',
126
+ 'ledger:query': 'job-forge ledger:query',
122
127
  // One command to pull the latest harness and any locally-pinned MCP
123
128
  // packages. npm update is a no-op on packages not in package.json, so
124
129
  // listing @razroo/gmail-mcp + @geometra/mcp is safe for consumers that
@@ -128,7 +133,7 @@ const consumerPkg = {
128
133
  dependencies: {
129
134
  'job-forge': '^2.0.0',
130
135
  },
131
- engines: { node: '>=18' },
136
+ engines: { node: '>=20.6.0' },
132
137
  };
133
138
  write('package.json', JSON.stringify(consumerPkg, null, 2) + '\n');
134
139
 
@@ -218,6 +223,7 @@ Before doing any work, remember where things live in *this* project:
218
223
  | Application tracker | \`data/applications/YYYY-MM-DD.md\` | **Day-based**. One markdown table per day. **There is NO \`applications.md\` — do not look for it.** |
219
224
  | Inbox of pending URLs | \`data/pipeline.md\` | The queue for \`/job-forge pipeline\` |
220
225
  | Scanner dedup history | \`data/scan-history.tsv\` | Only touch in \`/job-forge scan\` |
226
+ | Local workflow ledger | \`.jobforge-ledger/events.jsonl\` | Deterministic append-only state; use \`job-forge ledger:*\` |
221
227
  | Scanner config | \`portals.yml\` (project root) | Company configs |
222
228
  | Profile / identity | \`config/profile.yml\` | Candidate name, email, target roles |
223
229
  | CV | \`cv.md\` (project root) | Markdown, source of truth |
@@ -305,6 +311,7 @@ data/applications.md
305
311
  data/pipeline.md
306
312
  data/scan-history.tsv
307
313
  data/token-usage.tsv
314
+ .jobforge-ledger/
308
315
  reports/
309
316
  !reports/.gitkeep
310
317
  batch/batch-state.tsv
@@ -361,6 +368,7 @@ job-forge sync # re-run if symlinks drift
361
368
  \`\`\`bash
362
369
  job-forge merge # merge batch/tracker-additions/*.tsv into the tracker
363
370
  job-forge verify # verify pipeline integrity
371
+ job-forge ledger:status # local deterministic workflow ledger status
364
372
  job-forge pdf cv.md out.pdf
365
373
  job-forge tokens --days 1 # per-session opencode token usage
366
374
  \`\`\`
package/bin/job-forge.mjs CHANGED
@@ -20,6 +20,7 @@
20
20
  * trace:* Inspect local agent transcripts via iso-trace
21
21
  * telemetry:* Summarize JobForge pipeline status from traces + tracker files
22
22
  * guard:* Audit JobForge trace policy with iso-guard
23
+ * ledger:* Query local deterministic workflow state via iso-ledger
23
24
  * sync Re-run the harness symlink sync (bin/sync.mjs)
24
25
  * help, --help Show this message
25
26
  */
@@ -74,6 +75,15 @@ const guardAliases = {
74
75
  'guard:explain': 'explain',
75
76
  };
76
77
 
78
+ const ledgerAliases = {
79
+ 'ledger:status': 'status',
80
+ 'ledger:rebuild': 'rebuild',
81
+ 'ledger:verify': 'verify',
82
+ 'ledger:has': 'has',
83
+ 'ledger:query': 'query',
84
+ 'ledger:path': 'path',
85
+ };
86
+
77
87
  const [, , cmd, ...rest] = process.argv;
78
88
 
79
89
  function printHelp() {
@@ -100,13 +110,17 @@ Commands:
100
110
  telemetry:watch Watch latest run status
101
111
  guard:audit Audit latest/local trace policy with iso-guard
102
112
  guard:explain Show the active iso-guard policy
113
+ ledger:status Show local workflow ledger status
114
+ ledger:rebuild Rebuild .jobforge-ledger/events.jsonl from tracker/pipeline files
115
+ ledger:has Check URL or company+role state without loading tracker files
116
+ ledger:verify Validate the local workflow ledger
103
117
  sync Re-create harness symlinks in the current project
104
118
 
105
119
  Deterministic helpers (prefer these over LLM-derived values):
106
120
  next-num Print next sequential report number (e.g. 521)
107
121
  slugify NAME Convert a company/role name to a filename-safe slug
108
122
  today Print today's date in YYYY-MM-DD
109
- tracker-line Emit a 9-col TSV row for batch/tracker-additions/
123
+ tracker-line Render and validate a tracker TSV row for batch/tracker-additions/
110
124
 
111
125
  Cost visibility:
112
126
  session-report Summarize recent session costs, warn on >budget sessions
@@ -128,6 +142,7 @@ Pass --help after a command to see its own flags, e.g.:
128
142
  job-forge telemetry:show ses_...
129
143
  job-forge guard:audit
130
144
  job-forge guard:explain
145
+ job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied
131
146
 
132
147
  Project directory resolves to $JOB_FORGE_PROJECT or cwd.`);
133
148
  }
@@ -182,6 +197,21 @@ if (cmd === 'guard' || guardAliases[cmd]) {
182
197
  process.exit(result.status ?? 1);
183
198
  }
184
199
 
200
+ if (cmd === 'ledger' || ledgerAliases[cmd]) {
201
+ const ledgerArgs = cmd === 'ledger'
202
+ ? (rest.length === 0 ? ['help'] : rest)
203
+ : [ledgerAliases[cmd], ...rest];
204
+
205
+ const scriptPath = join(PKG_ROOT, 'scripts/ledger.mjs');
206
+ const result = spawnSync(process.execPath, [scriptPath, ...ledgerArgs], {
207
+ stdio: 'inherit',
208
+ cwd: PROJECT_DIR,
209
+ env: process.env,
210
+ });
211
+
212
+ process.exit(result.status ?? 1);
213
+ }
214
+
185
215
  const rel = commands[cmd];
186
216
  if (!rel) {
187
217
  console.error(`Unknown command: ${cmd}\n`);
@@ -17,6 +17,7 @@ my-search/
17
17
  ├── config/profile.yml # personal
18
18
  ├── portals.yml # personal
19
19
  ├── data/ # personal (gitignored)
20
+ ├── .jobforge-ledger/ # local workflow events (personal, gitignored)
20
21
  ├── reports/ # personal (gitignored)
21
22
  ├── AGENTS.md # personal overrides (opencode + codex)
22
23
  ├── CLAUDE.md # personal overrides (Claude Code); @-imports CLAUDE.harness.md
@@ -159,6 +160,7 @@ article-digest.md → Proof points for matching
159
160
  config/profile.yml → Candidate identity
160
161
  portals.yml → Scanner configuration
161
162
  data/pipeline.md → Pending URLs and `local:jds/...` inbox (see modes/pipeline.md)
163
+ .jobforge-ledger/events.jsonl → Append-only workflow events for cheap local duplicate/status checks
162
164
  jds/*.md → Saved job descriptions referenced from the pipeline (`local:jds/{file}`)
163
165
  templates/states.yml → Canonical status values
164
166
  templates/cv-template.html → PDF generation template
@@ -171,11 +173,12 @@ Create `data/pipeline.md` when you start using the URL inbox (`/job-forge pipeli
171
173
 
172
174
  - Reports: `{###}-{company-slug}-{YYYY-MM-DD}.md` (3-digit zero-padded)
173
175
  - PDFs: `cv-candidate-{company-slug}-{YYYY-MM-DD}.pdf`
174
- - Tracker TSVs: `batch/tracker-additions/{num}-{company-slug}.tsv` (one file per evaluation; merged files move under `batch/tracker-additions/merged/`)
176
+ - Tracker TSVs: `batch/tracker-additions/{num}-{company-slug}.tsv` (one file per evaluation; merged files move under `batch/tracker-additions/merged/`; shape enforced by `templates/contracts.json`)
177
+ - Ledger: `.jobforge-ledger/events.jsonl` (created by `job-forge ledger:rebuild`, `tracker-line --write`, or `merge`; gitignored personal state)
175
178
 
176
179
  ## Pipeline Integrity
177
180
 
178
- From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify-pipeline.mjs`. When a tracker file exists, it validates canonical statuses (using `templates/states.yml` when that file is present and parseable), warns on probable duplicate company/role rows, checks that report column markdown links resolve to files in the repo, validates score column format (`X.X/5`, `N/A`, or `DUP`), rejects table rows with too few columns, flags markdown bold inside the score column, and warns if any `batch/tracker-additions/*.tsv` files are still waiting to be merged. It also compares state ids from `templates/states.yml` to an internal fallback list and warns when the two sets drift. **Fresh clone:** the command exits successfully when neither `data/applications.md` nor root `applications.md` exists yet; pending-TSV and states-drift checks still run so contributors see unmerged batch output early. Optional setup validation after you add `cv.md` and `config/profile.yml`: `npm run sync-check` (`cv-sync-check.mjs`).
181
+ From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify-pipeline.mjs`. When a tracker file exists, it validates canonical statuses (using `templates/states.yml` when that file is present and parseable), validates every tracker row against `templates/contracts.json`, warns on probable duplicate company/role rows, checks that report column markdown links resolve to files in the repo, validates score column format (`X.X/5`, `N/A`, or `DUP`), rejects table rows with too few columns, flags markdown bold inside the score column, and warns if any `batch/tracker-additions/*.tsv` files are still waiting to be merged. If `.jobforge-ledger/events.jsonl` exists, verify also validates the append-only ledger. It also compares state ids from `templates/states.yml` to an internal fallback list and warns when the two sets drift. **Fresh clone:** the command exits successfully when neither `data/applications.md` nor root `applications.md` exists yet; pending-TSV and states-drift checks still run so contributors see unmerged batch output early. Optional setup validation after you add `cv.md` and `config/profile.yml`: `npm run sync-check` (`cv-sync-check.mjs`).
179
182
 
180
183
  **`verify-pipeline.mjs` checks (same order as the script header):**
181
184
 
@@ -184,11 +187,13 @@ From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify
184
187
  3. Report column markdown links resolve to files under the repo root.
185
188
  4. Score column matches `X.X/5`, `N/A`, or `DUP`.
186
189
  5. Table data rows have enough pipe-delimited columns.
187
- 6. No unmerged `batch/tracker-additions/*.tsv` files (warns if any remain).
188
- 7. Score column has no markdown bold.
189
- 8. Warn when state ids in `templates/states.yml` drift from the script’s built-in fallback list (or when the file exists but ids failed to parse).
190
+ 6. Tracker rows satisfy the `jobforge.tracker-row` contract in `templates/contracts.json`.
191
+ 7. No unmerged `batch/tracker-additions/*.tsv` files (warns if any remain).
192
+ 8. Score column has no markdown bold.
193
+ 9. Warn when state ids in `templates/states.yml` drift from the script’s built-in fallback list (or when the file exists but ids failed to parse).
194
+ 10. Validate `.jobforge-ledger/events.jsonl` when present.
190
195
 
191
- When the tracker file is missing, checks 1–5 and 7 are skipped; checks 6 and 8 still run.
196
+ When the tracker file is missing, checks 1-6 and 8 are skipped; checks 7, 9, and 10 still run when applicable.
192
197
 
193
198
  ## Contributing touchpoints
194
199
 
@@ -210,6 +215,7 @@ Scripts maintain data consistency. In a consumer project they're invoked via the
210
215
  | `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 |
211
216
  | `scripts/telemetry.mjs` | `npx job-forge telemetry:status` / `telemetry:show` | JobForge operational telemetry derived from OpenCode traces plus tracker TSV state |
212
217
  | `scripts/guard.mjs` | `npx job-forge guard:audit` / `guard:explain` | Deterministic `@razroo/iso-guard` policy audits over local OpenCode traces |
218
+ | `scripts/ledger.mjs` | `npx job-forge ledger:status` / `ledger:has` / `ledger:rebuild` | Deterministic `@razroo/iso-ledger` state over tracker, TSV, and pipeline files |
213
219
  | `tracker-lib.mjs` | _(library)_ | Shared helpers for reading/writing day-based tracker files — imported by merge/dedup/verify/normalize |
214
220
  | `bin/sync.mjs` | `npx job-forge sync` | Creates the harness symlinks in a consumer project (also runs as `postinstall`) |
215
221
  | `bin/create-job-forge.mjs` | `npx create-job-forge <dir>` | Scaffolds a new personal project |
@@ -125,6 +125,23 @@ npx job-forge telemetry:watch
125
125
 
126
126
  Telemetry is also local-only and passive. It reads OpenCode's SQLite DB and files under `batch/tracker-additions/`; agents do not need to remember to emit custom events.
127
127
 
128
+ ## JobForge ledger
129
+
130
+ The ledger is append-only local workflow state backed by `@razroo/iso-ledger`. It is not an MCP and does not add prompt, tool-schema, or state-trace tokens. Use it when you want a cheap deterministic check before loading growing tracker files:
131
+
132
+ ```bash
133
+ npx job-forge ledger:rebuild
134
+ npx job-forge ledger:status
135
+ npx job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied
136
+ npx job-forge ledger:verify
137
+ ```
138
+
139
+ `tracker-line --write` records tracker-addition events, `merge` records add/update/skip outcomes, and `ledger:rebuild` backfills events from `data/applications/`, `batch/tracker-additions/`, `batch/tracker-additions/merged/`, and `data/pipeline.md`.
140
+
141
+ ## JobForge artifact contracts
142
+
143
+ Machine-readable artifact shapes live in `templates/contracts.json` and are enforced by `@razroo/iso-contract`. `job-forge tracker-line` renders tracker additions through the `jobforge.tracker-row` contract, `merge` validates pending TSV/table rows before writing tracker files, and `verify` validates existing tracker rows against the same contract. Custom forks can extend `templates/contracts.json`, but keep the tracker status enum aligned with `templates/states.yml`.
144
+
128
145
  ## JobForge guard audits
129
146
 
130
147
  Guard audits run deterministic `@razroo/iso-guard` policies over the same local OpenCode traces. The default policy lives at `templates/guards/jobforge-baseline.yaml` and checks rules that are reliable from transcript data, including max two task dispatches per assistant message, no task-status polling via `task`, no raw proxy configuration in task prompts, and no child session task recursion.
package/docs/README.md CHANGED
@@ -31,7 +31,7 @@ The harness exposes a single CLI (`job-forge`) installed as a `bin` entry. In a
31
31
 
32
32
  | What you need | Where to read |
33
33
  |---------------|---------------|
34
- | Full command list (`verify`, `merge`, `dedup`, `normalize`, `pdf`, `sync-check`, `tokens`, `trace`, `telemetry`, `guard`, `sync`). | [SETUP.md — Tracker and scripts (terminal)](SETUP.md#tracker-and-scripts-terminal). |
34
+ | Full command list (`verify`, `merge`, `dedup`, `normalize`, `pdf`, `sync-check`, `tokens`, `trace`, `telemetry`, `guard`, `ledger`, `sync`). | [SETUP.md — Tracker and scripts (terminal)](SETUP.md#tracker-and-scripts-terminal). |
35
35
  | What each harness `.mjs` script does. | [ARCHITECTURE.md — Pipeline integrity](ARCHITECTURE.md#pipeline-integrity) and the scripts table underneath. |
36
36
  | Batch runner, TSV layout, and `batch/tracker-additions/` merge flow. | [batch/README.md](../batch/README.md). |
37
37
  | PR gate for harness contributions (`npm run verify` + `npm run build:dashboard`). | [CONTRIBUTING.md — Development](../CONTRIBUTING.md#development). |
package/docs/SETUP.md CHANGED
@@ -125,6 +125,7 @@ From your project root, these commands maintain the tracker and pipeline checks.
125
125
  |--------|---------|-----------|
126
126
  | Pipeline health check | `npx job-forge verify` | `npm run verify` |
127
127
  | Merge `batch/tracker-additions/*.tsv` into the tracker | `npx job-forge merge` | `npm run merge` |
128
+ | Inspect tracker row contract | `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` | _(none)_ |
128
129
  | Map status column to canonical labels | `npx job-forge normalize` | `npm run normalize` |
129
130
  | Merge duplicate company/role rows | `npx job-forge dedup` | `npm run dedup` |
130
131
  | Generate ATS-optimized CV PDF | `npx job-forge pdf` | `npm run pdf` |
@@ -138,6 +139,9 @@ From your project root, these commands maintain the tracker and pipeline checks.
138
139
  | Show one JobForge run by session id/prefix | `npx job-forge telemetry:show <id>` | `npm run telemetry:show -- <id>` |
139
140
  | Audit latest JobForge trace policy | `npx job-forge guard:audit` | `npm run guard:audit` |
140
141
  | Show the active guard policy | `npx job-forge guard:explain` | `npm run guard:explain` |
142
+ | Show local workflow ledger status | `npx job-forge ledger:status` | `npm run ledger:status` |
143
+ | Rebuild local workflow ledger from tracker/pipeline files | `npx job-forge ledger:rebuild` | `npm run ledger:rebuild` |
144
+ | Check duplicate/status event without loading tracker files | `npx job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied` | `npm run ledger:has -- --company ...` |
141
145
  | Re-create harness symlinks | `npx job-forge sync` | `npm run sync` |
142
146
  | Build optional dashboard TUI (Go on `PATH`) | `(cd node_modules/job-forge/dashboard && go build .)` | `npm run build:dashboard` (harness repo only) |
143
147
 
@@ -71,6 +71,14 @@ Or paste a JD directly to run the full pipeline.
71
71
  Token usage check (terminal, outside opencode):
72
72
  npx job-forge tokens --days 1 # today's sessions with input/cache breakdown
73
73
  npx job-forge tokens --session <id> # drill into one session for cache-bust hunting
74
+
75
+ Local workflow ledger (terminal, outside opencode):
76
+ npx job-forge ledger:status # .jobforge-ledger/events.jsonl summary
77
+ npx job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied
78
+
79
+ Artifact contracts (terminal, outside opencode):
80
+ npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json
81
+ npx job-forge tracker-line ... --write # renders + validates tracker TSV locally
74
82
  ```
75
83
 
76
84
  ---
@@ -145,6 +153,9 @@ Step 1 — Enumerate candidates
145
153
  - Build ordered list: candidates = [job_1, job_2, ..., job_N]
146
154
 
147
155
  Step 2 — Dedup against already-applied
156
+ - If .jobforge-ledger/events.jsonl exists, use npx job-forge ledger:has as a
157
+ fast prefilter for obvious company+role Applied duplicates. A ledger match
158
+ can be dropped before dispatch without loading tracker files into context.
148
159
  - For each candidate, grep all four sources for URL and company+role:
149
160
  data/pipeline.md, data/applications/*.md, batch/tracker-additions/*.tsv,
150
161
  batch/tracker-additions/merged/*.tsv
@@ -7,7 +7,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
7
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
8
  why: 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 recoveries
9
9
 
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 and pick a replacement from the remaining candidate list. Do not count duplicates toward a requested "apply to N jobs" total, and do not delegate obvious duplicates just so a subagent can return SKIP.
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 `.jobforge-ledger/events.jsonl` exists, `npx job-forge ledger:has --company "..." --role "..." --status Applied` may be used as a fast prefilter; a match is enough to drop that duplicate before dispatch. For candidates not rejected by the ledger, the four-source grep is still mandatory. If any source shows APPLIED / Applied, skip the dispatch and pick a replacement from the remaining candidate list. Do not count duplicates toward a requested "apply to N jobs" total, and do not delegate obvious duplicates just so a subagent can return SKIP.
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
12
12
 
13
13
  - [H3] Before every batch of `task` dispatches that will use Geometra, call `geometra_list_sessions` then `geometra_disconnect({closeBrowser: true})`. Every round, no exceptions. Name this cleanup as an explicit "step 0" in your first-response plan for any multi-apply request — it is the most frequently skipped guardrail in practice, and skipping it produces cascade "Not connected" failures on the next dispatch.
@@ -19,7 +19,7 @@ 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.
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, `.jobforge-ledger/events.jsonl`, or `iso-trace`) rather than spawning a "check task status" subagent.
23
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
24
 
25
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.
@@ -54,17 +54,23 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
54
54
  - [D7] For standalone `batch` runs, prefer `batch/batch-runner.sh` instead of hand-rolling the loop. It delegates to `@razroo/iso-orchestrator`, persists workflow records in `.jobforge-runs/`, caps bundle fan-out, and mutexes state/report-number writes. Use `JOBFORGE_LEGACY_BATCH_RUNNER=1` only as a fallback.
55
55
  why: the old Bash loop encoded resumability and parallelism manually; the iso-orchestrator path makes the durable control state inspectable and prevents report-number collisions under parallel bundles
56
56
 
57
+ - [D8] Use `job-forge ledger:*` for cheap local workflow-state checks when available. `iso-ledger` is not an MCP and adds no prompt/tool schema tokens; it records tracker TSV writes, merge outcomes, rebuilt tracker snapshots, and pipeline items in `.jobforge-ledger/events.jsonl`.
58
+ why: state-trace remains working memory, while iso-ledger is deterministic append-only workflow truth that can answer duplicate/status questions without loading growing markdown/TSV files into the model context
59
+
60
+ - [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
61
+ why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
62
+
57
63
  ## Procedure
58
64
 
59
65
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
60
66
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
61
67
  3. Read the active mode file [D3]; decide inline vs delegated work [D1].
62
- 4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
68
+ 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
63
69
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
64
70
  6. Keep multi-job form-filling out of the orchestrator [H4].
65
71
  7. Cross-check subagent facts against authoritative files [H7].
66
72
  8. Apply score gate [D4].
67
- 9. Merge TSV outcomes [H6].
73
+ 9. Merge contract-validated TSV outcomes [H6, D9].
68
74
  10. Verify tracker before ending [H6].
69
75
 
70
76
  ## Routing
@@ -0,0 +1,97 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { dirname, join, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import {
5
+ contractNames,
6
+ explainContract,
7
+ formatIssue,
8
+ getContract,
9
+ loadContractCatalog,
10
+ parseRecord,
11
+ renderRecord,
12
+ validateRecord,
13
+ } from '@razroo/iso-contract';
14
+ import { DEFAULT_STATES, loadCanonicalStates } from './canonical-states.mjs';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const PKG_ROOT = resolve(__dirname, '..');
18
+ export const CONTRACTS_RELATIVE_PATH = 'templates/contracts.json';
19
+
20
+ export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
21
+ return projectDir;
22
+ }
23
+
24
+ export function jobForgeContractsPath(projectDir = resolveProjectDir()) {
25
+ const projectPath = join(projectDir, CONTRACTS_RELATIVE_PATH);
26
+ if (existsSync(projectPath)) return projectPath;
27
+ return join(PKG_ROOT, CONTRACTS_RELATIVE_PATH);
28
+ }
29
+
30
+ export function loadJobForgeContractCatalog(projectDir = resolveProjectDir()) {
31
+ const path = jobForgeContractsPath(projectDir);
32
+ const input = JSON.parse(readFileSync(path, 'utf-8'));
33
+ applyCanonicalStatusValues(input, projectDir);
34
+ return loadContractCatalog(input);
35
+ }
36
+
37
+ export function listJobForgeContracts(projectDir = resolveProjectDir()) {
38
+ return contractNames(loadJobForgeContractCatalog(projectDir));
39
+ }
40
+
41
+ export function getJobForgeContract(name, projectDir = resolveProjectDir()) {
42
+ return getContract(loadJobForgeContractCatalog(projectDir), name);
43
+ }
44
+
45
+ export function explainJobForgeContract(name, projectDir = resolveProjectDir()) {
46
+ return explainContract(getJobForgeContract(name, projectDir));
47
+ }
48
+
49
+ export function getTrackerRowContract(projectDir = resolveProjectDir()) {
50
+ return getJobForgeContract('jobforge.tracker-row', projectDir);
51
+ }
52
+
53
+ export function validateTrackerRow(record, options = {}) {
54
+ const projectDir = resolveProjectDir(options.projectDir);
55
+ const normalized = normalizeTrackerRowRecord(record, options);
56
+ return validateRecord(getTrackerRowContract(projectDir), normalized);
57
+ }
58
+
59
+ export function parseTrackerRow(text, formatName = 'tsv', options = {}) {
60
+ const projectDir = resolveProjectDir(options.projectDir);
61
+ const contract = getTrackerRowContract(projectDir);
62
+ const parsed = parseRecord(contract, text, formatName);
63
+ const normalized = normalizeTrackerRowRecord(parsed.record, options);
64
+ const validation = validateRecord(contract, normalized);
65
+ return { record: validation.record, validation, format: formatName };
66
+ }
67
+
68
+ export function renderTrackerRow(record, formatName = 'tsv', options = {}) {
69
+ const projectDir = resolveProjectDir(options.projectDir);
70
+ const normalized = normalizeTrackerRowRecord(record, options);
71
+ return renderRecord(getTrackerRowContract(projectDir), normalized, formatName);
72
+ }
73
+
74
+ export function formatContractIssues(result) {
75
+ return result.issues.map(formatIssue).join('; ');
76
+ }
77
+
78
+ export function canonicalStatusValues(projectDir = resolveProjectDir()) {
79
+ return loadCanonicalStates(projectDir) || loadCanonicalStates(PKG_ROOT) || DEFAULT_STATES;
80
+ }
81
+
82
+ function normalizeTrackerRowRecord(record, options = {}) {
83
+ const out = { ...record };
84
+ if (out.status !== undefined && options.normalizeStatus) {
85
+ out.status = options.normalizeStatus(String(out.status));
86
+ }
87
+ return out;
88
+ }
89
+
90
+ function applyCanonicalStatusValues(input, projectDir) {
91
+ const statuses = canonicalStatusValues(projectDir);
92
+ for (const contract of input.contracts || []) {
93
+ if (contract.name !== 'jobforge.tracker-row') continue;
94
+ const field = (contract.fields || []).find((item) => item.name === 'status');
95
+ if (field) field.values = statuses;
96
+ }
97
+ }