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.
- package/.cursor/rules/main.mdc +10 -4
- package/.opencode/skills/job-forge.md +11 -0
- package/AGENTS.md +10 -4
- package/CLAUDE.md +10 -4
- package/README.md +6 -1
- package/bin/create-job-forge.mjs +9 -1
- package/bin/job-forge.mjs +31 -1
- package/docs/ARCHITECTURE.md +12 -6
- package/docs/CUSTOMIZATION.md +17 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +4 -0
- package/iso/commands/job-forge.md +11 -0
- package/iso/instructions.md +10 -4
- package/lib/jobforge-contracts.mjs +97 -0
- package/lib/jobforge-ledger.mjs +214 -0
- package/merge-tracker.mjs +49 -55
- package/modes/reference-setup.md +17 -4
- package/package.json +8 -1
- package/scripts/ledger.mjs +359 -0
- package/scripts/telemetry.mjs +14 -0
- package/scripts/tracker-line.mjs +15 -1
- package/templates/contracts.json +54 -0
- package/verify-pipeline.mjs +57 -6
package/.cursor/rules/main.mdc
CHANGED
|
@@ -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,
|
|
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
|
package/bin/create-job-forge.mjs
CHANGED
|
@@ -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: '>=
|
|
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
|
|
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`);
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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.
|
|
188
|
-
7.
|
|
189
|
-
8.
|
|
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
|
|
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 |
|
package/docs/CUSTOMIZATION.md
CHANGED
|
@@ -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
|
package/iso/instructions.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
|
|
@@ -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
|
+
}
|